From ad29abbaa3a4c72c3fb4d3d8b1395e628521adfb Mon Sep 17 00:00:00 2001 From: goalie2002 Date: Fri, 12 Sep 2025 09:59:22 -0700 Subject: [PATCH 01/10] WIP --- .../asset_viewer/video_viewer.widget.dart | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) 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 fa7f204596..b7d600ce43 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -385,6 +385,10 @@ class NativeVideoViewer extends HookConsumerWidget { } }); + void logFunction(String message) { + log.info(message); + } + return Stack( children: [ // This remains under the video to avoid flickering @@ -399,11 +403,23 @@ class NativeVideoViewer extends HookConsumerWidget { child: AspectRatio( key: ValueKey(asset), aspectRatio: aspectRatio.value!, - child: isCurrent ? NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController) : null, + child: isCurrent + ? GestureDetector( + onScaleUpdate: (_) => logFunction("Scale update 1"), + onScaleStart: (_) => logFunction("Scale start 1"), + onTap: () => logFunction("Single Tap 1"), + child: NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController), + ) + : null, ), ), ), - if (showControls) const Center(child: VideoViewerControls()), + // if (showControls) const Center(child: VideoViewerControls()), + GestureDetector( + // onTap: () => logFunction("Single Tap"), + onScaleUpdate: (_) => logFunction("Scale update"), + onScaleStart: (_) => logFunction("Scale start"), + ), ], ); } From 814579f731bfcbbeeb3f22a1e959bdae8a39d91d Mon Sep 17 00:00:00 2001 From: goalie2002 Date: Fri, 12 Sep 2025 09:59:22 -0700 Subject: [PATCH 02/10] WIP --- .../asset_viewer/video_viewer.widget.dart | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) 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 fa7f204596..b7d600ce43 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -385,6 +385,10 @@ class NativeVideoViewer extends HookConsumerWidget { } }); + void logFunction(String message) { + log.info(message); + } + return Stack( children: [ // This remains under the video to avoid flickering @@ -399,11 +403,23 @@ class NativeVideoViewer extends HookConsumerWidget { child: AspectRatio( key: ValueKey(asset), aspectRatio: aspectRatio.value!, - child: isCurrent ? NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController) : null, + child: isCurrent + ? GestureDetector( + onScaleUpdate: (_) => logFunction("Scale update 1"), + onScaleStart: (_) => logFunction("Scale start 1"), + onTap: () => logFunction("Single Tap 1"), + child: NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController), + ) + : null, ), ), ), - if (showControls) const Center(child: VideoViewerControls()), + // if (showControls) const Center(child: VideoViewerControls()), + GestureDetector( + // onTap: () => logFunction("Single Tap"), + onScaleUpdate: (_) => logFunction("Scale update"), + onScaleStart: (_) => logFunction("Scale start"), + ), ], ); } From 37c60417aeb2c660a8d00405021fa015def57d9c Mon Sep 17 00:00:00 2001 From: goalie2002 Date: Fri, 12 Sep 2025 22:40:34 -0700 Subject: [PATCH 03/10] wip --- .../asset_viewer/asset_viewer.page.dart | 13 +--- .../asset_viewer/video_viewer.widget.dart | 75 ++++++++++++------- 2 files changed, 50 insertions(+), 38 deletions(-) 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 899b6ed545..433f878a9a 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -561,14 +561,16 @@ class _AssetViewerState extends ConsumerState { } PhotoViewGalleryPageOptions _videoBuilder(BuildContext ctx, BaseAsset asset) { + final image = getFullImageProvider(asset, size: ctx.sizeData); return PhotoViewGalleryPageOptions.customChild( onDragStart: _onDragStart, onDragUpdate: _onDragUpdate, onDragEnd: _onDragEnd, onTapDown: _onTapDown, + disableScaleGestures: false, heroAttributes: PhotoViewHeroAttributes(tag: '${asset.heroTag}_$heroOffset'), filterQuality: FilterQuality.high, - maxScale: 1.0, + // maxScale: 1.0, basePosition: Alignment.center, child: SizedBox( width: ctx.width, @@ -576,14 +578,7 @@ class _AssetViewerState extends ConsumerState { child: NativeVideoViewer( key: _getVideoPlayerKey(asset.heroTag), asset: asset, - image: Image( - key: ValueKey(asset), - image: getFullImageProvider(asset, size: ctx.sizeData), - fit: BoxFit.contain, - height: ctx.height, - width: ctx.width, - alignment: Alignment.center, - ), + image: Image(key: ValueKey(asset), image: image, fit: BoxFit.contain, alignment: Alignment.center), ), ), ); 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 b7d600ce43..6f0dff8750 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/extensions/build_context_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'; @@ -124,6 +125,8 @@ class NativeVideoViewer extends HookConsumerWidget { final videoSource = useMemoized>(() => createSource()); final aspectRatio = useState(null); + final videoWidth = useState(null); + final videoHeight = useState(null); useMemoized(() async { if (!context.mounted || aspectRatio.value != null) { return null; @@ -385,41 +388,55 @@ class NativeVideoViewer extends HookConsumerWidget { } }); - void logFunction(String message) { - log.info(message); + final contextAspectRatio = context.width / context.height; + double renderedWidth = 0.0; + double renderedHeight = 0.0; + + if (aspectRatio.value != null && (aspectRatio.value! > contextAspectRatio)) { + renderedWidth = context.width; + renderedHeight = context.width / aspectRatio.value!; + } else if (aspectRatio.value != null) { + renderedHeight = context.height; + renderedWidth = context.height * aspectRatio.value!; + } else { + renderedHeight = 0; + renderedWidth = 0; } + final horizontalMargin = (context.width - renderedWidth) / 2; + final verticalMargin = (context.height - renderedHeight) / 2; + + log.info("Margins: h: $horizontalMargin, v: $verticalMargin"); + return Stack( children: [ - // This remains under the video to avoid flickering - // For motion videos, this is the image portion of the asset - Center(key: ValueKey(asset.heroTag), child: image), - if (aspectRatio.value != null && !isCasting) - Visibility.maintain( - key: ValueKey(asset), - visible: isVisible.value, - child: Center( - key: ValueKey(asset), - child: AspectRatio( - key: ValueKey(asset), - aspectRatio: aspectRatio.value!, - child: isCurrent - ? GestureDetector( - onScaleUpdate: (_) => logFunction("Scale update 1"), - onScaleStart: (_) => logFunction("Scale start 1"), - onTap: () => logFunction("Single Tap 1"), - child: NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController), - ) - : null, - ), - ), + InteractiveViewer( + panEnabled: true, + scaleEnabled: true, + minScale: 0.8, + maxScale: 4.0, + boundaryMargin: EdgeInsets.fromLTRB(-horizontalMargin, -verticalMargin, -horizontalMargin, -verticalMargin), + child: Stack( + children: [ + if (aspectRatio.value != null && !isCasting) + Visibility.maintain( + visible: isVisible.value, + child: SizedBox.expand( + child: Center( + child: AspectRatio( + key: ValueKey(asset), + aspectRatio: aspectRatio.value!, + child: isCurrent + ? NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController) + : null, + ), + ), + ), + ), + ], ), - // if (showControls) const Center(child: VideoViewerControls()), - GestureDetector( - // onTap: () => logFunction("Single Tap"), - onScaleUpdate: (_) => logFunction("Scale update"), - onScaleStart: (_) => logFunction("Scale start"), ), + // if (showControls) const Center(child: VideoViewerControls()), ], ); } From 9cdc864e20cb6874ee8a3b70f13d81f523ca65aa Mon Sep 17 00:00:00 2001 From: goalie2002 Date: Sat, 13 Sep 2025 20:12:18 -0700 Subject: [PATCH 04/10] Functional implementation, still need to bug test. --- .../asset_viewer/asset_viewer.page.dart | 24 ++++--- .../asset_viewer/video_viewer.widget.dart | 64 ++++++++----------- .../video_viewer_controls.widget.dart | 7 +- .../asset_viewer/center_play_button.dart | 31 ++++----- 4 files changed, 59 insertions(+), 67 deletions(-) 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 433f878a9a..8a90370f35 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -561,24 +561,28 @@ class _AssetViewerState extends ConsumerState { } PhotoViewGalleryPageOptions _videoBuilder(BuildContext ctx, BaseAsset asset) { - final image = getFullImageProvider(asset, size: ctx.sizeData); return PhotoViewGalleryPageOptions.customChild( onDragStart: _onDragStart, onDragUpdate: _onDragUpdate, onDragEnd: _onDragEnd, onTapDown: _onTapDown, - disableScaleGestures: false, + disableScaleGestures: true, heroAttributes: PhotoViewHeroAttributes(tag: '${asset.heroTag}_$heroOffset'), filterQuality: FilterQuality.high, - // maxScale: 1.0, basePosition: Alignment.center, - child: SizedBox( - width: ctx.width, - height: ctx.height, - child: NativeVideoViewer( - key: _getVideoPlayerKey(asset.heroTag), - asset: asset, - image: Image(key: ValueKey(asset), image: image, fit: BoxFit.contain, alignment: Alignment.center), + minScale: PhotoViewComputedScale.contained, + initialScale: PhotoViewComputedScale.contained, + tightMode: true, + child: NativeVideoViewer( + key: _getVideoPlayerKey(asset.heroTag), + asset: asset, + image: Image( + key: ValueKey(asset), + image: getFullImageProvider(asset, size: ctx.sizeData), + height: ctx.height, + width: ctx.width, + fit: BoxFit.contain, + alignment: Alignment.center, ), ), ); 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 6f0dff8750..ed8ec2a516 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -27,6 +27,7 @@ import 'package:immich_mobile/utils/hooks/interval_hook.dart'; import 'package:logging/logging.dart'; import 'package:native_video_player/native_video_player.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; +import 'package:immich_mobile/widgets/photo_view/photo_view.dart'; bool _isCurrentAsset(BaseAsset asset, BaseAsset? currentAsset) { if (asset is RemoteAsset) { @@ -398,46 +399,35 @@ class NativeVideoViewer extends HookConsumerWidget { } else if (aspectRatio.value != null) { renderedHeight = context.height; renderedWidth = context.height * aspectRatio.value!; - } else { - renderedHeight = 0; - renderedWidth = 0; } - final horizontalMargin = (context.width - renderedWidth) / 2; - final verticalMargin = (context.height - renderedHeight) / 2; - - log.info("Margins: h: $horizontalMargin, v: $verticalMargin"); - - return Stack( - children: [ - InteractiveViewer( - panEnabled: true, - scaleEnabled: true, - minScale: 0.8, - maxScale: 4.0, - boundaryMargin: EdgeInsets.fromLTRB(-horizontalMargin, -verticalMargin, -horizontalMargin, -verticalMargin), - child: Stack( - children: [ - if (aspectRatio.value != null && !isCasting) - Visibility.maintain( - visible: isVisible.value, - child: SizedBox.expand( - child: Center( - child: AspectRatio( - key: ValueKey(asset), - aspectRatio: aspectRatio.value!, - child: isCurrent - ? NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController) - : null, - ), - ), - ), + log.info("Rendered: h: $renderedHeight, w: $renderedWidth"); + log.info("showControls: $showControls, isCurrent: $isCurrent, isVisible: ${isVisible.value}"); + + return SizedBox( + width: context.width, + height: context.height, + child: Stack( + children: [ + Center(key: ValueKey(asset.heroTag), child: image), + if (aspectRatio.value != null && !isCasting) + Visibility.maintain( + key: ValueKey(asset), + visible: isVisible.value, + child: PhotoView.customChild( + key: ValueKey(asset), + enableRotation: false, + childSize: Size(renderedWidth, renderedHeight), + child: AspectRatio( + key: ValueKey(asset), + aspectRatio: aspectRatio.value!, + child: isCurrent ? NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController) : null, ), - ], - ), - ), - // if (showControls) const Center(child: VideoViewerControls()), - ], + ), + ), + if (showControls) const Center(child: VideoViewerControls()), + ], + ), ); } } diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart index c1324b8ac0..3da73235eb 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart @@ -82,16 +82,17 @@ class VideoViewerControls extends HookConsumerWidget { } return GestureDetector( - behavior: HitTestBehavior.opaque, + behavior: HitTestBehavior.translucent, onTap: showControlsAndStartHideTimer, - child: AbsorbPointer( - absorbing: !showControls, + child: IgnorePointer( + ignoring: !showControls, child: Stack( children: [ if (showBuffering) const Center(child: DelayedLoadingIndicator(fadeInDuration: Duration(milliseconds: 400))) else GestureDetector( + behavior: HitTestBehavior.translucent, onTap: () => ref.read(assetViewerProvider.notifier).setControls(false), child: CenterPlayButton( backgroundColor: Colors.black54, diff --git a/mobile/lib/widgets/asset_viewer/center_play_button.dart b/mobile/lib/widgets/asset_viewer/center_play_button.dart index 26d0a41129..55d8be8095 100644 --- a/mobile/lib/widgets/asset_viewer/center_play_button.dart +++ b/mobile/lib/widgets/asset_viewer/center_play_button.dart @@ -21,23 +21,20 @@ class CenterPlayButton extends StatelessWidget { @override Widget build(BuildContext context) { - return ColoredBox( - color: Colors.transparent, - child: Center( - child: UnconstrainedBox( - child: AnimatedOpacity( - opacity: show ? 1.0 : 0.0, - duration: const Duration(milliseconds: 100), - child: DecoratedBox( - decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle), - child: IconButton( - iconSize: 32, - padding: const EdgeInsets.all(12.0), - icon: isFinished - ? Icon(Icons.replay, color: iconColor) - : AnimatedPlayPause(color: iconColor, playing: isPlaying), - onPressed: onPressed, - ), + return Center( + child: UnconstrainedBox( + child: AnimatedOpacity( + opacity: show ? 1.0 : 0.0, + duration: const Duration(milliseconds: 100), + child: DecoratedBox( + decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle), + child: IconButton( + iconSize: 32, + padding: const EdgeInsets.all(12.0), + icon: isFinished + ? Icon(Icons.replay, color: iconColor) + : AnimatedPlayPause(color: iconColor, playing: isPlaying), + onPressed: onPressed, ), ), ), From 84c898ce160666166f96254992a27aff5e012ea3 Mon Sep 17 00:00:00 2001 From: goalie2002 Date: Sat, 13 Sep 2025 20:51:48 -0700 Subject: [PATCH 05/10] Fixed flickering bugs --- .../widgets/asset_viewer/video_viewer.widget.dart | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) 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 ed8ec2a516..d788ebbb16 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -24,10 +24,10 @@ 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'; -import 'package:immich_mobile/widgets/photo_view/photo_view.dart'; bool _isCurrentAsset(BaseAsset asset, BaseAsset? currentAsset) { if (asset is RemoteAsset) { @@ -126,8 +126,7 @@ class NativeVideoViewer extends HookConsumerWidget { final videoSource = useMemoized>(() => createSource()); final aspectRatio = useState(null); - final videoWidth = useState(null); - final videoHeight = useState(null); + useMemoized(() async { if (!context.mounted || aspectRatio.value != null) { return null; @@ -401,15 +400,12 @@ class NativeVideoViewer extends HookConsumerWidget { renderedWidth = context.height * aspectRatio.value!; } - log.info("Rendered: h: $renderedHeight, w: $renderedWidth"); - log.info("showControls: $showControls, isCurrent: $isCurrent, isVisible: ${isVisible.value}"); - return SizedBox( width: context.width, height: context.height, child: Stack( children: [ - Center(key: ValueKey(asset.heroTag), child: image), + if (!isVisible.value || controller.value == null) Center(key: ValueKey(asset.heroTag), child: image), if (aspectRatio.value != null && !isCasting) Visibility.maintain( key: ValueKey(asset), @@ -417,6 +413,7 @@ class NativeVideoViewer extends HookConsumerWidget { child: PhotoView.customChild( key: ValueKey(asset), enableRotation: false, + backgroundDecoration: const BoxDecoration(color: Colors.transparent), childSize: Size(renderedWidth, renderedHeight), child: AspectRatio( key: ValueKey(asset), From a1f624e07cdaf7e52dea5376045bca87c02a8d9b Mon Sep 17 00:00:00 2001 From: goalie2002 Date: Mon, 15 Sep 2025 12:30:00 -0700 Subject: [PATCH 06/10] Fixed bug with drag actions interfering with zoom panning. Fixed video being zoomable when bottom sheet is shown. Code cleanup. --- .../asset_viewer/asset_viewer.page.dart | 13 +++++++--- .../asset_viewer/video_viewer.widget.dart | 26 ++++++++++++------- 2 files changed, 26 insertions(+), 13 deletions(-) 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 8a90370f35..e61254ca1e 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -85,6 +85,7 @@ class _AssetViewerState extends ConsumerState { // PhotoViewGallery takes care of disposing it's controllers PhotoViewControllerBase? viewController; StreamSubscription? reloadSubscription; + final ValueNotifier scaleStateNotifier = ValueNotifier(PhotoViewScaleState.initial); late Platform platform; late final int heroOffset; @@ -131,6 +132,7 @@ class _AssetViewerState extends ConsumerState { _prevPreCacheStream?.removeListener(_dummyListener); _nextPreCacheStream?.removeListener(_dummyListener); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + scaleStateNotifier.dispose(); super.dispose(); } @@ -243,6 +245,7 @@ class _AssetViewerState extends ConsumerState { void _onPageChanged(int index, PhotoViewControllerBase? controller) { _onAssetChanged(index); viewController = controller; + scaleStateNotifier.value = PhotoViewScaleState.initial; // reset video zoom state } void _onDragStart( @@ -254,9 +257,13 @@ class _AssetViewerState extends ConsumerState { viewController = controller; dragDownPosition = details.localPosition; initialPhotoViewState = controller.value; + final isZoomed = scaleStateController.scaleState == PhotoViewScaleState.zoomedIn || - scaleStateController.scaleState == PhotoViewScaleState.covering; + scaleStateController.scaleState == PhotoViewScaleState.covering || + scaleStateNotifier.value == PhotoViewScaleState.zoomedIn || + scaleStateNotifier.value == PhotoViewScaleState.covering; + if (!showingBottomSheet && isZoomed) { blockGestures = true; } @@ -570,12 +577,12 @@ class _AssetViewerState extends ConsumerState { heroAttributes: PhotoViewHeroAttributes(tag: '${asset.heroTag}_$heroOffset'), filterQuality: FilterQuality.high, basePosition: Alignment.center, - minScale: PhotoViewComputedScale.contained, - initialScale: PhotoViewComputedScale.contained, tightMode: true, child: NativeVideoViewer( key: _getVideoPlayerKey(asset.heroTag), asset: asset, + scaleStateNotifier: scaleStateNotifier, + disableScaleGestures: showingBottomSheet, image: Image( key: ValueKey(asset), image: getFullImageProvider(asset, size: ctx.sizeData), 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 d788ebbb16..14a57a15d1 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -51,6 +51,8 @@ class NativeVideoViewer extends HookConsumerWidget { final bool showControls; final int playbackDelayFactor; final Widget image; + final ValueNotifier? scaleStateNotifier; + final bool disableScaleGestures; const NativeVideoViewer({ super.key, @@ -58,6 +60,8 @@ class NativeVideoViewer extends HookConsumerWidget { required this.image, this.showControls = true, this.playbackDelayFactor = 1, + this.scaleStateNotifier, + this.disableScaleGestures = false, }); @override @@ -388,16 +392,16 @@ class NativeVideoViewer extends HookConsumerWidget { } }); - final contextAspectRatio = context.width / context.height; - double renderedWidth = 0.0; - double renderedHeight = 0.0; + Size? videoContextSize; - if (aspectRatio.value != null && (aspectRatio.value! > contextAspectRatio)) { - renderedWidth = context.width; - renderedHeight = context.width / aspectRatio.value!; - } else if (aspectRatio.value != null) { - renderedHeight = context.height; - renderedWidth = context.height * aspectRatio.value!; + if (aspectRatio.value != null) { + final contextAspectRatio = context.width / context.height; + + if (aspectRatio.value! > contextAspectRatio) { + videoContextSize = Size(context.width, context.width / aspectRatio.value!); + } else { + videoContextSize = Size(context.height * aspectRatio.value!, context.height); + } } return SizedBox( @@ -413,8 +417,10 @@ class NativeVideoViewer extends HookConsumerWidget { child: PhotoView.customChild( key: ValueKey(asset), enableRotation: false, + disableScaleGestures: disableScaleGestures, backgroundDecoration: const BoxDecoration(color: Colors.transparent), - childSize: Size(renderedWidth, renderedHeight), + scaleStateChangedCallback: (state) => scaleStateNotifier?.value = state, + childSize: videoContextSize, child: AspectRatio( key: ValueKey(asset), aspectRatio: aspectRatio.value!, From 4acdd4d51241184c5a0a3509d0e092e4d4302eba Mon Sep 17 00:00:00 2001 From: goalie2002 Date: Mon, 15 Sep 2025 12:51:57 -0700 Subject: [PATCH 07/10] Add comments and simplify video controls --- .../asset_viewer/video_viewer.widget.dart | 2 ++ .../video_viewer_controls.widget.dart | 33 +++++++++++-------- 2 files changed, 22 insertions(+), 13 deletions(-) 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 14a57a15d1..27fd98871f 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -409,6 +409,7 @@ class NativeVideoViewer extends HookConsumerWidget { 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(key: ValueKey(asset.heroTag), child: image), if (aspectRatio.value != null && !isCasting) Visibility.maintain( @@ -418,6 +419,7 @@ class NativeVideoViewer extends HookConsumerWidget { key: ValueKey(asset), 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, diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart index 3da73235eb..fd269fbcfe 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart @@ -81,9 +81,20 @@ class VideoViewerControls extends HookConsumerWidget { } } + void toggleControlsVisibility() { + if (showBuffering) { + return; + } + if (showControls) { + ref.read(assetViewerProvider.notifier).setControls(false); + } else { + showControlsAndStartHideTimer(); + } + } + return GestureDetector( behavior: HitTestBehavior.translucent, - onTap: showControlsAndStartHideTimer, + onTap: toggleControlsVisibility, child: IgnorePointer( ignoring: !showControls, child: Stack( @@ -91,18 +102,14 @@ class VideoViewerControls extends HookConsumerWidget { if (showBuffering) const Center(child: DelayedLoadingIndicator(fadeInDuration: Duration(milliseconds: 400))) else - GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () => ref.read(assetViewerProvider.notifier).setControls(false), - child: CenterPlayButton( - backgroundColor: Colors.black54, - iconColor: Colors.white, - isFinished: state == VideoPlaybackState.completed, - isPlaying: - state == VideoPlaybackState.playing || (cast.isCasting && cast.castState == CastState.playing), - show: assetIsVideo && showControls, - onPressed: togglePlay, - ), + CenterPlayButton( + backgroundColor: Colors.black54, + iconColor: Colors.white, + isFinished: state == VideoPlaybackState.completed, + isPlaying: + state == VideoPlaybackState.playing || (cast.isCasting && cast.castState == CastState.playing), + show: assetIsVideo && showControls, + onPressed: togglePlay, ), ], ), From de28bfe36eff21219ab514caa8a87cca5a7cc7a1 Mon Sep 17 00:00:00 2001 From: goalie2002 Date: Mon, 15 Sep 2025 13:10:36 -0700 Subject: [PATCH 08/10] Clearer variable name --- .../widgets/asset_viewer/asset_viewer.page.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 635a5b30fa..f634d095e4 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -84,7 +84,7 @@ class _AssetViewerState extends ConsumerState { // PhotoViewGallery takes care of disposing it's controllers PhotoViewControllerBase? viewController; StreamSubscription? reloadSubscription; - final ValueNotifier scaleStateNotifier = ValueNotifier(PhotoViewScaleState.initial); + final ValueNotifier videoScaleStateNotifier = ValueNotifier(PhotoViewScaleState.initial); late final int heroOffset; late PhotoViewControllerValue initialPhotoViewState; @@ -129,7 +129,7 @@ class _AssetViewerState extends ConsumerState { _prevPreCacheStream?.removeListener(_dummyListener); _nextPreCacheStream?.removeListener(_dummyListener); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - scaleStateNotifier.dispose(); + videoScaleStateNotifier.dispose(); super.dispose(); } @@ -242,7 +242,7 @@ class _AssetViewerState extends ConsumerState { void _onPageChanged(int index, PhotoViewControllerBase? controller) { _onAssetChanged(index); viewController = controller; - scaleStateNotifier.value = PhotoViewScaleState.initial; // reset video zoom state + videoScaleStateNotifier.value = PhotoViewScaleState.initial; // reset video zoom state } void _onDragStart( @@ -258,8 +258,8 @@ class _AssetViewerState extends ConsumerState { final isZoomed = scaleStateController.scaleState == PhotoViewScaleState.zoomedIn || scaleStateController.scaleState == PhotoViewScaleState.covering || - scaleStateNotifier.value == PhotoViewScaleState.zoomedIn || - scaleStateNotifier.value == PhotoViewScaleState.covering; + videoScaleStateNotifier.value == PhotoViewScaleState.zoomedIn || + videoScaleStateNotifier.value == PhotoViewScaleState.covering; if (!showingBottomSheet && isZoomed) { blockGestures = true; @@ -578,7 +578,7 @@ class _AssetViewerState extends ConsumerState { child: NativeVideoViewer( key: _getVideoPlayerKey(asset.heroTag), asset: asset, - scaleStateNotifier: scaleStateNotifier, + scaleStateNotifier: videoScaleStateNotifier, disableScaleGestures: showingBottomSheet, image: Image( key: ValueKey(asset), From 36332337b00bbac8a7764fb76b5d42399db9e2ac Mon Sep 17 00:00:00 2001 From: goalie2002 Date: Mon, 15 Sep 2025 13:59:15 -0700 Subject: [PATCH 09/10] Fix bug where the redundant onTapDown would interfere with zooming gestures --- .../lib/presentation/widgets/asset_viewer/asset_viewer.page.dart | 1 - 1 file changed, 1 deletion(-) 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 f634d095e4..1d236ebd91 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -569,7 +569,6 @@ class _AssetViewerState extends ConsumerState { onDragStart: _onDragStart, onDragUpdate: _onDragUpdate, onDragEnd: _onDragEnd, - onTapDown: _onTapDown, disableScaleGestures: true, heroAttributes: PhotoViewHeroAttributes(tag: '${asset.heroTag}_$heroOffset'), filterQuality: FilterQuality.high, From b1e732623f90130517c9991429d3abd4e77e2d7b Mon Sep 17 00:00:00 2001 From: goalie2002 Date: Mon, 15 Sep 2025 16:43:04 -0700 Subject: [PATCH 10/10] Fix zoom not working the second time when viewing a video. --- .../widgets/asset_viewer/asset_viewer.page.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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 1d236ebd91..b8d837ad7e 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -559,13 +559,9 @@ class _AssetViewerState extends ConsumerState { ); } - GlobalKey _getVideoPlayerKey(String id) { - videoPlayerKeys.putIfAbsent(id, () => GlobalKey()); - return videoPlayerKeys[id]!; - } - PhotoViewGalleryPageOptions _videoBuilder(BuildContext ctx, BaseAsset asset) { return PhotoViewGalleryPageOptions.customChild( + key: ValueKey(asset.heroTag), onDragStart: _onDragStart, onDragUpdate: _onDragUpdate, onDragEnd: _onDragEnd, @@ -575,7 +571,7 @@ class _AssetViewerState extends ConsumerState { basePosition: Alignment.center, tightMode: true, child: NativeVideoViewer( - key: _getVideoPlayerKey(asset.heroTag), + key: ValueKey(asset.heroTag), asset: asset, scaleStateNotifier: videoScaleStateNotifier, disableScaleGestures: showingBottomSheet,