feat(db): add local_trashed_asset table and integrate with restoration flow

- Add new `local_trashed_asset` table to store metadata of trashed assets
- Save trashed asset info into `local_trashed_asset` before deletion
- Use `local_trashed_asset` as source for asset restoration
- Implement file restoration by `mediaId`
This commit is contained in:
Peter Ombodi 2025-09-09 18:54:37 +03:00
parent 57540f6259
commit 020dfa7818
15 changed files with 8285 additions and 31 deletions

View file

@ -157,14 +157,22 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
val fileName = call.argument<String>("fileName") val fileName = call.argument<String>("fileName")
val type = call.argument<Int>("type") val type = call.argument<Int>("type")
val checksum = call.argument<String>("checksum") val checksum = call.argument<String>("checksum")
val mediaId = call.argument<String>("mediaId")
if (fileName != null && type != null && checksum != null) { if (fileName != null && type != null && checksum != null) {
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) { if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) {
restoreFromTrash(fileName, type, checksum, result) restoreFromTrash(fileName, type, checksum, result)
} else { } else {
result.error("PERMISSION_DENIED", "Media permission required", null) result.error("PERMISSION_DENIED", "Media permission required", null)
} }
} else
if (mediaId != null && type != null) {
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) {
restoreFromTrashById(mediaId, type, result)
} else { } else {
result.error("INVALID_NAME", "The file name or checksum is not specified.", null) result.error("PERMISSION_DENIED", "Media permission required", null)
}
} else {
result.error("INVALID_PARAMS", "Required params are not specified.", null)
} }
} }
@ -225,6 +233,30 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
uri.let { toggleTrash(listOf(it), false, result) } uri.let { toggleTrash(listOf(it), false, result) }
} }
@RequiresApi(Build.VERSION_CODES.R)
private fun restoreFromTrashById(mediaId: String, type: Int, result: Result) {
val id = mediaId.toLongOrNull()
if (id == null) {
result.error("INVALID_ID", "The file id is not a valid number: $mediaId", null)
return
}
if (!isInTrash(id)) {
result.error("TrashNotFound", "Item with id=$id not found in trash", null)
return
}
val primary = ContentUris.withAppendedId(contentUriForType(type), id)
val fallback = ContentUris.withAppendedId(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), id)
try {
Log.i(TAG, "restoreFromTrashById: primary=$primary (type=$type,id=$id)")
restoreUris(listOf(primary), result)
} catch (e: Exception) {
Log.w(TAG, "restoreFromTrashById: primary failed, try fallback=$fallback", e)
restoreUris(listOf(fallback), result)
}
}
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
private fun toggleTrash(contentUris: List<Uri>, isTrashed: Boolean, result: Result) { private fun toggleTrash(contentUris: List<Uri>, isTrashed: Boolean, result: Result) {
val activity = activityBinding?.activity val activity = activityBinding?.activity
@ -292,13 +324,7 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
} ?: continue } ?: continue
if (sha1Bytes.contentEquals(expectedBytes)) { if (sha1Bytes.contentEquals(expectedBytes)) {
// same order as AssetType from dart val contentUri = contentUriForType(type)
val contentUri = when (type) {
1 -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
2 -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
3 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
else -> queryUri
}
return ContentUris.withAppendedId(contentUri, id) return ContentUris.withAppendedId(contentUri, id)
} }
} }
@ -346,6 +372,30 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
return false return false
} }
@RequiresApi(Build.VERSION_CODES.R)
private fun isInTrash(id: Long): Boolean {
val contentResolver = context?.contentResolver ?: return false
val filesUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
val args = Bundle().apply {
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${MediaStore.Files.FileColumns._ID}=?")
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(id.toString()))
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
putInt(ContentResolver.QUERY_ARG_LIMIT, 1)
}
return contentResolver.query(filesUri, arrayOf(MediaStore.Files.FileColumns._ID), args, null)
?.use { it.moveToFirst() } == true
}
@RequiresApi(Build.VERSION_CODES.R)
private fun restoreUris(uris: List<Uri>, result: Result) {
if (uris.isEmpty()) {
result.error("TrashError", "No URIs to restore", null)
return
}
Log.i(TAG, "restoreUris: count=${uris.size}, first=${uris.first()}")
toggleTrash(uris, false, result)
}
private fun sha1OfStream(ins: InputStream): ByteArray { private fun sha1OfStream(ins: InputStream): ByteArray {
val buf = ByteArray(BUFFER_SIZE) val buf = ByteArray(BUFFER_SIZE)
val md = MessageDigest.getInstance("SHA-1") val md = MessageDigest.getInstance("SHA-1")
@ -357,6 +407,16 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
} }
return md.digest() return md.digest()
} }
@RequiresApi(Build.VERSION_CODES.Q)
private fun contentUriForType(type: Int): Uri =
when (type) {
// same order as AssetType from dart
1 -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
2 -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
3 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
else -> MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
}
} }
private const val TAG = "BackgroundServicePlugin" private const val TAG = "BackgroundServicePlugin"

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,37 @@
class LocalTrashedAsset {
final String localId;
final String remoteId;
final DateTime createdAt;
const LocalTrashedAsset({required this.localId, required this.remoteId, required this.createdAt});
LocalTrashedAsset copyWith({String? localId, String? remoteId, DateTime? createdAt}) {
return LocalTrashedAsset(
localId: localId ?? this.localId,
remoteId: remoteId ?? this.remoteId,
createdAt: createdAt ?? this.createdAt,
);
}
@override
bool operator ==(Object other) {
if (other is! LocalTrashedAsset) return false;
if (identical(this, other)) return true;
return other.localId == localId && other.remoteId == remoteId && other.createdAt == createdAt;
}
@override
int get hashCode {
return localId.hashCode ^ remoteId.hashCode ^ createdAt.hashCode;
}
@override
String toString() {
return '''LocalTrashedAsset: {
localId: $localId,
remoteId: $remoteId,
createdAt: $createdAt
}''';
}
}

