feat(mobile): people sync

This commit is contained in:
wuzihao051119 2025-07-06 19:44:32 +08:00
parent 4b7fb162ee
commit 55e7622e32
21 changed files with 2449 additions and 31 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,10 @@
enum SourceType {
// do not change this order!
machineLearning,
exif,
manual,
}
class Face {
}

View File

@ -1,7 +1,8 @@
import 'dart:convert'; import 'dart:convert';
class Person { // TODO: Remove PersonDto once Isar is removed
const Person({ class PersonDto {
const PersonDto({
required this.id, required this.id,
this.birthDate, this.birthDate,
required this.isHidden, required this.isHidden,
@ -22,7 +23,7 @@ class Person {
return 'Person(id: $id, birthDate: $birthDate, isHidden: $isHidden, name: $name, thumbnailPath: $thumbnailPath, updatedAt: $updatedAt)'; return 'Person(id: $id, birthDate: $birthDate, isHidden: $isHidden, name: $name, thumbnailPath: $thumbnailPath, updatedAt: $updatedAt)';
} }
Person copyWith({ PersonDto copyWith({
String? id, String? id,
DateTime? birthDate, DateTime? birthDate,
bool? isHidden, bool? isHidden,
@ -30,7 +31,7 @@ class Person {
String? thumbnailPath, String? thumbnailPath,
DateTime? updatedAt, DateTime? updatedAt,
}) { }) {
return Person( return PersonDto(
id: id ?? this.id, id: id ?? this.id,
birthDate: birthDate ?? this.birthDate, birthDate: birthDate ?? this.birthDate,
isHidden: isHidden ?? this.isHidden, isHidden: isHidden ?? this.isHidden,
@ -51,8 +52,8 @@ class Person {
}; };
} }
factory Person.fromMap(Map<String, dynamic> map) { factory PersonDto.fromMap(Map<String, dynamic> map) {
return Person( return PersonDto(
id: map['id'] as String, id: map['id'] as String,
birthDate: map['birthDate'] != null birthDate: map['birthDate'] != null
? DateTime.fromMillisecondsSinceEpoch(map['birthDate'] as int) ? DateTime.fromMillisecondsSinceEpoch(map['birthDate'] as int)
@ -68,11 +69,11 @@ class Person {
String toJson() => json.encode(toMap()); String toJson() => json.encode(toMap());
factory Person.fromJson(String source) => factory PersonDto.fromJson(String source) =>
Person.fromMap(json.decode(source) as Map<String, dynamic>); PersonDto.fromMap(json.decode(source) as Map<String, dynamic>);
@override @override
bool operator ==(covariant Person other) { bool operator ==(covariant PersonDto other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
return other.id == id && return other.id == id &&
@ -93,3 +94,60 @@ class Person {
updatedAt.hashCode; 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? thumbnailPath;
final bool isHidden;
final DateTime? birthDate;
final String? faceAssetId;
final bool isFavorite;
final String? color;
const Person({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.ownerId,
required this.name,
this.thumbnailPath,
required this.isHidden,
this.birthDate,
this.faceAssetId,
required this.isFavorite,
this.color,
});
Person copyWith({
String? id,
DateTime? createdAt,
DateTime? updatedAt,
String? ownerId,
String? name,
String? thumbnailPath,
bool? isHidden,
DateTime? birthDate,
String? faceAssetId,
bool? isFavorite,
String? color,
}) {
return Person(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
ownerId: ownerId ?? this.ownerId,
name: name ?? this.name,
thumbnailPath: thumbnailPath ?? this.thumbnailPath,
isHidden: isHidden ?? this.isHidden,
birthDate: birthDate ?? this.birthDate,
faceAssetId: faceAssetId ?? this.faceAssetId,
isFavorite: isFavorite ?? this.isFavorite,
color: color ?? this.color,
);
}
}

View File

@ -173,6 +173,14 @@ class SyncStreamService {
data.cast(), data.cast(),
debugLabel: 'partner', debugLabel: 'partner',
); );
// case SyncEntityType.personV1:
// return _syncStreamRepository.updatePeopleV1(data.cast());
// case SyncEntityType.personDeleteV1:
// return _syncStreamRepository.deletePeopleV1(data.cast());
// case SyncEntityType.faceV1:
// return _syncStreamRepository.updateFacesV1(data.cast());
// case SyncEntityType.faceDeleteV1:
// return _syncStreamRepository.deleteFacesV1(data.cast());
default: default:
_logger.warning("Unknown sync data type: $type"); _logger.warning("Unknown sync data type: $type");
} }

View File

@ -0,0 +1,40 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/face_model.dart';
import 'package:immich_mobile/infrastructure/entities/person.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class AssetFaceEntity extends Table with DriftDefaultsMixin {
const AssetFaceEntity();
TextColumn get id => text()();
TextColumn get personId =>
text().nullable().references(PersonEntity, #id, onDelete: KeyAction.setNull)();
TextColumn get assetId =>
text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)();
IntColumn get imageHeight => integer()();
IntColumn get imageWidth => integer()();
IntColumn get boundingBoxX1 => integer()();
IntColumn get boundingBoxY1 => integer()();
IntColumn get boundingBoxX2 => integer()();
IntColumn get boundingBoxY2 => integer()();
IntColumn get sourceType => intEnum<SourceType>()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get deletedAt => dateTime().nullable()();
@override
Set<Column> get primaryKey => {id};
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.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()();
TextColumn get thumbnailPath => text().nullable()();
BoolColumn get isHidden => boolean()();
DateTimeColumn get birthDate => dateTime().nullable()();
TextColumn get faceAssetId =>
text().nullable().references(AssetFaceEntity, #id, onDelete: KeyAction.setNull)();
BoolColumn get isFavorite => boolean()();
TextColumn get color => text().nullable()();
@override
Set<Column> get primaryKey => {id};
}

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart'; import 'package:drift_flutter/drift_flutter.dart';
import 'package:immich_mobile/domain/interfaces/db.interface.dart'; import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart';
@ -10,6 +11,7 @@ import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.dart'; import 'package:immich_mobile/infrastructure/entities/memory.entity.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
import 'package:immich_mobile/infrastructure/entities/person.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart';
@ -52,6 +54,8 @@ class IsarDatabaseRepository implements IDatabaseRepository {
MemoryEntity, MemoryEntity,
MemoryAssetEntity, MemoryAssetEntity,
StackEntity, StackEntity,
PersonEntity,
AssetFaceEntity,
], ],
include: { include: {
'package:immich_mobile/infrastructure/entities/merged_asset.drift', 'package:immich_mobile/infrastructure/entities/merged_asset.drift',

View File

@ -29,9 +29,13 @@ import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.
as i13; as i13;
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
as i14; as i14;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'
as i15; as i15;
import 'package:drift/internal/modular.dart' as i16; import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
as i16;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
as i17;
import 'package:drift/internal/modular.dart' as i18;
abstract class $Drift extends i0.GeneratedDatabase { abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e); $Drift(i0.QueryExecutor e) : super(e);
@ -61,8 +65,11 @@ abstract class $Drift extends i0.GeneratedDatabase {
late final i13.$MemoryAssetEntityTable memoryAssetEntity = late final i13.$MemoryAssetEntityTable memoryAssetEntity =
i13.$MemoryAssetEntityTable(this); i13.$MemoryAssetEntityTable(this);
late final i14.$StackEntityTable stackEntity = i14.$StackEntityTable(this); late final i14.$StackEntityTable stackEntity = i14.$StackEntityTable(this);
i15.MergedAssetDrift get mergedAssetDrift => i16.ReadDatabaseContainer(this) late final i15.$AssetFaceEntityTable assetFaceEntity =
.accessor<i15.MergedAssetDrift>(i15.MergedAssetDrift.new); i15.$AssetFaceEntityTable(this);
late final i16.$PersonEntityTable personEntity = i16.$PersonEntityTable(this);
i17.MergedAssetDrift get mergedAssetDrift => i18.ReadDatabaseContainer(this)
.accessor<i17.MergedAssetDrift>(i17.MergedAssetDrift.new);
@override @override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables => Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>(); allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@ -84,7 +91,9 @@ abstract class $Drift extends i0.GeneratedDatabase {
remoteAlbumUserEntity, remoteAlbumUserEntity,
memoryEntity, memoryEntity,
memoryAssetEntity, memoryAssetEntity,
stackEntity stackEntity,
assetFaceEntity,
personEntity
]; ];
@override @override
i0.StreamQueryUpdateRules get streamUpdateRules => i0.StreamQueryUpdateRules get streamUpdateRules =>
@ -216,6 +225,27 @@ abstract class $Drift extends i0.GeneratedDatabase {
i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete), i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete),
], ],
), ),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('remote_asset_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('asset_face_entity', kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('user_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('person_entity', kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('asset_face_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('person_entity', kind: i0.UpdateKind.update),
],
),
], ],
); );
@override @override
@ -255,4 +285,8 @@ class $DriftManager {
i13.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity); i13.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
i14.$$StackEntityTableTableManager get stackEntity => i14.$$StackEntityTableTableManager get stackEntity =>
i14.$$StackEntityTableTableManager(_db, _db.stackEntity); i14.$$StackEntityTableTableManager(_db, _db.stackEntity);
i15.$$AssetFaceEntityTableTableManager get assetFaceEntity =>
i15.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity);
i16.$$PersonEntityTableTableManager get personEntity =>
i16.$$PersonEntityTableTableManager(_db, _db.personEntity);
} }

View File

@ -56,6 +56,8 @@ class SyncApiRepository {
SyncRequestType.memoryToAssetsV1, SyncRequestType.memoryToAssetsV1,
SyncRequestType.stacksV1, SyncRequestType.stacksV1,
SyncRequestType.partnerStacksV1, SyncRequestType.partnerStacksV1,
SyncRequestType.peopleV1,
SyncRequestType.facesV1,
], ],
).toJson(), ).toJson(),
); );
@ -160,7 +162,6 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.albumToAssetV1: SyncAlbumToAssetV1.fromJson, SyncEntityType.albumToAssetV1: SyncAlbumToAssetV1.fromJson,
SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson, SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson,
SyncEntityType.albumToAssetDeleteV1: SyncAlbumToAssetDeleteV1.fromJson, SyncEntityType.albumToAssetDeleteV1: SyncAlbumToAssetDeleteV1.fromJson,
SyncEntityType.syncAckV1: _SyncAckV1.fromJson,
SyncEntityType.memoryV1: SyncMemoryV1.fromJson, SyncEntityType.memoryV1: SyncMemoryV1.fromJson,
SyncEntityType.memoryDeleteV1: SyncMemoryDeleteV1.fromJson, SyncEntityType.memoryDeleteV1: SyncMemoryDeleteV1.fromJson,
SyncEntityType.memoryToAssetV1: SyncMemoryAssetV1.fromJson, SyncEntityType.memoryToAssetV1: SyncMemoryAssetV1.fromJson,
@ -170,6 +171,11 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.partnerStackV1: SyncStackV1.fromJson, SyncEntityType.partnerStackV1: SyncStackV1.fromJson,
SyncEntityType.partnerStackBackfillV1: SyncStackV1.fromJson, SyncEntityType.partnerStackBackfillV1: SyncStackV1.fromJson,
SyncEntityType.partnerStackDeleteV1: SyncStackDeleteV1.fromJson, SyncEntityType.partnerStackDeleteV1: SyncStackDeleteV1.fromJson,
SyncEntityType.personV1: SyncPersonV1.fromJson,
SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson,
SyncEntityType.faceV1: SyncFaceV1.fromJson,
SyncEntityType.faceDeleteV1: SyncFaceDeleteV1.fromJson,
SyncEntityType.syncAckV1: _SyncAckV1.fromJson,
}; };
class _SyncAckV1 { class _SyncAckV1 {

View File

@ -237,7 +237,7 @@ class SearchFilter {
String? filename; String? filename;
String? description; String? description;
String? language; String? language;
Set<Person> people; Set<PersonDto> people;
SearchLocationFilter location; SearchLocationFilter location;
SearchCameraFilter camera; SearchCameraFilter camera;
SearchDateFilter date; SearchDateFilter date;
@ -282,7 +282,7 @@ class SearchFilter {
String? filename, String? filename,
String? description, String? description,
String? language, String? language,
Set<Person>? people, Set<PersonDto>? people,
SearchLocationFilter? location, SearchLocationFilter? location,
SearchCameraFilter? camera, SearchCameraFilter? camera,
SearchDateFilter? date, SearchDateFilter? date,

View File

@ -147,7 +147,7 @@ class SearchPage extends HookConsumerWidget {
); );
showPeoplePicker() { showPeoplePicker() {
handleOnSelect(Set<Person> value) { handleOnSelect(Set<PersonDto> value) {
filter.value = filter.value.copyWith( filter.value = filter.value.copyWith(
people: value, people: value,
); );

View File

@ -69,6 +69,8 @@ final _features = [
await db.memoryEntity.deleteAll(); await db.memoryEntity.deleteAll();
await db.memoryAssetEntity.deleteAll(); await db.memoryAssetEntity.deleteAll();
await db.stackEntity.deleteAll(); await db.stackEntity.deleteAll();
await db.personEntity.deleteAll();
await db.assetFaceEntity.deleteAll();
}, },
), ),
_Feature( _Feature(

View File

@ -9,7 +9,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'people.provider.g.dart'; part 'people.provider.g.dart';
@riverpod @riverpod
Future<List<Person>> getAllPeople( Future<List<PersonDto>> getAllPeople(
Ref ref, Ref ref,
) async { ) async {
final PersonService personService = ref.read(personServiceProvider); final PersonService personService = ref.read(personServiceProvider);

View File

@ -6,11 +6,12 @@ part of 'people.provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$getAllPeopleHash() => r'226947af3b09ce62224916543958dd1d5e2ba651'; String _$getAllPeopleHash() => r'2c5e6a207683f15ab209650615fdf9cb7f76c736';
/// See also [getAllPeople]. /// See also [getAllPeople].
@ProviderFor(getAllPeople) @ProviderFor(getAllPeople)
final getAllPeopleProvider = AutoDisposeFutureProvider<List<Person>>.internal( final getAllPeopleProvider =
AutoDisposeFutureProvider<List<PersonDto>>.internal(
getAllPeople, getAllPeople,
name: r'getAllPeopleProvider', name: r'getAllPeopleProvider',
debugGetCreateSourceHash: debugGetCreateSourceHash:
@ -21,7 +22,7 @@ final getAllPeopleProvider = AutoDisposeFutureProvider<List<Person>>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
typedef GetAllPeopleRef = AutoDisposeFutureProviderRef<List<Person>>; typedef GetAllPeopleRef = AutoDisposeFutureProviderRef<List<PersonDto>>;
String _$personAssetsHash() => r'c1d35ee0e024bd6915e21bc724be4b458a14bc24'; String _$personAssetsHash() => r'c1d35ee0e024bd6915e21bc724be4b458a14bc24';
/// Copied from Dart SDK /// Copied from Dart SDK

View File

@ -43,6 +43,8 @@ class AuthRepository extends DatabaseRepository {
_drift.memoryEntity.deleteAll(), _drift.memoryEntity.deleteAll(),
_drift.memoryAssetEntity.deleteAll(), _drift.memoryAssetEntity.deleteAll(),
_drift.stackEntity.deleteAll(), _drift.stackEntity.deleteAll(),
_drift.personEntity.deleteAll(),
_drift.assetFaceEntity.deleteAll(),
]); ]);
}); });
} }

View File

@ -13,19 +13,19 @@ class PersonApiRepository extends ApiRepository {
PersonApiRepository(this._api); PersonApiRepository(this._api);
Future<List<Person>> getAll() async { Future<List<PersonDto>> getAll() async {
final dto = await checkNull(_api.getAllPeople()); final dto = await checkNull(_api.getAllPeople());
return dto.people.map(_toPerson).toList(); return dto.people.map(_toPerson).toList();
} }
Future<Person> update(String id, {String? name}) async { Future<PersonDto> update(String id, {String? name}) async {
final dto = await checkNull( final dto = await checkNull(
_api.updatePerson(id, PersonUpdateDto(name: name)), _api.updatePerson(id, PersonUpdateDto(name: name)),
); );
return _toPerson(dto); return _toPerson(dto);
} }
static Person _toPerson(PersonResponseDto dto) => Person( static PersonDto _toPerson(PersonResponseDto dto) => PersonDto(
birthDate: dto.birthDate, birthDate: dto.birthDate,
id: dto.id, id: dto.id,
isHidden: dto.isHidden, isHidden: dto.isHidden,

View File

@ -28,7 +28,7 @@ class PersonService {
this._assetRepository, this._assetRepository,
); );
Future<List<Person>> getAllPeople() async { Future<List<PersonDto>> getAllPeople() async {
try { try {
return await _personApiRepository.getAll(); return await _personApiRepository.getAll();
} catch (error, stack) { } catch (error, stack) {
@ -48,7 +48,7 @@ class PersonService {
return []; return [];
} }
Future<Person?> updateName(String id, String name) async { Future<PersonDto?> updateName(String id, String name) async {
try { try {
return await _personApiRepository.update(id, name: name); return await _personApiRepository.update(id, name: name);
} catch (error, stack) { } catch (error, stack) {

View File

@ -14,8 +14,8 @@ import 'package:immich_mobile/widgets/common/search_field.dart';
class PeoplePicker extends HookConsumerWidget { class PeoplePicker extends HookConsumerWidget {
const PeoplePicker({super.key, required this.onSelect, this.filter}); const PeoplePicker({super.key, required this.onSelect, this.filter});
final Function(Set<Person>) onSelect; final Function(Set<PersonDto>) onSelect;
final Set<Person>? filter; final Set<PersonDto>? filter;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -24,7 +24,7 @@ class PeoplePicker extends HookConsumerWidget {
final searchQuery = useState(''); final searchQuery = useState('');
final people = ref.watch(getAllPeopleProvider); final people = ref.watch(getAllPeopleProvider);
final headers = ApiService.getRequestHeaders(); final headers = ApiService.getRequestHeaders();
final selectedPeople = useState<Set<Person>>(filter ?? {}); final selectedPeople = useState<Set<PersonDto>>(filter ?? {});
return Column( return Column(
children: [ children: [

View File

@ -101,6 +101,14 @@ void main() {
debugLabel: any(named: 'debugLabel'), debugLabel: any(named: 'debugLabel'),
), ),
).thenAnswer(successHandler); ).thenAnswer(successHandler);
// when(() => mockSyncStreamRepo.updatePeopleV1(any()))
// .thenAnswer(successHandler);
// when(() => mockSyncStreamRepo.deletePeopleV1(any()))
// .thenAnswer(successHandler);
// when(() => mockSyncStreamRepo.updateFacesV1(any()))
// .thenAnswer(successHandler);
// when(() => mockSyncStreamRepo.deleteFacesV1(any()))
// .thenAnswer(successHandler);
sut = SyncStreamService( sut = SyncStreamService(
syncApiRepository: mockSyncApiRepo, syncApiRepository: mockSyncApiRepo,