From 03d19bd24475d7f3131127e8fb62a57a476499b6 Mon Sep 17 00:00:00 2001 From: bwees Date: Sat, 13 Sep 2025 13:52:14 -0500 Subject: [PATCH 01/13] feat: show "appears in" albums on asset viewer bottom sheet fix: multiple RemoteAlbumPages in navigation stack this also allows us to not have to set the current album before navigating to RemoteAlbumPage chore: clarification comments handle nested album pages fix: hide "appears in" when an asset is not in any albums fix: way more bottom padding for some reason we can't query the safe area here :/ --- .../domain/services/remote_album.service.dart | 4 ++ .../repositories/remote_album.repository.dart | 12 ++++ mobile/lib/main.dart | 2 +- .../pages/drift_remote_album.page.dart | 15 +++-- .../widgets/album/album_selector.widget.dart | 41 ++---------- .../widgets/album/album_tile.dart | 51 +++++++++++++++ .../asset_viewer/bottom_sheet.widget.dart | 65 ++++++++++++++++++- .../common/remote_album_sliver_app_bar.dart | 3 +- 8 files changed, 147 insertions(+), 46 deletions(-) create mode 100644 mobile/lib/presentation/widgets/album/album_tile.dart diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart index 13dfadb8d85a3..67e91188e27ad 100644 --- a/mobile/lib/domain/services/remote_album.service.dart +++ b/mobile/lib/domain/services/remote_album.service.dart @@ -164,6 +164,10 @@ class RemoteAlbumService { return _repository.getCount(); } + Future> getAlbumsContainingAsset(String assetId) { + return _repository.getAlbumsContainingAsset(assetId); + } + Future> _sortByNewestAsset(List albums) async { // map album IDs to their newest asset dates final Map> assetTimestampFutures = {}; diff --git a/mobile/lib/infrastructure/repositories/remote_album.repository.dart b/mobile/lib/infrastructure/repositories/remote_album.repository.dart index 0526cfb7aa0de..a6f4893881d63 100644 --- a/mobile/lib/infrastructure/repositories/remote_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_album.repository.dart @@ -382,6 +382,18 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { return query.map((row) => row.read(_db.remoteAssetEntity.id)!).get(); } + + Future> getAlbumsContainingAsset(String assetId) async { + final albumIdsQuery = _db.remoteAlbumAssetEntity.select()..where((row) => row.assetId.equals(assetId)); + + final albumIds = (await albumIdsQuery.get()).map((e) => e.albumId).toSet(); + + if (albumIds.isEmpty) { + return []; + } + + return getAll().then((albums) => albums.where((album) => albumIds.contains(album.id)).toList()); + } } extension on RemoteAlbumEntityData { diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index c3804d97f6a7a..9884dde45f78c 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -254,7 +254,7 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve theme: getThemeData(colorScheme: immichTheme.light, locale: context.locale), routerConfig: router.config( deepLinkBuilder: _deepLinkBuilder, - navigatorObservers: () => [AppNavigationObserver(ref: ref)], + navigatorObservers: () => [AppNavigationObserver(ref: ref), HeroController()], ), ), ); diff --git a/mobile/lib/presentation/pages/drift_remote_album.page.dart b/mobile/lib/presentation/pages/drift_remote_album.page.dart index 2d70978ea5008..1eb5b9d7af28d 100644 --- a/mobile/lib/presentation/pages/drift_remote_album.page.dart +++ b/mobile/lib/presentation/pages/drift_remote_album.page.dart @@ -238,14 +238,15 @@ class _RemoteAlbumPageState extends ConsumerState { final isOwner = user != null ? user.id == _album.ownerId : false; return PopScope( + canPop: false, onPopInvokedWithResult: (didPop, _) { - if (didPop) { - Future.microtask(() { - if (mounted) { - ref.read(currentRemoteAlbumProvider.notifier).dispose(); - ref.read(remoteAlbumProvider.notifier).refresh(); - } - }); + if (didPop || !mounted) { + return; + } + final hasAncestor = context.findAncestorWidgetOfExactType() != null; + Navigator.of(context).pop(); + if (!hasAncestor) { + ref.read(currentRemoteAlbumProvider.notifier).dispose(); } }, child: ProviderScope( diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index cbac6c8b93956..a7d3e927ca1b5 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -12,7 +12,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; -import 'package:immich_mobile/pages/common/large_leading_tile.dart'; +import 'package:immich_mobile/presentation/widgets/album/album_tile.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; @@ -516,38 +516,6 @@ class _AlbumList extends ConsumerWidget { sliver: SliverList.builder( itemBuilder: (_, index) { final album = albums[index]; - final albumTile = LargeLeadingTile( - title: Text( - album.name, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), - ), - subtitle: Text( - '${'items_count'.t(context: context, args: {'count': album.assetCount})} • ${album.ownerId != userId ? 'shared_by_user'.t(context: context, args: {'user': album.ownerName}) : 'owned'.t(context: context)}', - overflow: TextOverflow.ellipsis, - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ), - onTap: () => onAlbumSelected(album), - leadingPadding: const EdgeInsets.only(right: 16), - leading: album.thumbnailAssetId != null - ? ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(15)), - child: SizedBox(width: 80, height: 80, child: Thumbnail.remote(remoteId: album.thumbnailAssetId!)), - ) - : SizedBox( - width: 80, - height: 80, - child: Container( - decoration: BoxDecoration( - color: context.colorScheme.surfaceContainer, - borderRadius: const BorderRadius.all(Radius.circular(16)), - border: Border.all(color: context.colorScheme.outline.withAlpha(50), width: 1), - ), - child: const Icon(Icons.photo_album_rounded, size: 24, color: Colors.grey), - ), - ), - ); final isOwner = album.ownerId == userId; if (isOwner) { @@ -576,11 +544,14 @@ class _AlbumList extends ConsumerWidget { onDismissed: (direction) async { await ref.read(remoteAlbumProvider.notifier).deleteAlbum(album.id); }, - child: albumTile, + child: AlbumTile(album: album, isOwner: isOwner, onAlbumSelected: onAlbumSelected), ), ); } else { - return Padding(padding: const EdgeInsets.only(bottom: 8.0), child: albumTile); + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: AlbumTile(album: album, isOwner: isOwner, onAlbumSelected: onAlbumSelected), + ); } }, itemCount: albums.length, diff --git a/mobile/lib/presentation/widgets/album/album_tile.dart b/mobile/lib/presentation/widgets/album/album_tile.dart new file mode 100644 index 0000000000000..561b018ef8818 --- /dev/null +++ b/mobile/lib/presentation/widgets/album/album_tile.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/pages/common/large_leading_tile.dart'; +import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; + +class AlbumTile extends StatelessWidget { + const AlbumTile({super.key, required this.album, required this.isOwner, this.onAlbumSelected}); + + final RemoteAlbum album; + final bool isOwner; + final Function(RemoteAlbum)? onAlbumSelected; + + @override + Widget build(BuildContext context) { + return LargeLeadingTile( + title: Text( + album.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), + ), + subtitle: Text( + '${'items_count'.t(context: context, args: {'count': album.assetCount})} • ${isOwner ? 'owned'.t(context: context) : 'shared_by_user'.t(context: context, args: {'user': album.ownerName})}', + overflow: TextOverflow.ellipsis, + style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), + ), + onTap: () => onAlbumSelected?.call(album), + leadingPadding: const EdgeInsets.only(right: 16), + leading: album.thumbnailAssetId != null + ? ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(15)), + child: SizedBox(width: 80, height: 80, child: Thumbnail.remote(remoteId: album.thumbnailAssetId!)), + ) + : SizedBox( + width: 80, + height: 80, + child: Container( + decoration: BoxDecoration( + color: context.colorScheme.surfaceContainer, + borderRadius: const BorderRadius.all(Radius.circular(16)), + border: Border.all(color: context.colorScheme.outline.withAlpha(50), width: 1), + ), + child: const Icon(Icons.photo_album_rounded, size: 24, color: Colors.grey), + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart index c4fbd2cfe35a6..6305c544d1c31 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -1,3 +1,5 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -8,11 +10,13 @@ import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/album/album_tile.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; @@ -20,6 +24,7 @@ import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart'; +import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/action_button.utils.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -132,6 +137,61 @@ class _AssetDetailBottomSheet extends ConsumerWidget { await ref.read(actionProvider.notifier).editDateTime(ActionSource.viewer, context); } + Widget _buildAppearsInList(WidgetRef ref, BuildContext context) { + final isRemote = ref.watch(currentAssetNotifier)?.hasRemote ?? false; + if (!isRemote) { + return const SizedBox.shrink(); + } + + final remoteAsset = ref.watch(currentAssetNotifier) as RemoteAsset; + final albums = ref.watch(remoteAlbumServiceProvider).getAlbumsContainingAsset(remoteAsset.id); + final userId = ref.watch(currentUserProvider)?.id; + + return FutureBuilder( + future: albums, + builder: (_, snap) { + final albums = snap.data ?? []; + if (albums.isEmpty) { + return const SizedBox.shrink(); + } + + albums.sortBy((a) => a.name); + + return Padding( + padding: const EdgeInsets.only(left: 16, right: 16, top: 8), + child: Column( + spacing: 12, + children: [ + if (albums.isNotEmpty) + _SheetTile( + title: 'appears_in'.t(context: context).toUpperCase(), + titleStyle: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(200), + fontWeight: FontWeight.w600, + ), + ), + ...albums.map((album) { + final isOwner = album.ownerId == userId; + return AlbumTile( + album: album, + isOwner: isOwner, + onAlbumSelected: (album) async { + final prevAlbum = ref.read(currentRemoteAlbumProvider); + ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album); + await context.router.push(RemoteAlbumRoute(album: album)); + if (prevAlbum != null) { + ref.read(currentRemoteAlbumProvider.notifier).setAlbum(prevAlbum); + } + }, + ); + }), + ], + ), + ); + }, + ); + } + @override Widget build(BuildContext context, WidgetRef ref) { final asset = ref.watch(currentAssetNotifier); @@ -217,7 +277,10 @@ class _AssetDetailBottomSheet extends ConsumerWidget { color: context.textTheme.bodyMedium?.color?.withAlpha(155), ), ), - const SizedBox(height: 64), + // Appears in (Albums) + _buildAppearsInList(ref, context), + // padding at the bottom to avoid cut-off + const SizedBox(height: 100), ], ); } diff --git a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart index f75dd6e803abb..c0661bad4831e 100644 --- a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart @@ -18,7 +18,6 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/album/remote_album_shared_user_icons.dart'; class RemoteAlbumSliverAppBar extends ConsumerStatefulWidget { @@ -89,7 +88,7 @@ class _MesmerizingSliverAppBarState extends ConsumerState context.navigateTo(const TabShellRoute(children: [DriftAlbumsRoute()])), + onPressed: () => context.maybePop(), ), actions: [ if (widget.onToggleAlbumOrder != null) From e765864fb8656c2122ee7f065722b347cb8ef634 Mon Sep 17 00:00:00 2001 From: bwees Date: Tue, 16 Sep 2025 19:48:04 -0500 Subject: [PATCH 02/13] fix: bottom sheet now is usable when navigating to another asset viewer --- .../widgets/asset_viewer/bottom_sheet.widget.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart index 6305c544d1c31..4a4fc77c668c5 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -11,6 +11,7 @@ import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/album/album_tile.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; @@ -178,7 +179,8 @@ class _AssetDetailBottomSheet extends ConsumerWidget { onAlbumSelected: (album) async { final prevAlbum = ref.read(currentRemoteAlbumProvider); ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album); - await context.router.push(RemoteAlbumRoute(album: album)); + ref.invalidate(assetViewerProvider); + context.router.popAndPush(RemoteAlbumRoute(album: album)); if (prevAlbum != null) { ref.read(currentRemoteAlbumProvider.notifier).setAlbum(prevAlbum); } From e2c61d1c8469928eb7ac7025c57264becfd75db7 Mon Sep 17 00:00:00 2001 From: bwees Date: Wed, 17 Sep 2025 16:01:05 -0500 Subject: [PATCH 03/13] fix: rebase conflict --- mobile/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 9884dde45f78c..c3804d97f6a7a 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -254,7 +254,7 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve theme: getThemeData(colorScheme: immichTheme.light, locale: context.locale), routerConfig: router.config( deepLinkBuilder: _deepLinkBuilder, - navigatorObservers: () => [AppNavigationObserver(ref: ref), HeroController()], + navigatorObservers: () => [AppNavigationObserver(ref: ref)], ), ), ); From b6d33f32b354f94cef7584f5cca728d51e07664f Mon Sep 17 00:00:00 2001 From: bwees Date: Sun, 21 Sep 2025 23:19:12 -0500 Subject: [PATCH 04/13] fix: restore ancestors album to currentRemoteAlbumProvider when popping --- .../pages/drift_remote_album.page.dart | 14 ++++++++++++-- .../widgets/asset_viewer/bottom_sheet.widget.dart | 4 ---- mobile/lib/routing/router.dart | 2 +- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/mobile/lib/presentation/pages/drift_remote_album.page.dart b/mobile/lib/presentation/pages/drift_remote_album.page.dart index 1eb5b9d7af28d..53c3ec438a7c1 100644 --- a/mobile/lib/presentation/pages/drift_remote_album.page.dart +++ b/mobile/lib/presentation/pages/drift_remote_album.page.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -243,10 +244,19 @@ class _RemoteAlbumPageState extends ConsumerState { if (didPop || !mounted) { return; } - final hasAncestor = context.findAncestorWidgetOfExactType() != null; + + final ancestors = context.router.stack.take(context.router.stack.length - 1); + final ancestorPage = ancestors.lastWhereOrNull((route) { + return route.name == RemoteAlbumRoute.page.name; + }); + Navigator.of(context).pop(); - if (!hasAncestor) { + + if (ancestorPage == null) { ref.read(currentRemoteAlbumProvider.notifier).dispose(); + } else { + final album = (ancestorPage.routeData.args as RemoteAlbumRouteArgs).album; + ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album); } }, child: ProviderScope( diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart index 4a4fc77c668c5..4c3dab70556c6 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -177,13 +177,9 @@ class _AssetDetailBottomSheet extends ConsumerWidget { album: album, isOwner: isOwner, onAlbumSelected: (album) async { - final prevAlbum = ref.read(currentRemoteAlbumProvider); ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album); ref.invalidate(assetViewerProvider); context.router.popAndPush(RemoteAlbumRoute(album: album)); - if (prevAlbum != null) { - ref.read(currentRemoteAlbumProvider.notifier).setAlbum(prevAlbum); - } }, ); }), diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 7554c7b1cf161..5c0299c4143da 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -303,7 +303,7 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftBackupAlbumSelectionRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: LocalTimelineRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: MainTimelineRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: RemoteAlbumRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: RemoteAlbumRoute.page, guards: [_authGuard]), AutoRoute( page: AssetViewerRoute.page, guards: [_authGuard, _duplicateGuard], From 12d6668569090dafcd86f552390c87bb1b413021 Mon Sep 17 00:00:00 2001 From: bwees Date: Thu, 23 Oct 2025 16:58:06 -0500 Subject: [PATCH 05/13] fix: view flashing when dismissing a album viewer --- .../presentation/pages/drift_remote_album.page.dart | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/mobile/lib/presentation/pages/drift_remote_album.page.dart b/mobile/lib/presentation/pages/drift_remote_album.page.dart index 53c3ec438a7c1..18611baaaec59 100644 --- a/mobile/lib/presentation/pages/drift_remote_album.page.dart +++ b/mobile/lib/presentation/pages/drift_remote_album.page.dart @@ -240,7 +240,7 @@ class _RemoteAlbumPageState extends ConsumerState { return PopScope( canPop: false, - onPopInvokedWithResult: (didPop, _) { + onPopInvokedWithResult: (didPop, _) async { if (didPop || !mounted) { return; } @@ -250,13 +250,18 @@ class _RemoteAlbumPageState extends ConsumerState { return route.name == RemoteAlbumRoute.page.name; }); + final albumNotifier = ref.read(currentRemoteAlbumProvider.notifier); + Navigator.of(context).pop(); + // wait for the pop animation to finish + await Future.delayed(const Duration(milliseconds: 300)); + if (ancestorPage == null) { - ref.read(currentRemoteAlbumProvider.notifier).dispose(); + albumNotifier.dispose(); } else { final album = (ancestorPage.routeData.args as RemoteAlbumRouteArgs).album; - ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album); + albumNotifier.setAlbum(album); } }, child: ProviderScope( From 5c0ae187997d83145375646ebd98df01696cbc40 Mon Sep 17 00:00:00 2001 From: bwees Date: Thu, 23 Oct 2025 17:44:34 -0500 Subject: [PATCH 06/13] chore: code review changes --- .../presentation/pages/drift_remote_album.page.dart | 12 +++--------- .../widgets/asset_viewer/bottom_sheet.widget.dart | 10 +++++----- .../lib/providers/infrastructure/album.provider.dart | 5 +++++ 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/mobile/lib/presentation/pages/drift_remote_album.page.dart b/mobile/lib/presentation/pages/drift_remote_album.page.dart index 18611baaaec59..d5ccf7ba56a8d 100644 --- a/mobile/lib/presentation/pages/drift_remote_album.page.dart +++ b/mobile/lib/presentation/pages/drift_remote_album.page.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -245,11 +244,7 @@ class _RemoteAlbumPageState extends ConsumerState { return; } - final ancestors = context.router.stack.take(context.router.stack.length - 1); - final ancestorPage = ancestors.lastWhereOrNull((route) { - return route.name == RemoteAlbumRoute.page.name; - }); - + final ancestor = context.findAncestorWidgetOfExactType(); final albumNotifier = ref.read(currentRemoteAlbumProvider.notifier); Navigator.of(context).pop(); @@ -257,11 +252,10 @@ class _RemoteAlbumPageState extends ConsumerState { // wait for the pop animation to finish await Future.delayed(const Duration(milliseconds: 300)); - if (ancestorPage == null) { + if (ancestor == null) { albumNotifier.dispose(); } else { - final album = (ancestorPage.routeData.args as RemoteAlbumRouteArgs).album; - albumNotifier.setAlbum(album); + albumNotifier.setAlbum(ancestor.album); } }, child: ProviderScope( diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart index 4c3dab70556c6..8ef810cac13ad 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -145,13 +145,11 @@ class _AssetDetailBottomSheet extends ConsumerWidget { } final remoteAsset = ref.watch(currentAssetNotifier) as RemoteAsset; - final albums = ref.watch(remoteAlbumServiceProvider).getAlbumsContainingAsset(remoteAsset.id); final userId = ref.watch(currentUserProvider)?.id; + final assetAlbums = ref.watch(albumsContainingAssetProvider(remoteAsset.id)); - return FutureBuilder( - future: albums, - builder: (_, snap) { - final albums = snap.data ?? []; + return assetAlbums.when( + data: (albums) { if (albums.isEmpty) { return const SizedBox.shrink(); } @@ -187,6 +185,8 @@ class _AssetDetailBottomSheet extends ConsumerWidget { ), ); }, + loading: () => const SizedBox.shrink(), + error: (_, __) => const SizedBox.shrink(), ); } diff --git a/mobile/lib/providers/infrastructure/album.provider.dart b/mobile/lib/providers/infrastructure/album.provider.dart index 8388480974af6..1ddabc1604dbf 100644 --- a/mobile/lib/providers/infrastructure/album.provider.dart +++ b/mobile/lib/providers/infrastructure/album.provider.dart @@ -1,4 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/services/local_album.service.dart'; @@ -40,3 +41,7 @@ final remoteAlbumProvider = NotifierProvider, String>( + (ref, assetId) => ref.watch(remoteAlbumServiceProvider).getAlbumsContainingAsset(assetId), +); From 34e87737021faff31bf8aa22036be46e4a056fe1 Mon Sep 17 00:00:00 2001 From: bwees Date: Thu, 23 Oct 2025 17:44:43 -0500 Subject: [PATCH 07/13] fix: styling and padding --- .../asset_viewer/bottom_sheet.widget.dart | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart index 8ef810cac13ad..7da8dcfb9e35b 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -156,33 +156,36 @@ class _AssetDetailBottomSheet extends ConsumerWidget { albums.sortBy((a) => a.name); - return Padding( - padding: const EdgeInsets.only(left: 16, right: 16, top: 8), - child: Column( - spacing: 12, - children: [ - if (albums.isNotEmpty) - _SheetTile( - title: 'appears_in'.t(context: context).toUpperCase(), - titleStyle: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), + return Column( + spacing: 12, + children: [ + if (albums.isNotEmpty) + _SheetTile( + title: 'appears_in'.t(context: context).toUpperCase(), + titleStyle: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(200), + fontWeight: FontWeight.w600, ), - ...albums.map((album) { - final isOwner = album.ownerId == userId; - return AlbumTile( - album: album, - isOwner: isOwner, - onAlbumSelected: (album) async { - ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album); - ref.invalidate(assetViewerProvider); - context.router.popAndPush(RemoteAlbumRoute(album: album)); - }, - ); - }), - ], - ), + ), + Padding( + padding: const EdgeInsets.only(left: 24), + child: Column( + spacing: 12, + children: albums.map((album) { + final isOwner = album.ownerId == userId; + return AlbumTile( + album: album, + isOwner: isOwner, + onAlbumSelected: (album) async { + ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album); + ref.invalidate(assetViewerProvider); + context.router.popAndPush(RemoteAlbumRoute(album: album)); + }, + ); + }).toList(), + ), + ), + ], ); }, loading: () => const SizedBox.shrink(), From 43f173f69df46b973719736c18980161727b1803 Mon Sep 17 00:00:00 2001 From: bwees Date: Fri, 24 Oct 2025 10:05:40 -0500 Subject: [PATCH 08/13] chore: rework currentRemoteAlbumProvider to be scoped by the Remote album page --- .../presentation/pages/drift_album.page.dart | 2 - .../pages/drift_create_album.page.dart | 2 - .../pages/drift_remote_album.page.dart | 55 ++++++------------- .../widgets/album/album_selector.widget.dart | 3 +- .../asset_viewer/bottom_sheet.widget.dart | 2 +- .../current_album.provider.dart | 32 +++++------ mobile/lib/services/deep_link.service.dart | 6 +- 7 files changed, 33 insertions(+), 69 deletions(-) diff --git a/mobile/lib/presentation/pages/drift_album.page.dart b/mobile/lib/presentation/pages/drift_album.page.dart index 0835c741ad9e2..a159c6c54ad48 100644 --- a/mobile/lib/presentation/pages/drift_album.page.dart +++ b/mobile/lib/presentation/pages/drift_album.page.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart'; @@ -43,7 +42,6 @@ class _DriftAlbumsPageState extends ConsumerState { ), AlbumSelector( onAlbumSelected: (album) { - ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album); context.router.push(RemoteAlbumRoute(album: album)); }, ), diff --git a/mobile/lib/presentation/pages/drift_create_album.page.dart b/mobile/lib/presentation/pages/drift_create_album.page.dart index 2e263ba1dbfe5..57e5cb09a925a 100644 --- a/mobile/lib/presentation/pages/drift_create_album.page.dart +++ b/mobile/lib/presentation/pages/drift_create_album.page.dart @@ -8,7 +8,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; @@ -180,7 +179,6 @@ class _DriftCreateAlbumPageState extends ConsumerState { ); if (album != null) { - ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album); unawaited(context.replaceRoute(RemoteAlbumRoute(album: album))); } } diff --git a/mobile/lib/presentation/pages/drift_remote_album.page.dart b/mobile/lib/presentation/pages/drift_remote_album.page.dart index d5ccf7ba56a8d..fb4c0378bc286 100644 --- a/mobile/lib/presentation/pages/drift_remote_album.page.dart +++ b/mobile/lib/presentation/pages/drift_remote_album.page.dart @@ -237,45 +237,24 @@ class _RemoteAlbumPageState extends ConsumerState { final user = ref.watch(currentUserProvider); final isOwner = user != null ? user.id == _album.ownerId : false; - return PopScope( - canPop: false, - onPopInvokedWithResult: (didPop, _) async { - if (didPop || !mounted) { - return; - } - - final ancestor = context.findAncestorWidgetOfExactType(); - final albumNotifier = ref.read(currentRemoteAlbumProvider.notifier); - - Navigator.of(context).pop(); - - // wait for the pop animation to finish - await Future.delayed(const Duration(milliseconds: 300)); - - if (ancestor == null) { - albumNotifier.dispose(); - } else { - albumNotifier.setAlbum(ancestor.album); - } - }, - child: ProviderScope( - overrides: [ - timelineServiceProvider.overrideWith((ref) { - final timelineService = ref.watch(timelineFactoryProvider).remoteAlbum(albumId: _album.id); - ref.onDispose(timelineService.dispose); - return timelineService; - }), - ], - child: Timeline( - appBar: RemoteAlbumSliverAppBar( - icon: Icons.photo_album_outlined, - onShowOptions: () => showOptionSheet(context), - onToggleAlbumOrder: isOwner ? () => toggleAlbumOrder() : null, - onEditTitle: isOwner ? () => showEditTitleAndDescription(context) : null, - onActivity: () => showActivity(context), - ), - bottomSheet: RemoteAlbumBottomSheet(album: _album), + return ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith((ref) { + final timelineService = ref.watch(timelineFactoryProvider).remoteAlbum(albumId: _album.id); + ref.onDispose(timelineService.dispose); + return timelineService; + }), + currentRemoteAlbumScopedProvider.overrideWithValue(_album), + ], + child: Timeline( + appBar: RemoteAlbumSliverAppBar( + icon: Icons.photo_album_outlined, + onShowOptions: () => showOptionSheet(context), + onToggleAlbumOrder: isOwner ? () => toggleAlbumOrder() : null, + onEditTitle: isOwner ? () => showEditTitleAndDescription(context) : null, + onActivity: () => showActivity(context), ), + bottomSheet: RemoteAlbumBottomSheet(album: _album), ), ); } diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index a7d3e927ca1b5..f18c255e1512f 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -15,7 +15,6 @@ import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/presentation/widgets/album/album_tile.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -680,7 +679,7 @@ class AddToAlbumHeader extends ConsumerWidget { return; } - ref.read(currentRemoteAlbumProvider.notifier).setAlbum(newAlbum); + // ref.read(currentRemoteAlbumProvider.notifier).setAlbum(newAlbum); ref.read(multiSelectProvider.notifier).reset(); unawaited(context.pushRoute(RemoteAlbumRoute(album: newAlbum))); } diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart index 7da8dcfb9e35b..6c8d3251b23ea 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -177,7 +177,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget { album: album, isOwner: isOwner, onAlbumSelected: (album) async { - ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album); + // ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album); ref.invalidate(assetViewerProvider); context.router.popAndPush(RemoteAlbumRoute(album: album)); }, diff --git a/mobile/lib/providers/infrastructure/current_album.provider.dart b/mobile/lib/providers/infrastructure/current_album.provider.dart index 0d95674ec76f3..6c2fc248bab99 100644 --- a/mobile/lib/providers/infrastructure/current_album.provider.dart +++ b/mobile/lib/providers/infrastructure/current_album.provider.dart @@ -1,36 +1,30 @@ -import 'dart:async'; - import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; -final currentRemoteAlbumProvider = AutoDisposeNotifierProvider( +final currentRemoteAlbumScopedProvider = Provider((ref) => null); +final currentRemoteAlbumProvider = NotifierProvider( CurrentAlbumNotifier.new, + dependencies: [currentRemoteAlbumScopedProvider, remoteAlbumServiceProvider], ); -class CurrentAlbumNotifier extends AutoDisposeNotifier { - KeepAliveLink? _keepAliveLink; - StreamSubscription? _assetSubscription; - +class CurrentAlbumNotifier extends Notifier { @override - RemoteAlbum? build() => null; + RemoteAlbum? build() { + final album = ref.watch(currentRemoteAlbumScopedProvider); - void setAlbum(RemoteAlbum album) { - _keepAliveLink?.close(); - _assetSubscription?.cancel(); - state = album; + if (album == null) { + return null; + } - _assetSubscription = ref.watch(remoteAlbumServiceProvider).watchAlbum(album.id).listen((updatedAlbum) { + final watcher = ref.watch(remoteAlbumServiceProvider).watchAlbum(album.id).listen((updatedAlbum) { if (updatedAlbum != null) { state = updatedAlbum; } }); - _keepAliveLink = ref.keepAlive(); - } - void dispose() { - _keepAliveLink?.close(); - _assetSubscription?.cancel(); - state = null; + ref.onDispose(watcher.cancel); + + return album; } } diff --git a/mobile/lib/services/deep_link.service.dart b/mobile/lib/services/deep_link.service.dart index 7c8ddce265ebc..50d64941f59a6 100644 --- a/mobile/lib/services/deep_link.service.dart +++ b/mobile/lib/services/deep_link.service.dart @@ -10,7 +10,6 @@ import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart' as beta_asset_provider; -import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -29,7 +28,6 @@ final deepLinkServiceProvider = Provider( // Below is used for beta timeline ref.watch(timelineFactoryProvider), ref.watch(beta_asset_provider.assetServiceProvider), - ref.watch(currentRemoteAlbumProvider.notifier), ref.watch(remoteAlbumServiceProvider), ref.watch(driftMemoryServiceProvider), ), @@ -46,7 +44,6 @@ class DeepLinkService { /// Used for beta timeline final TimelineFactory _betaTimelineFactory; final beta_asset_service.AssetService _betaAssetService; - final CurrentAlbumNotifier _betaCurrentAlbumNotifier; final RemoteAlbumService _betaRemoteAlbumService; final DriftMemoryService _betaMemoryServiceProvider; @@ -58,7 +55,6 @@ class DeepLinkService { this._currentAlbum, this._betaTimelineFactory, this._betaAssetService, - this._betaCurrentAlbumNotifier, this._betaRemoteAlbumService, this._betaMemoryServiceProvider, ); @@ -176,7 +172,7 @@ class DeepLinkService { return null; } - _betaCurrentAlbumNotifier.setAlbum(album); + // _betaCurrentAlbumNotifier.setAlbum(album); return RemoteAlbumRoute(album: album); } else { // TODO: Remove this when beta is default From 0f2a2ec1fe1ef256bfcff6f2f5dad0d8d42f9f3b Mon Sep 17 00:00:00 2001 From: bwees Date: Fri, 24 Oct 2025 10:44:33 -0500 Subject: [PATCH 09/13] fix: override remote album provider on required pages --- .../pages/drift_activities.page.dart | 113 +++++++++--------- .../pages/drift_album_options.page.dart | 92 +++++++------- .../pages/drift_remote_album.page.dart | 4 +- .../widgets/album/album_selector.widget.dart | 1 - .../asset_viewer/asset_viewer.page.dart | 18 ++- .../asset_viewer/bottom_sheet.widget.dart | 1 - .../widgets/timeline/fixed/segment.model.dart | 2 + mobile/lib/routing/router.gr.dart | 66 ++++++++-- .../album/remote_album_shared_user_icons.dart | 2 +- 9 files changed, 182 insertions(+), 117 deletions(-) diff --git a/mobile/lib/presentation/pages/drift_activities.page.dart b/mobile/lib/presentation/pages/drift_activities.page.dart index d8f8799f7d42b..731bcb5dbabb4 100644 --- a/mobile/lib/presentation/pages/drift_activities.page.dart +++ b/mobile/lib/presentation/pages/drift_activities.page.dart @@ -3,6 +3,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -18,12 +19,13 @@ import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; @RoutePage() class DriftActivitiesPage extends HookConsumerWidget { - const DriftActivitiesPage({super.key}); + final RemoteAlbum album; + + const DriftActivitiesPage({super.key, required this.album}); @override Widget build(BuildContext context, WidgetRef ref) { - final album = ref.watch(currentRemoteAlbumProvider)!; - final asset = ref.read(currentAssetNotifier) as RemoteAsset?; + final asset = ref.watch(currentAssetNotifier) as RemoteAsset?; final user = ref.watch(currentUserProvider); final activityNotifier = ref.read(albumActivityProvider(album.id, asset?.id).notifier); @@ -43,62 +45,65 @@ class DriftActivitiesPage extends HookConsumerWidget { scrollToBottom(); } - return Scaffold( - appBar: AppBar( - title: asset == null ? Text(album.name) : null, - actions: [const LikeActivityActionButton(menuItem: true)], - actionsPadding: const EdgeInsets.only(right: 8), - ), - body: activities.widgetWhen( - onData: (data) { - final liked = data.firstWhereOrNull( - (a) => a.type == ActivityType.like && a.user.id == user?.id && a.assetId == asset?.id, - ); + return ProviderScope( + overrides: [currentRemoteAlbumScopedProvider.overrideWithValue(album)], + child: Scaffold( + appBar: AppBar( + title: asset == null ? Text(album.name) : null, + actions: [const LikeActivityActionButton(menuItem: true)], + actionsPadding: const EdgeInsets.only(right: 8), + ), + body: activities.widgetWhen( + onData: (data) { + final liked = data.firstWhereOrNull( + (a) => a.type == ActivityType.like && a.user.id == user?.id && a.assetId == asset?.id, + ); - return SafeArea( - child: Stack( - children: [ - ListView.builder( - controller: listViewScrollController, - itemCount: data.length + 1, - itemBuilder: (context, index) { - if (index == data.length) { - return const SizedBox(height: 80); - } - final activity = data[index]; - final canDelete = activity.user.id == user?.id || album.ownerId == user?.id; - return Padding( - padding: const EdgeInsets.all(5), - child: DismissibleActivity( - activity.id, - ActivityTile(activity), - onDismiss: canDelete - ? (activityId) async => await activityNotifier.removeActivity(activity.id) - : null, + return SafeArea( + child: Stack( + children: [ + ListView.builder( + controller: listViewScrollController, + itemCount: data.length + 1, + itemBuilder: (context, index) { + if (index == data.length) { + return const SizedBox(height: 80); + } + final activity = data[index]; + final canDelete = activity.user.id == user?.id || album.ownerId == user?.id; + return Padding( + padding: const EdgeInsets.all(5), + child: DismissibleActivity( + activity.id, + ActivityTile(activity), + onDismiss: canDelete + ? (activityId) async => await activityNotifier.removeActivity(activity.id) + : null, + ), + ); + }, + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + decoration: BoxDecoration( + color: context.scaffoldBackgroundColor, + border: Border(top: BorderSide(color: context.colorScheme.secondaryContainer, width: 1)), + ), + child: DriftActivityTextField( + isEnabled: album.isActivityEnabled, + likeId: liked?.id, + onSubmit: onAddComment, ), - ); - }, - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - decoration: BoxDecoration( - color: context.scaffoldBackgroundColor, - border: Border(top: BorderSide(color: context.colorScheme.secondaryContainer, width: 1)), - ), - child: DriftActivityTextField( - isEnabled: album.isActivityEnabled, - likeId: liked?.id, - onSubmit: onAddComment, ), ), - ), - ], - ), - ); - }, + ], + ), + ); + }, + ), + resizeToAvoidBottomInset: true, ), - resizeToAvoidBottomInset: true, ); } } diff --git a/mobile/lib/presentation/pages/drift_album_options.page.dart b/mobile/lib/presentation/pages/drift_album_options.page.dart index 2116e5c5cc5a9..9db6e98613b53 100644 --- a/mobile/lib/presentation/pages/drift_album_options.page.dart +++ b/mobile/lib/presentation/pages/drift_album_options.page.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; @@ -22,15 +23,11 @@ import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; @RoutePage() class DriftAlbumOptionsPage extends HookConsumerWidget { - const DriftAlbumOptionsPage({super.key}); + final RemoteAlbum album; + const DriftAlbumOptionsPage({super.key, required this.album}); @override Widget build(BuildContext context, WidgetRef ref) { - final album = ref.watch(currentRemoteAlbumProvider); - if (album == null) { - return const SizedBox(); - } - final sharedUsersAsync = ref.watch(remoteAlbumSharedUsersProvider(album.id)); final userId = ref.watch(authProvider).userId; final activityEnabled = useState(album.isActivityEnabled); @@ -191,48 +188,51 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { ); } - return Scaffold( - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios_new_rounded), - onPressed: () => context.maybePop(null), + return ProviderScope( + overrides: [currentRemoteAlbumScopedProvider.overrideWithValue(album)], + child: Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_rounded), + onPressed: () => context.maybePop(null), + ), + centerTitle: true, + title: Text("options".t(context: context)), ), - centerTitle: true, - title: Text("options".t(context: context)), - ), - body: ListView( - children: [ - const SizedBox(height: 8), - if (isOwner) - SwitchListTile.adaptive( - value: activityEnabled.value, - onChanged: (bool value) async { - activityEnabled.value = value; - await ref.read(remoteAlbumProvider.notifier).setActivityStatus(album.id, value); - }, - activeThumbColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor, - dense: true, - title: Text( - "comments_and_likes", - style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), - ).t(context: context), - subtitle: Text( - "let_others_respond", - style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ).t(context: context), - ), - buildSectionTitle("shared_album_section_people_title".t(context: context)), - if (isOwner) ...[ - ListTile( - leading: const Icon(Icons.person_add_rounded), - title: Text("invite_people".t(context: context)), - onTap: () async => addUsers(), - ), - const Divider(indent: 16), + body: ListView( + children: [ + const SizedBox(height: 8), + if (isOwner) + SwitchListTile.adaptive( + value: activityEnabled.value, + onChanged: (bool value) async { + activityEnabled.value = value; + await ref.read(remoteAlbumProvider.notifier).setActivityStatus(album.id, value); + }, + activeThumbColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor, + dense: true, + title: Text( + "comments_and_likes", + style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), + ).t(context: context), + subtitle: Text( + "let_others_respond", + style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), + ).t(context: context), + ), + buildSectionTitle("shared_album_section_people_title".t(context: context)), + if (isOwner) ...[ + ListTile( + leading: const Icon(Icons.person_add_rounded), + title: Text("invite_people".t(context: context)), + onTap: () async => addUsers(), + ), + const Divider(indent: 16), + ], + buildOwnerInfo(), + buildSharedUsersList(), ], - buildOwnerInfo(), - buildSharedUsersList(), - ], + ), ), ); } diff --git a/mobile/lib/presentation/pages/drift_remote_album.page.dart b/mobile/lib/presentation/pages/drift_remote_album.page.dart index fb4c0378bc286..9a52f28deb8fd 100644 --- a/mobile/lib/presentation/pages/drift_remote_album.page.dart +++ b/mobile/lib/presentation/pages/drift_remote_album.page.dart @@ -168,7 +168,7 @@ class _RemoteAlbumPageState extends ConsumerState { } Future showActivity(BuildContext context) async { - unawaited(context.pushRoute(const DriftActivitiesRoute())); + unawaited(context.pushRoute(DriftActivitiesRoute(album: _album))); } Future showOptionSheet(BuildContext context) async { @@ -224,7 +224,7 @@ class _RemoteAlbumPageState extends ConsumerState { : null, onShowOptions: () { context.pop(); - context.pushRoute(const DriftAlbumOptionsRoute()); + context.pushRoute(DriftAlbumOptionsRoute(album: _album)); }, ); }, diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index f18c255e1512f..0d5b9a7636320 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -679,7 +679,6 @@ class AddToAlbumHeader extends ConsumerWidget { return; } - // ref.read(currentRemoteAlbumProvider.notifier).setAlbum(newAlbum); ref.read(multiSelectProvider.notifier).reset(); unawaited(context.pushRoute(RemoteAlbumRoute(album: newAlbum))); } 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 2e3009d9341ba..f8a2c37ccd1f1 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -5,6 +5,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; @@ -13,6 +14,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/scroll_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_status_floating_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/activities_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; @@ -20,7 +22,6 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_bar.widge import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/top_app_bar.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart'; -import 'package:immich_mobile/presentation/widgets/asset_viewer/activities_bottom_sheet.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/asset_viewer/is_motion_video_playing.provider.dart'; @@ -28,6 +29,7 @@ import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provi import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; @@ -39,15 +41,25 @@ class AssetViewerPage extends StatelessWidget { final int initialIndex; final TimelineService timelineService; final int? heroOffset; + final RemoteAlbum? currentAlbum; - const AssetViewerPage({super.key, required this.initialIndex, required this.timelineService, this.heroOffset}); + const AssetViewerPage({ + super.key, + required this.initialIndex, + required this.timelineService, + this.heroOffset, + this.currentAlbum, + }); @override Widget build(BuildContext context) { // This is necessary to ensure that the timeline service is available // since the Timeline and AssetViewer are on different routes / Widget subtrees. return ProviderScope( - overrides: [timelineServiceProvider.overrideWithValue(timelineService)], + overrides: [ + timelineServiceProvider.overrideWithValue(timelineService), + currentRemoteAlbumScopedProvider.overrideWithValue(currentAlbum), + ], child: AssetViewer(initialIndex: initialIndex, heroOffset: heroOffset), ); } diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart index 6c8d3251b23ea..50f8fa0402a7e 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -177,7 +177,6 @@ class _AssetDetailBottomSheet extends ConsumerWidget { album: album, isOwner: isOwner, onAlbumSelected: (album) async { - // ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album); ref.invalidate(assetViewerProvider); context.router.popAndPush(RemoteAlbumRoute(album: album)); }, diff --git a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart index a0a98c34d721a..b879b33f684ac 100644 --- a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart +++ b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart @@ -16,6 +16,7 @@ import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart' import 'package:immich_mobile/presentation/widgets/timeline/timeline_drag_region.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; @@ -163,6 +164,7 @@ class _AssetTileWidget extends ConsumerWidget { initialIndex: assetIndex, timelineService: ref.read(timelineServiceProvider), heroOffset: heroOffset, + currentAlbum: ref.read(currentRemoteAlbumProvider), ), ), ); diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 4e488a30c7732..4e60e4fb6aff7 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -448,6 +448,7 @@ class AssetViewerRoute extends PageRouteInfo { required int initialIndex, required TimelineService timelineService, int? heroOffset, + RemoteAlbum? currentAlbum, List? children, }) : super( AssetViewerRoute.name, @@ -456,6 +457,7 @@ class AssetViewerRoute extends PageRouteInfo { initialIndex: initialIndex, timelineService: timelineService, heroOffset: heroOffset, + currentAlbum: currentAlbum, ), initialChildren: children, ); @@ -471,6 +473,7 @@ class AssetViewerRoute extends PageRouteInfo { initialIndex: args.initialIndex, timelineService: args.timelineService, heroOffset: args.heroOffset, + currentAlbum: args.currentAlbum, ); }, ); @@ -482,6 +485,7 @@ class AssetViewerRouteArgs { required this.initialIndex, required this.timelineService, this.heroOffset, + this.currentAlbum, }); final Key? key; @@ -492,9 +496,11 @@ class AssetViewerRouteArgs { final int? heroOffset; + final RemoteAlbum? currentAlbum; + @override String toString() { - return 'AssetViewerRouteArgs{key: $key, initialIndex: $initialIndex, timelineService: $timelineService, heroOffset: $heroOffset}'; + return 'AssetViewerRouteArgs{key: $key, initialIndex: $initialIndex, timelineService: $timelineService, heroOffset: $heroOffset, currentAlbum: $currentAlbum}'; } } @@ -706,36 +712,78 @@ class DownloadInfoRoute extends PageRouteInfo { /// generated route for /// [DriftActivitiesPage] -class DriftActivitiesRoute extends PageRouteInfo { - const DriftActivitiesRoute({List? children}) - : super(DriftActivitiesRoute.name, initialChildren: children); +class DriftActivitiesRoute extends PageRouteInfo { + DriftActivitiesRoute({ + Key? key, + required RemoteAlbum album, + List? children, + }) : super( + DriftActivitiesRoute.name, + args: DriftActivitiesRouteArgs(key: key, album: album), + initialChildren: children, + ); static const String name = 'DriftActivitiesRoute'; static PageInfo page = PageInfo( name, builder: (data) { - return const DriftActivitiesPage(); + final args = data.argsAs(); + return DriftActivitiesPage(key: args.key, album: args.album); }, ); } +class DriftActivitiesRouteArgs { + const DriftActivitiesRouteArgs({this.key, required this.album}); + + final Key? key; + + final RemoteAlbum album; + + @override + String toString() { + return 'DriftActivitiesRouteArgs{key: $key, album: $album}'; + } +} + /// generated route for /// [DriftAlbumOptionsPage] -class DriftAlbumOptionsRoute extends PageRouteInfo { - const DriftAlbumOptionsRoute({List? children}) - : super(DriftAlbumOptionsRoute.name, initialChildren: children); +class DriftAlbumOptionsRoute extends PageRouteInfo { + DriftAlbumOptionsRoute({ + Key? key, + required RemoteAlbum album, + List? children, + }) : super( + DriftAlbumOptionsRoute.name, + args: DriftAlbumOptionsRouteArgs(key: key, album: album), + initialChildren: children, + ); static const String name = 'DriftAlbumOptionsRoute'; static PageInfo page = PageInfo( name, builder: (data) { - return const DriftAlbumOptionsPage(); + final args = data.argsAs(); + return DriftAlbumOptionsPage(key: args.key, album: args.album); }, ); } +class DriftAlbumOptionsRouteArgs { + const DriftAlbumOptionsRouteArgs({this.key, required this.album}); + + final Key? key; + + final RemoteAlbum album; + + @override + String toString() { + return 'DriftAlbumOptionsRouteArgs{key: $key, album: $album}'; + } +} + /// generated route for /// [DriftAlbumsPage] class DriftAlbumsRoute extends PageRouteInfo { diff --git a/mobile/lib/widgets/album/remote_album_shared_user_icons.dart b/mobile/lib/widgets/album/remote_album_shared_user_icons.dart index 9f88b23f923f1..8913e94136ac9 100644 --- a/mobile/lib/widgets/album/remote_album_shared_user_icons.dart +++ b/mobile/lib/widgets/album/remote_album_shared_user_icons.dart @@ -25,7 +25,7 @@ class RemoteAlbumSharedUserIcons extends ConsumerWidget { } return GestureDetector( - onTap: () => context.pushRoute(const DriftAlbumOptionsRoute()), + onTap: () => context.pushRoute(DriftAlbumOptionsRoute(album: currentAlbum)), child: SizedBox( height: 50, child: ListView.builder( From 34d2f1c825f641e6975f67e4eac5e667d5582512 Mon Sep 17 00:00:00 2001 From: bwees Date: Mon, 27 Oct 2025 10:14:06 -0500 Subject: [PATCH 10/13] chore: convert query to all SQL calls instead of matching in Dart --- .../repositories/remote_album.repository.dart | 46 ++++++++++++++++--- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/mobile/lib/infrastructure/repositories/remote_album.repository.dart b/mobile/lib/infrastructure/repositories/remote_album.repository.dart index a6f4893881d63..be09c15ef324a 100644 --- a/mobile/lib/infrastructure/repositories/remote_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_album.repository.dart @@ -384,15 +384,47 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { } Future> getAlbumsContainingAsset(String assetId) async { - final albumIdsQuery = _db.remoteAlbumAssetEntity.select()..where((row) => row.assetId.equals(assetId)); - - final albumIds = (await albumIdsQuery.get()).map((e) => e.albumId).toSet(); + final assetCount = _db.remoteAlbumAssetEntity.assetId.count(distinct: true); - if (albumIds.isEmpty) { - return []; - } + final query = + _db.remoteAlbumEntity.select().join([ + leftOuterJoin( + _db.remoteAlbumAssetEntity, + _db.remoteAlbumAssetEntity.albumId.equalsExp(_db.remoteAlbumEntity.id), + useColumns: false, + ), + leftOuterJoin( + _db.remoteAssetEntity, + _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId), + useColumns: false, + ), + leftOuterJoin( + _db.userEntity, + _db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId), + useColumns: false, + ), + leftOuterJoin( + _db.remoteAlbumUserEntity, + _db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id), + useColumns: false, + ), + ]) + ..where(_db.remoteAlbumAssetEntity.assetId.equals(assetId)) + ..addColumns([assetCount]) + ..addColumns([_db.remoteAlbumUserEntity.userId.count(distinct: true)]) + ..addColumns([_db.userEntity.name]); - return getAll().then((albums) => albums.where((album) => albumIds.contains(album.id)).toList()); + return query + .map( + (row) => row + .readTable(_db.remoteAlbumEntity) + .toDto( + ownerName: row.read(_db.userEntity.name) ?? '', + isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 0, + assetCount: row.read(assetCount) ?? 0, + ), + ) + .get(); } } From 819642c40a1c61e2d3726355e51742379e46d632 Mon Sep 17 00:00:00 2001 From: bwees Date: Mon, 27 Oct 2025 10:27:02 -0500 Subject: [PATCH 11/13] fix: album query --- .../repositories/remote_album.repository.dart | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/mobile/lib/infrastructure/repositories/remote_album.repository.dart b/mobile/lib/infrastructure/repositories/remote_album.repository.dart index be09c15ef324a..d7d4a250ada30 100644 --- a/mobile/lib/infrastructure/repositories/remote_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_album.repository.dart @@ -384,8 +384,18 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { } Future> getAlbumsContainingAsset(String assetId) async { - final assetCount = _db.remoteAlbumAssetEntity.assetId.count(distinct: true); + // Note: this needs to be 2 queries as the where clause filtering causes the assetCount to always be 1 + final albumIdsQuery = _db.remoteAlbumAssetEntity.selectOnly() + ..addColumns([_db.remoteAlbumAssetEntity.albumId]) + ..where(_db.remoteAlbumAssetEntity.assetId.equals(assetId)); + + final albumIds = await albumIdsQuery.map((row) => row.read(_db.remoteAlbumAssetEntity.albumId)!).get(); + + if (albumIds.isEmpty) { + return []; + } + final assetCount = _db.remoteAlbumAssetEntity.assetId.count(distinct: true); final query = _db.remoteAlbumEntity.select().join([ leftOuterJoin( @@ -409,10 +419,11 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { useColumns: false, ), ]) - ..where(_db.remoteAlbumAssetEntity.assetId.equals(assetId)) + ..where(_db.remoteAlbumEntity.id.isIn(albumIds) & _db.remoteAssetEntity.deletedAt.isNull()) ..addColumns([assetCount]) ..addColumns([_db.remoteAlbumUserEntity.userId.count(distinct: true)]) - ..addColumns([_db.userEntity.name]); + ..addColumns([_db.userEntity.name]) + ..groupBy([_db.remoteAlbumEntity.id]); return query .map( From d40a43a081c4defda9ba3aaced65d2c338f0aabc Mon Sep 17 00:00:00 2001 From: bwees Date: Mon, 27 Oct 2025 10:50:28 -0500 Subject: [PATCH 12/13] fix: unawaited future --- .../widgets/asset_viewer/bottom_sheet.widget.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart index 50f8fa0402a7e..00e16fa87007f 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -178,7 +180,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget { isOwner: isOwner, onAlbumSelected: (album) async { ref.invalidate(assetViewerProvider); - context.router.popAndPush(RemoteAlbumRoute(album: album)); + unawaited(context.router.popAndPush(RemoteAlbumRoute(album: album))); }, ); }).toList(), From 780f6299787a5e7ff2bdaed88c6245540646b1c6 Mon Sep 17 00:00:00 2001 From: Brandon Wees Date: Tue, 28 Oct 2025 11:34:48 -0500 Subject: [PATCH 13/13] Update deep_link.service.dart --- mobile/lib/services/deep_link.service.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/mobile/lib/services/deep_link.service.dart b/mobile/lib/services/deep_link.service.dart index 50d64941f59a6..d67362aac2078 100644 --- a/mobile/lib/services/deep_link.service.dart +++ b/mobile/lib/services/deep_link.service.dart @@ -172,7 +172,6 @@ class DeepLinkService { return null; } - // _betaCurrentAlbumNotifier.setAlbum(album); return RemoteAlbumRoute(album: album); } else { // TODO: Remove this when beta is default