feat(mobile): drift person detail page

This commit is contained in:
wuzihao051119 2025-07-22 18:07:07 +08:00
parent ac44f6d1e0
commit f58db3be88
5 changed files with 154 additions and 1 deletions

View File

@ -68,6 +68,9 @@ class TimelineFactory {
TimelineService fromAssets(List<BaseAsset> assets) =>
TimelineService(_timelineRepository.fromAssets(assets));
TimelineService person(String personId) =>
TimelineService(_timelineRepository.person(personId, groupBy));
}
class TimelineService {

View File

@ -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<List<Bucket>> _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<List<BaseAsset>> _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<bool> Function($RemoteAssetEntityTable row) filter,
GroupAssetsBy groupBy = GroupAssetsBy.day,

View File

@ -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,
),
),
);
}
}

View File

@ -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: '/'),

View File

@ -960,6 +960,43 @@ class DriftPartnerRoute extends PageRouteInfo<void> {
);
}
/// generated route for
/// [DriftPersonDetailPage]
class DriftPersonDetailRoute extends PageRouteInfo<DriftPersonDetailRouteArgs> {
DriftPersonDetailRoute({
Key? key,
required Person person,
List<PageRouteInfo>? 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<DriftPersonDetailRouteArgs>();
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<DriftPlaceDetailRouteArgs> {