From 241231c73e1a58e1bcbcf1f57da38befa2c7cb6c Mon Sep 17 00:00:00 2001 From: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:08:17 +0530 Subject: [PATCH] feat: all local assets in album asset selection --- .../lib/domain/services/timeline.service.dart | 3 +- .../entities/merged_asset.drift | 52 +++++++++++-------- .../repositories/timeline.repository.dart | 37 ++++++++----- .../drift_asset_selection_timeline.page.dart | 2 +- .../repositories/merged_asset_drift_test.dart | 4 +- .../timeline_repository_test.dart | 46 ++++++++++++++++ 6 files changed, 108 insertions(+), 36 deletions(-) diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index b20ba306ff..33ceb9454c 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -49,7 +49,8 @@ class TimelineFactory { return group == GroupAssetsBy.auto ? GroupAssetsBy.day : group; } - TimelineService main(List timelineUsers) => TimelineService(_timelineRepository.main(timelineUsers, groupBy)); + TimelineService main(List timelineUsers, {bool ignoreBackupSelection = false}) => + TimelineService(_timelineRepository.main(timelineUsers, groupBy, ignoreBackupSelection: ignoreBackupSelection)); TimelineService localAlbum({required String albumId}) => TimelineService(_timelineRepository.localAlbum(albumId, groupBy)); diff --git a/mobile/lib/infrastructure/entities/merged_asset.drift b/mobile/lib/infrastructure/entities/merged_asset.drift index d0321ab1ef..5dabb22d95 100644 --- a/mobile/lib/infrastructure/entities/merged_asset.drift +++ b/mobile/lib/infrastructure/entities/merged_asset.drift @@ -4,7 +4,7 @@ import 'local_asset.entity.dart'; import 'local_album.entity.dart'; import 'local_album_asset.entity.dart'; -mergedAsset: +mergedAsset(:ignore_backup_selection AS BOOLEAN): SELECT rae.id as remote_id, (SELECT lae.id FROM local_asset_entity lae WHERE lae.checksum = rae.checksum LIMIT 1) as local_id, @@ -73,20 +73,25 @@ FROM WHERE NOT EXISTS ( SELECT 1 FROM remote_asset_entity rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN :user_ids ) -AND EXISTS ( - SELECT 1 FROM local_album_asset_entity laa - INNER JOIN local_album_entity la on laa.album_id = la.id - WHERE laa.asset_id = lae.id AND la.backup_selection = 0 -- selected -) -AND NOT EXISTS ( - SELECT 1 FROM local_album_asset_entity laa - INNER JOIN local_album_entity la on laa.album_id = la.id - WHERE laa.asset_id = lae.id AND la.backup_selection = 2 -- excluded +AND ( + :ignore_backup_selection + OR ( + EXISTS ( + SELECT 1 FROM local_album_asset_entity laa + INNER JOIN local_album_entity la on laa.album_id = la.id + WHERE laa.asset_id = lae.id AND la.backup_selection = 0 -- selected + ) + AND NOT EXISTS ( + SELECT 1 FROM local_album_asset_entity laa + INNER JOIN local_album_entity la on laa.album_id = la.id + WHERE laa.asset_id = lae.id AND la.backup_selection = 2 -- excluded + ) + ) ) ORDER BY created_at DESC LIMIT $limit; -mergedBucket(:group_by AS INTEGER): +mergedBucket(:group_by AS INTEGER, :ignore_backup_selection AS BOOLEAN): SELECT COUNT(*) as asset_count, bucket_date @@ -106,7 +111,7 @@ FROM FROM remote_asset_entity rae LEFT JOIN - stack_entity se ON rae.stack_id = se.id + stack_entity se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 -- timeline visibility @@ -126,15 +131,20 @@ FROM WHERE NOT EXISTS ( SELECT 1 FROM remote_asset_entity rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN :user_ids ) - AND EXISTS ( - SELECT 1 FROM local_album_asset_entity laa - INNER JOIN local_album_entity la on laa.album_id = la.id - WHERE laa.asset_id = lae.id AND la.backup_selection = 0 -- selected - ) - AND NOT EXISTS ( - SELECT 1 FROM local_album_asset_entity laa - INNER JOIN local_album_entity la on laa.album_id = la.id - WHERE laa.asset_id = lae.id AND la.backup_selection = 2 -- excluded + AND ( + :ignore_backup_selection + OR ( + EXISTS ( + SELECT 1 FROM local_album_asset_entity laa + INNER JOIN local_album_entity la on laa.album_id = la.id + WHERE laa.asset_id = lae.id AND la.backup_selection = 0 -- selected + ) + AND NOT EXISTS ( + SELECT 1 FROM local_album_asset_entity laa + INNER JOIN local_album_entity la on laa.album_id = la.id + WHERE laa.asset_id = lae.id AND la.backup_selection = 2 -- excluded + ) + ) ) ) GROUP BY bucket_date diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index 6a9fbdb204..d05e16e51b 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -50,26 +50,39 @@ class DriftTimelineRepository extends DriftDatabaseRepository { .map((users) => users..add(userId)); } - TimelineQuery main(List userIds, GroupAssetsBy groupBy) => ( - bucketSource: () => _watchMainBucket(userIds, groupBy: groupBy), - assetSource: (offset, count) => _getMainBucketAssets(userIds, offset: offset, count: count), - origin: TimelineOrigin.main, + TimelineQuery main(List userIds, GroupAssetsBy groupBy, {bool ignoreBackupSelection = false}) => ( + bucketSource: () => _watchMainBucket(userIds, groupBy: groupBy, ignoreBackupSelection: ignoreBackupSelection), + assetSource: (offset, count) => + _getMainBucketAssets(userIds, offset: offset, count: count, ignoreBackupSelection: ignoreBackupSelection), + origin: .main, ); - Stream> _watchMainBucket(List userIds, {GroupAssetsBy groupBy = GroupAssetsBy.day}) { - if (groupBy == GroupAssetsBy.none) { + Stream> _watchMainBucket( + List userIds, { + GroupAssetsBy groupBy = .day, + bool ignoreBackupSelection = false, + }) { + if (groupBy == .none) { throw UnsupportedError("GroupAssetsBy.none is not supported for watchMainBucket"); } - return _db.mergedAssetDrift.mergedBucket(userIds: userIds, groupBy: groupBy.index).map((row) { - final date = row.bucketDate.truncateDate(groupBy); - return TimeBucket(date: date, assetCount: row.assetCount); - }).watch(); + return _db.mergedAssetDrift + .mergedBucket(userIds: userIds, groupBy: groupBy.index, ignoreBackupSelection: ignoreBackupSelection) + .map((row) { + final date = row.bucketDate.truncateDate(groupBy); + return TimeBucket(date: date, assetCount: row.assetCount); + }) + .watch(); } - Future> _getMainBucketAssets(List userIds, {required int offset, required int count}) { + Future> _getMainBucketAssets( + List userIds, { + required int offset, + required int count, + bool ignoreBackupSelection = false, + }) { return _db.mergedAssetDrift - .mergedAsset(userIds: userIds, limit: (_) => Limit(count, offset)) + .mergedAsset(userIds: userIds, limit: (_) => Limit(count, offset), ignoreBackupSelection: ignoreBackupSelection) .map( (row) => row.remoteId != null && row.ownerId != null ? RemoteAsset( diff --git a/mobile/lib/presentation/pages/drift_asset_selection_timeline.page.dart b/mobile/lib/presentation/pages/drift_asset_selection_timeline.page.dart index a12ca4932b..42e36b0f8b 100644 --- a/mobile/lib/presentation/pages/drift_asset_selection_timeline.page.dart +++ b/mobile/lib/presentation/pages/drift_asset_selection_timeline.page.dart @@ -22,7 +22,7 @@ class DriftAssetSelectionTimelinePage extends ConsumerWidget { ), timelineServiceProvider.overrideWith((ref) { final timelineUsers = ref.watch(timelineUsersProvider).valueOrNull ?? []; - final timelineService = ref.watch(timelineFactoryProvider).main(timelineUsers); + final timelineService = ref.watch(timelineFactoryProvider).main(timelineUsers, ignoreBackupSelection: true); ref.onDispose(timelineService.dispose); return timelineService; }), diff --git a/mobile/test/infrastructure/repositories/merged_asset_drift_test.dart b/mobile/test/infrastructure/repositories/merged_asset_drift_test.dart index a5fe6f35c4..d9e6f36b3c 100644 --- a/mobile/test/infrastructure/repositories/merged_asset_drift_test.dart +++ b/mobile/test/infrastructure/repositories/merged_asset_drift_test.dart @@ -43,7 +43,9 @@ void main() { ), ); - final buckets = await db.mergedAssetDrift.mergedBucket(groupBy: GroupAssetsBy.day.index, userIds: [userId]).get(); + final buckets = await db.mergedAssetDrift + .mergedBucket(groupBy: GroupAssetsBy.day.index, userIds: [userId], ignoreBackupSelection: false) + .get(); expect(buckets, hasLength(1)); expect(buckets.single.assetCount, 1); diff --git a/mobile/test/medium/repositories/timeline_repository_test.dart b/mobile/test/medium/repositories/timeline_repository_test.dart index d78d9b1ef7..17e924fc2c 100644 --- a/mobile/test/medium/repositories/timeline_repository_test.dart +++ b/mobile/test/medium/repositories/timeline_repository_test.dart @@ -102,4 +102,50 @@ void main() { expect(remote.localId, local.id); }); }); + + group('main backup selection', () { + test('excludes local assets whose album is not selected for backup', () async { + final user = await ctx.newUser(); + final album = await ctx.newLocalAlbum(backupSelection: .none); + final asset = await ctx.newLocalAsset(); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id); + + final query = sut.main([user.id], .day); + + expect(await query.bucketSource().first, isEmpty); + expect(await query.assetSource(0, 10), isEmpty); + }); + + test('excludes local assets in an excluded album even when also in a selected album', () async { + final user = await ctx.newUser(); + final selected = await ctx.newLocalAlbum(backupSelection: .selected); + final excluded = await ctx.newLocalAlbum(backupSelection: .excluded); + final asset = await ctx.newLocalAsset(); + await ctx.newLocalAlbumAsset(albumId: selected.id, assetId: asset.id); + await ctx.newLocalAlbumAsset(albumId: excluded.id, assetId: asset.id); + + final query = sut.main([user.id], .day); + + expect(await query.bucketSource().first, isEmpty); + expect(await query.assetSource(0, 10), isEmpty); + }); + + test('ignoreBackupSelection includes local assets regardless of album selection', () async { + final user = await ctx.newUser(); + final none = await ctx.newLocalAlbum(backupSelection: .none); + final excluded = await ctx.newLocalAlbum(backupSelection: .excluded); + final unselectedAsset = await ctx.newLocalAsset(); + final excludedAsset = await ctx.newLocalAsset(); + await ctx.newLocalAlbumAsset(albumId: none.id, assetId: unselectedAsset.id); + await ctx.newLocalAlbumAsset(albumId: excluded.id, assetId: excludedAsset.id); + + final query = sut.main([user.id], .day, ignoreBackupSelection: true); + + final buckets = await query.bucketSource().first; + expect(buckets.fold(0, (sum, bucket) => sum + bucket.assetCount), 2); + + final assets = await query.assetSource(0, 10); + expect(assets.map((asset) => (asset as LocalAsset).id), containsAll([unselectedAsset.id, excludedAsset.id])); + }); + }); }