diff --git a/mobile/drift_schemas/main/drift_schema_v4.json b/mobile/drift_schemas/main/drift_schema_v4.json new file mode 100644 index 000000000..82ef30ada Binary files /dev/null and b/mobile/drift_schemas/main/drift_schema_v4.json differ diff --git a/mobile/lib/domain/models/asset_face.model.dart b/mobile/lib/domain/models/asset_face.model.dart new file mode 100644 index 000000000..f432b923e --- /dev/null +++ b/mobile/lib/domain/models/asset_face.model.dart @@ -0,0 +1,98 @@ +// Model for an asset face stored in the server +class AssetFace { + final String id; + final String assetId; + final String? personId; + final int imageWidth; + final int imageHeight; + final int boundingBoxX1; + final int boundingBoxY1; + final int boundingBoxX2; + final int boundingBoxY2; + final String sourceType; + + const AssetFace({ + required this.id, + required this.assetId, + this.personId, + required this.imageWidth, + required this.imageHeight, + required this.boundingBoxX1, + required this.boundingBoxY1, + required this.boundingBoxX2, + required this.boundingBoxY2, + required this.sourceType, + }); + + AssetFace copyWith({ + String? id, + String? assetId, + String? personId, + int? imageWidth, + int? imageHeight, + int? boundingBoxX1, + int? boundingBoxY1, + int? boundingBoxX2, + int? boundingBoxY2, + String? sourceType, + }) { + return AssetFace( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId ?? this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + } + + @override + String toString() { + return '''AssetFace { + id: $id, + assetId: $assetId, + personId: ${personId ?? ""}, + imageWidth: $imageWidth, + imageHeight: $imageHeight, + boundingBoxX1: $boundingBoxX1, + boundingBoxY1: $boundingBoxY1, + boundingBoxX2: $boundingBoxX2, + boundingBoxY2: $boundingBoxY2, + sourceType: $sourceType, +}'''; + } + + @override + bool operator ==(covariant AssetFace other) { + if (identical(this, other)) return true; + + return other.id == id && + other.assetId == assetId && + other.personId == personId && + other.imageWidth == imageWidth && + other.imageHeight == imageHeight && + other.boundingBoxX1 == boundingBoxX1 && + other.boundingBoxY1 == boundingBoxY1 && + other.boundingBoxX2 == boundingBoxX2 && + other.boundingBoxY2 == boundingBoxY2 && + other.sourceType == sourceType; + } + + @override + int get hashCode { + return id.hashCode ^ + assetId.hashCode ^ + personId.hashCode ^ + imageWidth.hashCode ^ + imageHeight.hashCode ^ + boundingBoxX1.hashCode ^ + boundingBoxY1.hashCode ^ + boundingBoxX2.hashCode ^ + boundingBoxY2.hashCode ^ + sourceType.hashCode; + } +} diff --git a/mobile/lib/domain/models/person.model.dart b/mobile/lib/domain/models/person.model.dart index d9eee9ae0..2a6b31cb1 100644 --- a/mobile/lib/domain/models/person.model.dart +++ b/mobile/lib/domain/models/person.model.dart @@ -103,7 +103,6 @@ class Person { final String ownerId; final String name; final String? faceAssetId; - final String thumbnailPath; final bool isFavorite; final bool isHidden; final String? color; @@ -116,7 +115,6 @@ class Person { required this.ownerId, required this.name, this.faceAssetId, - required this.thumbnailPath, required this.isFavorite, required this.isHidden, required this.color, @@ -130,7 +128,6 @@ class Person { String? ownerId, String? name, String? faceAssetId, - String? thumbnailPath, bool? isFavorite, bool? isHidden, String? color, @@ -143,7 +140,6 @@ class Person { 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, @@ -160,7 +156,6 @@ class Person { ownerId: $ownerId, name: $name, faceAssetId: ${faceAssetId ?? ""}, - thumbnailPath: $thumbnailPath, isFavorite: $isFavorite, isHidden: $isHidden, color: ${color ?? ""}, @@ -178,7 +173,6 @@ class Person { other.ownerId == ownerId && other.name == name && other.faceAssetId == faceAssetId && - other.thumbnailPath == thumbnailPath && other.isFavorite == isFavorite && other.isHidden == isHidden && other.color == color && @@ -193,7 +187,6 @@ class Person { ownerId.hashCode ^ name.hashCode ^ faceAssetId.hashCode ^ - thumbnailPath.hashCode ^ isFavorite.hashCode ^ isHidden.hashCode ^ color.hashCode ^ diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index 9a7d91ced..ca8295fc8 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -244,6 +244,10 @@ class SyncStreamService { return _syncStreamRepository.updatePeopleV1(data.cast()); case SyncEntityType.personDeleteV1: return _syncStreamRepository.deletePeopleV1(data.cast()); + case SyncEntityType.assetFaceV1: + return _syncStreamRepository.updateAssetFacesV1(data.cast()); + case SyncEntityType.assetFaceDeleteV1: + return _syncStreamRepository.deleteAssetFacesV1(data.cast()); default: _logger.warning("Unknown sync data type: $type"); } diff --git a/mobile/lib/infrastructure/entities/asset_face.entity.dart b/mobile/lib/infrastructure/entities/asset_face.entity.dart new file mode 100644 index 000000000..c54e4e184 --- /dev/null +++ b/mobile/lib/infrastructure/entities/asset_face.entity.dart @@ -0,0 +1,34 @@ +import 'package:drift/drift.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 assetId => + text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get personId => text() + .nullable() + .references(PersonEntity, #id, onDelete: KeyAction.setNull)(); + + IntColumn get imageWidth => integer()(); + + IntColumn get imageHeight => integer()(); + + IntColumn get boundingBoxX1 => integer()(); + + IntColumn get boundingBoxY1 => integer()(); + + IntColumn get boundingBoxX2 => integer()(); + + IntColumn get boundingBoxY2 => integer()(); + + TextColumn get sourceType => text()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/asset_face.entity.drift.dart b/mobile/lib/infrastructure/entities/asset_face.entity.drift.dart new file mode 100644 index 000000000..140af60de Binary files /dev/null and b/mobile/lib/infrastructure/entities/asset_face.entity.drift.dart differ diff --git a/mobile/lib/infrastructure/entities/person.entity.dart b/mobile/lib/infrastructure/entities/person.entity.dart index 68dd04cb5..75543baca 100644 --- a/mobile/lib/infrastructure/entities/person.entity.dart +++ b/mobile/lib/infrastructure/entities/person.entity.dart @@ -16,11 +16,8 @@ class PersonEntity extends Table with DriftDefaultsMixin { 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()(); diff --git a/mobile/lib/infrastructure/entities/person.entity.drift.dart b/mobile/lib/infrastructure/entities/person.entity.drift.dart index f0ced63f0..70639adc2 100644 Binary files a/mobile/lib/infrastructure/entities/person.entity.drift.dart and b/mobile/lib/infrastructure/entities/person.entity.drift.dart differ diff --git a/mobile/lib/infrastructure/repositories/asset_face.repository.dart b/mobile/lib/infrastructure/repositories/asset_face.repository.dart new file mode 100644 index 000000000..a9ad753d8 --- /dev/null +++ b/mobile/lib/infrastructure/repositories/asset_face.repository.dart @@ -0,0 +1,33 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/asset_face.model.dart'; +import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +class DriftAssetFaceRepository extends DriftDatabaseRepository { + final Drift _db; + const DriftAssetFaceRepository(this._db) : super(_db); + + Future> getAll() { + return _db.assetFaceEntity + .select() + .map((assetFace) => assetFace.toDto()) + .get(); + } +} + +extension on AssetFaceEntityData { + AssetFace toDto() { + return AssetFace( + id: id, + assetId: assetId, + personId: personId, + imageWidth: imageWidth, + imageHeight: imageHeight, + boundingBoxX1: boundingBoxX1, + boundingBoxY1: boundingBoxY1, + boundingBoxX2: boundingBoxX2, + boundingBoxY2: boundingBoxY2, + sourceType: sourceType, + ); + } +} diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 7562cf6ff..ced148f85 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -5,6 +5,7 @@ import 'package:drift_flutter/drift_flutter.dart'; import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/interfaces/db.interface.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/asset_face.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_asset.entity.dart'; @@ -56,6 +57,7 @@ class IsarDatabaseRepository implements IDatabaseRepository { MemoryAssetEntity, StackEntity, PersonEntity, + AssetFaceEntity, ], include: { 'package:immich_mobile/infrastructure/entities/merged_asset.drift', @@ -72,7 +74,7 @@ class Drift extends $Drift implements IDatabaseRepository { ); @override - int get schemaVersion => 3; + int get schemaVersion => 4; @override MigrationStrategy get migration => MigrationStrategy( @@ -94,6 +96,10 @@ class Drift extends $Drift implements IDatabaseRepository { // Removed foreign key constraint on stack.primaryAssetId await m.alterTable(TableMigration(v3.stackEntity)); }, + from3To4: (m, v4) async { + await m.alterTable(TableMigration(v4.personEntity)); + await m.create(v4.assetFaceEntity); + }, ), ); diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index 0f822e57e..7b722dfff 100644 Binary files a/mobile/lib/infrastructure/repositories/db.repository.drift.dart and b/mobile/lib/infrastructure/repositories/db.repository.drift.dart differ diff --git a/mobile/lib/infrastructure/repositories/db.repository.steps.dart b/mobile/lib/infrastructure/repositories/db.repository.steps.dart index a0703c371..d8c35707e 100644 Binary files a/mobile/lib/infrastructure/repositories/db.repository.steps.dart and b/mobile/lib/infrastructure/repositories/db.repository.steps.dart differ diff --git a/mobile/lib/infrastructure/repositories/person.repository.dart b/mobile/lib/infrastructure/repositories/person.repository.dart index 859765d63..fa336c548 100644 --- a/mobile/lib/infrastructure/repositories/person.repository.dart +++ b/mobile/lib/infrastructure/repositories/person.repository.dart @@ -26,7 +26,6 @@ extension on PersonEntityData { ownerId: ownerId, name: name, faceAssetId: faceAssetId, - thumbnailPath: thumbnailPath, isFavorite: isFavorite, isHidden: isHidden, color: color, diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index 11d58663e..e8be84eff 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -58,6 +58,7 @@ class SyncApiRepository { SyncRequestType.partnerStacksV1, SyncRequestType.userMetadataV1, SyncRequestType.peopleV1, + SyncRequestType.assetFacesV1, ], ).toJson(), ); @@ -176,6 +177,8 @@ const _kResponseMap = { SyncEntityType.userMetadataDeleteV1: SyncUserMetadataDeleteV1.fromJson, SyncEntityType.personV1: SyncPersonV1.fromJson, SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson, + SyncEntityType.assetFaceV1: SyncAssetFaceV1.fromJson, + SyncEntityType.assetFaceDeleteV1: SyncAssetFaceDeleteV1.fromJson, }; class _SyncAckV1 { diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index e141c387b..1cca90356 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/models/user_metadata.model.dart'; +import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'; 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'; @@ -546,11 +547,62 @@ class SyncStreamRepository extends DriftDatabaseRepository { Iterable data, ) async { try { - await _db.personEntity.deleteWhere( - (row) => row.id.isIn(data.map((e) => e.personId)), - ); + await _db.batch((batch) { + for (final person in data) { + batch.deleteWhere( + _db.personEntity, + (row) => row.id.equals(person.personId), + ); + } + }); } catch (error, stack) { _logger.severe('Error: deletePeopleV1', error, stack); + rethrow; + } + } + + Future updateAssetFacesV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final assetFace in data) { + final companion = AssetFaceEntityCompanion( + assetId: Value(assetFace.assetId), + personId: Value(assetFace.personId), + imageWidth: Value(assetFace.imageWidth), + imageHeight: Value(assetFace.imageHeight), + boundingBoxX1: Value(assetFace.boundingBoxX1), + boundingBoxY1: Value(assetFace.boundingBoxY1), + boundingBoxX2: Value(assetFace.boundingBoxX2), + boundingBoxY2: Value(assetFace.boundingBoxY2), + sourceType: Value(assetFace.sourceType), + ); + + batch.insert( + _db.assetFaceEntity, + companion.copyWith(id: Value(assetFace.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: updateAssetFacesV1', error, stack); + rethrow; + } + } + + Future deleteAssetFacesV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final assetFace in data) { + batch.deleteWhere( + _db.assetFaceEntity, + (row) => row.id.equals(assetFace.assetFaceId), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: deleteAssetFacesV1', error, stack); + rethrow; } } } 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 7ee151f94..2334fc522 100644 --- a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart +++ b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart @@ -121,6 +121,7 @@ final _features = [ await db.memoryAssetEntity.deleteAll(); await db.stackEntity.deleteAll(); await db.personEntity.deleteAll(); + await db.assetFaceEntity.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 acd7b219b..d1803498b 100644 --- a/mobile/lib/presentation/pages/dev/media_stat.page.dart +++ b/mobile/lib/presentation/pages/dev/media_stat.page.dart @@ -170,6 +170,10 @@ final _remoteStats = [ name: 'People', load: (db) => db.managers.personEntity.count(), ), + _Stat( + name: 'AssetFaces', + load: (db) => db.managers.assetFaceEntity.count(), + ), ]; @RoutePage() diff --git a/mobile/lib/providers/infrastructure/asset_face.provider.dart b/mobile/lib/providers/infrastructure/asset_face.provider.dart new file mode 100644 index 000000000..386609ba9 --- /dev/null +++ b/mobile/lib/providers/infrastructure/asset_face.provider.dart @@ -0,0 +1,7 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/infrastructure/repositories/asset_face.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; + +final driftAssetFaceProvider = Provider( + (ref) => DriftAssetFaceRepository(ref.watch(driftProvider)), +); diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index 5cf357d5a..1c51cedf9 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -34,11 +34,12 @@ class AuthRepository extends DatabaseRepository { _drift.userMetadataEntity.deleteAll(), _drift.partnerEntity.deleteAll(), _drift.stackEntity.deleteAll(), - _drift.personEntity.deleteAll(), + _drift.assetFaceEntity.deleteAll(), ]); // Drift deletions - parent entities await Future.wait([ _drift.memoryEntity.deleteAll(), + _drift.personEntity.deleteAll(), _drift.remoteAlbumEntity.deleteAll(), _drift.remoteAssetEntity.deleteAll(), _drift.userEntity.deleteAll(), diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index deb19dfcf..f9d9c4fbe 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -109,6 +109,10 @@ void main() { .thenAnswer(successHandler); when(() => mockSyncStreamRepo.deletePeopleV1(any())) .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updateAssetFacesV1(any())) + .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.deleteAssetFacesV1(any())) + .thenAnswer(successHandler); sut = SyncStreamService( syncApiRepository: mockSyncApiRepo, diff --git a/mobile/test/drift/main/generated/schema.dart b/mobile/test/drift/main/generated/schema.dart index 209e70d78..22131b11b 100644 Binary files a/mobile/test/drift/main/generated/schema.dart and b/mobile/test/drift/main/generated/schema.dart differ diff --git a/mobile/test/drift/main/generated/schema_v4.dart b/mobile/test/drift/main/generated/schema_v4.dart new file mode 100644 index 000000000..d02e2ff9c Binary files /dev/null and b/mobile/test/drift/main/generated/schema_v4.dart differ