From 7bae49ebd51e0c4d465f1f9c9d00af02481da205 Mon Sep 17 00:00:00 2001 From: Daimolean <92239625+wuzihao051119@users.noreply.github.com> Date: Fri, 18 Jul 2025 22:21:39 +0800 Subject: [PATCH] feat(mobile): people sync (#19777) * feat(mobile): drift people sync * merge main --------- Co-authored-by: Alex --- .../drift_schemas/main/drift_schema_v1.json | Bin 27992 -> 30630 bytes .../drift_schemas/main/drift_schema_v2.json | Bin 27992 -> 30630 bytes mobile/lib/domain/models/memory.model.dart | 16 ++- mobile/lib/domain/models/person.model.dart | 125 ++++++++++++++++-- mobile/lib/domain/models/stack.model.dart | 35 +---- .../domain/services/sync_stream.service.dart | 4 + .../entities/person.entity.dart | 34 +++++ .../entities/person.entity.drift.dart | Bin 0 -> 36665 bytes .../repositories/db.repository.dart | 2 + .../repositories/db.repository.drift.dart | Bin 11199 -> 11793 bytes .../repositories/db.repository.steps.dart | Bin 36018 -> 39147 bytes .../repositories/person.repository.dart | 36 +++++ .../repositories/sync_api.repository.dart | 3 + .../repositories/sync_stream.repository.dart | 43 ++++++ .../models/search/search_filter.model.dart | 4 +- mobile/lib/pages/search/search.page.dart | 2 +- .../pages/dev/feat_in_development.page.dart | 1 + .../pages/dev/media_stat.page.dart | 4 + .../pages/search/drift_search.page.dart | 2 +- .../infrastructure/person.provider.dart | 7 + .../{ => infrastructure}/stack.provider.dart | 0 .../lib/providers/search/people.provider.dart | 2 +- .../providers/search/people.provider.g.dart | Bin 9220 -> 9230 bytes mobile/lib/repositories/auth.repository.dart | 1 + .../repositories/person_api.repository.dart | 6 +- mobile/lib/services/person.service.dart | 4 +- .../search/search_filter/people_picker.dart | 6 +- .../services/sync_stream_service_test.dart | 4 + .../test/drift/main/generated/schema_v1.dart | Bin 175636 -> 192419 bytes .../test/drift/main/generated/schema_v2.dart | Bin 175636 -> 192419 bytes 30 files changed, 290 insertions(+), 51 deletions(-) create mode 100644 mobile/lib/infrastructure/entities/person.entity.dart create mode 100644 mobile/lib/infrastructure/entities/person.entity.drift.dart create mode 100644 mobile/lib/infrastructure/repositories/person.repository.dart create mode 100644 mobile/lib/providers/infrastructure/person.provider.dart rename mobile/lib/providers/{ => infrastructure}/stack.provider.dart (100%) diff --git a/mobile/drift_schemas/main/drift_schema_v1.json b/mobile/drift_schemas/main/drift_schema_v1.json index c19bcfb945525dd24fb8fb5fbf917d2042395539..978a9ba8ad5eb3c37eb33e46b0f38a56b1a06b23 100644 GIT binary patch delta 259 zcmca{i*ea|#tmV)96HrXnJG$EhLd?TjMxfNi;DB}CKvkL-aJo{hk3G(?^2eu#N^b; z8&#w@K@7*@;?$DKA4QudFEHbp+#tlto|l-J6JL-xxzRc_Je$AeW({>bFP z22nrxqntcYyNz^meolT7hHkLvWX2$m$rpH>RgyA`N;2Y85=&As6oRE(fHIReCN@v5 KVUK5utpxzTnqOG} delta 12 TcmZ4Xp7F*l#tmV)OtG~9DNzNP diff --git a/mobile/drift_schemas/main/drift_schema_v2.json b/mobile/drift_schemas/main/drift_schema_v2.json index c19bcfb945525dd24fb8fb5fbf917d2042395539..978a9ba8ad5eb3c37eb33e46b0f38a56b1a06b23 100644 GIT binary patch delta 259 zcmca{i*ea|#tmV)96HrXnJG$EhLd?TjMxfNi;DB}CKvkL-aJo{hk3G(?^2eu#N^b; z8&#w@K@7*@;?$DKA4QudFEHbp+#tlto|l-J6JL-xxzRc_Je$AeW({>bFP z22nrxqntcYyNz^meolT7hHkLvWX2$m$rpH>RgyA`N;2Y85=&As6oRE(fHIReCN@v5 KVUK5utpxzTnqOG} delta 12 TcmZ4Xp7F*l#tmV)OtG~9DNzNP diff --git a/mobile/lib/domain/models/memory.model.dart b/mobile/lib/domain/models/memory.model.dart index ba2a43428..38d1c7ef7 100644 --- a/mobile/lib/domain/models/memory.model.dart +++ b/mobile/lib/domain/models/memory.model.dart @@ -124,7 +124,21 @@ class DriftMemory { @override String toString() { - return 'Memory(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, ownerId: $ownerId, type: $type, data: $data, isSaved: $isSaved, memoryAt: $memoryAt, seenAt: $seenAt, showAt: $showAt, hideAt: $hideAt, assets: $assets)'; + return '''Memory { + id: $id, + createdAt: $createdAt, + updatedAt: $updatedAt, + deletedAt: ${deletedAt ?? ""}, + ownerId: $ownerId, + type: $type, + data: $data, + isSaved: $isSaved, + memoryAt: $memoryAt, + seenAt: ${seenAt ?? ""}, + showAt: ${showAt ?? ""}, + hideAt: ${hideAt ?? ""}, + assets: $assets +}'''; } @override diff --git a/mobile/lib/domain/models/person.model.dart b/mobile/lib/domain/models/person.model.dart index 10453f768..d9eee9ae0 100644 --- a/mobile/lib/domain/models/person.model.dart +++ b/mobile/lib/domain/models/person.model.dart @@ -1,7 +1,8 @@ import 'dart:convert'; -class Person { - const Person({ +// TODO: Remove PersonDto once Isar is removed +class PersonDto { + const PersonDto({ required this.id, this.birthDate, required this.isHidden, @@ -22,7 +23,7 @@ class Person { return 'Person(id: $id, birthDate: $birthDate, isHidden: $isHidden, name: $name, thumbnailPath: $thumbnailPath, updatedAt: $updatedAt)'; } - Person copyWith({ + PersonDto copyWith({ String? id, DateTime? birthDate, bool? isHidden, @@ -30,7 +31,7 @@ class Person { String? thumbnailPath, DateTime? updatedAt, }) { - return Person( + return PersonDto( id: id ?? this.id, birthDate: birthDate ?? this.birthDate, isHidden: isHidden ?? this.isHidden, @@ -51,8 +52,8 @@ class Person { }; } - factory Person.fromMap(Map map) { - return Person( + factory PersonDto.fromMap(Map map) { + return PersonDto( id: map['id'] as String, birthDate: map['birthDate'] != null ? DateTime.fromMillisecondsSinceEpoch(map['birthDate'] as int) @@ -68,11 +69,11 @@ class Person { String toJson() => json.encode(toMap()); - factory Person.fromJson(String source) => - Person.fromMap(json.decode(source) as Map); + factory PersonDto.fromJson(String source) => + PersonDto.fromMap(json.decode(source) as Map); @override - bool operator ==(covariant Person other) { + bool operator ==(covariant PersonDto other) { if (identical(this, other)) return true; return other.id == id && @@ -93,3 +94,109 @@ class Person { updatedAt.hashCode; } } + +// Model for a person stored in the server +class Person { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final String thumbnailPath; + final bool isFavorite; + final bool isHidden; + final String? color; + final DateTime? birthDate; + + const Person({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.thumbnailPath, + required this.isFavorite, + required this.isHidden, + required this.color, + this.birthDate, + }); + + Person copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + String? faceAssetId, + String? thumbnailPath, + bool? isFavorite, + bool? isHidden, + String? color, + DateTime? birthDate, + }) { + return Person( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + thumbnailPath: thumbnailPath ?? this.thumbnailPath, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + String toString() { + return '''Person { + id: $id, + createdAt: $createdAt, + updatedAt: $updatedAt, + ownerId: $ownerId, + name: $name, + faceAssetId: ${faceAssetId ?? ""}, + thumbnailPath: $thumbnailPath, + isFavorite: $isFavorite, + isHidden: $isHidden, + color: ${color ?? ""}, + birthDate: ${birthDate ?? ""} +}'''; + } + + @override + bool operator ==(covariant Person other) { + if (identical(this, other)) return true; + + return other.id == id && + other.createdAt == createdAt && + other.updatedAt == updatedAt && + other.ownerId == ownerId && + other.name == name && + other.faceAssetId == faceAssetId && + other.thumbnailPath == thumbnailPath && + other.isFavorite == isFavorite && + other.isHidden == isHidden && + other.color == color && + other.birthDate == birthDate; + } + + @override + int get hashCode { + return id.hashCode ^ + createdAt.hashCode ^ + updatedAt.hashCode ^ + ownerId.hashCode ^ + name.hashCode ^ + faceAssetId.hashCode ^ + thumbnailPath.hashCode ^ + isFavorite.hashCode ^ + isHidden.hashCode ^ + color.hashCode ^ + birthDate.hashCode; + } +} diff --git a/mobile/lib/domain/models/stack.model.dart b/mobile/lib/domain/models/stack.model.dart index d7faf07a2..6a449a1eb 100644 --- a/mobile/lib/domain/models/stack.model.dart +++ b/mobile/lib/domain/models/stack.model.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - // Model for a stack stored in the server class Stack { final String id; @@ -32,34 +30,15 @@ class Stack { ); } - Map toMap() { - return { - 'id': id, - 'createdAt': createdAt.millisecondsSinceEpoch, - 'updatedAt': updatedAt.millisecondsSinceEpoch, - 'ownerId': ownerId, - 'primaryAssetId': primaryAssetId, - }; - } - - factory Stack.fromMap(Map map) { - return Stack( - id: map['id'] as String, - createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt'] as int), - updatedAt: DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] as int), - ownerId: map['ownerId'] as String, - primaryAssetId: map['primaryAssetId'] as String, - ); - } - - String toJson() => json.encode(toMap()); - - factory Stack.fromJson(String source) => - Stack.fromMap(json.decode(source) as Map); - @override String toString() { - return 'Stack(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, ownerId: $ownerId, primaryAssetId: $primaryAssetId)'; + return '''Stack { + id: $id, + createdAt: $createdAt, + updatedAt: $updatedAt, + ownerId: $ownerId, + primaryAssetId: $primaryAssetId +}'''; } @override diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index 618386504..9a7d91ced 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -240,6 +240,10 @@ class SyncStreamService { return _syncStreamRepository.deleteUserMetadatasV1( data.cast(), ); + case SyncEntityType.personV1: + return _syncStreamRepository.updatePeopleV1(data.cast()); + case SyncEntityType.personDeleteV1: + return _syncStreamRepository.deletePeopleV1(data.cast()); default: _logger.warning("Unknown sync data type: $type"); } diff --git a/mobile/lib/infrastructure/entities/person.entity.dart b/mobile/lib/infrastructure/entities/person.entity.dart new file mode 100644 index 000000000..68dd04cb5 --- /dev/null +++ b/mobile/lib/infrastructure/entities/person.entity.dart @@ -0,0 +1,34 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class PersonEntity extends Table with DriftDefaultsMixin { + const PersonEntity(); + + TextColumn get id => text()(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + TextColumn get ownerId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get name => text()(); + + // TODO: foreign key refering to asset faces + TextColumn get faceAssetId => text().nullable()(); + + TextColumn get thumbnailPath => text()(); + + BoolColumn get isFavorite => boolean()(); + + BoolColumn get isHidden => boolean()(); + + TextColumn get color => text().nullable()(); + + DateTimeColumn get birthDate => dateTime().nullable()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/person.entity.drift.dart b/mobile/lib/infrastructure/entities/person.entity.drift.dart new file mode 100644 index 0000000000000000000000000000000000000000..f0ced63f0e9871b10d8a2d41f127d64cd0ffea3e GIT binary patch literal 36665 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 delta 66 zcmV-I0KNZ_T)$Va)diC=1|pOA1rw8I1|k7Avxx@h1p{d@HIu{)ECDu?@eD5kHIpF? Y6#+G~Hx0)EvlI~Y6SEs7cnAW03i+NC)&Kwi diff --git a/mobile/lib/infrastructure/repositories/db.repository.steps.dart b/mobile/lib/infrastructure/repositories/db.repository.steps.dart index 4b27ce830c50d1e492a6196f1cbba975313936eb..76e5df76d02b49288433e1c6b7505a2670f2c883 100644 GIT binary patch delta 457 zcmdlqlj-$FrVZiD9102rsYS*4d7FKiAInY-kQJ6NhKji6m1LGwD%dInXCxM+8X8Z& z$ZyFCRy$chpm4GrclTrkNl_*<%gGPfM3~GCKoq0-WJOL|6xNPzReb&kI9NF1CGE-7g^FZ>PnZ@xL z5Z*;cGqBmo`8oNM4dbO)z>J5b(vu7HqvA7DCNIoL0QzvFiNWLp5uB6l%3> getAll(String userId) { + final query = _db.personEntity.select() + ..where((e) => e.ownerId.equals(userId)); + + return query.map((person) { + return person.toDto(); + }).get(); + } +} + +extension on PersonEntityData { + Person toDto() { + return Person( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + ownerId: ownerId, + name: name, + faceAssetId: faceAssetId, + thumbnailPath: thumbnailPath, + isFavorite: isFavorite, + isHidden: isHidden, + color: color, + birthDate: birthDate, + ); + } +} diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index d1ecbd580..11d58663e 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -57,6 +57,7 @@ class SyncApiRepository { SyncRequestType.stacksV1, SyncRequestType.partnerStacksV1, SyncRequestType.userMetadataV1, + SyncRequestType.peopleV1, ], ).toJson(), ); @@ -173,6 +174,8 @@ const _kResponseMap = { SyncEntityType.partnerStackDeleteV1: SyncStackDeleteV1.fromJson, SyncEntityType.userMetadataV1: SyncUserMetadataV1.fromJson, SyncEntityType.userMetadataDeleteV1: SyncUserMetadataDeleteV1.fromJson, + SyncEntityType.personV1: SyncPersonV1.fromJson, + SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson, }; class _SyncAckV1 { diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index d28c68ed7..fb5c7fdb3 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -9,6 +9,7 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'; @@ -511,6 +512,48 @@ class SyncStreamRepository extends DriftDatabaseRepository { rethrow; } } + + Future updatePeopleV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final person in data) { + final companion = PersonEntityCompanion( + createdAt: Value(person.createdAt), + updatedAt: Value(person.updatedAt), + ownerId: Value(person.ownerId), + name: Value(person.name), + faceAssetId: Value(person.faceAssetId), + thumbnailPath: Value(person.thumbnailPath), + isFavorite: Value(person.isFavorite), + isHidden: Value(person.isHidden), + color: Value(person.color), + birthDate: Value(person.birthDate), + ); + + batch.insert( + _db.personEntity, + companion.copyWith(id: Value(person.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: updatePeopleV1', error, stack); + rethrow; + } + } + + Future deletePeopleV1( + Iterable data, + ) async { + try { + await _db.personEntity.deleteWhere( + (row) => row.id.isIn(data.map((e) => e.personId)), + ); + } catch (error, stack) { + _logger.severe('Error: deletePeopleV1', error, stack); + } + } } extension on AssetTypeEnum { diff --git a/mobile/lib/models/search/search_filter.model.dart b/mobile/lib/models/search/search_filter.model.dart index 835e6aff8..efe6f923a 100644 --- a/mobile/lib/models/search/search_filter.model.dart +++ b/mobile/lib/models/search/search_filter.model.dart @@ -237,7 +237,7 @@ class SearchFilter { String? filename; String? description; String? language; - Set people; + Set people; SearchLocationFilter location; SearchCameraFilter camera; SearchDateFilter date; @@ -282,7 +282,7 @@ class SearchFilter { String? filename, String? description, String? language, - Set? people, + Set? people, SearchLocationFilter? location, SearchCameraFilter? camera, SearchDateFilter? date, diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart index dba3199fa..3e5c153f8 100644 --- a/mobile/lib/pages/search/search.page.dart +++ b/mobile/lib/pages/search/search.page.dart @@ -147,7 +147,7 @@ class SearchPage extends HookConsumerWidget { ); showPeoplePicker() { - handleOnSelect(Set value) { + handleOnSelect(Set value) { filter.value = filter.value.copyWith( people: value, ); 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 2815b2db8..e94329acd 100644 --- a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart +++ b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart @@ -112,6 +112,7 @@ final _features = [ await db.memoryEntity.deleteAll(); await db.memoryAssetEntity.deleteAll(); await db.stackEntity.deleteAll(); + await db.personEntity.deleteAll(); }, ), _Feature( diff --git a/mobile/lib/presentation/pages/dev/media_stat.page.dart b/mobile/lib/presentation/pages/dev/media_stat.page.dart index e61dcdf90..acd7b219b 100644 --- a/mobile/lib/presentation/pages/dev/media_stat.page.dart +++ b/mobile/lib/presentation/pages/dev/media_stat.page.dart @@ -166,6 +166,10 @@ final _remoteStats = [ name: 'Stacks', load: (db) => db.managers.stackEntity.count(), ), + _Stat( + name: 'People', + load: (db) => db.managers.personEntity.count(), + ), ]; @RoutePage() diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index 5a5eb44b3..7101a42b0 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -151,7 +151,7 @@ class DriftSearchPage extends HookConsumerWidget { ); showPeoplePicker() { - handleOnSelect(Set value) { + handleOnSelect(Set value) { filter.value = filter.value.copyWith( people: value, ); diff --git a/mobile/lib/providers/infrastructure/person.provider.dart b/mobile/lib/providers/infrastructure/person.provider.dart new file mode 100644 index 000000000..a733104b3 --- /dev/null +++ b/mobile/lib/providers/infrastructure/person.provider.dart @@ -0,0 +1,7 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/infrastructure/repositories/person.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; + +final driftPersonProvider = Provider( + (ref) => DriftPersonRepository(ref.watch(driftProvider)), +); diff --git a/mobile/lib/providers/stack.provider.dart b/mobile/lib/providers/infrastructure/stack.provider.dart similarity index 100% rename from mobile/lib/providers/stack.provider.dart rename to mobile/lib/providers/infrastructure/stack.provider.dart diff --git a/mobile/lib/providers/search/people.provider.dart b/mobile/lib/providers/search/people.provider.dart index d03d533aa..f6ac9d112 100644 --- a/mobile/lib/providers/search/people.provider.dart +++ b/mobile/lib/providers/search/people.provider.dart @@ -9,7 +9,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'people.provider.g.dart'; @riverpod -Future> getAllPeople( +Future> getAllPeople( Ref ref, ) async { final PersonService personService = ref.read(personServiceProvider); diff --git a/mobile/lib/providers/search/people.provider.g.dart b/mobile/lib/providers/search/people.provider.g.dart index 391edd362c3a9e8866232490989868199a83246f..4625891abbd5817d6e9492409044913a7e41e7d4 100644 GIT binary patch delta 81 zcmZqi==0d1z^I;VnrfD4WMFP)VVq`YnwVr{U}> getAll() async { + Future> getAll() async { final dto = await checkNull(_api.getAllPeople()); return dto.people.map(_toPerson).toList(); } - Future update(String id, {String? name}) async { + Future update(String id, {String? name}) async { final dto = await checkNull( _api.updatePerson(id, PersonUpdateDto(name: name)), ); return _toPerson(dto); } - static Person _toPerson(PersonResponseDto dto) => Person( + static PersonDto _toPerson(PersonResponseDto dto) => PersonDto( birthDate: dto.birthDate, id: dto.id, isHidden: dto.isHidden, diff --git a/mobile/lib/services/person.service.dart b/mobile/lib/services/person.service.dart index a591ad4f2..08b18dfd1 100644 --- a/mobile/lib/services/person.service.dart +++ b/mobile/lib/services/person.service.dart @@ -28,7 +28,7 @@ class PersonService { this._assetRepository, ); - Future> getAllPeople() async { + Future> getAllPeople() async { try { return await _personApiRepository.getAll(); } catch (error, stack) { @@ -48,7 +48,7 @@ class PersonService { return []; } - Future updateName(String id, String name) async { + Future updateName(String id, String name) async { try { return await _personApiRepository.update(id, name: name); } catch (error, stack) { diff --git a/mobile/lib/widgets/search/search_filter/people_picker.dart b/mobile/lib/widgets/search/search_filter/people_picker.dart index 44d01d274..05f699b44 100644 --- a/mobile/lib/widgets/search/search_filter/people_picker.dart +++ b/mobile/lib/widgets/search/search_filter/people_picker.dart @@ -14,8 +14,8 @@ import 'package:immich_mobile/widgets/common/search_field.dart'; class PeoplePicker extends HookConsumerWidget { const PeoplePicker({super.key, required this.onSelect, this.filter}); - final Function(Set) onSelect; - final Set? filter; + final Function(Set) onSelect; + final Set? filter; @override Widget build(BuildContext context, WidgetRef ref) { @@ -24,7 +24,7 @@ class PeoplePicker extends HookConsumerWidget { final searchQuery = useState(''); final people = ref.watch(getAllPeopleProvider); final headers = ApiService.getRequestHeaders(); - final selectedPeople = useState>(filter ?? {}); + final selectedPeople = useState>(filter ?? {}); return Column( children: [ diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index c9fd8104e..deb19dfcf 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -105,6 +105,10 @@ void main() { .thenAnswer(successHandler); when(() => mockSyncStreamRepo.deleteUserMetadatasV1(any())) .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updatePeopleV1(any())) + .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.deletePeopleV1(any())) + .thenAnswer(successHandler); sut = SyncStreamService( syncApiRepository: mockSyncApiRepo, diff --git a/mobile/test/drift/main/generated/schema_v1.dart b/mobile/test/drift/main/generated/schema_v1.dart index 38dbb9f827ad478c791060a52cf1a5aa47e3b258..d7b88ea3cfd39d425ddbad156c3a17cbaea3c663 100644 GIT binary patch delta 4442 zcmd5=ZETa*71q5Zj_riR`LZ2{B(L*9Z001+)($N!n2fS5OhCK>(K;YFiNUUnox~2% z(GX};Xr(qO9MnT;!-t?h)(MuVbo}T#5~ltbLKRu1X{+d(RNL4zHWH~-)oN9C-}}Dz z`n5Oh_aeo<_uQ}NJm=${>ofPPmp{%wHb$<~-YcfA@K7=l$EiONPnp)<6i-D{Px%zB zcWP#@m=pRld2nO?KJl*r|D%&&qvo7_p}sJ@^x>LZwb6q!JBbsNb2YFxryBhSNfX-s zPAoZHp;QDGUaKoiMTQUb#zWCq&wwflP5Rl)?_k#hVuq&46EOdWZMeRb&sG|9oneO5*P<7cP4;?OYoFJ3SWXyng6 zB-;ha$|8A1v}YH|QLKJ~ehK-T4`E=53~-)1u;(c0b#ux?Du|wFf8%biI7E^R{(@TY zZ(fhqPl+{$Gd|XN(kw8-@#S{(?WT%DNg7`ywT@lZ^8JT(-J?++*%-sx>+6%wC1sV@;vrgTTfOD^Juy9-H(|Fobe6^SOB zr4>6NXmeQPsqn7N1zm3{esQ3ng|Eu6D3{1H-wJ*bAXxAPX@JO%^8EDv{opQjXwh=) z^O`clfLFz-C?i&!bJ0>5FL7b)OJXspntRv5;MD*#SP!=l+--4~H2;Foj#au_vyJsu z=(mm)%+s;>G^sObnDF5^aT$HO_;IIne4KlQc!fEp_K?>^oB5p`oA=YV(ef2($Gm@$ zwH!Nc`-=Fu-*MmX{I6cu6@sasJ8@c z(6TGIE53|x=%JIxyq*(0416NwH6sOiutKfES4XqPx z9!wt%(hqQ@iFQIsw0+b-Z5xk%?XF;#PRNjx`VDIR$54ibhW!M(4366rzFx?3V?3>1 zI^Z&-Ja_dfylJBPWj*uG{!ziXzFKs%QAv3QBq0@ebaQbWnvK#vWe^(n=uS3gSWX&1 z$0PgMfYJko-IC2hjEmRF4}@_Bhv_-28K+*DI9*q2;IU1q=RFKQNvd&g6}6OTE?qBJ zWF(^P6XJp=f7Mbd)Md*=6I&OEE1x@QJy!%e2At&rMVQT$ZCLoa9lLgrYPjJ32HreG z?cIq(@$gV*zfXyW4ul1X27JEE*z&QE`EKZ7Vknvlt8FcH`Lyuxr5znY$v$0oSh`t_ z_r6Emc;_(PgFkwTehrFd8k~khl=k*?aN}}5t;CP=sm0EtS&T_OV4%(kXJ%U~#PM>u z5J2m2d5IdgazrEMc&e;Pk|D>Wghd)lnSC)<>xi`3QtuHaWOalc?-o!6Yz`^2};uN><$MN_;pL^D2#MEcD0{ zJL66iF&W*bWa?{JE_vY3oyM&D(6|G5$G#;Ku3-mJ`o-B|L6LDC_fErn-e+_9w{fBj zCc|Vd<4+y7qBmNtJ%{vcGs5pTL#cubV{{CbPgroioF#%?>;tCmkJ)JjOno|rui9x%#E_yg zY$D4A!vsNsw1t+&GrDqJhSX<;;J&reDrp1A>Dd%nIcg7Z$=rF;J$TWOViPUx~=lw_B+3oknGLl?EmX9Z0AE8&arkW56=EtRX=}4^ygbdG%MT5COm4U z&;DQBwT(A=uh7FLT)aZ_Yqc^?`{6^;9_4zcUH8Hpx8J01P`vXqYArBMV9$6bTyvXo I;wpXa-?;({%>V!Z delta 38 wcmV+>0NMYe-wTx73V?(Gv;tzJx1Qw!cm}sq>;jbnlQ5tdldd5Mmmco|g-m7;A^-pY diff --git a/mobile/test/drift/main/generated/schema_v2.dart b/mobile/test/drift/main/generated/schema_v2.dart index 8345cef9061b8970a3fe164069c25e31f5585cbe..e3edac350147f824a5605110c519a9d88ff91b20 100644 GIT binary patch delta 4442 zcmd5=ZETa*71q5Zj_riR`LZ2{B(L*9Z001+)($N!n2fS5OhCK>(K;YFiNUUnox~2% z(GX};Xr(qO9MnT;!-t?h)(MuVbo}T#5~ltbLKRu1X{+d(RNL4zHWH~-)oN9C-}}Dz z`n5Oh_aeo<_uQ}NJm=${>ofPPmp{%wHb$<~-YcfA@K7=l$EiONPnp)<6i-D{Px%zB zcWP#@m=pRld2nO?KJl*r|D%&&qvo7_p}sJ@^x>LZwb6q!JBbsNb2YFxryBhSNfX-s zPAoZHp;QDGUaKoiMTQUb#zWCq&wwflP5Rl)?_k#hVuq&46EOdWZMeRb&sG|9oneO5*P<7cP4;?OYoFJ3SWXyng6 zB-;ha$|8A1v}YH|QLKJ~ehK-T4`E=53~-)1u;(c0b#ux?Du|wFf8%biI7E^R{(@TY zZ(fhqPl+{$Gd|XN(kw8-@#S{(?WT%DNg7`ywT@lZ^8JT(-J?++*%-sx>+6%wC1sV@;vrgTTfOD^Juy9-H(|Fobe6^SOB zr4>6NXmeQPsqn7N1zm3{esQ3ng|Eu6D3{1H-wJ*bAXxAPX@JO%^8EDv{opQjXwh=) z^O`clfLFz-C?i&!bJ0>5FL7b)OJXspntRv5;MD*#SP!=l+--4~H2;Foj#au_vyJsu z=(mm)%+s;>G^sObnDF5^aT$HO_;IIne4KlQc!fEp_K?>^oB5p`oA=YV(ef2($Gm@$ zwH!Nc`-=Fu-*MmX{I6cu6@sasJ8@c z(6TGIE53|x=%JIxyq*(0416NwH6sOiutKfES4XqPx z9!wt%(hqQ@iFQIsw0+b-Z5xk%?XF;#PRNjx`VDIR$54ibhW!M(4366rzFx?3V?3>1 zI^Z&-Ja_dfylJBPWj*uG{!ziXzFKs%QAv3QBq0@ebaQbWnvK#vWe^(n=uS3gSWX&1 z$0PgMfYJko-IC2hjEmRF4}@_Bhv_-28K+*DI9*q2;IU1q=RFKQNvd&g6}6OTE?qBJ zWF(^P6XJp=f7Mbd)Md*=6I&OEE1x@QJy!%e2At&rMVQT$ZCLoa9lLgrYPjJ32HreG z?cIq(@$gV*zfXyW4ul1X27JEE*z&QE`EKZ7Vknvlt8FcH`Lyuxr5znY$v$0oSh`t_ z_r6Emc;_(PgFkwTehrFd8k~khl=k*?aN}}5t;CP=sm0EtS&T_OV4%(kXJ%U~#PM>u z5J2m2d5IdgazrEMc&e;Pk|D>Wghd)lnSC)<>xi`3QtuHaWOalc?-o!6Yz`^2};uN><$MN_;pL^D2#MEcD0{ zJL66iF&W*bWa?{JE_vY3oyM&D(6|G5$G#;Ku3-mJ`o-B|L6LDC_fErn-e+_9w{fBj zCc|Vd<4+y7qBmNtJ%{vcGs5pTL#cubV{{CbPgroioF#%?>;tCmkJ)JjOno|rui9x%#E_yg zY$D4A!vsNsw1t+&GrDqJhSX<;;J&reDrp1A>Dd%nIcg7Z$=rF;J$TWOViPUx~=lw_B+3oknGLl?EmX9Z0AE8&arkW56=EtRX=}4^ygbdG%MT5COm4U z&;DQBwT(A=uh7FLT)aZ_Yqc^?`{6^;9_4zcUH8Hpx8J01P`vXqYArBMV9$6bTyvXo I;wpXa-?;({%>V!Z delta 38 wcmV+>0NMYe-wTx73V?(Gv;tzJx1Qw!cm}sq>;jbnlQ5tdldd5Mmmco|g-m7;A^-pY