feat(mobile): asset face sync (#20022)

* feat(mobile): asset face sync

* fix: lint

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Daimolean 2025-07-23 01:17:52 +08:00 committed by GitHub
parent ab61bcfcc8
commit ac44f6d1e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 7270 additions and 81 deletions

File diff suppressed because one or more lines are too long

View File

@ -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 ?? "<NA>"},
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;
}
}

View File

@ -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 ?? "<NA>"},
thumbnailPath: $thumbnailPath,
isFavorite: $isFavorite,
isHidden: $isHidden,
color: ${color ?? "<NA>"},
@ -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 ^

View File

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

View File

@ -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<Column> get primaryKey => {id};
}

File diff suppressed because it is too large Load Diff

View File

@ -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()();

View File

@ -17,7 +17,6 @@ typedef $$PersonEntityTableCreateCompanionBuilder = i1.PersonEntityCompanion
required String ownerId,
required String name,
i0.Value<String?> faceAssetId,
required String thumbnailPath,
required bool isFavorite,
required bool isHidden,
i0.Value<String?> color,
@ -31,7 +30,6 @@ typedef $$PersonEntityTableUpdateCompanionBuilder = i1.PersonEntityCompanion
i0.Value<String> ownerId,
i0.Value<String> name,
i0.Value<String?> faceAssetId,
i0.Value<String> thumbnailPath,
i0.Value<bool> isFavorite,
i0.Value<bool> isHidden,
i0.Value<String?> color,
@ -94,10 +92,6 @@ class $$PersonEntityTableFilterComposer
column: $table.faceAssetId,
builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get thumbnailPath => $composableBuilder(
column: $table.thumbnailPath,
builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<bool> get isFavorite => $composableBuilder(
column: $table.isFavorite, builder: (column) => i0.ColumnFilters(column));
@ -160,10 +154,6 @@ class $$PersonEntityTableOrderingComposer
column: $table.faceAssetId,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get thumbnailPath => $composableBuilder(
column: $table.thumbnailPath,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<bool> get isFavorite => $composableBuilder(
column: $table.isFavorite,
builder: (column) => i0.ColumnOrderings(column));
@ -225,9 +215,6 @@ class $$PersonEntityTableAnnotationComposer
i0.GeneratedColumn<String> get faceAssetId => $composableBuilder(
column: $table.faceAssetId, builder: (column) => column);
i0.GeneratedColumn<String> get thumbnailPath => $composableBuilder(
column: $table.thumbnailPath, builder: (column) => column);
i0.GeneratedColumn<bool> get isFavorite => $composableBuilder(
column: $table.isFavorite, builder: (column) => column);
@ -293,7 +280,6 @@ class $$PersonEntityTableTableManager extends i0.RootTableManager<
i0.Value<String> ownerId = const i0.Value.absent(),
i0.Value<String> name = const i0.Value.absent(),
i0.Value<String?> faceAssetId = const i0.Value.absent(),
i0.Value<String> thumbnailPath = const i0.Value.absent(),
i0.Value<bool> isFavorite = const i0.Value.absent(),
i0.Value<bool> isHidden = const i0.Value.absent(),
i0.Value<String?> color = const i0.Value.absent(),
@ -306,7 +292,6 @@ class $$PersonEntityTableTableManager extends i0.RootTableManager<
ownerId: ownerId,
name: name,
faceAssetId: faceAssetId,
thumbnailPath: thumbnailPath,
isFavorite: isFavorite,
isHidden: isHidden,
color: color,
@ -319,7 +304,6 @@ class $$PersonEntityTableTableManager extends i0.RootTableManager<
required String ownerId,
required String name,
i0.Value<String?> faceAssetId = const i0.Value.absent(),
required String thumbnailPath,
required bool isFavorite,
required bool isHidden,
i0.Value<String?> color = const i0.Value.absent(),
@ -332,7 +316,6 @@ class $$PersonEntityTableTableManager extends i0.RootTableManager<
ownerId: ownerId,
name: name,
faceAssetId: faceAssetId,
thumbnailPath: thumbnailPath,
isFavorite: isFavorite,
isHidden: isHidden,
color: color,
@ -443,12 +426,6 @@ class $PersonEntityTable extends i2.PersonEntity
late final i0.GeneratedColumn<String> faceAssetId =
i0.GeneratedColumn<String>('face_asset_id', aliasedName, true,
type: i0.DriftSqlType.string, requiredDuringInsert: false);
static const i0.VerificationMeta _thumbnailPathMeta =
const i0.VerificationMeta('thumbnailPath');
@override
late final i0.GeneratedColumn<String> thumbnailPath =
i0.GeneratedColumn<String>('thumbnail_path', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true);
static const i0.VerificationMeta _isFavoriteMeta =
const i0.VerificationMeta('isFavorite');
@override
@ -487,7 +464,6 @@ class $PersonEntityTable extends i2.PersonEntity
ownerId,
name,
faceAssetId,
thumbnailPath,
isFavorite,
isHidden,
color,
@ -535,14 +511,6 @@ class $PersonEntityTable extends i2.PersonEntity
faceAssetId.isAcceptableOrUnknown(
data['face_asset_id']!, _faceAssetIdMeta));
}
if (data.containsKey('thumbnail_path')) {
context.handle(
_thumbnailPathMeta,
thumbnailPath.isAcceptableOrUnknown(
data['thumbnail_path']!, _thumbnailPathMeta));
} else if (isInserting) {
context.missing(_thumbnailPathMeta);
}
if (data.containsKey('is_favorite')) {
context.handle(
_isFavoriteMeta,
@ -586,8 +554,6 @@ class $PersonEntityTable extends i2.PersonEntity
.read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!,
faceAssetId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string, data['${effectivePrefix}face_asset_id']),
thumbnailPath: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string, data['${effectivePrefix}thumbnail_path'])!,
isFavorite: attachedDatabase.typeMapping
.read(i0.DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!,
isHidden: attachedDatabase.typeMapping
@ -618,7 +584,6 @@ class PersonEntityData extends i0.DataClass
final String ownerId;
final String name;
final String? faceAssetId;
final String thumbnailPath;
final bool isFavorite;
final bool isHidden;
final String? color;
@ -630,7 +595,6 @@ class PersonEntityData extends i0.DataClass
required this.ownerId,
required this.name,
this.faceAssetId,
required this.thumbnailPath,
required this.isFavorite,
required this.isHidden,
this.color,
@ -646,7 +610,6 @@ class PersonEntityData extends i0.DataClass
if (!nullToAbsent || faceAssetId != null) {
map['face_asset_id'] = i0.Variable<String>(faceAssetId);
}
map['thumbnail_path'] = i0.Variable<String>(thumbnailPath);
map['is_favorite'] = i0.Variable<bool>(isFavorite);
map['is_hidden'] = i0.Variable<bool>(isHidden);
if (!nullToAbsent || color != null) {
@ -668,7 +631,6 @@ class PersonEntityData extends i0.DataClass
ownerId: serializer.fromJson<String>(json['ownerId']),
name: serializer.fromJson<String>(json['name']),
faceAssetId: serializer.fromJson<String?>(json['faceAssetId']),
thumbnailPath: serializer.fromJson<String>(json['thumbnailPath']),
isFavorite: serializer.fromJson<bool>(json['isFavorite']),
isHidden: serializer.fromJson<bool>(json['isHidden']),
color: serializer.fromJson<String?>(json['color']),
@ -685,7 +647,6 @@ class PersonEntityData extends i0.DataClass
'ownerId': serializer.toJson<String>(ownerId),
'name': serializer.toJson<String>(name),
'faceAssetId': serializer.toJson<String?>(faceAssetId),
'thumbnailPath': serializer.toJson<String>(thumbnailPath),
'isFavorite': serializer.toJson<bool>(isFavorite),
'isHidden': serializer.toJson<bool>(isHidden),
'color': serializer.toJson<String?>(color),
@ -700,7 +661,6 @@ class PersonEntityData extends i0.DataClass
String? ownerId,
String? name,
i0.Value<String?> faceAssetId = const i0.Value.absent(),
String? thumbnailPath,
bool? isFavorite,
bool? isHidden,
i0.Value<String?> color = const i0.Value.absent(),
@ -712,7 +672,6 @@ class PersonEntityData extends i0.DataClass
ownerId: ownerId ?? this.ownerId,
name: name ?? this.name,
faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId,
thumbnailPath: thumbnailPath ?? this.thumbnailPath,
isFavorite: isFavorite ?? this.isFavorite,
isHidden: isHidden ?? this.isHidden,
color: color.present ? color.value : this.color,
@ -727,9 +686,6 @@ class PersonEntityData extends i0.DataClass
name: data.name.present ? data.name.value : this.name,
faceAssetId:
data.faceAssetId.present ? data.faceAssetId.value : this.faceAssetId,
thumbnailPath: data.thumbnailPath.present
? data.thumbnailPath.value
: this.thumbnailPath,
isFavorite:
data.isFavorite.present ? data.isFavorite.value : this.isFavorite,
isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden,
@ -747,7 +703,6 @@ class PersonEntityData extends i0.DataClass
..write('ownerId: $ownerId, ')
..write('name: $name, ')
..write('faceAssetId: $faceAssetId, ')
..write('thumbnailPath: $thumbnailPath, ')
..write('isFavorite: $isFavorite, ')
..write('isHidden: $isHidden, ')
..write('color: $color, ')
@ -758,7 +713,7 @@ class PersonEntityData extends i0.DataClass
@override
int get hashCode => Object.hash(id, createdAt, updatedAt, ownerId, name,
faceAssetId, thumbnailPath, isFavorite, isHidden, color, birthDate);
faceAssetId, isFavorite, isHidden, color, birthDate);
@override
bool operator ==(Object other) =>
identical(this, other) ||
@ -769,7 +724,6 @@ class PersonEntityData extends i0.DataClass
other.ownerId == this.ownerId &&
other.name == this.name &&
other.faceAssetId == this.faceAssetId &&
other.thumbnailPath == this.thumbnailPath &&
other.isFavorite == this.isFavorite &&
other.isHidden == this.isHidden &&
other.color == this.color &&
@ -783,7 +737,6 @@ class PersonEntityCompanion extends i0.UpdateCompanion<i1.PersonEntityData> {
final i0.Value<String> ownerId;
final i0.Value<String> name;
final i0.Value<String?> faceAssetId;
final i0.Value<String> thumbnailPath;
final i0.Value<bool> isFavorite;
final i0.Value<bool> isHidden;
final i0.Value<String?> color;
@ -795,7 +748,6 @@ class PersonEntityCompanion extends i0.UpdateCompanion<i1.PersonEntityData> {
this.ownerId = const i0.Value.absent(),
this.name = const i0.Value.absent(),
this.faceAssetId = const i0.Value.absent(),
this.thumbnailPath = const i0.Value.absent(),
this.isFavorite = const i0.Value.absent(),
this.isHidden = const i0.Value.absent(),
this.color = const i0.Value.absent(),
@ -808,7 +760,6 @@ class PersonEntityCompanion extends i0.UpdateCompanion<i1.PersonEntityData> {
required String ownerId,
required String name,
this.faceAssetId = const i0.Value.absent(),
required String thumbnailPath,
required bool isFavorite,
required bool isHidden,
this.color = const i0.Value.absent(),
@ -816,7 +767,6 @@ class PersonEntityCompanion extends i0.UpdateCompanion<i1.PersonEntityData> {
}) : id = i0.Value(id),
ownerId = i0.Value(ownerId),
name = i0.Value(name),
thumbnailPath = i0.Value(thumbnailPath),
isFavorite = i0.Value(isFavorite),
isHidden = i0.Value(isHidden);
static i0.Insertable<i1.PersonEntityData> custom({
@ -826,7 +776,6 @@ class PersonEntityCompanion extends i0.UpdateCompanion<i1.PersonEntityData> {
i0.Expression<String>? ownerId,
i0.Expression<String>? name,
i0.Expression<String>? faceAssetId,
i0.Expression<String>? thumbnailPath,
i0.Expression<bool>? isFavorite,
i0.Expression<bool>? isHidden,
i0.Expression<String>? color,
@ -839,7 +788,6 @@ class PersonEntityCompanion extends i0.UpdateCompanion<i1.PersonEntityData> {
if (ownerId != null) 'owner_id': ownerId,
if (name != null) 'name': name,
if (faceAssetId != null) 'face_asset_id': faceAssetId,
if (thumbnailPath != null) 'thumbnail_path': thumbnailPath,
if (isFavorite != null) 'is_favorite': isFavorite,
if (isHidden != null) 'is_hidden': isHidden,
if (color != null) 'color': color,
@ -854,7 +802,6 @@ class PersonEntityCompanion extends i0.UpdateCompanion<i1.PersonEntityData> {
i0.Value<String>? ownerId,
i0.Value<String>? name,
i0.Value<String?>? faceAssetId,
i0.Value<String>? thumbnailPath,
i0.Value<bool>? isFavorite,
i0.Value<bool>? isHidden,
i0.Value<String?>? color,
@ -866,7 +813,6 @@ class PersonEntityCompanion extends i0.UpdateCompanion<i1.PersonEntityData> {
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,
@ -895,9 +841,6 @@ class PersonEntityCompanion extends i0.UpdateCompanion<i1.PersonEntityData> {
if (faceAssetId.present) {
map['face_asset_id'] = i0.Variable<String>(faceAssetId.value);
}
if (thumbnailPath.present) {
map['thumbnail_path'] = i0.Variable<String>(thumbnailPath.value);
}
if (isFavorite.present) {
map['is_favorite'] = i0.Variable<bool>(isFavorite.value);
}
@ -922,7 +865,6 @@ class PersonEntityCompanion extends i0.UpdateCompanion<i1.PersonEntityData> {
..write('ownerId: $ownerId, ')
..write('name: $name, ')
..write('faceAssetId: $faceAssetId, ')
..write('thumbnailPath: $thumbnailPath, ')
..write('isFavorite: $isFavorite, ')
..write('isHidden: $isHidden, ')
..write('color: $color, ')

View File

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

View File

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

View File

@ -31,9 +31,11 @@ import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.
as i14;
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
as i15;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'
as i16;
import 'package:drift/internal/modular.dart' as i17;
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 {
$Drift(i0.QueryExecutor e) : super(e);
@ -64,8 +66,10 @@ abstract class $Drift extends i0.GeneratedDatabase {
late final i14.$MemoryAssetEntityTable memoryAssetEntity =
i14.$MemoryAssetEntityTable(this);
late final i15.$PersonEntityTable personEntity = i15.$PersonEntityTable(this);
i16.MergedAssetDrift get mergedAssetDrift => i17.ReadDatabaseContainer(this)
.accessor<i16.MergedAssetDrift>(i16.MergedAssetDrift.new);
late final i16.$AssetFaceEntityTable assetFaceEntity =
i16.$AssetFaceEntityTable(this);
i17.MergedAssetDrift get mergedAssetDrift => i18.ReadDatabaseContainer(this)
.accessor<i17.MergedAssetDrift>(i17.MergedAssetDrift.new);
@override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@ -88,7 +92,8 @@ abstract class $Drift extends i0.GeneratedDatabase {
remoteAlbumUserEntity,
memoryEntity,
memoryAssetEntity,
personEntity
personEntity,
assetFaceEntity
];
@override
i0.StreamQueryUpdateRules get streamUpdateRules =>
@ -227,6 +232,20 @@ abstract class $Drift extends i0.GeneratedDatabase {
i0.TableUpdate('person_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('person_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('asset_face_entity', kind: i0.UpdateKind.update),
],
),
],
);
@override
@ -268,4 +287,6 @@ class $DriftManager {
i14.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
i15.$$PersonEntityTableTableManager get personEntity =>
i15.$$PersonEntityTableTableManager(_db, _db.personEntity);
i16.$$AssetFaceEntityTableTableManager get assetFaceEntity =>
i16.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity);
}

View File

@ -1266,9 +1266,451 @@ final class Schema3 extends i0.VersionedSchema {
i1.GeneratedColumn<String> _column_75(String aliasedName) =>
i1.GeneratedColumn<String>('primary_asset_id', aliasedName, false,
type: i1.DriftSqlType.string);
final class Schema4 extends i0.VersionedSchema {
Schema4({required super.database}) : super(version: 4);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
localAssetEntity,
stackEntity,
idxLocalAssetChecksum,
uQRemoteAssetOwnerChecksum,
idxRemoteAssetChecksum,
userMetadataEntity,
partnerEntity,
localAlbumEntity,
localAlbumAssetEntity,
remoteExifEntity,
remoteAlbumEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
];
late final Shape0 userEntity = Shape0(
source: i0.VersionedTable(
entityName: 'user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
_column_4,
_column_5,
_column_6,
_column_7,
],
attachedDatabase: database,
),
alias: null);
late final Shape1 remoteAssetEntity = Shape1(
source: i0.VersionedTable(
entityName: 'remote_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_13,
_column_14,
_column_15,
_column_16,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
],
attachedDatabase: database,
),
alias: null);
late final Shape2 localAssetEntity = Shape2(
source: i0.VersionedTable(
entityName: 'local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_22,
_column_14,
_column_23,
],
attachedDatabase: database,
),
alias: null);
late final Shape3 stackEntity = Shape3(
source: i0.VersionedTable(
entityName: 'stack_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_0,
_column_9,
_column_5,
_column_15,
_column_75,
],
attachedDatabase: database,
),
alias: null);
final i1.Index idxLocalAssetChecksum = i1.Index('idx_local_asset_checksum',
'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)');
final i1.Index uQRemoteAssetOwnerChecksum = i1.Index(
'UQ_remote_asset_owner_checksum',
'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)');
final i1.Index idxRemoteAssetChecksum = i1.Index('idx_remote_asset_checksum',
'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)');
late final Shape4 userMetadataEntity = Shape4(
source: i0.VersionedTable(
entityName: 'user_metadata_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(user_id, "key")',
],
columns: [
_column_25,
_column_26,
_column_27,
],
attachedDatabase: database,
),
alias: null);
late final Shape5 partnerEntity = Shape5(
source: i0.VersionedTable(
entityName: 'partner_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(shared_by_id, shared_with_id)',
],
columns: [
_column_28,
_column_29,
_column_30,
],
attachedDatabase: database,
),
alias: null);
late final Shape6 localAlbumEntity = Shape6(
source: i0.VersionedTable(
entityName: 'local_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_0,
_column_1,
_column_5,
_column_31,
_column_32,
_column_33,
],
attachedDatabase: database,
),
alias: null);
late final Shape7 localAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'local_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(asset_id, album_id)',
],
columns: [
_column_34,
_column_35,
],
attachedDatabase: database,
),
alias: null);
late final Shape8 remoteExifEntity = Shape8(
source: i0.VersionedTable(
entityName: 'remote_exif_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(asset_id)',
],
columns: [
_column_36,
_column_37,
_column_38,
_column_39,
_column_40,
_column_41,
_column_11,
_column_10,
_column_42,
_column_43,
_column_44,
_column_45,
_column_46,
_column_47,
_column_48,
_column_49,
_column_50,
_column_51,
_column_52,
_column_53,
_column_54,
_column_55,
],
attachedDatabase: database,
),
alias: null);
late final Shape9 remoteAlbumEntity = Shape9(
source: i0.VersionedTable(
entityName: 'remote_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_0,
_column_1,
_column_56,
_column_9,
_column_5,
_column_15,
_column_57,
_column_58,
_column_59,
],
attachedDatabase: database,
),
alias: null);
late final Shape7 remoteAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'remote_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(asset_id, album_id)',
],
columns: [
_column_36,
_column_60,
],
attachedDatabase: database,
),
alias: null);
late final Shape10 remoteAlbumUserEntity = Shape10(
source: i0.VersionedTable(
entityName: 'remote_album_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(album_id, user_id)',
],
columns: [
_column_60,
_column_25,
_column_61,
],
attachedDatabase: database,
),
alias: null);
late final Shape11 memoryEntity = Shape11(
source: i0.VersionedTable(
entityName: 'memory_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_0,
_column_9,
_column_5,
_column_18,
_column_15,
_column_8,
_column_62,
_column_63,
_column_64,
_column_65,
_column_66,
_column_67,
],
attachedDatabase: database,
),
alias: null);
late final Shape12 memoryAssetEntity = Shape12(
source: i0.VersionedTable(
entityName: 'memory_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(asset_id, memory_id)',
],
columns: [
_column_36,
_column_68,
],
attachedDatabase: database,
),
alias: null);
late final Shape14 personEntity = Shape14(
source: i0.VersionedTable(
entityName: 'person_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_0,
_column_9,
_column_5,
_column_15,
_column_1,
_column_69,
_column_71,
_column_72,
_column_73,
_column_74,
],
attachedDatabase: database,
),
alias: null);
late final Shape15 assetFaceEntity = Shape15(
source: i0.VersionedTable(
entityName: 'asset_face_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_0,
_column_36,
_column_76,
_column_77,
_column_78,
_column_79,
_column_80,
_column_81,
_column_82,
_column_83,
],
attachedDatabase: database,
),
alias: null);
}
class Shape14 extends i0.VersionedTable {
Shape14({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<String> get ownerId =>
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get faceAssetId =>
columnsByName['face_asset_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get isFavorite =>
columnsByName['is_favorite']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get isHidden =>
columnsByName['is_hidden']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<String> get color =>
columnsByName['color']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get birthDate =>
columnsByName['birth_date']! as i1.GeneratedColumn<DateTime>;
}
class Shape15 extends i0.VersionedTable {
Shape15({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get assetId =>
columnsByName['asset_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get personId =>
columnsByName['person_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get imageWidth =>
columnsByName['image_width']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get imageHeight =>
columnsByName['image_height']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get boundingBoxX1 =>
columnsByName['bounding_box_x1']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get boundingBoxY1 =>
columnsByName['bounding_box_y1']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get boundingBoxX2 =>
columnsByName['bounding_box_x2']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get boundingBoxY2 =>
columnsByName['bounding_box_y2']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get sourceType =>
columnsByName['source_type']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<String> _column_76(String aliasedName) =>
i1.GeneratedColumn<String>('person_id', aliasedName, true,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES person_entity (id) ON DELETE SET NULL'));
i1.GeneratedColumn<int> _column_77(String aliasedName) =>
i1.GeneratedColumn<int>('image_width', aliasedName, false,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<int> _column_78(String aliasedName) =>
i1.GeneratedColumn<int>('image_height', aliasedName, false,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<int> _column_79(String aliasedName) =>
i1.GeneratedColumn<int>('bounding_box_x1', aliasedName, false,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<int> _column_80(String aliasedName) =>
i1.GeneratedColumn<int>('bounding_box_y1', aliasedName, false,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<int> _column_81(String aliasedName) =>
i1.GeneratedColumn<int>('bounding_box_x2', aliasedName, false,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<int> _column_82(String aliasedName) =>
i1.GeneratedColumn<int>('bounding_box_y2', aliasedName, false,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<String> _column_83(String aliasedName) =>
i1.GeneratedColumn<String>('source_type', aliasedName, false,
type: i1.DriftSqlType.string);
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@ -1282,6 +1724,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from2To3(migrator, schema);
return 3;
case 3:
final schema = Schema4(database: database);
final migrator = i1.Migrator(database, schema);
await from3To4(migrator, schema);
return 4;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@ -1291,9 +1738,11 @@ i0.MigrationStepWithVersion migrationSteps({
i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
}) =>
i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
from2To3: from2To3,
from3To4: from3To4,
));

View File

@ -26,7 +26,6 @@ extension on PersonEntityData {
ownerId: ownerId,
name: name,
faceAssetId: faceAssetId,
thumbnailPath: thumbnailPath,
isFavorite: isFavorite,
isHidden: isHidden,
color: color,

View File

@ -58,6 +58,7 @@ class SyncApiRepository {
SyncRequestType.partnerStacksV1,
SyncRequestType.userMetadataV1,
SyncRequestType.peopleV1,
SyncRequestType.assetFacesV1,
],
).toJson(),
);
@ -176,6 +177,8 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.userMetadataDeleteV1: SyncUserMetadataDeleteV1.fromJson,
SyncEntityType.personV1: SyncPersonV1.fromJson,
SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson,
SyncEntityType.assetFaceV1: SyncAssetFaceV1.fromJson,
SyncEntityType.assetFaceDeleteV1: SyncAssetFaceDeleteV1.fromJson,
};
class _SyncAckV1 {

View File

@ -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<SyncPersonDeleteV1> 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<void> updateAssetFacesV1(Iterable<SyncAssetFaceV1> 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<void> deleteAssetFacesV1(Iterable<SyncAssetFaceDeleteV1> 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;
}
}
}

View File

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

View File

@ -170,6 +170,10 @@ final _remoteStats = [
name: 'People',
load: (db) => db.managers.personEntity.count(),
),
_Stat(
name: 'AssetFaces',
load: (db) => db.managers.assetFaceEntity.count(),
),
];
@RoutePage()

View File

@ -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<DriftAssetFaceRepository>(
(ref) => DriftAssetFaceRepository(ref.watch(driftProvider)),
);

View File

@ -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(),

View File

@ -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,

View File

@ -6,6 +6,7 @@ import 'package:drift/internal/migrations.dart';
import 'schema_v1.dart' as v1;
import 'schema_v2.dart' as v2;
import 'schema_v3.dart' as v3;
import 'schema_v4.dart' as v4;
class GeneratedHelper implements SchemaInstantiationHelper {
@override
@ -17,10 +18,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v2.DatabaseAtV2(db);
case 3:
return v3.DatabaseAtV3(db);
case 4:
return v4.DatabaseAtV4(db);
default:
throw MissingSchemaException(version, versions);
}
}
static const versions = const [1, 2, 3];
static const versions = const [1, 2, 3, 4];
}

File diff suppressed because it is too large Load Diff