From 5579554532e0319321da97166640cb8a729ae9a1 Mon Sep 17 00:00:00 2001 From: Daimolean <92239625+wuzihao051119@users.noreply.github.com> Date: Mon, 7 Jul 2025 12:05:40 +0800 Subject: [PATCH] feat(mobile): drift trash page (#19745) * feat(mobile): drift trash page * feat: include local indicator * remove join in bucket * remove indicator join --------- Co-authored-by: Alex Tran --- .../lib/domain/services/timeline.service.dart | 7 +++ .../repositories/timeline.repository.dart | 48 +++++++++++++++++++ .../pages/dev/drift_trash.page.dart | 33 +++++++++++++ .../pages/dev/feat_in_development.page.dart | 5 ++ mobile/lib/routing/router.dart | 5 ++ mobile/lib/routing/router.gr.dart | 16 +++++++ 6 files changed, 114 insertions(+) create mode 100644 mobile/lib/presentation/pages/dev/drift_trash.page.dart diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index 748bad5047..fc8b2a86fd 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -54,6 +54,13 @@ class TimelineFactory { _timelineRepository.watchRemoteBucket(albumId, groupBy: groupBy), ); + TimelineService trash(String userId) => TimelineService( + assetSource: (offset, count) => _timelineRepository + .getTrashBucketAssets(userId, offset: offset, count: count), + bucketSource: () => + _timelineRepository.watchTrashBucket(userId, groupBy: groupBy), + ); + TimelineService archive(String userId) => TimelineService( assetSource: (offset, count) => _timelineRepository .getArchiveBucketAssets(userId, offset: offset, count: count), diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index b234f92c87..acddb38534 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -214,6 +214,54 @@ class DriftTimelineRepository extends DriftDatabaseRepository { .get(); } + Stream> watchTrashBucket( + String userId, { + GroupAssetsBy groupBy = GroupAssetsBy.day, + }) { + if (groupBy == GroupAssetsBy.none) { + return _db.remoteAssetEntity + .count( + where: (row) => + row.deletedAt.isNotNull() & 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.ownerId.equals(userId) & + _db.remoteAssetEntity.deletedAt.isNotNull(), + ) + ..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> getTrashBucketAssets( + String userId, { + required int offset, + required int count, + }) { + final query = _db.remoteAssetEntity.select() + ..where( + (row) => row.deletedAt.isNotNull() & row.ownerId.equals(userId), + ) + ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) + ..limit(count, offset: offset); + + return query.map((row) => row.toDto()).get(); + } + Stream> watchArchiveBucket( String userId, { GroupAssetsBy groupBy = GroupAssetsBy.day, diff --git a/mobile/lib/presentation/pages/dev/drift_trash.page.dart b/mobile/lib/presentation/pages/dev/drift_trash.page.dart new file mode 100644 index 0000000000..cbcfe50112 --- /dev/null +++ b/mobile/lib/presentation/pages/dev/drift_trash.page.dart @@ -0,0 +1,33 @@ +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 DriftTrashPage extends StatelessWidget { + const DriftTrashPage({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 trash'); + } + + final timelineService = + ref.watch(timelineFactoryProvider).trash(user.id); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + ), + ], + child: const Timeline(), + ); + } +} diff --git a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart index 823615f503..8bfd02ab31 100644 --- a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart +++ b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart @@ -99,6 +99,11 @@ final _features = [ icon: Icons.timeline_rounded, onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()), ), + _Feature( + name: 'Trash', + icon: Icons.delete_outline_rounded, + onTap: (ctx, _) => ctx.pushRoute(const DriftTrashRoute()), + ), _Feature( name: 'Archive', icon: Icons.archive_outlined, diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 9e27feaa8b..85e8700300 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -66,6 +66,7 @@ import 'package:immich_mobile/pages/search/person_result.page.dart'; import 'package:immich_mobile/pages/search/recently_taken.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/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/feat_in_development.page.dart'; import 'package:immich_mobile/presentation/pages/dev/local_timeline.page.dart'; @@ -393,6 +394,10 @@ class AppRouter extends RootStackRouter { page: DriftMemoryRoute.page, guards: [_authGuard, _duplicateGuard], ), + AutoRoute( + page: DriftTrashRoute.page, + guards: [_authGuard, _duplicateGuard], + ), AutoRoute( page: DriftArchiveRoute.page, guards: [_authGuard, _duplicateGuard], diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 8c162cea7a..0a861898aa 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -686,6 +686,22 @@ class DriftMemoryRouteArgs { } } +/// generated route for +/// [DriftTrashPage] +class DriftTrashRoute extends PageRouteInfo { + const DriftTrashRoute({List? children}) + : super(DriftTrashRoute.name, initialChildren: children); + + static const String name = 'DriftTrashRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftTrashPage(); + }, + ); +} + /// generated route for /// [EditImagePage] class EditImageRoute extends PageRouteInfo {