View file

@ -85,7 +85,7 @@ class SyncStreamService {
case SyncEntityType.assetV1: case SyncEntityType.assetV1:
final remoteSyncAssets = data.cast<SyncAssetV1>(); final remoteSyncAssets = data.cast<SyncAssetV1>();
await _trashSyncService.handleRemoteChanges( await _trashSyncService.handleRemoteChanges(
remoteSyncAssets.map<TrashSyncItem>((e) => (checksum: e.checksum, deletedAt: e.deletedAt)), remoteSyncAssets.map<TrashSyncItem>((e) => (remoteId: e.id, checksum: e.checksum, deletedAt: e.deletedAt)),
); );
return _syncStreamRepository.updateAssetsV1(remoteSyncAssets); return _syncStreamRepository.updateAssetsV1(remoteSyncAssets);
case SyncEntityType.assetDeleteV1: case SyncEntityType.assetDeleteV1:

View file

@ -1,4 +1,5 @@
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/local_trashed_asset.model.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
@ -7,7 +8,7 @@ import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
typedef TrashSyncItem = ({String checksum, DateTime? deletedAt}); typedef TrashSyncItem = ({String remoteId, String checksum, DateTime? deletedAt});
class TrashSyncService { class TrashSyncService {
final AppSettingsService _appSettingsService; final AppSettingsService _appSettingsService;
@ -35,24 +36,25 @@ class TrashSyncService {
if (!_platform.isAndroid || !_appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid)) { if (!_platform.isAndroid || !_appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid)) {
return Future.value(); return Future.value();
} }
final trashedAssetsChecksums = <String>{}; final trashedAssetsItems = <TrashSyncItem>[];
final modifiedAssetsChecksums = <String>{}; final modifiedAssetsChecksums = <String>{};
for (var syncItem in syncItems) { for (var syncItem in syncItems) {
if (syncItem.deletedAt != null) { if (syncItem.deletedAt != null) {
trashedAssetsChecksums.add(syncItem.checksum); trashedAssetsItems.add(syncItem);
} else { } else {
modifiedAssetsChecksums.add(syncItem.checksum); modifiedAssetsChecksums.add(syncItem.checksum);
} }
} }
await _applyRemoteTrashToLocal(trashedAssetsChecksums); await _applyRemoteTrashToLocal(trashedAssetsItems);
await _applyRemoteRestoreToLocal(modifiedAssetsChecksums); await _applyRemoteRestoreToLocal(modifiedAssetsChecksums);
} }
Future<void> _applyRemoteTrashToLocal(Iterable<String> trashedAssetsChecksums) async { Future<void> _applyRemoteTrashToLocal(Iterable<TrashSyncItem> trashedAssets) async {
if (trashedAssetsChecksums.isEmpty) { if (trashedAssets.isEmpty) {
return Future.value(); return Future.value();
} else { } else {
final localAssetsToTrash = await _localAssetRepository.getBackupSelectedAssets(trashedAssetsChecksums); final trashedAssetsMap = <String, String>{for (final e in trashedAssets) e.checksum: e.remoteId};
final localAssetsToTrash = await _localAssetRepository.getBackupSelectedAssets(trashedAssetsMap.keys);
if (localAssetsToTrash.isNotEmpty) { if (localAssetsToTrash.isNotEmpty) {
final mediaUrls = await Future.wait( final mediaUrls = await Future.wait(
localAssetsToTrash.map( localAssetsToTrash.map(
@ -61,9 +63,14 @@ class TrashSyncService {
); );
_logger.info("Moving to trash ${mediaUrls.join(", ")} assets"); _logger.info("Moving to trash ${mediaUrls.join(", ")} assets");
await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList()); await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
await _localAssetRepository.delete(localAssetsToTrash.map((asset) => asset.id)); final itemsToTrash = <LocalRemoteIds>[];
for (final asset in localAssetsToTrash) {
final remoteId = trashedAssetsMap[asset.checksum]!;
itemsToTrash.add((localId: asset.id, remoteId: remoteId));
}
await _localAssetRepository.trash(itemsToTrash);
} else { } else {
_logger.info("No assets found in backup-enabled albums for checksums: $trashedAssetsChecksums"); _logger.info("No assets found in backup-enabled albums for assets: $trashedAssetsMap");
} }
} }
} }
@ -77,12 +84,21 @@ class TrashSyncService {
isTrashed: true, isTrashed: true,
); );
if (remoteAssetsToRestore.isNotEmpty) { if (remoteAssetsToRestore.isNotEmpty) {
_logger.info( final remoteAssetMap = <String, AssetType>{for (final e in remoteAssetsToRestore) e.id: e.type};
"Restoring from trash ${remoteAssetsToRestore.map((e) => "${e.name}/${e.checksum}").join(", ")} assets", _logger.info("remoteAssetsToRestore: $remoteAssetMap");
); final localTrashedAssets = await _localAssetRepository.getLocalTrashedAssets(remoteAssetMap.keys);
for (RemoteAsset asset in remoteAssetsToRestore) { if (localTrashedAssets.isNotEmpty) {
await _localFilesManager.restoreFromTrash(asset.name, asset.type.index, asset.checksum!); for (final LocalTrashedAsset asset in localTrashedAssets) {
} _logger.info("Restoring from trash, localId: ${asset.localId}, remoteId: ${asset.remoteId}");
final type = remoteAssetMap[asset.remoteId]!;
await _localFilesManager.restoreFromTrashById(asset.localId, type.index);
await _localAssetRepository.deleteLocalTrashedAssets(remoteAssetMap.keys);
}
} else {
_logger.info("No local assets found for restoration");
}
} else {
_logger.info("No remote assets found for restoration");
} }
} }
} }

