diff --git a/lib/widgets/common/magnifier/controller/controller.dart b/lib/widgets/common/magnifier/controller/controller.dart index 5eb76aea2..0e9d41e15 100644 --- a/lib/widgets/common/magnifier/controller/controller.dart +++ b/lib/widgets/common/magnifier/controller/controller.dart @@ -18,13 +18,14 @@ class MagnifierController { late ScaleStateChange _currentScaleState, previousScaleState; MagnifierController({ - Offset initialPosition = Offset.zero, + MagnifierState? initialState, }) : super() { - initial = MagnifierState( - position: initialPosition, - scale: null, - source: ChangeSource.internal, - ); + initial = initialState ?? + const MagnifierState( + position: Offset.zero, + scale: null, + source: ChangeSource.internal, + ); previousState = initial; _currentState = initial; _setState(initial); diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index af99aaad8..0d9666b5f 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -23,6 +23,7 @@ import 'package:aves/widgets/viewer/visual/state.dart'; import 'package:aves/widgets/viewer/visual/subtitle/subtitle.dart'; import 'package:aves/widgets/viewer/visual/vector.dart'; import 'package:aves/widgets/viewer/visual/video.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -51,6 +52,13 @@ class _EntryPageViewState extends State { late ImageStreamListener _videoCoverStreamListener; final ValueNotifier _videoCoverInfoNotifier = ValueNotifier(null); + MagnifierController? _dismissedCoverMagnifierController; + + MagnifierController get dismissedCoverMagnifierController { + _dismissedCoverMagnifierController ??= MagnifierController(); + return _dismissedCoverMagnifierController!; + } + AvesEntry get mainEntry => widget.mainEntry; AvesEntry get entry => widget.pageEntry; @@ -176,91 +184,110 @@ class _EntryPageViewState extends State { Widget _buildVideoView() { final videoController = context.read().getController(entry); if (videoController == null) return const SizedBox(); - return Stack( - fit: StackFit.expand, - children: [ - ValueListenableBuilder( - valueListenable: videoController.sarNotifier, - builder: (context, sar, child) { - return Stack( - children: [ - _buildMagnifier( - displaySize: entry.videoDisplaySize(sar), - child: VideoView( - entry: entry, - controller: videoController, - ), + return ValueListenableBuilder( + valueListenable: videoController.sarNotifier, + builder: (context, sar, child) { + final videoDisplaySize = entry.videoDisplaySize(sar); + return Stack( + fit: StackFit.expand, + children: [ + Stack( + children: [ + _buildMagnifier( + displaySize: videoDisplaySize, + child: VideoView( + entry: entry, + controller: videoController, ), + ), + VideoSubtitles( + controller: videoController, + viewStateNotifier: _viewStateNotifier, + ), + if (settings.videoShowRawTimedText) VideoSubtitles( controller: videoController, viewStateNotifier: _viewStateNotifier, + debugMode: true, ), - if (settings.videoShowRawTimedText) - VideoSubtitles( - controller: videoController, - viewStateNotifier: _viewStateNotifier, - debugMode: true, - ), - ], - ); - }), - // fade out image to ease transition with the player - StreamBuilder( - stream: videoController.statusStream, - builder: (context, snapshot) { - final showCover = !videoController.isReady; - return IgnorePointer( - ignoring: !showCover, - child: AnimatedOpacity( - opacity: showCover ? 1 : 0, - curve: Curves.easeInCirc, - duration: Durations.viewerVideoPlayerTransition, - child: ValueListenableBuilder( - valueListenable: _videoCoverInfoNotifier, - builder: (context, videoCoverInfo, child) { - if (videoCoverInfo == null) { - return GestureDetector( - onTap: _onTap, - child: ThumbnailImage( - entry: entry, - extent: context.select((mq) => mq.size.shortestSide), - fit: BoxFit.contain, - showLoadingBackground: false, - ), - ); - } + ], + ), + _buildVideoCover(videoController, videoDisplaySize), + ], + ); + }, + ); + } - // full cover image - return _buildMagnifier( - displaySize: Size( - videoCoverInfo.image.width.toDouble(), - videoCoverInfo.image.height.toDouble(), - ), - child: Image( - image: entry.uriImage, - ), - ); - }, - ), - ), - ); - }, - ), - ], + StreamBuilder _buildVideoCover(AvesVideoController videoController, Size videoDisplaySize) { + // fade out image to ease transition with the player + return StreamBuilder( + stream: videoController.statusStream, + builder: (context, snapshot) { + final showCover = !videoController.isReady; + return IgnorePointer( + ignoring: !showCover, + child: AnimatedOpacity( + opacity: showCover ? 1 : 0, + curve: Curves.easeInCirc, + duration: Durations.viewerVideoPlayerTransition, + child: ValueListenableBuilder( + valueListenable: _videoCoverInfoNotifier, + builder: (context, videoCoverInfo, child) { + if (videoCoverInfo != null) { + // full cover image may have a different size and different aspect ratio + final coverSize = Size( + videoCoverInfo.image.width.toDouble(), + videoCoverInfo.image.height.toDouble(), + ); + // when the cover is the same size as the video itself + // (which is often the case when the cover is not embedded but just a frame), + // we can reuse the same magnifier and preserve its state when switching from cover to video + final coverController = showCover || coverSize == videoDisplaySize ? _magnifierController : dismissedCoverMagnifierController; + return _buildMagnifier( + controller: coverController, + displaySize: coverSize, + child: Image( + image: entry.uriImage, + ), + ); + } + + // default to cached thumbnail, if any + final extent = entry.cachedThumbnails.firstOrNull?.key.extent; + if (extent != null && extent > 0) { + return GestureDetector( + onTap: _onTap, + child: ThumbnailImage( + entry: entry, + extent: extent, + fit: BoxFit.contain, + showLoadingBackground: false, + ), + ); + } + + return const SizedBox(); + }, + ), + ), + ); + }, ); } Widget _buildMagnifier({ + MagnifierController? controller, + Size? displaySize, ScaleLevel maxScale = maxScale, ScaleStateCycle scaleStateCycle = defaultScaleStateCycle, bool applyScale = true, - Size? displaySize, required Widget child, }) { return Magnifier( // key includes modified date to refresh when the image is modified by metadata (e.g. rotated) key: ValueKey('${entry.uri}_${entry.pageId}_${entry.dateModifiedSecs}'), - controller: _magnifierController, + controller: controller ?? _magnifierController, childSize: displaySize ?? entry.displaySize, minScale: minScale, maxScale: maxScale,