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 23038ec30f..00aafe447a 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -113,10 +113,10 @@ class _AssetViewerState extends ConsumerState { super.dispose(); } - bool get showingBottomSheet => ref.read(assetViewerProvider.select((s) => s.showingBottomSheet)); + bool get showingBottomSheet => ref.read(assetViewerProvider).showingBottomSheet; Color get backgroundColor { - final opacity = ref.read(assetViewerProvider.select((s) => s.backgroundOpacity)); + final opacity = ref.read(assetViewerProvider).backgroundOpacity; return Colors.black.withAlpha(opacity); } @@ -223,18 +223,17 @@ class _AssetViewerState extends ConsumerState { void _onPageChanged(int index, PhotoViewControllerBase? controller) { _onAssetChanged(index); viewController = controller; - initialPhotoViewState = controller?.value ?? initialPhotoViewState; // If the bottom sheet is showing, we need to adjust scale the asset to // emulate the zoom effect if (showingBottomSheet) { initialScale = controller?.scale; - // controller?.scale = _getScaleForBottomSheet; + controller?.scale = _getScaleForBottomSheet; } } void _onDragStart( - _, + BuildContext ctx, DragStartDetails details, PhotoViewControllerBase controller, PhotoViewScaleStateController scaleStateController, @@ -250,7 +249,7 @@ class _AssetViewerState extends ConsumerState { } } - void _onDragEnd(BuildContext ctx, _, __) { + void _onDragEnd(BuildContext ctx, DragEndDetails details, PhotoViewControllerValue value) { dragInProgress = false; if (shouldPopOnDrag) { @@ -281,7 +280,7 @@ class _AssetViewerState extends ConsumerState { ref.read(assetViewerProvider.notifier).setOpacity(255); } - void _onDragUpdate(BuildContext ctx, DragUpdateDetails details, _) { + void _onDragUpdate(BuildContext ctx, DragUpdateDetails details, PhotoViewControllerValue value) { if (blockGestures) { return; } @@ -335,7 +334,7 @@ class _AssetViewerState extends ConsumerState { ref.read(assetViewerProvider.notifier).setOpacity(backgroundOpacity); } - void _onTapDown(_, __, ___) { + void _onTapDown(BuildContext ctx, TapDownDetails details, PhotoViewControllerValue value) { if (!showingBottomSheet) { ref.read(assetViewerProvider.notifier).toggleControls(); } @@ -431,7 +430,7 @@ class _AssetViewerState extends ConsumerState { void _openBottomSheet(BuildContext ctx, {double extent = _kBottomSheetMinimumExtent}) { ref.read(assetViewerProvider.notifier).setBottomSheet(true); initialScale = viewController?.scale; - // viewController?.updateMultiple(scale: _getScaleForBottomSheet); + viewController?.updateMultiple(scale: _getScaleForBottomSheet); previousExtent = _kBottomSheetMinimumExtent; sheetCloseController = showBottomSheet( context: ctx, @@ -450,8 +449,8 @@ class _AssetViewerState extends ConsumerState { } void _handleSheetClose() { - // viewController?.animateMultiple(position: Offset.zero); - // viewController?.updateMultiple(scale: initialScale); + viewController?.animateMultiple(position: Offset.zero); + viewController?.updateMultiple(scale: initialScale); ref.read(assetViewerProvider.notifier).setBottomSheet(false); sheetCloseController = null; shouldPopOnDrag = false; @@ -472,7 +471,7 @@ class _AssetViewerState extends ConsumerState { BaseAsset asset = ref.read(timelineServiceProvider).getAsset(index); final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull; if (stackChildren != null && stackChildren.isNotEmpty) { - asset = stackChildren.elementAt(ref.read(assetViewerProvider.select((s) => s.stackIndex))); + asset = stackChildren.elementAt(ref.read(assetViewerProvider).stackIndex); } return Container( width: double.infinity, @@ -488,7 +487,7 @@ class _AssetViewerState extends ConsumerState { } } - void _onLongPress(_, __, ___) { + void _onLongPress(BuildContext ctx, LongPressStartDetails details, PhotoViewControllerValue value) { ref.read(isPlayingMotionVideoProvider.notifier).playing = true; } @@ -497,7 +496,7 @@ class _AssetViewerState extends ConsumerState { BaseAsset asset = ref.read(timelineServiceProvider).getAsset(index); final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull; if (stackChildren != null && stackChildren.isNotEmpty) { - asset = stackChildren.elementAt(ref.read(assetViewerProvider.select((s) => s.stackIndex))); + asset = stackChildren.elementAt(ref.read(assetViewerProvider).stackIndex); } final isPlayingMotionVideo = ref.read(isPlayingMotionVideoProvider); @@ -512,7 +511,12 @@ class _AssetViewerState extends ConsumerState { final size = ctx.sizeData; return PhotoViewGalleryPageOptions( key: ValueKey(asset.heroTag), - imageProvider: getFullImageProvider(asset, size: size), + // When the bottom sheet is shown and the asset is changed, + // the cached image can have different position and scale than the normal one, + // causing incorrect animation calculations once the image provider yields a new image. + // This is a workaround to ensure the animation is handled correctly in this case. + // TODO: handle this without needing to disable caching + imageProvider: getFullImageProvider(asset, size: size, showCached: !showingBottomSheet), heroAttributes: PhotoViewHeroAttributes(tag: '${asset.heroTag}_$heroOffset'), filterQuality: FilterQuality.high, tightMode: true, @@ -577,9 +581,11 @@ class _AssetViewerState extends ConsumerState { Widget build(BuildContext context) { // Rebuild the widget when the asset viewer state changes // Using multiple selectors to avoid unnecessary rebuilds for other state changes - ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet)); - ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)); - ref.watch(assetViewerProvider.select((s) => s.stackIndex)); + ref.watch( + assetViewerProvider.select( + (s) => s.showingBottomSheet.hashCode ^ s.backgroundOpacity.hashCode ^ s.stackIndex.hashCode, + ), + ); ref.watch(isPlayingMotionVideoProvider); // Listen for casting changes and send initial asset to the cast provider diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index 19eed71d44..2b1a1ff61f 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -6,12 +6,18 @@ import 'package:immich_mobile/presentation/widgets/images/local_image_provider.d import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; -ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920)}) { +ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920), bool showCached = true}) { // Create new provider and cache it final ImageProvider provider; if (_shouldUseLocalAsset(asset)) { final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!; - provider = LocalFullImageProvider(id: id, size: size, type: asset.type, updatedAt: asset.updatedAt); + provider = LocalFullImageProvider( + id: id, + size: size, + type: asset.type, + updatedAt: asset.updatedAt, + showCached: showCached, + ); } else { final String assetId; if (asset is LocalAsset && asset.hasRemote) { @@ -21,7 +27,7 @@ ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080 } else { throw ArgumentError("Unsupported asset type: ${asset.runtimeType}"); } - provider = RemoteFullImageProvider(assetId: assetId); + provider = RemoteFullImageProvider(assetId: assetId, showCached: showCached); } return provider; diff --git a/mobile/lib/presentation/widgets/images/local_image_provider.dart b/mobile/lib/presentation/widgets/images/local_image_provider.dart index 4da4b927f1..ca38387a7c 100644 --- a/mobile/lib/presentation/widgets/images/local_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/local_image_provider.dart @@ -95,8 +95,15 @@ class LocalFullImageProvider extends ImageProvider { final Size size; final AssetType type; final DateTime updatedAt; // temporary, only exists to fetch cached thumbnail until local disk cache is removed + final bool showCached; - const LocalFullImageProvider({required this.id, required this.size, required this.type, required this.updatedAt}); + const LocalFullImageProvider({ + required this.id, + required this.size, + required this.type, + required this.updatedAt, + this.showCached = true, + }); @override Future obtainKey(ImageConfiguration configuration) { @@ -107,7 +114,7 @@ class LocalFullImageProvider extends ImageProvider { ImageStreamCompleter loadImage(LocalFullImageProvider key, ImageDecoderCallback decode) { return OneFramePlaceholderImageStreamCompleter( _codec(key, decode), - initialImage: getCachedImage(LocalThumbProvider(id: key.id, updatedAt: key.updatedAt)), + initialImage: showCached ? getCachedImage(LocalThumbProvider(id: key.id, updatedAt: key.updatedAt)) : null, informationCollector: () => [ DiagnosticsProperty('Id', key.id), DiagnosticsProperty('Updated at', key.updatedAt), diff --git a/mobile/lib/presentation/widgets/images/remote_image_provider.dart b/mobile/lib/presentation/widgets/images/remote_image_provider.dart index 71c5ad446e..6ba1b9e3f6 100644 --- a/mobile/lib/presentation/widgets/images/remote_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/remote_image_provider.dart @@ -71,9 +71,10 @@ class RemoteThumbProvider extends ImageProvider { class RemoteFullImageProvider extends ImageProvider { final String assetId; + final bool showCached; final CacheManager? cacheManager; - const RemoteFullImageProvider({required this.assetId, this.cacheManager}); + const RemoteFullImageProvider({required this.assetId, this.cacheManager, this.showCached = true}); @override Future obtainKey(ImageConfiguration configuration) { @@ -85,7 +86,7 @@ class RemoteFullImageProvider extends ImageProvider { final cache = cacheManager ?? RemoteImageCacheManager(); return OneFramePlaceholderImageStreamCompleter( _codec(key, cache, decode), - initialImage: getCachedImage(RemoteThumbProvider(assetId: key.assetId)), + initialImage: showCached ? getCachedImage(RemoteThumbProvider(assetId: key.assetId)) : null, ); }