View file

@ -0,0 +1,25 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/local_trashed_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/local_trashed_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
@TableIndex.sql(
'CREATE INDEX IF NOT EXISTS idx_local_trashed_asset_remote_id ON local_trashed_asset_entity (remote_id)',
)
class LocalTrashedAssetEntity extends Table with DriftDefaultsMixin {
const LocalTrashedAssetEntity();
TextColumn get id => text()();
TextColumn get remoteId => text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
}
extension LocalTrashedAssetEntityDataDomainExtension on LocalTrashedAssetEntityData {
LocalTrashedAsset toDto() => LocalTrashedAsset(localId: id, remoteId: remoteId, createdAt: createdAt);
}

View file

@ -0,0 +1,614 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/local_trashed_asset.entity.drift.dart'
as i1;
import 'package:immich_mobile/infrastructure/entities/local_trashed_asset.entity.dart'
as i2;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
as i4;
import 'package:drift/internal/modular.dart' as i5;
typedef $$LocalTrashedAssetEntityTableCreateCompanionBuilder =
i1.LocalTrashedAssetEntityCompanion Function({
required String id,
required String remoteId,
i0.Value<DateTime> createdAt,
});
typedef $$LocalTrashedAssetEntityTableUpdateCompanionBuilder =
i1.LocalTrashedAssetEntityCompanion Function({
i0.Value<String> id,
i0.Value<String> remoteId,
i0.Value<DateTime> createdAt,
});
final class $$LocalTrashedAssetEntityTableReferences
extends
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$LocalTrashedAssetEntityTable,
i1.LocalTrashedAssetEntityData
> {
$$LocalTrashedAssetEntityTableReferences(
super.$_db,
super.$_table,
super.$_typedResult,
);
static i4.$RemoteAssetEntityTable _remoteIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity')
.createAlias(
i0.$_aliasNameGenerator(
i5.ReadDatabaseContainer(db)
.resultSet<i1.$LocalTrashedAssetEntityTable>(
'local_trashed_asset_entity',
)
.remoteId,
i5.ReadDatabaseContainer(
db,
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity').id,
),
);
i4.$$RemoteAssetEntityTableProcessedTableManager get remoteId {
final $_column = $_itemColumn<String>('remote_id')!;
final manager = i4
.$$RemoteAssetEntityTableTableManager(
$_db,
i5.ReadDatabaseContainer(
$_db,
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
)
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_remoteIdTable($_db));
if (item == null) return manager;
return i0.ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]),
);
}
}
class $$LocalTrashedAssetEntityTableFilterComposer
extends
i0.Composer<i0.GeneratedDatabase, i1.$LocalTrashedAssetEntityTable> {
$$LocalTrashedAssetEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get id => $composableBuilder(
column: $table.id,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt,
builder: (column) => i0.ColumnFilters(column),
);
i4.$$RemoteAssetEntityTableFilterComposer get remoteId {
final i4.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.remoteId,
referencedTable: i5.ReadDatabaseContainer(
$db,
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i4.$$RemoteAssetEntityTableFilterComposer(
$db: $db,
$table: i5.ReadDatabaseContainer(
$db,
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
}
class $$LocalTrashedAssetEntityTableOrderingComposer
extends
i0.Composer<i0.GeneratedDatabase, i1.$LocalTrashedAssetEntityTable> {
$$LocalTrashedAssetEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get id => $composableBuilder(
column: $table.id,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt,
builder: (column) => i0.ColumnOrderings(column),
);
i4.$$RemoteAssetEntityTableOrderingComposer get remoteId {
final i4.$$RemoteAssetEntityTableOrderingComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.remoteId,
referencedTable: i5.ReadDatabaseContainer(
$db,
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i4.$$RemoteAssetEntityTableOrderingComposer(
$db: $db,
$table: i5.ReadDatabaseContainer(
$db,
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
}
class $$LocalTrashedAssetEntityTableAnnotationComposer
extends
i0.Composer<i0.GeneratedDatabase, i1.$LocalTrashedAssetEntityTable> {
$$LocalTrashedAssetEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
i0.GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column);
i4.$$RemoteAssetEntityTableAnnotationComposer get remoteId {
final i4.$$RemoteAssetEntityTableAnnotationComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.remoteId,
referencedTable: i5.ReadDatabaseContainer(
$db,
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i4.$$RemoteAssetEntityTableAnnotationComposer(
$db: $db,
$table: i5.ReadDatabaseContainer(
$db,
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
}
class $$LocalTrashedAssetEntityTableTableManager
extends
i0.RootTableManager<
i0.GeneratedDatabase,
i1.$LocalTrashedAssetEntityTable,
i1.LocalTrashedAssetEntityData,
i1.$$LocalTrashedAssetEntityTableFilterComposer,
i1.$$LocalTrashedAssetEntityTableOrderingComposer,
i1.$$LocalTrashedAssetEntityTableAnnotationComposer,
$$LocalTrashedAssetEntityTableCreateCompanionBuilder,
$$LocalTrashedAssetEntityTableUpdateCompanionBuilder,
(
i1.LocalTrashedAssetEntityData,
i1.$$LocalTrashedAssetEntityTableReferences,
),
i1.LocalTrashedAssetEntityData,
i0.PrefetchHooks Function({bool remoteId})
> {
$$LocalTrashedAssetEntityTableTableManager(
i0.GeneratedDatabase db,
i1.$LocalTrashedAssetEntityTable table,
) : super(
i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$LocalTrashedAssetEntityTableFilterComposer(
$db: db,
$table: table,
),
createOrderingComposer: () =>
i1.$$LocalTrashedAssetEntityTableOrderingComposer(
$db: db,
$table: table,
),
createComputedFieldComposer: () =>
i1.$$LocalTrashedAssetEntityTableAnnotationComposer(
$db: db,
$table: table,
),
updateCompanionCallback:
({
i0.Value<String> id = const i0.Value.absent(),
i0.Value<String> remoteId = const i0.Value.absent(),
i0.Value<DateTime> createdAt = const i0.Value.absent(),
}) => i1.LocalTrashedAssetEntityCompanion(
id: id,
remoteId: remoteId,
createdAt: createdAt,
),
createCompanionCallback:
({
required String id,
required String remoteId,
i0.Value<DateTime> createdAt = const i0.Value.absent(),
}) => i1.LocalTrashedAssetEntityCompanion.insert(
id: id,
remoteId: remoteId,
createdAt: createdAt,
),
withReferenceMapper: (p0) => p0
.map(
(e) => (
e.readTable(table),
i1.$$LocalTrashedAssetEntityTableReferences(db, table, e),
),
)
.toList(),
prefetchHooksCallback: ({remoteId = 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 (remoteId) {
state =
state.withJoin(
currentTable: table,
currentColumn: table.remoteId,
referencedTable: i1
.$$LocalTrashedAssetEntityTableReferences
._remoteIdTable(db),
referencedColumn: i1
.$$LocalTrashedAssetEntityTableReferences
._remoteIdTable(db)
.id,
)
as T;
}
return state;
},
getPrefetchedDataCallback: (items) async {
return [];
},
);
},
),
);
}
typedef $$LocalTrashedAssetEntityTableProcessedTableManager =
i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$LocalTrashedAssetEntityTable,
i1.LocalTrashedAssetEntityData,
i1.$$LocalTrashedAssetEntityTableFilterComposer,
i1.$$LocalTrashedAssetEntityTableOrderingComposer,
i1.$$LocalTrashedAssetEntityTableAnnotationComposer,
$$LocalTrashedAssetEntityTableCreateCompanionBuilder,
$$LocalTrashedAssetEntityTableUpdateCompanionBuilder,
(
i1.LocalTrashedAssetEntityData,
i1.$$LocalTrashedAssetEntityTableReferences,
),
i1.LocalTrashedAssetEntityData,
i0.PrefetchHooks Function({bool remoteId})
>;
i0.Index get idxLocalTrashedAssetRemoteId => i0.Index(
'idx_local_trashed_asset_remote_id',
'CREATE INDEX IF NOT EXISTS idx_local_trashed_asset_remote_id ON local_trashed_asset_entity (remote_id)',
);
class $LocalTrashedAssetEntityTable extends i2.LocalTrashedAssetEntity
with
i0.TableInfo<
$LocalTrashedAssetEntityTable,
i1.LocalTrashedAssetEntityData
> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$LocalTrashedAssetEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
@override
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
'id',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
);
static const i0.VerificationMeta _remoteIdMeta = const i0.VerificationMeta(
'remoteId',
);
@override
late final i0.GeneratedColumn<String> remoteId = i0.GeneratedColumn<String>(
'remote_id',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES remote_asset_entity (id) ON DELETE CASCADE',
),
);
static const i0.VerificationMeta _createdAtMeta = const i0.VerificationMeta(
'createdAt',
);
@override
late final i0.GeneratedColumn<DateTime> createdAt =
i0.GeneratedColumn<DateTime>(
'created_at',
aliasedName,
false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i3.currentDateAndTime,
);
@override
List<i0.GeneratedColumn> get $columns => [id, remoteId, createdAt];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'local_trashed_asset_entity';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.LocalTrashedAssetEntityData> 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('remote_id')) {
context.handle(
_remoteIdMeta,
remoteId.isAcceptableOrUnknown(data['remote_id']!, _remoteIdMeta),
);
} else if (isInserting) {
context.missing(_remoteIdMeta);
}
if (data.containsKey('created_at')) {
context.handle(
_createdAtMeta,
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta),
);
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {id};
@override
i1.LocalTrashedAssetEntityData map(
Map<String, dynamic> data, {
String? tablePrefix,
}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.LocalTrashedAssetEntityData(
id: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}id'],
)!,
remoteId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}remote_id'],
)!,
createdAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime,
data['${effectivePrefix}created_at'],
)!,
);
}
@override
$LocalTrashedAssetEntityTable createAlias(String alias) {
return $LocalTrashedAssetEntityTable(attachedDatabase, alias);
}
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class LocalTrashedAssetEntityData extends i0.DataClass
implements i0.Insertable<i1.LocalTrashedAssetEntityData> {
final String id;
final String remoteId;
final DateTime createdAt;
const LocalTrashedAssetEntityData({
required this.id,
required this.remoteId,
required this.createdAt,
});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['id'] = i0.Variable<String>(id);
map['remote_id'] = i0.Variable<String>(remoteId);
map['created_at'] = i0.Variable<DateTime>(createdAt);
return map;
}
factory LocalTrashedAssetEntityData.fromJson(
Map<String, dynamic> json, {
i0.ValueSerializer? serializer,
}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return LocalTrashedAssetEntityData(
id: serializer.fromJson<String>(json['id']),
remoteId: serializer.fromJson<String>(json['remoteId']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'remoteId': serializer.toJson<String>(remoteId),
'createdAt': serializer.toJson<DateTime>(createdAt),
};
}
i1.LocalTrashedAssetEntityData copyWith({
String? id,
String? remoteId,
DateTime? createdAt,
}) => i1.LocalTrashedAssetEntityData(
id: id ?? this.id,
remoteId: remoteId ?? this.remoteId,
createdAt: createdAt ?? this.createdAt,
);
LocalTrashedAssetEntityData copyWithCompanion(
i1.LocalTrashedAssetEntityCompanion data,
) {
return LocalTrashedAssetEntityData(
id: data.id.present ? data.id.value : this.id,
remoteId: data.remoteId.present ? data.remoteId.value : this.remoteId,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
);
}
@override
String toString() {
return (StringBuffer('LocalTrashedAssetEntityData(')
..write('id: $id, ')
..write('remoteId: $remoteId, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, remoteId, createdAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.LocalTrashedAssetEntityData &&
other.id == this.id &&
other.remoteId == this.remoteId &&
other.createdAt == this.createdAt);
}
class LocalTrashedAssetEntityCompanion
extends i0.UpdateCompanion<i1.LocalTrashedAssetEntityData> {
final i0.Value<String> id;
final i0.Value<String> remoteId;
final i0.Value<DateTime> createdAt;
const LocalTrashedAssetEntityCompanion({
this.id = const i0.Value.absent(),
this.remoteId = const i0.Value.absent(),
this.createdAt = const i0.Value.absent(),
});
LocalTrashedAssetEntityCompanion.insert({
required String id,
required String remoteId,
this.createdAt = const i0.Value.absent(),
}) : id = i0.Value(id),
remoteId = i0.Value(remoteId);
static i0.Insertable<i1.LocalTrashedAssetEntityData> custom({
i0.Expression<String>? id,
i0.Expression<String>? remoteId,
i0.Expression<DateTime>? createdAt,
}) {
return i0.RawValuesInsertable({
if (id != null) 'id': id,
if (remoteId != null) 'remote_id': remoteId,
if (createdAt != null) 'created_at': createdAt,
});
}
i1.LocalTrashedAssetEntityCompanion copyWith({
i0.Value<String>? id,
i0.Value<String>? remoteId,
i0.Value<DateTime>? createdAt,
}) {
return i1.LocalTrashedAssetEntityCompanion(
id: id ?? this.id,
remoteId: remoteId ?? this.remoteId,
createdAt: createdAt ?? this.createdAt,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (id.present) {
map['id'] = i0.Variable<String>(id.value);
}
if (remoteId.present) {
map['remote_id'] = i0.Variable<String>(remoteId.value);
}
if (createdAt.present) {
map['created_at'] = i0.Variable<DateTime>(createdAt.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('LocalTrashedAssetEntityCompanion(')
..write('id: $id, ')
..write('remoteId: $remoteId, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
}

View file

@ -9,6 +9,7 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_trashed_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.dart'; import 'package:immich_mobile/infrastructure/entities/memory.entity.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
@ -60,6 +61,7 @@ class IsarDatabaseRepository implements IDatabaseRepository {
PersonEntity, PersonEntity,
AssetFaceEntity, AssetFaceEntity,
StoreEntity, StoreEntity,
LocalTrashedAssetEntity,
], ],
include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'}, include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'},
) )
@ -68,7 +70,7 @@ class Drift extends $Drift implements IDatabaseRepository {
: super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true))); : super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true)));
@override @override
int get schemaVersion => 9; int get schemaVersion => 10;
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
@ -126,6 +128,9 @@ class Drift extends $Drift implements IDatabaseRepository {
from8To9: (m, v9) async { from8To9: (m, v9) async {
await m.addColumn(v9.localAlbumEntity, v9.localAlbumEntity.linkedRemoteAlbumId); await m.addColumn(v9.localAlbumEntity, v9.localAlbumEntity.linkedRemoteAlbumId);
}, },
from9To10: (m, v10) async {
await m.create(v10.localTrashedAssetEntity);
},
), ),
); );
@ -148,6 +153,7 @@ class Drift extends $Drift implements IDatabaseRepository {
class DriftDatabaseRepository implements IDatabaseRepository { class DriftDatabaseRepository implements IDatabaseRepository {
final Drift _db; final Drift _db;
const DriftDatabaseRepository(this._db); const DriftDatabaseRepository(this._db);
@override @override

View file

@ -35,9 +35,11 @@ import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.da
as i16; as i16;
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
as i17; as i17;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' import 'package:immich_mobile/infrastructure/entities/local_trashed_asset.entity.drift.dart'
as i18; as i18;
import 'package:drift/internal/modular.dart' as i19; import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
as i19;
import 'package:drift/internal/modular.dart' as i20;
abstract class $Drift extends i0.GeneratedDatabase { abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e); $Drift(i0.QueryExecutor e) : super(e);
@ -72,9 +74,11 @@ abstract class $Drift extends i0.GeneratedDatabase {
late final i16.$AssetFaceEntityTable assetFaceEntity = i16 late final i16.$AssetFaceEntityTable assetFaceEntity = i16
.$AssetFaceEntityTable(this); .$AssetFaceEntityTable(this);
late final i17.$StoreEntityTable storeEntity = i17.$StoreEntityTable(this); late final i17.$StoreEntityTable storeEntity = i17.$StoreEntityTable(this);
i18.MergedAssetDrift get mergedAssetDrift => i19.ReadDatabaseContainer( late final i18.$LocalTrashedAssetEntityTable localTrashedAssetEntity = i18
.$LocalTrashedAssetEntityTable(this);
i19.MergedAssetDrift get mergedAssetDrift => i20.ReadDatabaseContainer(
this, this,
).accessor<i18.MergedAssetDrift>(i18.MergedAssetDrift.new); ).accessor<i19.MergedAssetDrift>(i19.MergedAssetDrift.new);
@override @override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables => Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>(); allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@ -102,7 +106,9 @@ abstract class $Drift extends i0.GeneratedDatabase {
personEntity, personEntity,
assetFaceEntity, assetFaceEntity,
storeEntity, storeEntity,
localTrashedAssetEntity,
i10.idxLatLng, i10.idxLatLng,
i18.idxLocalTrashedAssetRemoteId,
]; ];
@override @override
i0.StreamQueryUpdateRules i0.StreamQueryUpdateRules
@ -282,6 +288,18 @@ abstract class $Drift extends i0.GeneratedDatabase {
), ),
result: [i0.TableUpdate('asset_face_entity', kind: i0.UpdateKind.update)], result: [i0.TableUpdate('asset_face_entity', kind: i0.UpdateKind.update)],
), ),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName(
'remote_asset_entity',
limitUpdateKind: i0.UpdateKind.delete,
),
result: [
i0.TableUpdate(
'local_trashed_asset_entity',
kind: i0.UpdateKind.delete,
),
],
),
]); ]);
@override @override
i0.DriftDatabaseOptions get options => i0.DriftDatabaseOptions get options =>
@ -328,4 +346,9 @@ class $DriftManager {
i16.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity); i16.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity);
i17.$$StoreEntityTableTableManager get storeEntity => i17.$$StoreEntityTableTableManager get storeEntity =>
i17.$$StoreEntityTableTableManager(_db, _db.storeEntity); i17.$$StoreEntityTableTableManager(_db, _db.storeEntity);
i18.$$LocalTrashedAssetEntityTableTableManager get localTrashedAssetEntity =>
i18.$$LocalTrashedAssetEntityTableTableManager(
_db,
_db.localTrashedAssetEntity,
);
} }

