From b484a52252b4a651a334485db543ddb5ecb48d6f Mon Sep 17 00:00:00 2001 From: Brandon Wees Date: Tue, 14 Oct 2025 10:25:48 -0500 Subject: [PATCH] chore: refactor show view in timeline button (#22894) * chore: refactor show view in timeline button This refactor includes changes to notify asset viewer about where an asset was shown from. * chore: realized I could just pull from the timelineProvider instead of storing it in the asset viewer state * chore: rename enum to TimelineOrigin and update members * fix: update isOwner condition --------- Co-authored-by: Alex --- .../lib/domain/services/timeline.service.dart | 35 +++++++++++++++---- .../repositories/timeline.repository.dart | 17 ++++++++- .../pages/search/drift_search.page.dart | 3 +- .../asset_viewer/top_app_bar.widget.dart | 15 ++++---- mobile/lib/services/deep_link.service.dart | 7 ++-- 5 files changed, 60 insertions(+), 17 deletions(-) diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index 6a7a1a22b2..85fc5fc55d 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -16,7 +16,24 @@ typedef TimelineAssetSource = Future> Function(int index, int co typedef TimelineBucketSource = Stream> Function(); -typedef TimelineQuery = ({TimelineAssetSource assetSource, TimelineBucketSource bucketSource}); +typedef TimelineQuery = ({TimelineAssetSource assetSource, TimelineBucketSource bucketSource, TimelineOrigin origin}); + +enum TimelineOrigin { + main, + localAlbum, + remoteAlbum, + remoteAssets, + favorite, + trash, + archive, + lockedFolder, + video, + place, + person, + map, + search, + deepLink, +} class TimelineFactory { final DriftTimelineRepository _timelineRepository; @@ -57,7 +74,8 @@ class TimelineFactory { TimelineService person(String userId, String personId) => TimelineService(_timelineRepository.person(userId, personId, groupBy)); - TimelineService fromAssets(List assets) => TimelineService(_timelineRepository.fromAssets(assets)); + TimelineService fromAssets(List assets, TimelineOrigin type) => + TimelineService(_timelineRepository.fromAssets(assets, type)); TimelineService map(String userId, LatLngBounds bounds) => TimelineService(_timelineRepository.map(userId, bounds, groupBy)); @@ -66,6 +84,7 @@ class TimelineFactory { class TimelineService { final TimelineAssetSource _assetSource; final TimelineBucketSource _bucketSource; + final TimelineOrigin origin; final AsyncMutex _mutex = AsyncMutex(); int _bufferOffset = 0; List _buffer = []; @@ -74,11 +93,15 @@ class TimelineService { int _totalAssets = 0; int get totalAssets => _totalAssets; - TimelineService(TimelineQuery query) : this._(assetSource: query.assetSource, bucketSource: query.bucketSource); + TimelineService(TimelineQuery query) + : this._(assetSource: query.assetSource, bucketSource: query.bucketSource, origin: query.origin); - TimelineService._({required TimelineAssetSource assetSource, required TimelineBucketSource bucketSource}) - : _assetSource = assetSource, - _bucketSource = bucketSource { + TimelineService._({ + required TimelineAssetSource assetSource, + required TimelineBucketSource bucketSource, + required this.origin, + }) : _assetSource = assetSource, + _bucketSource = bucketSource { _bucketSubscription = _bucketSource().listen((buckets) { _mutex.run(() async { final totalAssets = buckets.fold(0, (acc, bucket) => acc + bucket.assetCount); diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index 05928d938f..1fc0ee43e5 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -35,6 +35,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { TimelineQuery main(List userIds, GroupAssetsBy groupBy) => ( bucketSource: () => _watchMainBucket(userIds, groupBy: groupBy), assetSource: (offset, count) => _getMainBucketAssets(userIds, offset: offset, count: count), + origin: TimelineOrigin.main, ); Stream> _watchMainBucket(List userIds, {GroupAssetsBy groupBy = GroupAssetsBy.day}) { @@ -91,6 +92,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { TimelineQuery localAlbum(String albumId, GroupAssetsBy groupBy) => ( bucketSource: () => _watchLocalAlbumBucket(albumId, groupBy: groupBy), assetSource: (offset, count) => _getLocalAlbumBucketAssets(albumId, offset: offset, count: count), + origin: TimelineOrigin.localAlbum, ); Stream> _watchLocalAlbumBucket(String albumId, {GroupAssetsBy groupBy = GroupAssetsBy.day}) { @@ -156,6 +158,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { TimelineQuery remoteAlbum(String albumId, GroupAssetsBy groupBy) => ( bucketSource: () => _watchRemoteAlbumBucket(albumId, groupBy: groupBy), assetSource: (offset, count) => _getRemoteAlbumBucketAssets(albumId, offset: offset, count: count), + origin: TimelineOrigin.remoteAlbum, ); Stream> _watchRemoteAlbumBucket(String albumId, {GroupAssetsBy groupBy = GroupAssetsBy.day}) { @@ -244,15 +247,17 @@ class DriftTimelineRepository extends DriftDatabaseRepository { .get(); } - TimelineQuery fromAssets(List assets) => ( + TimelineQuery fromAssets(List assets, TimelineOrigin origin) => ( bucketSource: () => Stream.value(_generateBuckets(assets.length)), assetSource: (offset, count) => Future.value(assets.skip(offset).take(count).toList(growable: false)), + origin: origin, ); TimelineQuery remote(String ownerId, GroupAssetsBy groupBy) => _remoteQueryBuilder( filter: (row) => row.deletedAt.isNull() & row.visibility.equalsValue(AssetVisibility.timeline) & row.ownerId.equals(ownerId), groupBy: groupBy, + origin: TimelineOrigin.remoteAssets, ); TimelineQuery favorite(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder( @@ -262,11 +267,13 @@ class DriftTimelineRepository extends DriftDatabaseRepository { row.ownerId.equals(userId) & row.visibility.equalsValue(AssetVisibility.timeline), groupBy: groupBy, + origin: TimelineOrigin.favorite, ); TimelineQuery trash(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder( filter: (row) => row.deletedAt.isNotNull() & row.ownerId.equals(userId), groupBy: groupBy, + origin: TimelineOrigin.trash, joinLocal: true, ); @@ -274,11 +281,13 @@ class DriftTimelineRepository extends DriftDatabaseRepository { filter: (row) => row.deletedAt.isNull() & row.ownerId.equals(userId) & row.visibility.equalsValue(AssetVisibility.archive), groupBy: groupBy, + origin: TimelineOrigin.archive, ); TimelineQuery locked(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder( filter: (row) => row.deletedAt.isNull() & row.visibility.equalsValue(AssetVisibility.locked) & row.ownerId.equals(userId), + origin: TimelineOrigin.lockedFolder, groupBy: groupBy, ); @@ -288,17 +297,20 @@ class DriftTimelineRepository extends DriftDatabaseRepository { row.type.equalsValue(AssetType.video) & row.visibility.equalsValue(AssetVisibility.timeline) & row.ownerId.equals(userId), + origin: TimelineOrigin.video, groupBy: groupBy, ); TimelineQuery place(String place, GroupAssetsBy groupBy) => ( bucketSource: () => _watchPlaceBucket(place, groupBy: groupBy), assetSource: (offset, count) => _getPlaceBucketAssets(place, offset: offset, count: count), + origin: TimelineOrigin.place, ); TimelineQuery person(String userId, String personId, GroupAssetsBy groupBy) => ( bucketSource: () => _watchPersonBucket(userId, personId, groupBy: groupBy), assetSource: (offset, count) => _getPersonBucketAssets(userId, personId, offset: offset, count: count), + origin: TimelineOrigin.person, ); Stream> _watchPlaceBucket(String place, {GroupAssetsBy groupBy = GroupAssetsBy.day}) { @@ -434,6 +446,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { TimelineQuery map(String userId, LatLngBounds bounds, GroupAssetsBy groupBy) => ( bucketSource: () => _watchMapBucket(userId, bounds, groupBy: groupBy), assetSource: (offset, count) => _getMapBucketAssets(userId, bounds, offset: offset, count: count), + origin: TimelineOrigin.map, ); Stream> _watchMapBucket( @@ -502,6 +515,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { @pragma('vm:prefer-inline') TimelineQuery _remoteQueryBuilder({ required Expression Function($RemoteAssetEntityTable row) filter, + required TimelineOrigin origin, GroupAssetsBy groupBy = GroupAssetsBy.day, bool joinLocal = false, }) { @@ -509,6 +523,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { bucketSource: () => _watchRemoteBucket(filter: filter, groupBy: groupBy), assetSource: (offset, count) => _getRemoteAssets(filter: filter, offset: offset, count: count, joinLocal: joinLocal), + origin: origin, ); } diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index 92716144f3..425c4985e9 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; @@ -624,7 +625,7 @@ class _SearchResultGrid extends ConsumerWidget { child: ProviderScope( overrides: [ timelineServiceProvider.overrideWith((ref) { - final timelineService = ref.watch(timelineFactoryProvider).fromAssets(assets); + final timelineService = ref.watch(timelineFactoryProvider).fromAssets(assets, TimelineOrigin.search); ref.onDispose(timelineService.dispose); return timelineService; }), diff --git a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart index c0e93a73cc..38ad9d17de 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart @@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.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'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; @@ -17,8 +18,8 @@ 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/providers/routes.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -39,13 +40,13 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { final isInLockedView = ref.watch(inLockedViewProvider); final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); - final previousRouteName = ref.watch(previousRouteNameProvider); - final tabRoute = ref.watch(tabProvider); + final timelineOrigin = ref.read(timelineServiceProvider).origin; final showViewInTimelineButton = - (previousRouteName != TabShellRoute.name || tabRoute == TabEnum.search) && - previousRouteName != AssetViewerRoute.name && - previousRouteName != null && - previousRouteName != LocalTimelineRoute.name && + timelineOrigin != TimelineOrigin.main && + timelineOrigin != TimelineOrigin.deepLink && + timelineOrigin != TimelineOrigin.trash && + timelineOrigin != TimelineOrigin.archive && + timelineOrigin != TimelineOrigin.localAlbum && isOwner; final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet)); diff --git a/mobile/lib/services/deep_link.service.dart b/mobile/lib/services/deep_link.service.dart index 6226781919..7c8ddce265 100644 --- a/mobile/lib/services/deep_link.service.dart +++ b/mobile/lib/services/deep_link.service.dart @@ -8,8 +8,8 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; 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/asset.provider.dart' as beta_asset_provider; 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'; @@ -150,7 +150,10 @@ class DeepLinkService { } AssetViewer.setAsset(ref, asset); - return AssetViewerRoute(initialIndex: 0, timelineService: _betaTimelineFactory.fromAssets([asset])); + return AssetViewerRoute( + initialIndex: 0, + timelineService: _betaTimelineFactory.fromAssets([asset], TimelineOrigin.deepLink), + ); } else { // TODO: Remove this when beta is default final asset = await _assetService.getAssetByRemoteId(assetId);