From f54924d46a723f853e2026c5beb3fa5d4545d371 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Mon, 2 Mar 2026 23:23:49 +0530 Subject: [PATCH] refactor: simplify video zooming (#26527) fix: simplify video zooming # Conflicts: # mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart # mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../asset_viewer/asset_page.widget.dart | 78 +++++++++---------- .../asset_viewer/asset_viewer.page.dart | 2 +- .../asset_viewer/video_viewer.widget.dart | 66 ++++------------ .../widgets/memory/memory_card.widget.dart | 1 - 4 files changed, 55 insertions(+), 92 deletions(-) diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart index 18fd8846b4..a61ce0e78b 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart @@ -14,15 +14,15 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_details.wi import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; import 'package:immich_mobile/widgets/photo_view/photo_view.dart'; @@ -53,7 +53,6 @@ class _AssetPageState extends ConsumerState { final _scrollController = ScrollController(); late final _proxyScrollController = ProxyScrollController(scrollController: _scrollController); - final ValueNotifier _videoScaleStateNotifier = ValueNotifier(PhotoViewScaleState.initial); double _snapOffset = 0.0; @@ -79,7 +78,6 @@ class _AssetPageState extends ConsumerState { _proxyScrollController.dispose(); _scaleBoundarySub?.cancel(); _eventSubscription?.cancel(); - _videoScaleStateNotifier.dispose(); super.dispose(); } @@ -249,17 +247,14 @@ class _AssetPageState extends ConsumerState { ref.read(isPlayingMotionVideoProvider.notifier).playing = true; void _onScaleStateChanged(PhotoViewScaleState scaleState) { - _isZoomed = - scaleState == PhotoViewScaleState.zoomedIn || - scaleState == PhotoViewScaleState.covering || - _videoScaleStateNotifier.value == PhotoViewScaleState.zoomedIn || - _videoScaleStateNotifier.value == PhotoViewScaleState.covering; + _isZoomed = switch (scaleState) { + PhotoViewScaleState.zoomedIn || PhotoViewScaleState.covering => true, + _ => false, + }; _viewer.setZoomed(_isZoomed); if (scaleState != PhotoViewScaleState.initial) { if (_dragStart == null) _viewer.setControls(false); - - ref.read(videoPlayerControlsProvider.notifier).pause(); return; } @@ -334,35 +329,40 @@ class _AssetPageState extends ConsumerState { ); } - return PhotoView.customChild( - key: Key(displayAsset.heroTag), - onDragStart: _onDragStart, - onDragUpdate: _onDragUpdate, - onDragEnd: _onDragEnd, - onDragCancel: _onDragCancel, - heroAttributes: heroAttributes, - filterQuality: FilterQuality.high, - basePosition: Alignment.center, - disableScaleGestures: true, - minScale: PhotoViewComputedScale.contained, - initialScale: PhotoViewComputedScale.contained, - tightMode: true, - onPageBuild: _onPageBuild, - enablePanAlways: true, - backgroundDecoration: backgroundDecoration, - child: NativeVideoViewer( - key: _NativeVideoViewerKey(displayAsset.heroTag), - asset: displayAsset, - scaleStateNotifier: _videoScaleStateNotifier, - disableScaleGestures: showingDetails, - image: Image( - image: getFullImageProvider(displayAsset, size: context.sizeData), - height: context.height, - width: context.width, - fit: BoxFit.contain, - alignment: Alignment.center, + return Stack( + children: [ + PhotoView.customChild( + key: Key(displayAsset.heroTag), + onDragStart: _onDragStart, + onDragUpdate: _onDragUpdate, + onDragEnd: _onDragEnd, + onDragCancel: _onDragCancel, + onTapUp: _onTapUp, + heroAttributes: heroAttributes, + basePosition: Alignment.center, + disableScaleGestures: showingDetails, + scaleStateChangedCallback: _onScaleStateChanged, + onPageBuild: _onPageBuild, + enablePanAlways: true, + backgroundDecoration: backgroundDecoration, + child: SizedBox( + width: context.width, + height: context.height, + child: NativeVideoViewer( + key: _NativeVideoViewerKey(displayAsset.heroTag), + asset: displayAsset, + image: Image( + image: getFullImageProvider(displayAsset, size: context.sizeData), + fit: BoxFit.contain, + height: context.height, + width: context.width, + alignment: Alignment.center, + ), + ), + ), ), - ), + const Center(child: VideoViewerControls()), + ], ); } diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart index d59e867d66..b353c6d80f 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -18,8 +18,8 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_page.widge import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_preloader.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; -import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_bottom_app_bar.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart index 30889835f6..701c527613 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -9,11 +9,9 @@ import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/setting.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; -import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; @@ -26,7 +24,6 @@ import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/debounce.dart'; import 'package:immich_mobile/utils/hooks/interval_hook.dart'; -import 'package:immich_mobile/widgets/photo_view/photo_view.dart'; import 'package:logging/logging.dart'; import 'package:native_video_player/native_video_player.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; @@ -51,21 +48,10 @@ bool _isCurrentAsset(BaseAsset asset, BaseAsset? currentAsset) { class NativeVideoViewer extends HookConsumerWidget { static final log = Logger('NativeVideoViewer'); final BaseAsset asset; - final bool showControls; final int playbackDelayFactor; final Widget image; - final ValueNotifier? scaleStateNotifier; - final bool disableScaleGestures; - const NativeVideoViewer({ - super.key, - required this.asset, - required this.image, - this.showControls = true, - this.playbackDelayFactor = 1, - this.scaleStateNotifier, - this.disableScaleGestures = false, - }); + const NativeVideoViewer({super.key, required this.asset, required this.image, this.playbackDelayFactor = 1}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -144,7 +130,6 @@ class NativeVideoViewer extends HookConsumerWidget { final videoSource = useMemoized>(() => createSource()); final aspectRatio = useState(null); - useMemoized(() async { if (!context.mounted || aspectRatio.value != null) { return null; @@ -320,20 +305,6 @@ class NativeVideoViewer extends HookConsumerWidget { Timer(const Duration(milliseconds: 200), checkIfBuffering); } - Size? videoContextSize(double? videoAspectRatio, BuildContext? context) { - Size? videoContextSize; - if (videoAspectRatio == null || context == null) { - return null; - } - final contextAspectRatio = context.width / context.height; - if (videoAspectRatio > contextAspectRatio) { - videoContextSize = Size(context.width, context.width / aspectRatio.value!); - } else { - videoContextSize = Size(context.height * aspectRatio.value!, context.height); - } - return videoContextSize; - } - ref.listen(currentAssetNotifier, (_, value) { final playerController = controller.value; if (playerController != null && value != asset) { @@ -414,29 +385,22 @@ class NativeVideoViewer extends HookConsumerWidget { } }); - return SizedBox( - width: context.width, - height: context.height, - child: Stack( - children: [ - // Hide thumbnail once video is visible to avoid it showing in background when zooming out on video. - if (!isVisible.value || controller.value == null) Center(child: image), - if (aspectRatio.value != null && !isCasting && isCurrent) - Visibility.maintain( - visible: isVisible.value, - child: PhotoView.customChild( - enableRotation: false, - disableScaleGestures: disableScaleGestures, - // Transparent to avoid a black flash when viewer becomes visible but video isn't loaded yet. - backgroundDecoration: const BoxDecoration(color: Colors.transparent), - scaleStateChangedCallback: (state) => scaleStateNotifier?.value = state, - childSize: videoContextSize(aspectRatio.value, context), - child: NativeVideoPlayerView(onViewReady: initController), + return Stack( + children: [ + // This remains under the video to avoid flickering + // For motion videos, this is the image portion of the asset + Center(child: image), + if (aspectRatio.value != null && !isCasting) + Visibility.maintain( + visible: isVisible.value, + child: Center( + child: AspectRatio( + aspectRatio: aspectRatio.value!, + child: isCurrent ? NativeVideoPlayerView(onViewReady: initController) : null, ), ), - if (showControls) const Center(child: VideoViewerControls()), - ], - ), + ), + ], ); } diff --git a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart index eaed60b204..7758944d37 100644 --- a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart @@ -58,7 +58,6 @@ class DriftMemoryCard extends StatelessWidget { child: NativeVideoViewer( key: ValueKey(asset.id), asset: asset, - showControls: false, playbackDelayFactor: 2, image: FullImage(asset, size: Size(context.width, context.height), fit: BoxFit.contain), ),