View file

@ -3820,6 +3820,394 @@ i1.GeneratedColumn<String> _column_90(String aliasedName) =>
'REFERENCES remote_album_entity (id) ON DELETE SET NULL', 'REFERENCES remote_album_entity (id) ON DELETE SET NULL',
), ),
); );
final class Schema10 extends i0.VersionedSchema {
Schema10({required super.database}) : super(version: 10);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
stackEntity,
localAssetEntity,
remoteAlbumEntity,
localAlbumEntity,
localAlbumAssetEntity,
idxLocalAssetChecksum,
idxRemoteAssetOwnerChecksum,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
storeEntity,
localTrashedAssetEntity,
idxLatLng,
idxLocalTrashedAssetChecksum,
];
late final Shape16 userEntity = Shape16(
source: i0.VersionedTable(
entityName: 'user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
_column_84,
_column_85,
_column_5,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape17 remoteAssetEntity = Shape17(
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,
_column_86,
],
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,
);
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 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 Shape19 localAlbumEntity = Shape19(
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_90,
_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,
);
final i1.Index idxLocalAssetChecksum = i1.Index(
'idx_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
);
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
'idx_remote_asset_owner_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
);
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
'UQ_remote_assets_owner_library_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
);
final i1.Index idxRemoteAssetChecksum = i1.Index(
'idx_remote_asset_checksum',
'CREATE INDEX IF NOT EXISTS 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 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 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,
);
late final Shape18 storeEntity = Shape18(
source: i0.VersionedTable(
entityName: 'store_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_87, _column_88, _column_89],
attachedDatabase: database,
),
alias: null,
);
late final Shape20 localTrashedAssetEntity = Shape20(
source: i0.VersionedTable(
entityName: 'local_trashed_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_0, _column_36, _column_13, _column_1, _column_9],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLatLng = i1.Index(
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
final i1.Index idxLocalTrashedAssetChecksum = i1.Index(
'idx_local_trashed_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_local_trashed_asset_checksum ON local_trashed_asset_entity (checksum)',
);
}
class Shape20 extends i0.VersionedTable {
Shape20({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 checksum =>
columnsByName['checksum']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
}
i0.MigrationStepWithVersion migrationSteps({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, 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, Schema3 schema) from2To3,
@ -3829,6 +4217,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7, required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8, required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9, required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@ -3872,6 +4261,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from8To9(migrator, schema); await from8To9(migrator, schema);
return 9; return 9;
case 9:
final schema = Schema10(database: database);
final migrator = i1.Migrator(database, schema);
await from9To10(migrator, schema);
return 10;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@ -3887,6 +4281,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7, required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8, required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9, required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
}) => i0.VersionedSchema.stepByStepHelper( }) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
from1To2: from1To2, from1To2: from1To2,
@ -3897,5 +4292,6 @@ i1.OnUpgrade stepByStep({
from6To7: from6To7, from6To7: from6To7,
from7To8: from7To8, from7To8: from7To8,
from8To9: from8To9, from8To9: from8To9,
from9To10: from9To10,
), ),
); );

