feat(mobile): drift recently taken page (#19814)

* feat(mobile): drift recently taken page

* fix: lint

* refactor(mobile): timeline queries (#19818)

refactor: remote assets query to take single user id

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>

---------

Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com>
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
Daimolean 2025-07-08 21:54:29 +08:00 committed by GitHub
parent df4a27e8a7
commit 172388c455
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 175 additions and 77 deletions

View File

@ -58,11 +58,11 @@ class TimelineFactory {
), ),
); );
TimelineService remoteAssets(List<String> timelineUsers) => TimelineService( TimelineService remoteAssets(String ownerId) => TimelineService(
assetSource: (offset, count) => _timelineRepository assetSource: (offset, count) => _timelineRepository
.getRemoteBucketAssets(timelineUsers, offset: offset, count: count), .getRemoteBucketAssets(ownerId, offset: offset, count: count),
bucketSource: () => _timelineRepository.watchRemoteBucket( bucketSource: () => _timelineRepository.watchRemoteBucket(
timelineUsers, ownerId,
groupBy: GroupAssetsBy.month, groupBy: GroupAssetsBy.month,
), ),
); );

View File

@ -18,13 +18,21 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
_db.remoteAlbumAssetEntity.albumId.equalsExp(_db.remoteAlbumEntity.id), _db.remoteAlbumAssetEntity.albumId.equalsExp(_db.remoteAlbumEntity.id),
useColumns: false, useColumns: false,
), ),
leftOuterJoin(
_db.remoteAssetEntity,
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId),
useColumns: false,
),
leftOuterJoin( leftOuterJoin(
_db.userEntity, _db.userEntity,
_db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId), _db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId),
useColumns: false,
), ),
]); ]);
query query
..where(_db.remoteAssetEntity.deletedAt.isNull())
..addColumns([assetCount]) ..addColumns([assetCount])
..addColumns([_db.userEntity.name])
..groupBy([_db.remoteAlbumEntity.id]); ..groupBy([_db.remoteAlbumEntity.id]);
if (sortBy.isNotEmpty) { if (sortBy.isNotEmpty) {
@ -43,7 +51,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
.map( .map(
(row) => row.readTable(_db.remoteAlbumEntity).toDto( (row) => row.readTable(_db.remoteAlbumEntity).toDto(
assetCount: row.read(assetCount) ?? 0, assetCount: row.read(assetCount) ?? 0,
ownerName: row.readTable(_db.userEntity).name, ownerName: row.read(_db.userEntity.name)!,
), ),
) )
.get(); .get();

View File

@ -124,6 +124,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
innerJoin( innerJoin(
_db.localAlbumAssetEntity, _db.localAlbumAssetEntity,
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
useColumns: false,
), ),
]) ])
..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) ..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
@ -147,6 +148,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
innerJoin( innerJoin(
_db.localAlbumAssetEntity, _db.localAlbumAssetEntity,
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
useColumns: false,
), ),
], ],
) )
@ -179,9 +181,13 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
_db.remoteAlbumAssetEntity, _db.remoteAlbumAssetEntity,
_db.remoteAlbumAssetEntity.assetId _db.remoteAlbumAssetEntity.assetId
.equalsExp(_db.remoteAssetEntity.id), .equalsExp(_db.remoteAssetEntity.id),
useColumns: false,
), ),
]) ])
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId)) ..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAlbumAssetEntity.albumId.equals(albumId),
)
..groupBy([dateExp]) ..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]); ..orderBy([OrderingTerm.desc(dateExp)]);
@ -203,10 +209,14 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
_db.remoteAlbumAssetEntity, _db.remoteAlbumAssetEntity,
_db.remoteAlbumAssetEntity.assetId _db.remoteAlbumAssetEntity.assetId
.equalsExp(_db.remoteAssetEntity.id), .equalsExp(_db.remoteAssetEntity.id),
useColumns: false,
), ),
], ],
) )
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId)) ..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAlbumAssetEntity.albumId.equals(albumId),
)
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]) ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
..limit(count, offset: offset); ..limit(count, offset: offset);
return query return query
@ -214,15 +224,17 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
.get(); .get();
} }
Stream<List<Bucket>> watchFavoriteBucket( Stream<List<Bucket>> watchRemoteBucket(
String userId, { String ownerId, {
GroupAssetsBy groupBy = GroupAssetsBy.day, GroupAssetsBy groupBy = GroupAssetsBy.day,
}) { }) {
if (groupBy == GroupAssetsBy.none) { if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity return _db.remoteAssetEntity
.count( .count(
where: (row) => where: (row) =>
row.isFavorite.equals(true) & row.ownerId.equals(userId), row.deletedAt.isNull() &
row.visibility.equalsValue(AssetVisibility.timeline) &
row.ownerId.equals(ownerId),
) )
.map(_generateBuckets) .map(_generateBuckets)
.watchSingle(); .watchSingle();
@ -234,6 +246,62 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final query = _db.remoteAssetEntity.selectOnly() final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp]) ..addColumns([assetCountExp, dateExp])
..where( ..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.timeline) &
_db.remoteAssetEntity.ownerId.equals(ownerId),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> getRemoteBucketAssets(
String ownerId, {
required int offset,
required int count,
}) {
final query = _db.remoteAssetEntity.select()
..where(
(row) =>
row.deletedAt.isNull() &
row.visibility.equalsValue(AssetVisibility.timeline) &
row.ownerId.equals(ownerId),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
Stream<List<Bucket>> watchFavoriteBucket(
String userId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.deletedAt.isNull() &
row.isFavorite.equals(true) &
row.ownerId.equals(userId),
)
.map(_generateBuckets)
.watchSingle();
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.ownerId.equals(userId) & _db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.isFavorite.equals(true), _db.remoteAssetEntity.isFavorite.equals(true),
) )
@ -254,7 +322,10 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
}) { }) {
final query = _db.remoteAssetEntity.select() final query = _db.remoteAssetEntity.select()
..where( ..where(
(row) => row.isFavorite.equals(true) & row.ownerId.equals(userId), (row) =>
row.deletedAt.isNull() &
row.isFavorite.equals(true) &
row.ownerId.equals(userId),
) )
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) ..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset); ..limit(count, offset: offset);
@ -318,6 +389,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
return _db.remoteAssetEntity return _db.remoteAssetEntity
.count( .count(
where: (row) => where: (row) =>
row.deletedAt.isNull() &
row.visibility.equalsValue(AssetVisibility.archive) & row.visibility.equalsValue(AssetVisibility.archive) &
row.ownerId.equals(userId), row.ownerId.equals(userId),
) )
@ -331,6 +403,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final query = _db.remoteAssetEntity.selectOnly() final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp]) ..addColumns([assetCountExp, dateExp])
..where( ..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.ownerId.equals(userId) & _db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility _db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.archive), .equalsValue(AssetVisibility.archive),
@ -353,6 +426,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final query = _db.remoteAssetEntity.select() final query = _db.remoteAssetEntity.select()
..where( ..where(
(row) => (row) =>
row.deletedAt.isNull() &
row.ownerId.equals(userId) & row.ownerId.equals(userId) &
row.visibility.equalsValue(AssetVisibility.archive), row.visibility.equalsValue(AssetVisibility.archive),
) )
@ -370,6 +444,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
return _db.remoteAssetEntity return _db.remoteAssetEntity
.count( .count(
where: (row) => where: (row) =>
row.deletedAt.isNull() &
row.visibility.equalsValue(AssetVisibility.locked) & row.visibility.equalsValue(AssetVisibility.locked) &
row.ownerId.equals(userId), row.ownerId.equals(userId),
) )
@ -383,6 +458,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final query = _db.remoteAssetEntity.selectOnly() final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp]) ..addColumns([assetCountExp, dateExp])
..where( ..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.ownerId.equals(userId) & _db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility _db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.locked), .equalsValue(AssetVisibility.locked),
@ -405,6 +481,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final query = _db.remoteAssetEntity.select() final query = _db.remoteAssetEntity.select()
..where( ..where(
(row) => (row) =>
row.deletedAt.isNull() &
row.visibility.equalsValue(AssetVisibility.locked) & row.visibility.equalsValue(AssetVisibility.locked) &
row.ownerId.equals(userId), row.ownerId.equals(userId),
) )
@ -422,6 +499,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
return _db.remoteAssetEntity return _db.remoteAssetEntity
.count( .count(
where: (row) => where: (row) =>
row.deletedAt.isNull() &
row.type.equalsValue(AssetType.video) & row.type.equalsValue(AssetType.video) &
row.visibility.equalsValue(AssetVisibility.timeline) & row.visibility.equalsValue(AssetVisibility.timeline) &
row.ownerId.equals(userId), row.ownerId.equals(userId),
@ -436,6 +514,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final query = _db.remoteAssetEntity.selectOnly() final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp]) ..addColumns([assetCountExp, dateExp])
..where( ..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.ownerId.equals(userId) & _db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.type.equalsValue(AssetType.video) & _db.remoteAssetEntity.type.equalsValue(AssetType.video) &
_db.remoteAssetEntity.visibility _db.remoteAssetEntity.visibility
@ -459,71 +538,16 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final query = _db.remoteAssetEntity.select() final query = _db.remoteAssetEntity.select()
..where( ..where(
(row) => (row) =>
_db.remoteAssetEntity.type.equalsValue(AssetType.video) & row.deletedAt.isNull() &
_db.remoteAssetEntity.visibility row.type.equalsValue(AssetType.video) &
.equalsValue(AssetVisibility.timeline) & row.visibility.equalsValue(AssetVisibility.timeline) &
_db.remoteAssetEntity.ownerId.equals(userId), row.ownerId.equals(userId),
) )
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) ..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset); ..limit(count, offset: offset);
return query.map((row) => row.toDto()).get(); return query.map((row) => row.toDto()).get();
} }
Stream<List<Bucket>> watchRemoteBucket(
List<String> userIds, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.deletedAt.isNull() &
row.visibility.equalsValue(AssetVisibility.timeline) &
row.ownerId.isIn(userIds),
)
.map(_generateBuckets)
.watchSingle();
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.timeline) &
_db.remoteAssetEntity.ownerId.isIn(userIds),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> getRemoteBucketAssets(
List<String> userIds, {
required int offset,
required int count,
}) {
final query = _db.remoteAssetEntity.select()
..where(
(row) =>
row.deletedAt.isNull() &
row.visibility.equalsValue(AssetVisibility.timeline) &
row.ownerId.isIn(userIds),
)
..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
} }
extension on Expression<DateTime> { extension on Expression<DateTime> {

View File

@ -0,0 +1,35 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@RoutePage()
class DriftRecentlyTakenPage extends StatelessWidget {
const DriftRecentlyTakenPage({super.key});
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception(
'User must be logged in to access recently taken',
);
}
final timelineService =
ref.watch(timelineFactoryProvider).remoteAssets(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
],
child: const Timeline(),
);
}
}

