diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index 0d31f06e74..2dc3e794ce 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -68,6 +68,9 @@ class TimelineFactory { TimelineService fromAssets(List assets) => TimelineService(_timelineRepository.fromAssets(assets)); + + TimelineService person(String personId) => + TimelineService(_timelineRepository.person(personId, groupBy)); } class TimelineService { diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index 7bbae9a80a..554fd9a345 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -441,6 +441,78 @@ class DriftTimelineRepository extends DriftDatabaseRepository { .get(); } + TimelineQuery person(String personId, GroupAssetsBy groupBy) => ( + bucketSource: () => _watchPersonBucket(personId, groupBy: groupBy), + assetSource: (offset, count) => + _getPersonBucketAssets(personId, offset: offset, count: count), + ); + + Stream> _watchPersonBucket( + String personId, { + GroupAssetsBy groupBy = GroupAssetsBy.day, + }) { + if (groupBy == GroupAssetsBy.none) { + // TODO: implement GroupAssetBy for person + throw UnsupportedError( + "GroupAssetsBy.none is not supported for watchPersonBucket", + ); + } + + final assetCountExp = _db.remoteAssetEntity.id.count(); + final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); + + final query = _db.remoteAssetEntity.selectOnly() + ..addColumns([assetCountExp, dateExp]) + ..join([ + innerJoin( + _db.assetFaceEntity, + _db.assetFaceEntity.assetId.equalsExp(_db.remoteAssetEntity.id), + useColumns: false, + ), + ]) + ..where( + _db.remoteAssetEntity.deletedAt.isNull() & + _db.remoteAssetEntity.visibility + .equalsValue(AssetVisibility.timeline) & + _db.assetFaceEntity.personId.equals(personId), + ) + ..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> _getPersonBucketAssets( + String personId, { + required int offset, + required int count, + }) { + final query = _db.remoteAssetEntity.select().join( + [ + innerJoin( + _db.assetFaceEntity, + _db.assetFaceEntity.assetId.equalsExp(_db.remoteAssetEntity.id), + useColumns: false, + ), + ], + ) + ..where( + _db.remoteAssetEntity.deletedAt.isNull() & + _db.remoteAssetEntity.visibility + .equalsValue(AssetVisibility.timeline) & + _db.assetFaceEntity.personId.equals(personId), + ) + ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]) + ..limit(count, offset: offset); + return query + .map((row) => row.readTable(_db.remoteAssetEntity).toDto()) + .get(); + } + TimelineQuery _remoteQueryBuilder({ required Expression Function($RemoteAssetEntityTable row) filter, GroupAssetsBy groupBy = GroupAssetsBy.day, diff --git a/mobile/lib/presentation/pages/drift_person_detail.dart b/mobile/lib/presentation/pages/drift_person_detail.dart new file mode 100644 index 0000000000..2873df8c0b --- /dev/null +++ b/mobile/lib/presentation/pages/drift_person_detail.dart @@ -0,0 +1,36 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/person.model.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; + +@RoutePage() +class DriftPersonDetailPage extends StatelessWidget { + final Person person; + + const DriftPersonDetailPage({super.key, required this.person}); + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith( + (ref) { + final timelineService = + ref.watch(timelineFactoryProvider).person(person.id); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + ), + ], + child: Timeline( + appBar: MesmerizingSliverAppBar( + title: person.name, + icon: Icons.person_outline, + ), + ), + ); + } +} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 5b3a92d4e2..ff2cec635f 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -6,6 +6,7 @@ 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/models/log.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; +import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/entities/album.entity.dart'; @@ -95,6 +96,7 @@ import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart' import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_trash.page.dart'; import 'package:immich_mobile/presentation/pages/drift_video.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_person_detail.dart'; import 'package:immich_mobile/presentation/pages/local_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/search/drift_search.page.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; @@ -486,7 +488,6 @@ class AppRouter extends RootStackRouter { page: ChangeExperienceRoute.page, guards: [_authGuard, _duplicateGuard], ), - AutoRoute( page: DriftPartnerRoute.page, guards: [_authGuard, _duplicateGuard], @@ -499,6 +500,10 @@ class AppRouter extends RootStackRouter { page: BetaSyncSettingsRoute.page, guards: [_authGuard, _duplicateGuard], ), + AutoRoute( + page: DriftPersonDetailRoute.page, + guards: [_authGuard, _duplicateGuard], + ), // required to handle all deeplinks in deep_link.service.dart // auto_route_library#1722 RedirectRoute(path: '*', redirectTo: '/'), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index a59b15856f..b88e00518c 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -960,6 +960,43 @@ class DriftPartnerRoute extends PageRouteInfo { ); } +/// generated route for +/// [DriftPersonDetailPage] +class DriftPersonDetailRoute extends PageRouteInfo { + DriftPersonDetailRoute({ + Key? key, + required Person person, + List? children, + }) : super( + DriftPersonDetailRoute.name, + args: DriftPersonDetailRouteArgs(key: key, person: person), + initialChildren: children, + ); + + static const String name = 'DriftPersonDetailRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return DriftPersonDetailPage(key: args.key, person: args.person); + }, + ); +} + +class DriftPersonDetailRouteArgs { + const DriftPersonDetailRouteArgs({this.key, required this.person}); + + final Key? key; + + final Person person; + + @override + String toString() { + return 'DriftPersonDetailRouteArgs{key: $key, person: $person}'; + } +} + /// generated route for /// [DriftPlaceDetailPage] class DriftPlaceDetailRoute extends PageRouteInfo {