View file

@ -2,10 +2,15 @@ import 'package:collection/collection.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/local_trashed_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/local_trashed_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_trashed_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
typedef LocalRemoteIds = ({String localId, String remoteId});
class DriftLocalAssetRepository extends DriftDatabaseRepository { class DriftLocalAssetRepository extends DriftDatabaseRepository {
final Drift _db; final Drift _db;
@ -58,6 +63,31 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
}); });
} }
Future<void> trash(Iterable<LocalRemoteIds> ids) async {
if (ids.isEmpty) return;
final Map<String, String> idToRemote = {for (final e in ids) e.localId: e.remoteId};
final localRows = await (_db.localAssetEntity.select()..where((t) => t.id.isIn(idToRemote.keys))).get();
await _db.batch((batch) {
for (final row in localRows) {
final remoteId = idToRemote[row.id];
if (remoteId == null) {
continue;
}
batch.insert(
_db.localTrashedAssetEntity,
LocalTrashedAssetEntityCompanion(id: Value(row.id), remoteId: Value(remoteId)),
mode: InsertMode.insertOrReplace,
);
}
for (final slice in idToRemote.keys.slices(32000)) {
batch.deleteWhere(_db.localAssetEntity, (e) => e.id.isIn(slice));
}
});
}
Future<LocalAsset?> getById(String id) { Future<LocalAsset?> getById(String id) {
final query = _db.localAssetEntity.select()..where((lae) => lae.id.equals(id)); final query = _db.localAssetEntity.select()..where((lae) => lae.id.equals(id));
@ -90,4 +120,23 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
..where((la) => la.checksum.isIn(checksums) & la.id.isInQuery(backedUpAssetIds)); ..where((la) => la.checksum.isIn(checksums) & la.id.isInQuery(backedUpAssetIds));
return query.map((row) => row.toDto()).get(); return query.map((row) => row.toDto()).get();
} }
Future<List<LocalTrashedAsset>> getLocalTrashedAssets(Iterable<String> remoteIds) {
if (remoteIds.isEmpty) {
return Future.value([]);
}
final query = _db.localTrashedAssetEntity.select()..where((t) => t.remoteId.isIn(remoteIds));
return query.map((row) => row.toDto()).get();
}
Future<void> deleteLocalTrashedAssets(Iterable<String> remoteIds) {
if (remoteIds.isEmpty) {
return Future.value();
}
return _db.batch((batch) {
for (final slice in remoteIds.slices(32000)) {
batch.deleteWhere(_db.localTrashedAssetEntity, (e) => e.remoteId.isIn(slice));
}
});
}
} }