View File

@ -132,6 +132,11 @@ final _features = [
icon: Icons.video_collection_outlined, icon: Icons.video_collection_outlined,
onTap: (ctx, _) => ctx.pushRoute(const DriftVideoRoute()), onTap: (ctx, _) => ctx.pushRoute(const DriftVideoRoute()),
), ),
_Feature(
name: 'Recently Taken',
icon: Icons.schedule_outlined,
onTap: (ctx, _) => ctx.pushRoute(const DriftRecentlyTakenRoute()),
),
]; ];
@RoutePage() @RoutePage()

View File

@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@RoutePage() @RoutePage()
class DriftAssetSelectionTimelinePage extends ConsumerWidget { class DriftAssetSelectionTimelinePage extends ConsumerWidget {
@ -29,10 +30,15 @@ class DriftAssetSelectionTimelinePage extends ConsumerWidget {
), ),
timelineServiceProvider.overrideWith( timelineServiceProvider.overrideWith(
(ref) { (ref) {
final timelineUsers = final user = ref.watch(currentUserProvider);
ref.watch(timelineUsersProvider).valueOrNull ?? []; if (user == null) {
throw Exception(
'User must be logged in to access recently taken',
);
}
final timelineService = final timelineService =
ref.watch(timelineFactoryProvider).remoteAssets(timelineUsers); ref.watch(timelineFactoryProvider).remoteAssets(user.id);
ref.onDispose(timelineService.dispose); ref.onDispose(timelineService.dispose);
return timelineService; return timelineService;
}, },

View File

@ -68,6 +68,7 @@ import 'package:immich_mobile/pages/search/recently_taken.page.dart';
import 'package:immich_mobile/pages/search/search.page.dart'; import 'package:immich_mobile/pages/search/search.page.dart';
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_favorite.page.dart'; import 'package:immich_mobile/presentation/pages/dev/drift_favorite.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_recently_taken.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_video.page.dart'; import 'package:immich_mobile/presentation/pages/dev/drift_video.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_trash.page.dart'; import 'package:immich_mobile/presentation/pages/dev/drift_trash.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_archive.page.dart'; import 'package:immich_mobile/presentation/pages/dev/drift_archive.page.dart';
@ -428,7 +429,10 @@ class AppRouter extends RootStackRouter {
page: DriftAssetSelectionTimelineRoute.page, page: DriftAssetSelectionTimelineRoute.page,
guards: [_authGuard, _duplicateGuard], guards: [_authGuard, _duplicateGuard],
), ),
AutoRoute(
page: DriftRecentlyTakenRoute.page,
guards: [_authGuard, _duplicateGuard],
),
// required to handle all deeplinks in deep_link.service.dart // required to handle all deeplinks in deep_link.service.dart
// auto_route_library#1722 // auto_route_library#1722
RedirectRoute(path: '*', redirectTo: '/'), RedirectRoute(path: '*', redirectTo: '/'),

View File

@ -783,6 +783,22 @@ class DriftMemoryRouteArgs {
} }
} }
/// generated route for
/// [DriftRecentlyTakenPage]
class DriftRecentlyTakenRoute extends PageRouteInfo<void> {
const DriftRecentlyTakenRoute({List<PageRouteInfo>? children})
: super(DriftRecentlyTakenRoute.name, initialChildren: children);
static const String name = 'DriftRecentlyTakenRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const DriftRecentlyTakenPage();
},
);
}
/// generated route for /// generated route for
/// [DriftTrashPage] /// [DriftTrashPage]
class DriftTrashRoute extends PageRouteInfo<void> { class DriftTrashRoute extends PageRouteInfo<void> {