diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart index 6df20fe0c8..1e3e37d668 100644 --- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart +++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart @@ -37,12 +37,14 @@ class GalleryViewerPage extends HookConsumerWidget { final Asset Function(int index) loadAsset; final int totalAssets; final int initialIndex; + final int heroOffset; GalleryViewerPage({ super.key, required this.initialIndex, required this.loadAsset, required this.totalAssets, + this.heroOffset = 0, }) : controller = PageController(initialPage: initialIndex); final PageController controller; @@ -589,7 +591,7 @@ class GalleryViewerPage extends HookConsumerWidget { }, imageProvider: provider, heroAttributes: PhotoViewHeroAttributes( - tag: asset.id, + tag: asset.id + heroOffset, ), filterQuality: FilterQuality.high, tightMode: true, @@ -606,7 +608,7 @@ class GalleryViewerPage extends HookConsumerWidget { onDragUpdate: (_, details, __) => handleSwipeUpDown(details), heroAttributes: PhotoViewHeroAttributes( - tag: asset.id, + tag: asset.id + heroOffset, ), filterQuality: FilterQuality.high, maxScale: 1.0, diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart index 21a33b51c6..891bde1004 100644 --- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart +++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:auto_route/auto_route.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -52,84 +53,61 @@ class ImmichAssetGrid extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { var settings = ref.watch(appSettingsServiceProvider); - // Needs to suppress hero animations when navigating to this widget - final enableHeroAnimations = useState(false); - final transitionDuration = ModalRoute.of(context)?.transitionDuration; - final perRow = useState( assetsPerRow ?? settings.getSetting(AppSettingsEnum.tilesPerRow)!, ); final scaleFactor = useState(7.0 - perRow.value); final baseScaleFactor = useState(7.0 - perRow.value); - useEffect( - () { - // Wait for transition to complete, then re-enable - if (transitionDuration == null) { - // No route transition found, maybe we opened this up first - enableHeroAnimations.value = true; - } else { - // Unfortunately, using the transition animation itself didn't - // seem to work reliably. So instead, wait until the duration of the - // animation has elapsed to re-enable the hero animations - Future.delayed(transitionDuration).then((_) { - enableHeroAnimations.value = true; - }); - } - return null; - }, - [], - ); - - Future onWillPop() async { - enableHeroAnimations.value = false; - return true; + /// assets need different hero tags across tabs / modals + /// otherwise, hero animations are performed across tabs (looks buggy!) + int heroOffset() { + const int range = 1152921504606846976; // 2^60 + final tabScope = TabsRouterScope.of(context); + if (tabScope != null) { + final int tabIndex = tabScope.controller.activeIndex; + return tabIndex * range; + } + return range * 7; } Widget buildAssetGridView(RenderList renderList) { - return WillPopScope( - onWillPop: onWillPop, - child: HeroMode( - enabled: enableHeroAnimations.value, - child: RawGestureDetector( - gestures: { - CustomScaleGestureRecognizer: - GestureRecognizerFactoryWithHandlers< - CustomScaleGestureRecognizer>( - () => CustomScaleGestureRecognizer(), - (CustomScaleGestureRecognizer scale) { - scale.onStart = (details) { - baseScaleFactor.value = scaleFactor.value; - }; + return RawGestureDetector( + gestures: { + CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers< + CustomScaleGestureRecognizer>( + () => CustomScaleGestureRecognizer(), + (CustomScaleGestureRecognizer scale) { + scale.onStart = (details) { + baseScaleFactor.value = scaleFactor.value; + }; - scale.onUpdate = (details) { - scaleFactor.value = - max(min(5.0, baseScaleFactor.value * details.scale), 1.0); - if (7 - scaleFactor.value.toInt() != perRow.value) { - perRow.value = 7 - scaleFactor.value.toInt(); - } - }; - scale.onEnd = (details) {}; - }) - }, - child: ImmichAssetGridView( - onRefresh: onRefresh, - assetsPerRow: perRow.value, - listener: listener, - showStorageIndicator: showStorageIndicator ?? - settings.getSetting(AppSettingsEnum.storageIndicator), - renderList: renderList, - margin: margin, - selectionActive: selectionActive, - preselectedAssets: preselectedAssets, - canDeselect: canDeselect, - dynamicLayout: dynamicLayout ?? - settings.getSetting(AppSettingsEnum.dynamicLayout), - showMultiSelectIndicator: showMultiSelectIndicator, - visibleItemsListener: visibleItemsListener, - topWidget: topWidget, - ), - ), + scale.onUpdate = (details) { + scaleFactor.value = + max(min(5.0, baseScaleFactor.value * details.scale), 1.0); + if (7 - scaleFactor.value.toInt() != perRow.value) { + perRow.value = 7 - scaleFactor.value.toInt(); + } + }; + }) + }, + child: ImmichAssetGridView( + onRefresh: onRefresh, + assetsPerRow: perRow.value, + listener: listener, + showStorageIndicator: showStorageIndicator ?? + settings.getSetting(AppSettingsEnum.storageIndicator), + renderList: renderList, + margin: margin, + selectionActive: selectionActive, + preselectedAssets: preselectedAssets, + canDeselect: canDeselect, + dynamicLayout: dynamicLayout ?? + settings.getSetting(AppSettingsEnum.dynamicLayout), + showMultiSelectIndicator: showMultiSelectIndicator, + visibleItemsListener: visibleItemsListener, + topWidget: topWidget, + heroOffset: heroOffset(), ), ); } diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart index fb7c9ddc0f..ec16762061 100644 --- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart +++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart @@ -34,6 +34,7 @@ class ImmichAssetGridView extends StatefulWidget { final void Function(ItemPosition start, ItemPosition end)? visibleItemsListener; final Widget? topWidget; + final int heroOffset; const ImmichAssetGridView({ super.key, @@ -50,6 +51,7 @@ class ImmichAssetGridView extends StatefulWidget { this.showMultiSelectIndicator = true, this.visibleItemsListener, this.topWidget, + this.heroOffset = 0, }); @override @@ -122,6 +124,7 @@ class ImmichAssetGridViewState extends State { : null, useGrayBoxPlaceholder: true, showStorageIndicator: widget.showStorageIndicator, + heroOffset: widget.heroOffset, ); } diff --git a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart index 1373df7d3c..39f1cf4ac3 100644 --- a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart +++ b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart @@ -18,6 +18,7 @@ class ThumbnailImage extends HookConsumerWidget { final bool multiselectEnabled; final Function? onSelect; final Function? onDeselect; + final int heroOffset; const ThumbnailImage({ Key? key, @@ -31,6 +32,7 @@ class ThumbnailImage extends HookConsumerWidget { this.multiselectEnabled = false, this.onDeselect, this.onSelect, + this.heroOffset = 0, }) : super(key: key); @override @@ -63,6 +65,7 @@ class ThumbnailImage extends HookConsumerWidget { initialIndex: index, loadAsset: loadAsset, totalAssets: totalAssets, + heroOffset: heroOffset, ), ); } @@ -72,32 +75,7 @@ class ThumbnailImage extends HookConsumerWidget { HapticFeedback.heavyImpact(); }, child: Hero( - createRectTween: (begin, end) { - double? top; - // Uses the [BoxFit.contain] algorithm - if (asset.width != null && asset.height != null) { - final assetAR = asset.width! / asset.height!; - final w = MediaQuery.of(context).size.width; - final deviceAR = MediaQuery.of(context).size.aspectRatio; - if (deviceAR < assetAR) { - top = asset.height! * w / asset.width!; - } else { - top = 0; - } - // get the height offset - } - - return MaterialRectCenterArcTween( - begin: Rect.fromLTRB( - 0, - top ?? 0.0, - MediaQuery.of(context).size.width, - MediaQuery.of(context).size.height, - ), - end: end, - ); - }, - tag: asset.id, + tag: asset.id + heroOffset, child: Stack( children: [ Container( diff --git a/mobile/lib/modules/memories/ui/memory_lane.dart b/mobile/lib/modules/memories/ui/memory_lane.dart index f3405aaa25..9021546072 100644 --- a/mobile/lib/modules/memories/ui/memory_lane.dart +++ b/mobile/lib/modules/memories/ui/memory_lane.dart @@ -31,7 +31,7 @@ class MemoryLane extends HookConsumerWidget { onTap: () { HapticFeedback.heavyImpact(); AutoRouter.of(context).push( - VerticalRouteView( + MemoryRoute( memories: memories, memoryIndex: index, ), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 3ab78b69d9..c25242258d 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -70,6 +70,7 @@ class _$AppRouter extends RootStackRouter { initialIndex: args.initialIndex, loadAsset: args.loadAsset, totalAssets: args.totalAssets, + heroOffset: args.heroOffset, ), ); }, @@ -290,8 +291,8 @@ class _$AppRouter extends RootStackRouter { child: const AllPeoplePage(), ); }, - VerticalRouteView.name: (routeData) { - final args = routeData.argsAs(); + MemoryRoute.name: (routeData) { + final args = routeData.argsAs(); return MaterialPageX( routeData: routeData, child: MemoryPage( @@ -506,7 +507,7 @@ class _$AppRouter extends RootStackRouter { ), RouteConfig( AlbumViewerRoute.name, - path: '/album-viewer-page', + path: '/', guards: [ authGuard, duplicateGuard, @@ -601,8 +602,8 @@ class _$AppRouter extends RootStackRouter { ], ), RouteConfig( - VerticalRouteView.name, - path: '/vertical-page-view', + MemoryRoute.name, + path: '/memory-page', guards: [ authGuard, duplicateGuard, @@ -680,6 +681,7 @@ class GalleryViewerRoute extends PageRouteInfo { required int initialIndex, required Asset Function(int) loadAsset, required int totalAssets, + int heroOffset = 0, }) : super( GalleryViewerRoute.name, path: '/gallery-viewer-page', @@ -688,6 +690,7 @@ class GalleryViewerRoute extends PageRouteInfo { initialIndex: initialIndex, loadAsset: loadAsset, totalAssets: totalAssets, + heroOffset: heroOffset, ), ); @@ -700,6 +703,7 @@ class GalleryViewerRouteArgs { required this.initialIndex, required this.loadAsset, required this.totalAssets, + this.heroOffset = 0, }); final Key? key; @@ -710,9 +714,11 @@ class GalleryViewerRouteArgs { final int totalAssets; + final int heroOffset; + @override String toString() { - return 'GalleryViewerRouteArgs{key: $key, initialIndex: $initialIndex, loadAsset: $loadAsset, totalAssets: $totalAssets}'; + return 'GalleryViewerRouteArgs{key: $key, initialIndex: $initialIndex, loadAsset: $loadAsset, totalAssets: $totalAssets, heroOffset: $heroOffset}'; } } @@ -1014,7 +1020,7 @@ class AlbumViewerRoute extends PageRouteInfo { required int albumId, }) : super( AlbumViewerRoute.name, - path: '/album-viewer-page', + path: '/', args: AlbumViewerRouteArgs( key: key, albumId: albumId, @@ -1302,26 +1308,26 @@ class AllPeopleRoute extends PageRouteInfo { /// generated route for /// [MemoryPage] -class VerticalRouteView extends PageRouteInfo { - VerticalRouteView({ +class MemoryRoute extends PageRouteInfo { + MemoryRoute({ required List memories, required int memoryIndex, Key? key, }) : super( - VerticalRouteView.name, - path: '/vertical-page-view', - args: VerticalRouteViewArgs( + MemoryRoute.name, + path: '/memory-page', + args: MemoryRouteArgs( memories: memories, memoryIndex: memoryIndex, key: key, ), ); - static const String name = 'VerticalRouteView'; + static const String name = 'MemoryRoute'; } -class VerticalRouteViewArgs { - const VerticalRouteViewArgs({ +class MemoryRouteArgs { + const MemoryRouteArgs({ required this.memories, required this.memoryIndex, this.key, @@ -1335,7 +1341,7 @@ class VerticalRouteViewArgs { @override String toString() { - return 'VerticalRouteViewArgs{memories: $memories, memoryIndex: $memoryIndex, key: $key}'; + return 'MemoryRouteArgs{memories: $memories, memoryIndex: $memoryIndex, key: $key}'; } }