diff --git a/mobile/lib/domain/models/asset/asset_edit.model.dart b/mobile/lib/domain/models/asset/asset_edit.model.dart new file mode 100644 index 0000000000..e97558dfdb --- /dev/null +++ b/mobile/lib/domain/models/asset/asset_edit.model.dart @@ -0,0 +1 @@ +enum AssetEditAction { rotate, crop, mirror, other } diff --git a/mobile/lib/domain/models/asset/base_asset.model.dart b/mobile/lib/domain/models/asset/base_asset.model.dart index 5774a13c90..f9ef9b5417 100644 --- a/mobile/lib/domain/models/asset/base_asset.model.dart +++ b/mobile/lib/domain/models/asset/base_asset.model.dart @@ -22,6 +22,7 @@ sealed class BaseAsset { final int? durationInSeconds; final bool isFavorite; final String? livePhotoVideoId; + final bool isEdited; const BaseAsset({ required this.name, @@ -34,6 +35,7 @@ sealed class BaseAsset { this.durationInSeconds, this.isFavorite = false, this.livePhotoVideoId, + required this.isEdited, }); bool get isImage => type == AssetType.image; diff --git a/mobile/lib/domain/models/asset/local_asset.model.dart b/mobile/lib/domain/models/asset/local_asset.model.dart index b7ef635f2d..887dfd3834 100644 --- a/mobile/lib/domain/models/asset/local_asset.model.dart +++ b/mobile/lib/domain/models/asset/local_asset.model.dart @@ -28,6 +28,7 @@ class LocalAsset extends BaseAsset { this.adjustmentTime, this.latitude, this.longitude, + required super.isEdited, }) : remoteAssetId = remoteId; @override @@ -107,6 +108,7 @@ class LocalAsset extends BaseAsset { DateTime? adjustmentTime, double? latitude, double? longitude, + bool? isEdited, }) { return LocalAsset( id: id ?? this.id, @@ -125,6 +127,7 @@ class LocalAsset extends BaseAsset { adjustmentTime: adjustmentTime ?? this.adjustmentTime, latitude: latitude ?? this.latitude, longitude: longitude ?? this.longitude, + isEdited: isEdited ?? this.isEdited, ); } } diff --git a/mobile/lib/domain/models/asset/remote_asset.model.dart b/mobile/lib/domain/models/asset/remote_asset.model.dart index 4974dc9118..fce89a3b05 100644 --- a/mobile/lib/domain/models/asset/remote_asset.model.dart +++ b/mobile/lib/domain/models/asset/remote_asset.model.dart @@ -28,6 +28,7 @@ class RemoteAsset extends BaseAsset { this.visibility = AssetVisibility.timeline, super.livePhotoVideoId, this.stackId, + required super.isEdited, }) : localAssetId = localId; @override @@ -61,6 +62,7 @@ class RemoteAsset extends BaseAsset { stackId: ${stackId ?? ""}, checksum: $checksum, livePhotoVideoId: ${livePhotoVideoId ?? ""}, + isEdited: $isEdited, }'''; } @@ -104,6 +106,7 @@ class RemoteAsset extends BaseAsset { AssetVisibility? visibility, String? livePhotoVideoId, String? stackId, + bool? isEdited, }) { return RemoteAsset( id: id ?? this.id, @@ -122,6 +125,7 @@ class RemoteAsset extends BaseAsset { visibility: visibility ?? this.visibility, livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, stackId: stackId ?? this.stackId, + isEdited: isEdited ?? this.isEdited, ); } } diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart index 8b324cf6cf..e4a129d322 100644 --- a/mobile/lib/domain/services/local_sync.service.dart +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -436,5 +436,6 @@ extension PlatformToLocalAsset on PlatformAsset { adjustmentTime: tryFromSecondsSinceEpoch(adjustmentTime, isUtc: true), latitude: latitude, longitude: longitude, + isEdited: false, ); } diff --git a/mobile/lib/domain/services/search.service.dart b/mobile/lib/domain/services/search.service.dart index 6ccc5a97bf..1f854d42fc 100644 --- a/mobile/lib/domain/services/search.service.dart +++ b/mobile/lib/domain/services/search.service.dart @@ -43,7 +43,7 @@ class SearchService { } return SearchResult( - assets: response.assets.items.map((e) => e.toDto()).toList(), + assets: response.assets.items.map((e) => e.toDto(false)).toList(), nextPage: response.assets.nextPage?.toInt(), ); } catch (error, stackTrace) { @@ -54,7 +54,7 @@ class SearchService { } extension on AssetResponseDto { - RemoteAsset toDto() { + RemoteAsset toDto(bool isEdited) { return RemoteAsset( id: id, name: originalFileName, @@ -77,6 +77,8 @@ extension on AssetResponseDto { thumbHash: thumbhash, localId: null, type: type.toAssetType(), + // its a remote asset so it will always show the edited version + isEdited: isEdited, ); } } diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index e14321a780..1e3490dd82 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -116,6 +116,10 @@ class SyncStreamService { return; case SyncEntityType.assetDeleteV1: return _syncStreamRepository.deleteAssetsV1(data.cast()); + case SyncEntityType.assetEditV1: + return _syncStreamRepository.updateAssetEditsV1(data.cast()); + case SyncEntityType.assetEditDeleteV1: + return _syncStreamRepository.deleteAssetEditsV1(data.cast()); case SyncEntityType.assetExifV1: return _syncStreamRepository.updateAssetsExifV1(data.cast()); case SyncEntityType.assetMetadataV1: diff --git a/mobile/lib/infrastructure/entities/asset_edit.entity.dart b/mobile/lib/infrastructure/entities/asset_edit.entity.dart new file mode 100644 index 0000000000..9820923421 --- /dev/null +++ b/mobile/lib/infrastructure/entities/asset_edit.entity.dart @@ -0,0 +1,23 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/asset/asset_edit.model.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class AssetEditEntity extends Table with DriftDefaultsMixin { + const AssetEditEntity(); + + TextColumn get id => text()(); + + TextColumn get assetId => text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)(); + + IntColumn get action => intEnum()(); + + BlobColumn get parameters => blob().map(editParameterConverter)(); + + @override + Set get primaryKey => {id}; +} + +final JsonTypeConverter2, Uint8List, Object?> editParameterConverter = TypeConverter.jsonb( + fromJson: (json) => json as Map, +); diff --git a/mobile/lib/infrastructure/entities/asset_edit.entity.drift.dart b/mobile/lib/infrastructure/entities/asset_edit.entity.drift.dart new file mode 100644 index 0000000000..17d5194f8b --- /dev/null +++ b/mobile/lib/infrastructure/entities/asset_edit.entity.drift.dart @@ -0,0 +1,678 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart' + as i1; +import 'package:immich_mobile/domain/models/asset/asset_edit.model.dart' as i2; +import 'dart:typed_data' as i3; +import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart' + as i4; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' + as i5; +import 'package:drift/internal/modular.dart' as i6; + +typedef $$AssetEditEntityTableCreateCompanionBuilder = + i1.AssetEditEntityCompanion Function({ + required String id, + required String assetId, + required i2.AssetEditAction action, + required Map parameters, + }); +typedef $$AssetEditEntityTableUpdateCompanionBuilder = + i1.AssetEditEntityCompanion Function({ + i0.Value id, + i0.Value assetId, + i0.Value action, + i0.Value> parameters, + }); + +final class $$AssetEditEntityTableReferences + extends + i0.BaseReferences< + i0.GeneratedDatabase, + i1.$AssetEditEntityTable, + i1.AssetEditEntityData + > { + $$AssetEditEntityTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static i5.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) => + i6.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .createAlias( + i0.$_aliasNameGenerator( + i6.ReadDatabaseContainer(db) + .resultSet('asset_edit_entity') + .assetId, + i6.ReadDatabaseContainer( + db, + ).resultSet('remote_asset_entity').id, + ), + ); + + i5.$$RemoteAssetEntityTableProcessedTableManager get assetId { + final $_column = $_itemColumn('asset_id')!; + + final manager = i5 + .$$RemoteAssetEntityTableTableManager( + $_db, + i6.ReadDatabaseContainer( + $_db, + ).resultSet('remote_asset_entity'), + ) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_assetIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$AssetEditEntityTableFilterComposer + extends i0.Composer { + $$AssetEditEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnWithTypeConverterFilters + get action => $composableBuilder( + column: $table.action, + builder: (column) => i0.ColumnWithTypeConverterFilters(column), + ); + + i0.ColumnWithTypeConverterFilters< + Map, + Map, + i3.Uint8List + > + get parameters => $composableBuilder( + column: $table.parameters, + builder: (column) => i0.ColumnWithTypeConverterFilters(column), + ); + + i5.$$RemoteAssetEntityTableFilterComposer get assetId { + final i5.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i6.ReadDatabaseContainer( + $db, + ).resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => i5.$$RemoteAssetEntityTableFilterComposer( + $db: $db, + $table: i6.ReadDatabaseContainer( + $db, + ).resultSet('remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$AssetEditEntityTableOrderingComposer + extends i0.Composer { + $$AssetEditEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get action => $composableBuilder( + column: $table.action, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get parameters => $composableBuilder( + column: $table.parameters, + builder: (column) => i0.ColumnOrderings(column), + ); + + i5.$$RemoteAssetEntityTableOrderingComposer get assetId { + final i5.$$RemoteAssetEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i6.ReadDatabaseContainer( + $db, + ).resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => i5.$$RemoteAssetEntityTableOrderingComposer( + $db: $db, + $table: i6.ReadDatabaseContainer( + $db, + ).resultSet('remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$AssetEditEntityTableAnnotationComposer + extends i0.Composer { + $$AssetEditEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter get action => + $composableBuilder(column: $table.action, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter, i3.Uint8List> + get parameters => $composableBuilder( + column: $table.parameters, + builder: (column) => column, + ); + + i5.$$RemoteAssetEntityTableAnnotationComposer get assetId { + final i5.$$RemoteAssetEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i6.ReadDatabaseContainer( + $db, + ).resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => i5.$$RemoteAssetEntityTableAnnotationComposer( + $db: $db, + $table: i6.ReadDatabaseContainer( + $db, + ).resultSet('remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$AssetEditEntityTableTableManager + extends + i0.RootTableManager< + i0.GeneratedDatabase, + i1.$AssetEditEntityTable, + i1.AssetEditEntityData, + i1.$$AssetEditEntityTableFilterComposer, + i1.$$AssetEditEntityTableOrderingComposer, + i1.$$AssetEditEntityTableAnnotationComposer, + $$AssetEditEntityTableCreateCompanionBuilder, + $$AssetEditEntityTableUpdateCompanionBuilder, + (i1.AssetEditEntityData, i1.$$AssetEditEntityTableReferences), + i1.AssetEditEntityData, + i0.PrefetchHooks Function({bool assetId}) + > { + $$AssetEditEntityTableTableManager( + i0.GeneratedDatabase db, + i1.$AssetEditEntityTable table, + ) : super( + i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$AssetEditEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + i1.$$AssetEditEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => i1 + .$$AssetEditEntityTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + i0.Value id = const i0.Value.absent(), + i0.Value assetId = const i0.Value.absent(), + i0.Value action = const i0.Value.absent(), + i0.Value> parameters = + const i0.Value.absent(), + }) => i1.AssetEditEntityCompanion( + id: id, + assetId: assetId, + action: action, + parameters: parameters, + ), + createCompanionCallback: + ({ + required String id, + required String assetId, + required i2.AssetEditAction action, + required Map parameters, + }) => i1.AssetEditEntityCompanion.insert( + id: id, + assetId: assetId, + action: action, + parameters: parameters, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + i1.$$AssetEditEntityTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({assetId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (assetId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.assetId, + referencedTable: i1 + .$$AssetEditEntityTableReferences + ._assetIdTable(db), + referencedColumn: i1 + .$$AssetEditEntityTableReferences + ._assetIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$AssetEditEntityTableProcessedTableManager = + i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$AssetEditEntityTable, + i1.AssetEditEntityData, + i1.$$AssetEditEntityTableFilterComposer, + i1.$$AssetEditEntityTableOrderingComposer, + i1.$$AssetEditEntityTableAnnotationComposer, + $$AssetEditEntityTableCreateCompanionBuilder, + $$AssetEditEntityTableUpdateCompanionBuilder, + (i1.AssetEditEntityData, i1.$$AssetEditEntityTableReferences), + i1.AssetEditEntityData, + i0.PrefetchHooks Function({bool assetId}) + >; + +class $AssetEditEntityTable extends i4.AssetEditEntity + with i0.TableInfo<$AssetEditEntityTable, i1.AssetEditEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $AssetEditEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', + aliasedName, + false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + ); + static const i0.VerificationMeta _assetIdMeta = const i0.VerificationMeta( + 'assetId', + ); + @override + late final i0.GeneratedColumn assetId = i0.GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + @override + late final i0.GeneratedColumnWithTypeConverter + action = + i0.GeneratedColumn( + 'action', + aliasedName, + false, + type: i0.DriftSqlType.int, + requiredDuringInsert: true, + ).withConverter( + i1.$AssetEditEntityTable.$converteraction, + ); + @override + late final i0.GeneratedColumnWithTypeConverter< + Map, + i3.Uint8List + > + parameters = + i0.GeneratedColumn( + 'parameters', + aliasedName, + false, + type: i0.DriftSqlType.blob, + requiredDuringInsert: true, + ).withConverter>( + i1.$AssetEditEntityTable.$converterparameters, + ); + @override + List get $columns => [id, assetId, action, parameters]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'asset_edit_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, { + bool isInserting = false, + }) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('asset_id')) { + context.handle( + _assetIdMeta, + assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta), + ); + } else if (isInserting) { + context.missing(_assetIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.AssetEditEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.AssetEditEntityData( + id: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + assetId: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + action: i1.$AssetEditEntityTable.$converteraction.fromSql( + attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, + data['${effectivePrefix}action'], + )!, + ), + parameters: i1.$AssetEditEntityTable.$converterparameters.fromSql( + attachedDatabase.typeMapping.read( + i0.DriftSqlType.blob, + data['${effectivePrefix}parameters'], + )!, + ), + ); + } + + @override + $AssetEditEntityTable createAlias(String alias) { + return $AssetEditEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 $converteraction = + const i0.EnumIndexConverter( + i2.AssetEditAction.values, + ); + static i0.JsonTypeConverter2, i3.Uint8List, Object?> + $converterparameters = i4.editParameterConverter; + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AssetEditEntityData extends i0.DataClass + implements i0.Insertable { + final String id; + final String assetId; + final i2.AssetEditAction action; + final Map parameters; + const AssetEditEntityData({ + required this.id, + required this.assetId, + required this.action, + required this.parameters, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = i0.Variable(id); + map['asset_id'] = i0.Variable(assetId); + { + map['action'] = i0.Variable( + i1.$AssetEditEntityTable.$converteraction.toSql(action), + ); + } + { + map['parameters'] = i0.Variable( + i1.$AssetEditEntityTable.$converterparameters.toSql(parameters), + ); + } + return map; + } + + factory AssetEditEntityData.fromJson( + Map json, { + i0.ValueSerializer? serializer, + }) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return AssetEditEntityData( + id: serializer.fromJson(json['id']), + assetId: serializer.fromJson(json['assetId']), + action: i1.$AssetEditEntityTable.$converteraction.fromJson( + serializer.fromJson(json['action']), + ), + parameters: i1.$AssetEditEntityTable.$converterparameters.fromJson( + serializer.fromJson(json['parameters']), + ), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'assetId': serializer.toJson(assetId), + 'action': serializer.toJson( + i1.$AssetEditEntityTable.$converteraction.toJson(action), + ), + 'parameters': serializer.toJson( + i1.$AssetEditEntityTable.$converterparameters.toJson(parameters), + ), + }; + } + + i1.AssetEditEntityData copyWith({ + String? id, + String? assetId, + i2.AssetEditAction? action, + Map? parameters, + }) => i1.AssetEditEntityData( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + action: action ?? this.action, + parameters: parameters ?? this.parameters, + ); + AssetEditEntityData copyWithCompanion(i1.AssetEditEntityCompanion data) { + return AssetEditEntityData( + id: data.id.present ? data.id.value : this.id, + assetId: data.assetId.present ? data.assetId.value : this.assetId, + action: data.action.present ? data.action.value : this.action, + parameters: data.parameters.present + ? data.parameters.value + : this.parameters, + ); + } + + @override + String toString() { + return (StringBuffer('AssetEditEntityData(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('action: $action, ') + ..write('parameters: $parameters') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, assetId, action, parameters); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.AssetEditEntityData && + other.id == this.id && + other.assetId == this.assetId && + other.action == this.action && + other.parameters == this.parameters); +} + +class AssetEditEntityCompanion + extends i0.UpdateCompanion { + final i0.Value id; + final i0.Value assetId; + final i0.Value action; + final i0.Value> parameters; + const AssetEditEntityCompanion({ + this.id = const i0.Value.absent(), + this.assetId = const i0.Value.absent(), + this.action = const i0.Value.absent(), + this.parameters = const i0.Value.absent(), + }); + AssetEditEntityCompanion.insert({ + required String id, + required String assetId, + required i2.AssetEditAction action, + required Map parameters, + }) : id = i0.Value(id), + assetId = i0.Value(assetId), + action = i0.Value(action), + parameters = i0.Value(parameters); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? assetId, + i0.Expression? action, + i0.Expression? parameters, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (assetId != null) 'asset_id': assetId, + if (action != null) 'action': action, + if (parameters != null) 'parameters': parameters, + }); + } + + i1.AssetEditEntityCompanion copyWith({ + i0.Value? id, + i0.Value? assetId, + i0.Value? action, + i0.Value>? parameters, + }) { + return i1.AssetEditEntityCompanion( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + action: action ?? this.action, + parameters: parameters ?? this.parameters, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (assetId.present) { + map['asset_id'] = i0.Variable(assetId.value); + } + if (action.present) { + map['action'] = i0.Variable( + i1.$AssetEditEntityTable.$converteraction.toSql(action.value), + ); + } + if (parameters.present) { + map['parameters'] = i0.Variable( + i1.$AssetEditEntityTable.$converterparameters.toSql(parameters.value), + ); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AssetEditEntityCompanion(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('action: $action, ') + ..write('parameters: $parameters') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.dart b/mobile/lib/infrastructure/entities/local_asset.entity.dart index 6591f922ae..56916878fc 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.dart @@ -30,7 +30,7 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { } extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData { - LocalAsset toDto({String? remoteId}) => LocalAsset( + LocalAsset toDto(bool isEdited, {String? remoteId}) => LocalAsset( id: id, name: name, checksum: checksum, @@ -47,5 +47,6 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData { latitude: latitude, longitude: longitude, cloudId: iCloudId, + isEdited: isEdited, ); } diff --git a/mobile/lib/infrastructure/entities/merged_asset.drift b/mobile/lib/infrastructure/entities/merged_asset.drift index 93d7f0c90d..c4f30d791e 100644 --- a/mobile/lib/infrastructure/entities/merged_asset.drift +++ b/mobile/lib/infrastructure/entities/merged_asset.drift @@ -3,6 +3,7 @@ import 'stack.entity.dart'; import 'local_asset.entity.dart'; import 'local_album.entity.dart'; import 'local_album_asset.entity.dart'; +import 'asset_edit.entity.dart'; mergedAsset: SELECT @@ -26,6 +27,7 @@ SELECT NULL as latitude, NULL as longitude, NULL as adjustmentTime + CASE WHEN EXISTS (SELECT 1 FROM asset_edit_entity aee WHERE aee.asset_id = rae.id) THEN 1 ELSE 0 END as is_edited FROM remote_asset_entity rae LEFT JOIN @@ -62,6 +64,7 @@ SELECT lae.latitude, lae.longitude, lae.adjustment_time + 0 as is_edited FROM local_asset_entity lae WHERE NOT EXISTS ( diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.dart index dcc885a2a9..f3ceb928fe 100644 --- a/mobile/lib/infrastructure/entities/remote_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.dart @@ -49,7 +49,7 @@ class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin } extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData { - RemoteAsset toDto({String? localId}) => RemoteAsset( + RemoteAsset toDto(bool isEdited, {String? localId}) => RemoteAsset( id: id, name: name, ownerId: ownerId, @@ -66,5 +66,6 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData { livePhotoVideoId: livePhotoVideoId, localId: localId, stackId: stackId, + isEdited: isEdited, ); } diff --git a/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart b/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart index 2eaff5d5fb..d239588529 100644 --- a/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart @@ -45,5 +45,6 @@ extension TrashedLocalAssetEntityDataDomainExtension on TrashedLocalAssetEntityD height: height, width: width, orientation: orientation, + isEdited: false, ); } diff --git a/mobile/lib/infrastructure/repositories/backup.repository.dart b/mobile/lib/infrastructure/repositories/backup.repository.dart index 0241711d4b..35eb4af6c0 100644 --- a/mobile/lib/infrastructure/repositories/backup.repository.dart +++ b/mobile/lib/infrastructure/repositories/backup.repository.dart @@ -112,6 +112,13 @@ class DriftBackupRepository extends DriftDatabaseRepository { query.where((lae) => lae.checksum.isNotNull()); } - return query.map((localAsset) => localAsset.toDto()).get(); + final assetsQuery = query.join([ + leftOuterJoin(_db.remoteAssetEntity, _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum)), + leftOuterJoin(_db.assetEditEntity, _db.assetEditEntity.assetId.equalsExp(_db.remoteAssetEntity.id)), + ]); + + return assetsQuery + .map((row) => row.readTable(_db.localAssetEntity).toDto(row.readTableOrNull(_db.assetEditEntity) != null)) + .get(); } } diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 482f7f04b2..e6e3601c12 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -4,6 +4,7 @@ import 'package:drift/drift.dart'; 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/asset_edit.entity.dart'; import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart'; import 'package:immich_mobile/infrastructure/entities/auth_user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; @@ -66,6 +67,7 @@ class IsarDatabaseRepository implements IDatabaseRepository { AssetFaceEntity, StoreEntity, TrashedLocalAssetEntity, + AssetEditEntity, ], include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'}, ) diff --git a/mobile/lib/infrastructure/repositories/local_album.repository.dart b/mobile/lib/infrastructure/repositories/local_album.repository.dart index a59e200923..039e87dc1a 100644 --- a/mobile/lib/infrastructure/repositories/local_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_album.repository.dart @@ -188,10 +188,17 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { final query = _db.localAlbumAssetEntity.select().join([ innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)), + leftOuterJoin( + _db.remoteAssetEntity, + _db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum), + ), + leftOuterJoin(_db.assetEditEntity, _db.assetEditEntity.assetId.equalsExp(_db.remoteAssetEntity.id)), ]) ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) ..orderBy([OrderingTerm.asc(_db.localAssetEntity.id)]); - return query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get(); + return query + .map((row) => row.readTable(_db.localAssetEntity).toDto(row.readTableOrNull(_db.assetEditEntity) != null)) + .get(); } Future> getAssetIds(String albumId) { @@ -239,11 +246,18 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { final query = _db.localAlbumAssetEntity.select().join([ innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)), + leftOuterJoin( + _db.remoteAssetEntity, + _db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum), + ), + leftOuterJoin(_db.assetEditEntity, _db.assetEditEntity.assetId.equalsExp(_db.remoteAssetEntity.id)), ]) ..where(_db.localAlbumAssetEntity.albumId.equals(albumId) & _db.localAssetEntity.checksum.isNull()) ..orderBy([OrderingTerm.asc(_db.localAssetEntity.id)]); - return query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get(); + return query + .map((row) => row.readTable(_db.localAssetEntity).toDto(row.readTableOrNull(_db.assetEditEntity) != null)) + .get(); } Future updateCloudMapping(Map cloudMapping) { @@ -417,12 +431,19 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { final query = _db.localAlbumAssetEntity.select().join([ innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)), + leftOuterJoin( + _db.remoteAssetEntity, + _db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum), + ), + leftOuterJoin(_db.assetEditEntity, _db.assetEditEntity.assetId.equalsExp(_db.remoteAssetEntity.id)), ]) ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) ..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)]) ..limit(1); - final results = await query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get(); + final results = await query + .map((row) => row.readTable(_db.localAssetEntity).toDto(row.readTableOrNull(_db.assetEditEntity) != null)) + .get(); return results.isNotEmpty ? results.first : null; } diff --git a/mobile/lib/infrastructure/repositories/local_asset.repository.dart b/mobile/lib/infrastructure/repositories/local_asset.repository.dart index 6a9181e604..2d0a7d1850 100644 --- a/mobile/lib/infrastructure/repositories/local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_asset.repository.dart @@ -23,10 +23,11 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum), useColumns: false, ), + leftOuterJoin(_db.assetEditEntity, _db.assetEditEntity.assetId.equalsExp(_db.remoteAssetEntity.id)), ])..where(_db.localAssetEntity.id.equals(id)); return query.map((row) { - final asset = row.readTable(_db.localAssetEntity).toDto(); + final asset = row.readTable(_db.localAssetEntity).toDto(row.readTableOrNull(_db.assetEditEntity) != null); return asset.copyWith(remoteId: row.read(_db.remoteAssetEntity.id)); }); } @@ -34,9 +35,14 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { Future get(String id) => _assetSelectable(id).getSingleOrNull(); Future> getByChecksum(String checksum) { - final query = _db.localAssetEntity.select()..where((lae) => lae.checksum.equals(checksum)); + final query = _db.localAssetEntity.select().join([ + leftOuterJoin(_db.remoteAssetEntity, _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum)), + leftOuterJoin(_db.assetEditEntity, _db.assetEditEntity.assetId.equalsExp(_db.remoteAssetEntity.id)), + ])..where(_db.localAssetEntity.checksum.equals(checksum)); - return query.map((row) => row.toDto()).get(); + return query + .map((row) => row.readTable(_db.localAssetEntity).toDto(row.readTableOrNull(_db.assetEditEntity) != null)) + .get(); } Stream watch(String id) => _assetSelectable(id).watchSingleOrNull(); @@ -70,9 +76,14 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { } Future getById(String id) { - final query = _db.localAssetEntity.select()..where((lae) => lae.id.equals(id)); + final query = _db.localAssetEntity.select().join([ + leftOuterJoin(_db.remoteAssetEntity, _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum)), + leftOuterJoin(_db.assetEditEntity, _db.assetEditEntity.assetId.equalsExp(_db.remoteAssetEntity.id)), + ])..where(_db.localAssetEntity.id.equals(id)); - return query.map((row) => row.toDto()).getSingleOrNull(); + return query + .map((row) => row.readTable(_db.localAssetEntity).toDto(row.readTableOrNull(_db.assetEditEntity) != null)) + .getSingleOrNull(); } Future getCount() { @@ -114,6 +125,11 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { await (_db.select(_db.localAlbumAssetEntity).join([ innerJoin(_db.localAlbumEntity, _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id)), innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)), + leftOuterJoin( + _db.remoteAssetEntity, + _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum), + ), + leftOuterJoin(_db.assetEditEntity, _db.assetEditEntity.assetId.equalsExp(_db.remoteAssetEntity.id)), ])..where( _db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) & _db.localAssetEntity.checksum.isIn(slice), @@ -123,7 +139,7 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { for (final row in rows) { final albumId = row.readTable(_db.localAlbumAssetEntity).albumId; final assetData = row.readTable(_db.localAssetEntity); - final asset = assetData.toDto(); + final asset = assetData.toDto(row.readTableOrNull(_db.assetEditEntity) != null); (result[albumId] ??= []).add(asset); } } @@ -149,6 +165,7 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { final query = _db.localAssetEntity.select().join([ innerJoin(_db.remoteAssetEntity, _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum)), + leftOuterJoin(_db.assetEditEntity, _db.assetEditEntity.assetId.equalsExp(_db.remoteAssetEntity.id)), ]); Expression whereClause = @@ -172,7 +189,9 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { query.where(whereClause); final rows = await query.get(); - return rows.map((row) => row.readTable(_db.localAssetEntity).toDto()).toList(); + return rows + .map((row) => row.readTable(_db.localAssetEntity).toDto(row.readTableOrNull(_db.assetEditEntity) != null)) + .toList(); } Future> getEmptyCloudIdAssets() { diff --git a/mobile/lib/infrastructure/repositories/memory.repository.dart b/mobile/lib/infrastructure/repositories/memory.repository.dart index 0dcf7200cc..c6f86f352f 100644 --- a/mobile/lib/infrastructure/repositories/memory.repository.dart +++ b/mobile/lib/infrastructure/repositories/memory.repository.dart @@ -22,6 +22,7 @@ class DriftMemoryRepository extends DriftDatabaseRepository { _db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline), ), + leftOuterJoin(_db.assetEditEntity, _db.assetEditEntity.assetId.equalsExp(_db.remoteAssetEntity.id)), ]) ..where(_db.memoryEntity.ownerId.equals(ownerId)) ..where(_db.memoryEntity.deletedAt.isNull()) @@ -42,9 +43,9 @@ class DriftMemoryRepository extends DriftDatabaseRepository { final existingMemory = memoriesMap[memory.id]; if (existingMemory != null) { - existingMemory.assets.add(asset.toDto()); + existingMemory.assets.add(asset.toDto(row.readTableOrNull(_db.assetEditEntity) != null)); } else { - final assets = [asset.toDto()]; + final assets = [asset.toDto(row.readTableOrNull(_db.assetEditEntity) != null)]; memoriesMap[memory.id] = memory.toDto().copyWith(assets: assets); } } @@ -62,6 +63,7 @@ class DriftMemoryRepository extends DriftDatabaseRepository { _db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline), ), + leftOuterJoin(_db.assetEditEntity, _db.assetEditEntity.assetId.equalsExp(_db.remoteAssetEntity.id)), ]) ..where(_db.memoryEntity.id.equals(memoryId)) ..where(_db.memoryEntity.deletedAt.isNull()) @@ -78,7 +80,7 @@ class DriftMemoryRepository extends DriftDatabaseRepository { for (final row in rows) { final asset = row.readTable(_db.remoteAssetEntity); - assets.add(asset.toDto()); + assets.add(asset.toDto(row.readTableOrNull(_db.assetEditEntity) != null)); } return memory.toDto().copyWith(assets: assets); diff --git a/mobile/lib/infrastructure/repositories/remote_album.repository.dart b/mobile/lib/infrastructure/repositories/remote_album.repository.dart index d7d4a250ad..4edf4c909d 100644 --- a/mobile/lib/infrastructure/repositories/remote_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_album.repository.dart @@ -233,9 +233,12 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { Future> getAssets(String albumId) { final query = _db.remoteAlbumAssetEntity.select().join([ innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)), + leftOuterJoin(_db.assetEditEntity, _db.assetEditEntity.assetId.equalsExp(_db.remoteAssetEntity.id)), ])..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId)); - return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); + return query + .map((row) => row.readTable(_db.remoteAssetEntity).toDto(row.readTableOrNull(_db.assetEditEntity) != null)) + .get(); } Future addAssets(String albumId, List assetIds) async { diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index 96c204ea0e..16c329dde7 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -17,17 +17,21 @@ class RemoteAssetRepository extends DriftDatabaseRepository { /// For testing purposes Future> getSome(String userId) { - final query = _db.remoteAssetEntity.select() - ..where( - (row) => + final query = + _db.remoteAssetEntity.select().join([ + leftOuterJoin(_db.assetEditEntity, _db.remoteAssetEntity.id.equalsExp(_db.assetEditEntity.assetId)), + ]) + ..where( _db.remoteAssetEntity.ownerId.equals(userId) & - _db.remoteAssetEntity.deletedAt.isNull() & - _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline), - ) - ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) - ..limit(10); + _db.remoteAssetEntity.deletedAt.isNull() & + _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline), + ) + ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]) + ..limit(10); - return query.map((row) => row.toDto()).get(); + return query + .map((row) => row.readTable(_db.remoteAssetEntity).toDto(row.readTableOrNull(_db.assetEditEntity) != null)) + .get(); } SingleOrNullSelectable _assetSelectable(String id) { @@ -38,12 +42,14 @@ class RemoteAssetRepository extends DriftDatabaseRepository { _db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum), useColumns: false, ), + leftOuterJoin(_db.assetEditEntity, _db.remoteAssetEntity.id.equalsExp(_db.assetEditEntity.assetId)), ]) ..where(_db.remoteAssetEntity.id.equals(id)) ..limit(1); return query.map((row) { - final asset = row.readTable(_db.remoteAssetEntity).toDto(); + final isEdited = row.readTableOrNull(_db.assetEditEntity) != null; + final asset = row.readTable(_db.remoteAssetEntity).toDto(isEdited); return asset.copyWith(localId: row.read(_db.localAssetEntity.id)); }); } @@ -57,9 +63,13 @@ class RemoteAssetRepository extends DriftDatabaseRepository { } Future getByChecksum(String checksum) { - final query = _db.remoteAssetEntity.select()..where((row) => row.checksum.equals(checksum)); + final query = _db.remoteAssetEntity.select().join([ + leftOuterJoin(_db.assetEditEntity, _db.remoteAssetEntity.id.equalsExp(_db.assetEditEntity.assetId)), + ])..where(_db.remoteAssetEntity.checksum.equals(checksum)); - return query.map((row) => row.toDto()).getSingleOrNull(); + return query + .map((row) => row.readTable(_db.remoteAssetEntity).toDto(row.readTableOrNull(_db.assetEditEntity) != null)) + .getSingleOrNull(); } Future> getStackChildren(RemoteAsset asset) { @@ -68,11 +78,16 @@ class RemoteAssetRepository extends DriftDatabaseRepository { return Future.value(const []); } - final query = _db.remoteAssetEntity.select() - ..where((row) => row.stackId.equals(stackId) & row.id.equals(asset.id).not()) - ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]); + final query = + _db.remoteAssetEntity.select().join([ + leftOuterJoin(_db.assetEditEntity, _db.remoteAssetEntity.id.equalsExp(_db.assetEditEntity.assetId)), + ]) + ..where(_db.remoteAssetEntity.stackId.equals(stackId) & _db.remoteAssetEntity.id.equals(asset.id).not()) + ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]); - return query.map((row) => row.toDto()).get(); + return query + .map((row) => row.readTable(_db.remoteAssetEntity).toDto(row.readTableOrNull(_db.assetEditEntity) != null)) + .get(); } Future getExif(String id) { diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index 7b59803891..3dd41319a9 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -44,6 +44,7 @@ class SyncApiRepository { SyncRequestType.authUsersV1, SyncRequestType.usersV1, SyncRequestType.assetsV1, + SyncRequestType.assetEditsV1, SyncRequestType.assetExifsV1, SyncRequestType.assetMetadataV1, SyncRequestType.partnersV1, @@ -148,6 +149,8 @@ const _kResponseMap = { SyncEntityType.partnerDeleteV1: SyncPartnerDeleteV1.fromJson, SyncEntityType.assetV1: SyncAssetV1.fromJson, SyncEntityType.assetDeleteV1: SyncAssetDeleteV1.fromJson, + SyncEntityType.assetEditV1: SyncAssetEditV1.fromJson, + SyncEntityType.assetEditDeleteV1: SyncAssetEditDeleteV1.fromJson, SyncEntityType.assetExifV1: SyncAssetExifV1.fromJson, SyncEntityType.assetMetadataV1: SyncAssetMetadataV1.fromJson, SyncEntityType.assetMetadataDeleteV1: SyncAssetMetadataDeleteV1.fromJson, diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index 95239a469c..65064a2532 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -4,10 +4,12 @@ import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/domain/models/asset/asset_edit.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.model.dart'; import 'package:immich_mobile/domain/models/user_metadata.model.dart'; +import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; @@ -26,8 +28,8 @@ import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole, UserMetadataKey; -import 'package:openapi/api.dart' hide AssetVisibility, AlbumUserRole, UserMetadataKey; +import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole, UserMetadataKey, AssetEditAction; +import 'package:openapi/api.dart' hide AssetVisibility, AlbumUserRole, UserMetadataKey, AssetEditAction; class SyncStreamRepository extends DriftDatabaseRepository { final Logger _logger = Logger('DriftSyncStreamRepository'); @@ -215,6 +217,40 @@ class SyncStreamRepository extends DriftDatabaseRepository { } } + Future updateAssetEditsV1(Iterable data) async { + print('Updating asset edits V1: ${data.length} items'); + try { + await _db.batch((batch) { + for (final edit in data) { + final companion = AssetEditEntityCompanion( + id: Value(edit.id), + assetId: Value(edit.assetId), + action: Value(edit.action.toAssetEditAction()), + parameters: Value(edit.parameters as Map), + ); + + batch.insert(_db.assetEditEntity, companion, onConflict: DoUpdate((_) => companion)); + } + }); + } catch (error, stack) { + _logger.severe('Error: updateAssetEditsV1', error, stack); + rethrow; + } + } + + Future deleteAssetEditsV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final edit in data) { + batch.deleteWhere(_db.assetEditEntity, (row) => row.assetId.equals(edit.assetId)); + } + }); + } catch (error, stack) { + _logger.severe('Error: deleteAssetEditsV1', error, stack); + rethrow; + } + } + Future updateAssetsExifV1(Iterable data, {String debugLabel = 'user'}) async { try { await _db.batch((batch) { @@ -764,3 +800,12 @@ extension on String { extension on UserAvatarColor { AvatarColor? toAvatarColor() => AvatarColor.values.firstWhereOrNull((c) => c.name == value); } + +extension on api.AssetEditAction { + AssetEditAction toAssetEditAction() => switch (this) { + api.AssetEditAction.crop => AssetEditAction.crop, + api.AssetEditAction.rotate => AssetEditAction.rotate, + api.AssetEditAction.mirror => AssetEditAction.rotate, + _ => AssetEditAction.other, + }; +} diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index e625b57c1a..2324b4cd92 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -70,6 +70,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { durationInSeconds: row.durationInSeconds, livePhotoVideoId: row.livePhotoVideoId, stackId: row.stackId, + isEdited: row.isEdited == 1, ) : LocalAsset( id: row.localId!, @@ -88,6 +89,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { latitude: row.latitude, longitude: row.longitude, adjustmentTime: row.adjustmentTime, + isEdited: false, ), ) .get(); @@ -148,6 +150,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum), useColumns: false, ), + leftOuterJoin(_db.assetEditEntity, _db.assetEditEntity.assetId.equalsExp(_db.remoteAssetEntity.id)), ]) ..addColumns([_db.remoteAssetEntity.id]) ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) @@ -155,7 +158,11 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ..limit(count, offset: offset); return query - .map((row) => row.readTable(_db.localAssetEntity).toDto(remoteId: row.read(_db.remoteAssetEntity.id))) + .map( + (row) => row + .readTable(_db.localAssetEntity) + .toDto(row.readTableOrNull(_db.assetEditEntity) != null, remoteId: row.read(_db.remoteAssetEntity.id)), + ) .get(); } @@ -236,6 +243,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { _db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum), useColumns: false, ), + leftOuterJoin(_db.assetEditEntity, _db.assetEditEntity.assetId.equalsExp(_db.remoteAssetEntity.id)), ])..where(_db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAlbumAssetEntity.albumId.equals(albumId)); if (isAscending) { @@ -247,7 +255,11 @@ class DriftTimelineRepository extends DriftDatabaseRepository { query.limit(count, offset: offset); return query - .map((row) => row.readTable(_db.remoteAssetEntity).toDto(localId: row.read(_db.localAssetEntity.id))) + .map( + (row) => row + .readTable(_db.remoteAssetEntity) + .toDto(row.readTableOrNull(_db.assetEditEntity) != null, localId: row.read(_db.localAssetEntity.id)), + ) .get(); } @@ -376,6 +388,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { _db.remoteExifEntity.assetId.equalsExp(_db.remoteAssetEntity.id), useColumns: false, ), + leftOuterJoin(_db.assetEditEntity, _db.assetEditEntity.assetId.equalsExp(_db.remoteAssetEntity.id)), ]) ..where( _db.remoteAssetEntity.deletedAt.isNull() & @@ -384,7 +397,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ) ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]) ..limit(count, offset: offset); - return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); + return query + .map((row) => row.readTable(_db.remoteAssetEntity).toDto(row.readTableOrNull(_db.assetEditEntity) != null)) + .get(); } Stream> _watchPersonBucket(String userId, String personId, {GroupAssetsBy groupBy = GroupAssetsBy.day}) { @@ -452,6 +467,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { _db.assetFaceEntity.assetId.equalsExp(_db.remoteAssetEntity.id), useColumns: false, ), + leftOuterJoin(_db.assetEditEntity, _db.assetEditEntity.assetId.equalsExp(_db.remoteAssetEntity.id)), ]) ..where( _db.remoteAssetEntity.deletedAt.isNull() & @@ -462,7 +478,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]) ..limit(count, offset: offset); - return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); + return query + .map((row) => row.readTable(_db.remoteAssetEntity).toDto(row.readTableOrNull(_db.assetEditEntity) != null)) + .get(); } TimelineQuery map(String userId, LatLngBounds bounds, GroupAssetsBy groupBy) => ( @@ -522,6 +540,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { _db.remoteExifEntity.assetId.equalsExp(_db.remoteAssetEntity.id), useColumns: false, ), + leftOuterJoin(_db.assetEditEntity, _db.assetEditEntity.assetId.equalsExp(_db.remoteAssetEntity.id)), ]) ..where( _db.remoteAssetEntity.ownerId.equals(userId) & @@ -531,7 +550,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ) ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]) ..limit(count, offset: offset); - return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); + return query + .map((row) => row.readTable(_db.remoteAssetEntity).toDto(row.readTableOrNull(_db.assetEditEntity) != null)) + .get(); } @pragma('vm:prefer-inline') @@ -589,6 +610,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { _db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum), useColumns: false, ), + leftOuterJoin(_db.assetEditEntity, _db.assetEditEntity.assetId.equalsExp(_db.remoteAssetEntity.id)), ]) ..addColumns([_db.localAssetEntity.id]) ..where(filter(_db.remoteAssetEntity)) @@ -596,15 +618,24 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ..limit(count, offset: offset); return query - .map((row) => row.readTable(_db.remoteAssetEntity).toDto(localId: row.read(_db.localAssetEntity.id))) + .map( + (row) => row + .readTable(_db.remoteAssetEntity) + .toDto(row.readTableOrNull(_db.assetEditEntity) != null, localId: row.read(_db.localAssetEntity.id)), + ) .get(); } else { - final query = _db.remoteAssetEntity.select() - ..where(filter) - ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) - ..limit(count, offset: offset); + final query = + _db.remoteAssetEntity.select().join([ + leftOuterJoin(_db.assetEditEntity, _db.remoteAssetEntity.id.equalsExp(_db.assetEditEntity.assetId)), + ]) + ..where(filter(_db.remoteAssetEntity)) + ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]) + ..limit(count, offset: offset); - return query.map((row) => row.toDto()).get(); + return query + .map((row) => row.readTable(_db.remoteAssetEntity).toDto(row.readTableOrNull(_db.assetEditEntity) != null)) + .get(); } } } diff --git a/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart b/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart index 7e93713c46..ce8c76edcf 100644 --- a/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart @@ -271,6 +271,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { _db.remoteAssetEntity, _db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum), ), + leftOuterJoin(_db.assetEditEntity, _db.assetEditEntity.assetId.equalsExp(_db.remoteAssetEntity.id)), ])..where( _db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) & _db.remoteAssetEntity.deletedAt.isNotNull(), @@ -279,7 +280,8 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { for (final row in rows) { final albumId = row.readTable(_db.localAlbumAssetEntity).albumId; - final asset = row.readTable(_db.localAssetEntity).toDto(); + final hasEdits = row.readTableOrNull(_db.assetEditEntity) != null; + final asset = row.readTable(_db.localAssetEntity).toDto(hasEdits); (result[albumId] ??= []).add(asset); } diff --git a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart b/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart index 579b4c1d58..9da21c72ee 100644 --- a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart +++ b/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart @@ -118,6 +118,7 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection ), _PropertyItem(label: 'Is Favorite', value: asset.isFavorite.toString()), _PropertyItem(label: 'Live Photo Video ID', value: asset.livePhotoVideoId), + _PropertyItem(label: 'Is Edited', value: asset.isEdited.toString()), ]); } diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index e77803c206..2415da2bc5 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -136,4 +136,4 @@ ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnai } bool _shouldUseLocalAsset(BaseAsset asset) => - asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage)); + asset.hasLocal && !asset.isEdited && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage)); diff --git a/mobile/lib/providers/cast.provider.dart b/mobile/lib/providers/cast.provider.dart index 75a2a35fb6..1cd5ded487 100644 --- a/mobile/lib/providers/cast.provider.dart +++ b/mobile/lib/providers/cast.provider.dart @@ -69,6 +69,7 @@ class CastNotifier extends StateNotifier { : AssetType.other, createdAt: asset.fileCreatedAt, updatedAt: asset.updatedAt, + isEdited: false, ); _gCastService.loadMedia(remoteAsset, reload); diff --git a/mobile/lib/repositories/file_media.repository.dart b/mobile/lib/repositories/file_media.repository.dart index 654be78fb4..3a3e50f370 100644 --- a/mobile/lib/repositories/file_media.repository.dart +++ b/mobile/lib/repositories/file_media.repository.dart @@ -25,6 +25,7 @@ class FileMediaRepository { type: AssetType.image, createdAt: entity.createDateTime, updatedAt: entity.modifiedDateTime, + isEdited: false, ); } diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 4b62ee7877..2b088a2dd6 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -560,6 +560,8 @@ Class | Method | HTTP request | Description - [SyncAlbumUserV1](doc//SyncAlbumUserV1.md) - [SyncAlbumV1](doc//SyncAlbumV1.md) - [SyncAssetDeleteV1](doc//SyncAssetDeleteV1.md) + - [SyncAssetEditDeleteV1](doc//SyncAssetEditDeleteV1.md) + - [SyncAssetEditV1](doc//SyncAssetEditV1.md) - [SyncAssetExifV1](doc//SyncAssetExifV1.md) - [SyncAssetFaceDeleteV1](doc//SyncAssetFaceDeleteV1.md) - [SyncAssetFaceV1](doc//SyncAssetFaceV1.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 50120d96a6..c491962e09 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -306,6 +306,8 @@ part 'model/sync_album_user_delete_v1.dart'; part 'model/sync_album_user_v1.dart'; part 'model/sync_album_v1.dart'; part 'model/sync_asset_delete_v1.dart'; +part 'model/sync_asset_edit_delete_v1.dart'; +part 'model/sync_asset_edit_v1.dart'; part 'model/sync_asset_exif_v1.dart'; part 'model/sync_asset_face_delete_v1.dart'; part 'model/sync_asset_face_v1.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 9d13b3e034..628b608d92 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -660,6 +660,10 @@ class ApiClient { return SyncAlbumV1.fromJson(value); case 'SyncAssetDeleteV1': return SyncAssetDeleteV1.fromJson(value); + case 'SyncAssetEditDeleteV1': + return SyncAssetEditDeleteV1.fromJson(value); + case 'SyncAssetEditV1': + return SyncAssetEditV1.fromJson(value); case 'SyncAssetExifV1': return SyncAssetExifV1.fromJson(value); case 'SyncAssetFaceDeleteV1': diff --git a/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart b/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart new file mode 100644 index 0000000000..86facd9534 --- /dev/null +++ b/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncAssetEditDeleteV1 { + /// Returns a new [SyncAssetEditDeleteV1] instance. + SyncAssetEditDeleteV1({ + required this.assetId, + }); + + String assetId; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAssetEditDeleteV1 && + other.assetId == assetId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetId.hashCode); + + @override + String toString() => 'SyncAssetEditDeleteV1[assetId=$assetId]'; + + Map toJson() { + final json = {}; + json[r'assetId'] = this.assetId; + return json; + } + + /// Returns a new [SyncAssetEditDeleteV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAssetEditDeleteV1? fromJson(dynamic value) { + upgradeDto(value, "SyncAssetEditDeleteV1"); + if (value is Map) { + final json = value.cast(); + + return SyncAssetEditDeleteV1( + assetId: mapValueOfType(json, r'assetId')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SyncAssetEditDeleteV1.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SyncAssetEditDeleteV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAssetEditDeleteV1-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SyncAssetEditDeleteV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetId', + }; +} + diff --git a/mobile/openapi/lib/model/sync_asset_edit_v1.dart b/mobile/openapi/lib/model/sync_asset_edit_v1.dart new file mode 100644 index 0000000000..17e8f3072b --- /dev/null +++ b/mobile/openapi/lib/model/sync_asset_edit_v1.dart @@ -0,0 +1,123 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncAssetEditV1 { + /// Returns a new [SyncAssetEditV1] instance. + SyncAssetEditV1({ + required this.action, + required this.assetId, + required this.id, + required this.parameters, + }); + + AssetEditAction action; + + String assetId; + + String id; + + Object parameters; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAssetEditV1 && + other.action == action && + other.assetId == assetId && + other.id == id && + other.parameters == parameters; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (action.hashCode) + + (assetId.hashCode) + + (id.hashCode) + + (parameters.hashCode); + + @override + String toString() => 'SyncAssetEditV1[action=$action, assetId=$assetId, id=$id, parameters=$parameters]'; + + Map toJson() { + final json = {}; + json[r'action'] = this.action; + json[r'assetId'] = this.assetId; + json[r'id'] = this.id; + json[r'parameters'] = this.parameters; + return json; + } + + /// Returns a new [SyncAssetEditV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAssetEditV1? fromJson(dynamic value) { + upgradeDto(value, "SyncAssetEditV1"); + if (value is Map) { + final json = value.cast(); + + return SyncAssetEditV1( + action: AssetEditAction.fromJson(json[r'action'])!, + assetId: mapValueOfType(json, r'assetId')!, + id: mapValueOfType(json, r'id')!, + parameters: mapValueOfType(json, r'parameters')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SyncAssetEditV1.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SyncAssetEditV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAssetEditV1-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SyncAssetEditV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'action', + 'assetId', + 'id', + 'parameters', + }; +} + diff --git a/mobile/openapi/lib/model/sync_entity_type.dart b/mobile/openapi/lib/model/sync_entity_type.dart index 1b4ca91f3b..289c294d85 100644 --- a/mobile/openapi/lib/model/sync_entity_type.dart +++ b/mobile/openapi/lib/model/sync_entity_type.dart @@ -28,6 +28,8 @@ class SyncEntityType { static const userDeleteV1 = SyncEntityType._(r'UserDeleteV1'); static const assetV1 = SyncEntityType._(r'AssetV1'); static const assetDeleteV1 = SyncEntityType._(r'AssetDeleteV1'); + static const assetEditV1 = SyncEntityType._(r'AssetEditV1'); + static const assetEditDeleteV1 = SyncEntityType._(r'AssetEditDeleteV1'); static const assetExifV1 = SyncEntityType._(r'AssetExifV1'); static const assetMetadataV1 = SyncEntityType._(r'AssetMetadataV1'); static const assetMetadataDeleteV1 = SyncEntityType._(r'AssetMetadataDeleteV1'); @@ -78,6 +80,8 @@ class SyncEntityType { userDeleteV1, assetV1, assetDeleteV1, + assetEditV1, + assetEditDeleteV1, assetExifV1, assetMetadataV1, assetMetadataDeleteV1, @@ -163,6 +167,8 @@ class SyncEntityTypeTypeTransformer { case r'UserDeleteV1': return SyncEntityType.userDeleteV1; case r'AssetV1': return SyncEntityType.assetV1; case r'AssetDeleteV1': return SyncEntityType.assetDeleteV1; + case r'AssetEditV1': return SyncEntityType.assetEditV1; + case r'AssetEditDeleteV1': return SyncEntityType.assetEditDeleteV1; case r'AssetExifV1': return SyncEntityType.assetExifV1; case r'AssetMetadataV1': return SyncEntityType.assetMetadataV1; case r'AssetMetadataDeleteV1': return SyncEntityType.assetMetadataDeleteV1; diff --git a/mobile/openapi/lib/model/sync_request_type.dart b/mobile/openapi/lib/model/sync_request_type.dart index c3dc1c4d61..2a09b92451 100644 --- a/mobile/openapi/lib/model/sync_request_type.dart +++ b/mobile/openapi/lib/model/sync_request_type.dart @@ -29,6 +29,7 @@ class SyncRequestType { static const albumAssetsV1 = SyncRequestType._(r'AlbumAssetsV1'); static const albumAssetExifsV1 = SyncRequestType._(r'AlbumAssetExifsV1'); static const assetsV1 = SyncRequestType._(r'AssetsV1'); + static const assetEditsV1 = SyncRequestType._(r'AssetEditsV1'); static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1'); static const assetMetadataV1 = SyncRequestType._(r'AssetMetadataV1'); static const authUsersV1 = SyncRequestType._(r'AuthUsersV1'); @@ -52,6 +53,7 @@ class SyncRequestType { albumAssetsV1, albumAssetExifsV1, assetsV1, + assetEditsV1, assetExifsV1, assetMetadataV1, authUsersV1, @@ -110,6 +112,7 @@ class SyncRequestTypeTypeTransformer { case r'AlbumAssetsV1': return SyncRequestType.albumAssetsV1; case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1; case r'AssetsV1': return SyncRequestType.assetsV1; + case r'AssetEditsV1': return SyncRequestType.assetEditsV1; case r'AssetExifsV1': return SyncRequestType.assetExifsV1; case r'AssetMetadataV1': return SyncRequestType.assetMetadataV1; case r'AuthUsersV1': return SyncRequestType.authUsersV1; diff --git a/mobile/test/fixtures/asset.stub.dart b/mobile/test/fixtures/asset.stub.dart index 8d92011999..f3d6ab42a8 100644 --- a/mobile/test/fixtures/asset.stub.dart +++ b/mobile/test/fixtures/asset.stub.dart @@ -64,6 +64,7 @@ abstract final class LocalAssetStub { type: AssetType.image, createdAt: DateTime(2025), updatedAt: DateTime(2025, 2), + isEdited: false, ); static final image2 = LocalAsset( @@ -72,5 +73,6 @@ abstract final class LocalAssetStub { type: AssetType.image, createdAt: DateTime(2000), updatedAt: DateTime(20021), + isEdited: false, ); } diff --git a/mobile/test/test_utils.dart b/mobile/test/test_utils.dart index 498607e3d2..9d94e71052 100644 --- a/mobile/test/test_utils.dart +++ b/mobile/test/test_utils.dart @@ -131,6 +131,7 @@ abstract final class TestUtils { isFavorite: false, width: width, height: height, + isEdited: false, ); } @@ -154,6 +155,7 @@ abstract final class TestUtils { width: width, height: height, orientation: orientation, + isEdited: false, ); } } diff --git a/mobile/test/test_utils/medium_factory.dart b/mobile/test/test_utils/medium_factory.dart index 19ad7166c6..486050b9cd 100644 --- a/mobile/test/test_utils/medium_factory.dart +++ b/mobile/test/test_utils/medium_factory.dart @@ -17,6 +17,7 @@ class MediumFactory { DateTime? createdAt, DateTime? updatedAt, String? checksum, + bool? isEdited, }) { final random = Random(); @@ -27,6 +28,7 @@ class MediumFactory { type: type ?? AssetType.image, createdAt: createdAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), updatedAt: updatedAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), + isEdited: isEdited ?? false, ); } diff --git a/mobile/test/utils/action_button_utils_test.dart b/mobile/test/utils/action_button_utils_test.dart index d93d59d3c7..b6af1ec824 100644 --- a/mobile/test/utils/action_button_utils_test.dart +++ b/mobile/test/utils/action_button_utils_test.dart @@ -13,6 +13,7 @@ LocalAsset createLocalAsset({ DateTime? createdAt, DateTime? updatedAt, bool isFavorite = false, + bool isEdited = false, }) { return LocalAsset( id: 'local-id', @@ -23,6 +24,7 @@ LocalAsset createLocalAsset({ createdAt: createdAt ?? DateTime.now(), updatedAt: updatedAt ?? DateTime.now(), isFavorite: isFavorite, + isEdited: isEdited, ); } @@ -34,6 +36,7 @@ RemoteAsset createRemoteAsset({ DateTime? createdAt, DateTime? updatedAt, bool isFavorite = false, + bool isEdited = false, }) { return RemoteAsset( id: 'remote-id', @@ -45,6 +48,7 @@ RemoteAsset createRemoteAsset({ createdAt: createdAt ?? DateTime.now(), updatedAt: updatedAt ?? DateTime.now(), isFavorite: isFavorite, + isEdited: isEdited, ); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 2f160e6bed..5635e43d1d 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -21031,6 +21031,44 @@ ], "type": "object" }, + "SyncAssetEditDeleteV1": { + "properties": { + "assetId": { + "type": "string" + } + }, + "required": [ + "assetId" + ], + "type": "object" + }, + "SyncAssetEditV1": { + "properties": { + "action": { + "allOf": [ + { + "$ref": "#/components/schemas/AssetEditAction" + } + ] + }, + "assetId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "parameters": { + "type": "object" + } + }, + "required": [ + "action", + "assetId", + "id", + "parameters" + ], + "type": "object" + }, "SyncAssetExifV1": { "properties": { "assetId": { @@ -21445,6 +21483,8 @@ "UserDeleteV1", "AssetV1", "AssetDeleteV1", + "AssetEditV1", + "AssetEditDeleteV1", "AssetExifV1", "AssetMetadataV1", "AssetMetadataDeleteV1", @@ -21707,6 +21747,7 @@ "AlbumAssetsV1", "AlbumAssetExifsV1", "AssetsV1", + "AssetEditsV1", "AssetExifsV1", "AssetMetadataV1", "AuthUsersV1", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 496e6906a2..91007be875 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -5683,6 +5683,8 @@ export enum SyncEntityType { UserDeleteV1 = "UserDeleteV1", AssetV1 = "AssetV1", AssetDeleteV1 = "AssetDeleteV1", + AssetEditV1 = "AssetEditV1", + AssetEditDeleteV1 = "AssetEditDeleteV1", AssetExifV1 = "AssetExifV1", AssetMetadataV1 = "AssetMetadataV1", AssetMetadataDeleteV1 = "AssetMetadataDeleteV1", @@ -5733,6 +5735,7 @@ export enum SyncRequestType { AlbumAssetsV1 = "AlbumAssetsV1", AlbumAssetExifsV1 = "AlbumAssetExifsV1", AssetsV1 = "AssetsV1", + AssetEditsV1 = "AssetEditsV1", AssetExifsV1 = "AssetExifsV1", AssetMetadataV1 = "AssetMetadataV1", AuthUsersV1 = "AuthUsersV1", diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index 6baf3c8ac7..6b60d1e302 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -2,6 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { ArrayMaxSize, IsInt, IsPositive, IsString } from 'class-validator'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { AssetEditAction, AssetEditActionParameter } from 'src/dtos/editing.dto'; import { AlbumUserRole, AssetOrder, @@ -128,6 +129,20 @@ export class SyncAssetDeleteV1 { assetId!: string; } +@ExtraModel() +export class SyncAssetEditV1 { + id!: string; + assetId!: string; + @ValidateEnum({ enum: AssetEditAction, name: 'AssetEditAction' }) + action!: AssetEditAction; + parameters!: AssetEditActionParameter[AssetEditAction]; +} + +@ExtraModel() +export class SyncAssetEditDeleteV1 { + assetId!: string; +} + @ExtraModel() export class SyncAssetExifV1 { assetId!: string; @@ -349,6 +364,8 @@ export type SyncItem = { [SyncEntityType.PartnerDeleteV1]: SyncPartnerDeleteV1; [SyncEntityType.AssetV1]: SyncAssetV1; [SyncEntityType.AssetDeleteV1]: SyncAssetDeleteV1; + [SyncEntityType.AssetEditV1]: SyncAssetEditV1; + [SyncEntityType.AssetEditDeleteV1]: SyncAssetEditDeleteV1; [SyncEntityType.AssetMetadataV1]: SyncAssetMetadataV1; [SyncEntityType.AssetMetadataDeleteV1]: SyncAssetMetadataDeleteV1; [SyncEntityType.AssetExifV1]: SyncAssetExifV1; diff --git a/server/src/enum.ts b/server/src/enum.ts index 8f0526d0e5..812f907f36 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -716,6 +716,7 @@ export enum SyncRequestType { AlbumAssetsV1 = 'AlbumAssetsV1', AlbumAssetExifsV1 = 'AlbumAssetExifsV1', AssetsV1 = 'AssetsV1', + AssetEditsV1 = 'AssetEditsV1', AssetExifsV1 = 'AssetExifsV1', AssetMetadataV1 = 'AssetMetadataV1', AuthUsersV1 = 'AuthUsersV1', @@ -740,6 +741,8 @@ export enum SyncEntityType { AssetV1 = 'AssetV1', AssetDeleteV1 = 'AssetDeleteV1', + AssetEditV1 = 'AssetEditV1', + AssetEditDeleteV1 = 'AssetEditDeleteV1', AssetExifV1 = 'AssetExifV1', AssetMetadataV1 = 'AssetMetadataV1', AssetMetadataDeleteV1 = 'AssetMetadataDeleteV1', diff --git a/server/src/repositories/sync.repository.ts b/server/src/repositories/sync.repository.ts index 511d7b589f..d2cb936e2a 100644 --- a/server/src/repositories/sync.repository.ts +++ b/server/src/repositories/sync.repository.ts @@ -52,6 +52,7 @@ export class SyncRepository { albumToAsset: AlbumToAssetSync; albumUser: AlbumUserSync; asset: AssetSync; + assetEdit: AssetEditSync; assetExif: AssetExifSync; assetFace: AssetFaceSync; assetMetadata: AssetMetadataSync; @@ -74,6 +75,7 @@ export class SyncRepository { this.albumToAsset = new AlbumToAssetSync(this.db); this.albumUser = new AlbumUserSync(this.db); this.asset = new AssetSync(this.db); + this.assetEdit = new AssetEditSync(this.db); this.assetExif = new AssetExifSync(this.db); this.assetFace = new AssetFaceSync(this.db); this.assetMetadata = new AssetMetadataSync(this.db); @@ -770,3 +772,27 @@ class AssetMetadataSync extends BaseSync { .stream(); } } + +class AssetEditSync extends BaseSync { + @GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true }) + getDeletes(options: SyncQueryOptions) { + return this.auditQuery('asset_edit_audit', options) + .select(['asset_edit_audit.id', 'assetId']) + .leftJoin('asset', 'asset.id', 'asset_edit_audit.assetId') + .where('asset.ownerId', '=', options.userId) + .stream(); + } + + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('asset_edit_audit', daysAgo); + } + + @GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true }) + getUpserts(options: SyncQueryOptions) { + return this.upsertQuery('asset_edit', options) + .select(['asset_edit.id', 'assetId', 'action', 'parameters', 'asset_edit.updateId']) + .innerJoin('asset', 'asset.id', 'asset_edit.assetId') + .where('asset.ownerId', '=', options.userId) + .stream(); + } +} diff --git a/server/src/schema/functions.ts b/server/src/schema/functions.ts index 385db37cf8..8ea27d6e46 100644 --- a/server/src/schema/functions.ts +++ b/server/src/schema/functions.ts @@ -255,3 +255,16 @@ export const asset_face_audit = registerFunction({ RETURN NULL; END`, }); + +export const asset_edit_audit = registerFunction({ + name: 'asset_edit_audit', + returnType: 'TRIGGER', + language: 'PLPGSQL', + body: ` + BEGIN + INSERT INTO asset_edit_audit ("assetId") + SELECT "assetId" + FROM OLD; + RETURN NULL; + END`, +}); diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts index 59c9f53d1a..0b12efc59d 100644 --- a/server/src/schema/index.ts +++ b/server/src/schema/index.ts @@ -28,6 +28,7 @@ import { AlbumUserTable } from 'src/schema/tables/album-user.table'; import { AlbumTable } from 'src/schema/tables/album.table'; import { ApiKeyTable } from 'src/schema/tables/api-key.table'; import { AssetAuditTable } from 'src/schema/tables/asset-audit.table'; +import { AssetEditAuditTable } from 'src/schema/tables/asset-edit-audit.table'; import { AssetEditTable } from 'src/schema/tables/asset-edit.table'; import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; import { AssetFaceAuditTable } from 'src/schema/tables/asset-face-audit.table'; @@ -88,6 +89,7 @@ export class ImmichDatabase { ApiKeyTable, AssetAuditTable, AssetEditTable, + AssetEditAuditTable, AssetFaceTable, AssetFaceAuditTable, AssetMetadataTable, @@ -182,6 +184,7 @@ export interface DB { asset: AssetTable; asset_audit: AssetAuditTable; asset_edit: AssetEditTable; + asset_edit_audit: AssetEditAuditTable; asset_exif: AssetExifTable; asset_face: AssetFaceTable; asset_face_audit: AssetFaceAuditTable; diff --git a/server/src/schema/migrations/1768280630012-CreateAssetEditAuditTable.ts b/server/src/schema/migrations/1768280630012-CreateAssetEditAuditTable.ts new file mode 100644 index 0000000000..145930315f --- /dev/null +++ b/server/src/schema/migrations/1768280630012-CreateAssetEditAuditTable.ts @@ -0,0 +1,43 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`CREATE OR REPLACE FUNCTION asset_edit_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO asset_edit_audit ("assetId") + SELECT "assetId" + FROM OLD; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE TABLE "asset_edit_audit" ( + "id" uuid NOT NULL DEFAULT immich_uuid_v7(), + "assetId" uuid NOT NULL, + "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), + CONSTRAINT "asset_edit_audit_pkey" PRIMARY KEY ("id") +);`.execute(db); + await sql`CREATE INDEX "asset_edit_audit_assetId_idx" ON "asset_edit_audit" ("assetId");`.execute(db); + await sql`CREATE INDEX "asset_edit_audit_deletedAt_idx" ON "asset_edit_audit" ("deletedAt");`.execute(db); + await sql`ALTER TABLE "asset_edit" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db); + await sql`CREATE INDEX "asset_edit_updateId_idx" ON "asset_edit" ("updateId");`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_edit_audit" + AFTER DELETE ON "asset_edit" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION asset_edit_audit();`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_asset_edit_audit', '{"type":"function","name":"asset_edit_audit","sql":"CREATE OR REPLACE FUNCTION asset_edit_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO asset_edit_audit (\\"assetId\\")\\n SELECT \\"assetId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_edit_audit', '{"type":"trigger","name":"asset_edit_audit","sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_audit\\"\\n AFTER DELETE ON \\"asset_edit\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION asset_edit_audit();"}'::jsonb);`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP TRIGGER "asset_edit_audit" ON "asset_edit";`.execute(db); + await sql`DROP INDEX "asset_edit_updateId_idx";`.execute(db); + await sql`ALTER TABLE "asset_edit" DROP COLUMN "updateId";`.execute(db); + await sql`DROP TABLE "asset_edit_audit";`.execute(db); + await sql`DROP FUNCTION asset_edit_audit;`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_asset_edit_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_edit_audit';`.execute(db); +} diff --git a/server/src/schema/tables/asset-edit-audit.table.ts b/server/src/schema/tables/asset-edit-audit.table.ts new file mode 100644 index 0000000000..d08350ef70 --- /dev/null +++ b/server/src/schema/tables/asset-edit-audit.table.ts @@ -0,0 +1,14 @@ +import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; +import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; + +@Table('asset_edit_audit') +export class AssetEditAuditTable { + @PrimaryGeneratedUuidV7Column() + id!: Generated; + + @Column({ type: 'uuid', index: true }) + assetId!: string; + + @CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) + deletedAt!: Generated; +} diff --git a/server/src/schema/tables/asset-edit.table.ts b/server/src/schema/tables/asset-edit.table.ts index 84d95ca3c9..39e46118a7 100644 --- a/server/src/schema/tables/asset-edit.table.ts +++ b/server/src/schema/tables/asset-edit.table.ts @@ -1,7 +1,16 @@ +import { UpdateIdColumn } from 'src/decorators'; import { AssetEditAction, AssetEditActionParameter } from 'src/dtos/editing.dto'; +import { asset_edit_audit } from 'src/schema/functions'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { Column, ForeignKeyColumn, Generated, PrimaryGeneratedColumn } from 'src/sql-tools'; +import { AfterDeleteTrigger, Column, ForeignKeyColumn, Generated, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; +@Table('asset_edit') +@AfterDeleteTrigger({ + scope: 'statement', + function: asset_edit_audit, + referencingOldTableAs: 'old', + when: 'pg_trigger_depth() = 0', +}) export class AssetEditTable { @PrimaryGeneratedColumn() id!: Generated; @@ -14,4 +23,7 @@ export class AssetEditTable { @Column({ type: 'jsonb' }) parameters!: AssetEditActionParameter[T]; + + @UpdateIdColumn({ index: true }) + updateId!: Generated; } diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index f354a71791..f38288bfb5 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -87,6 +87,7 @@ export const SYNC_TYPES_ORDER = [ SyncRequestType.AssetFacesV1, SyncRequestType.UserMetadataV1, SyncRequestType.AssetMetadataV1, + SyncRequestType.AssetEditsV1, ]; const throwSessionRequired = () => { @@ -172,6 +173,7 @@ export class SyncService extends BaseService { [SyncRequestType.UsersV1]: () => this.syncUsersV1(options, response, checkpointMap), [SyncRequestType.PartnersV1]: () => this.syncPartnersV1(options, response, checkpointMap), [SyncRequestType.AssetsV1]: () => this.syncAssetsV1(options, response, checkpointMap), + [SyncRequestType.AssetEditsV1]: () => this.syncAssetEditsV1(options, response, checkpointMap), [SyncRequestType.AssetExifsV1]: () => this.syncAssetExifsV1(options, response, checkpointMap), [SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(options, response, checkpointMap, session.id), [SyncRequestType.AssetMetadataV1]: () => this.syncAssetMetadataV1(options, response, checkpointMap, auth), @@ -192,8 +194,12 @@ export class SyncService extends BaseService { [SyncRequestType.UserMetadataV1]: () => this.syncUserMetadataV1(options, response, checkpointMap), }; + console.log('dtos types:', dto.types); + for (const type of SYNC_TYPES_ORDER.filter((type) => dto.types.includes(type))) { const handler = handlers[type]; + console.log('syncing type:', SyncRequestType[type]); + console.log('handling with:', handler); await handler(); } @@ -837,6 +843,23 @@ export class SyncService extends BaseService { } } + private async syncAssetEditsV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) { + console.log('syncing asset edits'); + const deleteType = SyncEntityType.AssetEditDeleteV1; + const deletes = this.syncRepository.assetEdit.getDeletes({ ...options, ack: checkpointMap[deleteType] }); + + for await (const { id, ...data } of deletes) { + send(response, { type: deleteType, ids: [id], data }); + } + const upsertType = SyncEntityType.AssetEditV1; + const upserts = this.syncRepository.assetEdit.getUpserts({ ...options, ack: checkpointMap[upsertType] }); + + for await (const { updateId, ...data } of upserts) { + console.log('upsert asset edit', updateId); + send(response, { type: upsertType, ids: [updateId], data }); + } + } + private async upsertBackfillCheckpoint(item: { type: SyncEntityType; sessionId: string; createId: string }) { const { type, sessionId, createId } = item; await this.syncCheckpointRepository.upsertAll([ diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index 319d382706..a3ca3bd325 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -8,6 +8,7 @@ import AddToStackAction from '$lib/components/asset-viewer/actions/add-to-stack-action.svelte'; import ArchiveAction from '$lib/components/asset-viewer/actions/archive-action.svelte'; import DeleteAction from '$lib/components/asset-viewer/actions/delete-action.svelte'; + import EditAction from '$lib/components/asset-viewer/actions/edit-action.svelte'; import KeepThisDeleteOthersAction from '$lib/components/asset-viewer/actions/keep-this-delete-others.svelte'; import RatingAction from '$lib/components/asset-viewer/actions/rating-action.svelte'; import RemoveAssetFromStack from '$lib/components/asset-viewer/actions/remove-asset-from-stack.svelte'; @@ -20,7 +21,7 @@ import UnstackAction from '$lib/components/asset-viewer/actions/unstack-action.svelte'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; - import { AppRoute } from '$lib/constants'; + import { AppRoute, ProjectionType } from '$lib/constants'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; import { getGlobalActions } from '$lib/services/app.service'; import { getAssetActions, handleReplaceAsset } from '$lib/services/asset.service'; @@ -72,7 +73,7 @@ onUndoDelete?: OnUndoDelete; onRunJob: (name: AssetJobName) => void; onPlaySlideshow: () => void; - // onEdit: () => void; + onEdit: () => void; onClose?: () => void; playOriginalVideo: boolean; setPlayOriginalVideo: (value: boolean) => void; @@ -92,7 +93,7 @@ onRunJob, onPlaySlideshow, onClose, - // onEdit, + onEdit, playOriginalVideo = false, setPlayOriginalVideo, }: Props = $props(); @@ -117,17 +118,17 @@ const sharedLink = getSharedLink(); // TODO: Enable when edits are ready for release - // let showEditorButton = $derived( - // isOwner && - // asset.type === AssetTypeEnum.Image && - // !( - // asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || - // (asset.originalPath && asset.originalPath.toLowerCase().endsWith('.insp')) - // ) && - // !(asset.originalPath && asset.originalPath.toLowerCase().endsWith('.gif')) && - // !(asset.originalPath && asset.originalPath.toLowerCase().endsWith('.svg')) && - // !asset.livePhotoVideoId, - // ); + let showEditorButton = $derived( + isOwner && + asset.type === AssetTypeEnum.Image && + !( + asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || + (asset.originalPath && asset.originalPath.toLowerCase().endsWith('.insp')) + ) && + !(asset.originalPath && asset.originalPath.toLowerCase().endsWith('.gif')) && + !(asset.originalPath && asset.originalPath.toLowerCase().endsWith('.svg')) && + !asset.livePhotoVideoId, + ); {/if} - + {/if} {#if isOwner} diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index c6a1efc069..5ba2e142ed 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -255,12 +255,12 @@ }); }; - // const showEditor = () => { - // if (assetViewerManager.isShowActivityPanel) { - // assetViewerManager.isShowActivityPanel = false; - // } - // isShowEditor = !isShowEditor; - // }; + const showEditor = () => { + if (assetViewerManager.isShowActivityPanel) { + assetViewerManager.isShowActivityPanel = false; + } + isShowEditor = !isShowEditor; + }; const handleRunJob = async (name: AssetJobName) => { try { @@ -432,6 +432,7 @@ preAction={handlePreAction} onAction={handleAction} {onUndoDelete} + onEdit={showEditor} onRunJob={handleRunJob} onPlaySlideshow={() => ($slideshowState = SlideshowState.PlaySlideshow)} onClose={onClose ? () => onClose(asset) : undefined}