diff --git a/mobile/lib/pages/common/gallery_stacked_children.dart b/mobile/lib/pages/common/gallery_stacked_children.dart new file mode 100644 index 0000000000..6a0314087c --- /dev/null +++ b/mobile/lib/pages/common/gallery_stacked_children.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart'; + +class GalleryStackedChildren extends HookConsumerWidget { + final ValueNotifier stackIndex; + + const GalleryStackedChildren(this.stackIndex, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asset = ref.watch(currentAssetProvider); + if (asset == null) { + return const SizedBox(); + } + + final stackId = asset.stackId; + if (stackId == null) { + return const SizedBox(); + } + + final stackElements = ref.watch(assetStackStateProvider(stackId)); + + return SizedBox( + height: 80, + child: ListView.builder( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + itemCount: stackElements.length, + padding: const EdgeInsets.only( + left: 5, + right: 5, + bottom: 30, + ), + itemBuilder: (context, index) { + final currentAsset = stackElements.elementAt(index); + final assetId = currentAsset.remoteId; + if (assetId == null) { + return const SizedBox(); + } + + return Padding( + key: ValueKey(assetId), + padding: const EdgeInsets.only(right: 5), + child: GestureDetector( + onTap: () { + stackIndex.value = index; + ref.read(currentAssetProvider.notifier).set(currentAsset); + }, + child: Container( + width: 60, + height: 60, + decoration: index == stackIndex.value + ? const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(6)), + border: Border.fromBorderSide( + BorderSide(color: Colors.white, width: 2), + ), + ) + : const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(6)), + border: null, + ), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(4)), + child: Image( + fit: BoxFit.cover, + image: ImmichRemoteImageProvider(assetId: assetId), + ), + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index e9a0e99a8b..0ff5608da1 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -8,19 +8,18 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/scroll_extensions.dart'; import 'package:immich_mobile/pages/common/download_panel.dart'; import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; +import 'package:immich_mobile/pages/common/gallery_stacked_children.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/widgets/asset_viewer/advanced_bottom_sheet.dart'; @@ -57,16 +56,10 @@ class GalleryViewerPage extends HookConsumerWidget { final totalAssets = useState(renderList.totalAssets); final isZoomed = useState(false); final isPlayingMotionVideo = useState(false); - final stackIndex = useState(-1); + final stackIndex = useState(0); final localPosition = useRef(null); final currentIndex = useValueNotifier(initialIndex); final loadAsset = renderList.loadAsset; - final currentAsset = loadAsset(currentIndex.value); - - final stack = showStack && currentAsset.stackCount > 0 - ? ref.watch(assetStackStateProvider(currentAsset)) - : []; - final stackElements = showStack ? [currentAsset, ...stack] : []; // // Update is playing motion video ref.listen( @@ -197,58 +190,6 @@ class GalleryViewerPage extends HookConsumerWidget { } }); - Widget buildStackedChildren() { - if (!showStack) { - return const SizedBox(); - } - - return ListView.builder( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - itemCount: stackElements.length, - padding: const EdgeInsets.only( - left: 5, - right: 5, - bottom: 30, - ), - itemBuilder: (context, index) { - final assetId = stackElements.elementAt(index).remoteId; - if (assetId == null) { - return const SizedBox(); - } - return Padding( - key: ValueKey(assetId), - padding: const EdgeInsets.only(right: 5), - child: GestureDetector( - onTap: () => stackIndex.value = index, - child: Container( - width: 60, - height: 60, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(6), - border: (stackIndex.value == -1 && index == 0) || - index == stackIndex.value - ? Border.all( - color: Colors.white, - width: 2, - ) - : null, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Image( - fit: BoxFit.cover, - image: ImmichRemoteImageProvider(assetId: assetId), - ), - ), - ), - ), - ); - }, - ); - } - PhotoViewGalleryPageOptions buildImage(BuildContext context, Asset asset) { return PhotoViewGalleryPageOptions( onDragStart: (_, details, __) { @@ -262,9 +203,7 @@ class GalleryViewerPage extends HookConsumerWidget { }, onLongPressStart: asset.isMotionPhoto ? (_, __, ___) { - if (asset.isMotionPhoto) { - isPlayingMotionVideo.value = true; - } + isPlayingMotionVideo.value = true; } : null, imageProvider: ImmichImage.imageProvider(asset: asset), @@ -316,7 +255,16 @@ class GalleryViewerPage extends HookConsumerWidget { } PhotoViewGalleryPageOptions buildAsset(BuildContext context, int index) { - final newAsset = loadAsset(index); + isPlayingMotionVideo.value = false; + var newAsset = loadAsset(index); + final stackId = newAsset.stackId; + if (stackId != null && currentIndex.value == index) { + final stackElements = + ref.read(assetStackStateProvider(newAsset.stackId!)); + if (stackIndex.value < stackElements.length) { + newAsset = stackElements.elementAt(stackIndex.value); + } + } if (newAsset.isImage && !isPlayingMotionVideo.value) { return buildImage(context, newAsset); @@ -324,8 +272,6 @@ class GalleryViewerPage extends HookConsumerWidget { return buildVideo(context, newAsset); } - log.info('GalleryViewerPage: Building gallery viewer page'); - return PopScope( // Change immersive mode back to normal "edgeToEdge" mode onPopInvokedWithResult: (didPop, _) => @@ -387,7 +333,7 @@ class GalleryViewerPage extends HookConsumerWidget { final newAsset = loadAsset(value); currentIndex.value = value; - stackIndex.value = -1; + stackIndex.value = 0; isPlayingMotionVideo.value = false; ref.read(currentAssetProvider.notifier).set(newAsset); @@ -418,13 +364,7 @@ class GalleryViewerPage extends HookConsumerWidget { right: 0, child: Column( children: [ - Visibility( - visible: stack.isNotEmpty, - child: SizedBox( - height: 80, - child: buildStackedChildren(), - ), - ), + GalleryStackedChildren(stackIndex), BottomGalleryBar( key: const ValueKey('bottom-bar'), renderList: renderList, diff --git a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart index c3e4414b39..407aef1610 100644 --- a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart +++ b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart @@ -7,49 +7,49 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'asset_stack.provider.g.dart'; class AssetStackNotifier extends StateNotifier> { - final Asset _asset; + final String _stackId; final Ref _ref; - AssetStackNotifier( - this._asset, - this._ref, - ) : super([]) { - fetchStackChildren(); + AssetStackNotifier(this._stackId, this._ref) : super([]) { + _fetchStack(_stackId); } - void fetchStackChildren() async { - if (mounted) { - state = await _ref.read(assetStackProvider(_asset).future); + void _fetchStack(String stackId) async { + if (!mounted) { + return; + } + + final stack = await _ref.read(assetStackProvider(stackId).future); + if (stack.isNotEmpty) { + state = stack; } } void removeChild(int index) { if (index < state.length) { state.removeAt(index); + state = List.from(state); } } } final assetStackStateProvider = StateNotifierProvider.autoDispose - .family, Asset>( - (ref, asset) => AssetStackNotifier(asset, ref), + .family, String>( + (ref, stackId) => AssetStackNotifier(stackId, ref), ); final assetStackProvider = - FutureProvider.autoDispose.family, Asset>((ref, asset) async { - // Guard [local asset] - if (asset.remoteId == null) { - return []; - } - - return await ref + FutureProvider.autoDispose.family, String>((ref, stackId) { + return ref .watch(dbProvider) .assets .filter() .isArchivedEqualTo(false) .isTrashedEqualTo(false) - .stackPrimaryAssetIdEqualTo(asset.remoteId) - .sortByFileCreatedAtDesc() + .stackIdEqualTo(stackId) + // orders primary asset first as its ID is null + .sortByStackPrimaryAssetId() + .thenByFileCreatedAtDesc() .findAll(); }); diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart index 9199720c32..82ea013811 100644 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart @@ -52,9 +52,10 @@ class BottomGalleryBar extends ConsumerWidget { } final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId; final showControls = ref.watch(showControlsProvider); + final stackId = asset.stackId; - final stackItems = showStack && asset.stackCount > 0 - ? ref.watch(assetStackStateProvider(asset)) + final stackItems = showStack && stackId != null + ? ref.watch(assetStackStateProvider(stackId)) : []; bool isStackPrimaryAsset = asset.stackPrimaryAssetId == null; final navStack = AutoRouter.of(context).stackData; @@ -66,9 +67,9 @@ class BottomGalleryBar extends ConsumerWidget { final isInAlbum = ref.watch(currentAlbumProvider)?.isRemote ?? false; void removeAssetFromStack() { - if (stackIndex > 0 && showStack) { + if (stackIndex > 0 && showStack && stackId != null) { ref - .read(assetStackStateProvider(asset).notifier) + .read(assetStackStateProvider(stackId).notifier) .removeChild(stackIndex - 1); } } @@ -137,7 +138,7 @@ class BottomGalleryBar extends ConsumerWidget { await ref .read(stackServiceProvider) - .deleteStack(asset.stackId!, [asset, ...stackItems]); + .deleteStack(asset.stackId!, stackItems); } void showStackActionItems() {