View file

@ -18,6 +18,10 @@ class LocalFilesManagerRepository {
return await _service.restoreFromTrash(fileName, type, checksum); return await _service.restoreFromTrash(fileName, type, checksum);
} }
Future<bool> restoreFromTrashById(String mediaId, int type) async {
return await _service.restoreFromTrashById(mediaId, type);
}
Future<bool> requestManageMediaPermission() async { Future<bool> requestManageMediaPermission() async {
return await _service.requestManageMediaPermission(); return await _service.requestManageMediaPermission();
} }

View file

@ -31,6 +31,15 @@ class LocalFilesManagerService {
} }
} }
Future<bool> restoreFromTrashById(String mediaId, int type) async {
try {
return await _channel.invokeMethod('restoreFromTrash', {'mediaId': mediaId, 'type': type});
} catch (e, s) {
_logger.warning('Error restore file from trash by Id', e, s);
return false;
}
}
Future<bool> requestManageMediaPermission() async { Future<bool> requestManageMediaPermission() async {
try { try {
return await _channel.invokeMethod('requestManageMediaPermission'); return await _channel.invokeMethod('requestManageMediaPermission');

View file

@ -12,6 +12,7 @@ import 'schema_v6.dart' as v6;
import 'schema_v7.dart' as v7; import 'schema_v7.dart' as v7;
import 'schema_v8.dart' as v8; import 'schema_v8.dart' as v8;
import 'schema_v9.dart' as v9; import 'schema_v9.dart' as v9;
import 'schema_v10.dart' as v10;
class GeneratedHelper implements SchemaInstantiationHelper { class GeneratedHelper implements SchemaInstantiationHelper {
@override @override
@ -35,10 +36,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v8.DatabaseAtV8(db); return v8.DatabaseAtV8(db);
case 9: case 9:
return v9.DatabaseAtV9(db); return v9.DatabaseAtV9(db);
case 10:
return v10.DatabaseAtV10(db);
default: default:
throw MissingSchemaException(version, versions); throw MissingSchemaException(version, versions);
} }
} }
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9]; static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
} }

File diff suppressed because it is too large Load diff