refactor(mobile): asset provider (#16159)

* refactor(mobile): asset provider

* wip

* wip: delete local assets

* wip: delete remote assets

* wip: deletion logic

* refactor

* pr feedback
This commit is contained in:
Alex 2025-02-18 13:10:55 -06:00 committed by GitHub
parent 70d08a2b2a
commit 9d4aee36e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 326 additions and 299 deletions

View File

@ -51,6 +51,8 @@ abstract interface class IAlbumRepository implements IDatabaseRepository {
Stream<Album?> watchAlbum(int id);
Stream<RenderList> getRenderListStream(Album album);
Future<void> clearTable();
}
enum AlbumSort { remoteId, localId }

View File

@ -41,7 +41,7 @@ abstract interface class IAssetRepository implements IDatabaseRepository {
Future<void> deleteAllByRemoteId(List<String> ids, {AssetState? state});
Future<void> deleteById(List<int> ids);
Future<void> deleteByIds(List<int> ids);
Future<List<Asset>> getMatches({
required List<Asset> assets,
@ -59,6 +59,10 @@ abstract interface class IAssetRepository implements IDatabaseRepository {
Future<List<String>> getAllDuplicatedAssetIds();
Future<List<Asset>> getStackAssets(String stackId);
Future<void> clearTable();
Stream<Asset?> watchAsset(int id, {bool fireImmediately = false});
}
enum AssetSort { checksum, ownerIdChecksum }

View File

@ -11,4 +11,6 @@ abstract interface class IETagRepository implements IDatabaseRepository {
Future<void> upsertAll(List<ETag> etags);
Future<void> deleteByIds(List<String> ids);
Future<void> clearTable();
}

View File

@ -9,4 +9,6 @@ abstract interface class IExifInfoRepository implements IDatabaseRepository {
Future<List<ExifInfo>> updateAll(List<ExifInfo> exifInfos);
Future<void> delete(int id);
Future<void> clearTable();
}

View File

@ -18,6 +18,8 @@ abstract interface class IUserRepository implements IDatabaseRepository {
Future<void> deleteById(List<int> ids);
Future<User> me();
Future<void> clearTable();
}
enum UserSort { id }

View File

@ -2,28 +2,40 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/locale_provider.dart';
import 'package:immich_mobile/providers/memory.provider.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/asset.service.dart';
import 'package:immich_mobile/services/etag.service.dart';
import 'package:immich_mobile/services/exif.service.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/services/user.service.dart';
import 'package:immich_mobile/utils/db.dart';
import 'package:immich_mobile/utils/renderlist_generator.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
final assetProvider = StateNotifierProvider<AssetNotifier, bool>((ref) {
return AssetNotifier(
ref.watch(assetServiceProvider),
ref.watch(albumServiceProvider),
ref.watch(userServiceProvider),
ref.watch(syncServiceProvider),
ref.watch(etagServiceProvider),
ref.watch(exifServiceProvider),
ref,
);
});
class AssetNotifier extends StateNotifier<bool> {
final AssetService _assetService;
final AlbumService _albumService;
final UserService _userService;
final SyncService _syncService;
final Isar _db;
final ETagService _etagService;
final ExifService _exifService;
final StateNotifierProviderRef _ref;
final log = Logger('AssetNotifier');
bool _getAllAssetInProgress = false;
@ -34,7 +46,8 @@ class AssetNotifier extends StateNotifier<bool> {
this._albumService,
this._userService,
this._syncService,
this._db,
this._etagService,
this._exifService,
this._ref,
) : super(false);
@ -48,7 +61,7 @@ class AssetNotifier extends StateNotifier<bool> {
_getAllAssetInProgress = true;
state = true;
if (clear) {
await clearAssetsAndAlbums(_db);
await clearAllAssets();
log.info("Manual refresh requested, cleared assets and albums from db");
}
final bool changedUsers = await _userService.refreshUsers();
@ -68,8 +81,15 @@ class AssetNotifier extends StateNotifier<bool> {
}
}
Future<void> clearAllAsset() {
return clearAssetsAndAlbums(_db);
Future<void> clearAllAssets() async {
await Store.delete(StoreKey.assetETag);
await Future.wait([
_assetService.clearTable(),
_exifService.clearTable(),
_albumService.clearTable(),
_userService.clearTable(),
_etagService.clearTable(),
]);
}
Future<void> onNewAssetUploaded(Asset newAsset) async {
@ -78,102 +98,43 @@ class AssetNotifier extends StateNotifier<bool> {
await _syncService.syncNewAssetToDb(newAsset);
}
Future<bool> deleteLocalOnlyAssets(
Iterable<Asset> deleteAssets, {
bool onlyBackedUp = false,
}) async {
Future<bool> deleteLocalAssets(List<Asset> assets) async {
_deleteInProgress = true;
state = true;
try {
// Filter the assets based on the backed-up status
final assets = onlyBackedUp
? deleteAssets.where((e) => e.storage == AssetState.merged)
: deleteAssets;
if (assets.isEmpty) {
return false; // No assets to delete
}
// Proceed with local deletion of the filtered assets
final localDeleted = await _deleteLocalAssets(assets);
if (localDeleted.isNotEmpty) {
final localOnlyIds = assets
.where((e) => e.storage == AssetState.local)
.map((e) => e.id)
.toList();
// Update merged assets to remote-only
final mergedAssets =
assets.where((e) => e.storage == AssetState.merged).map((e) {
e.localId = null;
return e;
}).toList();
// Update the local database
await _db.writeTxn(() async {
if (mergedAssets.isNotEmpty) {
await _db.assets
.putAll(mergedAssets); // Use the filtered merged assets
}
await _db.exifInfos.deleteAll(localOnlyIds);
await _db.assets.deleteAll(localOnlyIds);
});
return true;
}
await _assetService.deleteLocalAssets(assets);
return true;
} catch (error) {
log.severe("Failed to delete local assets", error);
return false;
} finally {
_deleteInProgress = false;
state = false;
}
return false;
}
Future<bool> deleteRemoteOnlyAssets(
/// Delete remote asset only
///
/// Default behavior is trashing the asset
Future<bool> deleteRemoteAssets(
Iterable<Asset> deleteAssets, {
bool force = false,
bool shouldDeletePermanently = false,
}) async {
_deleteInProgress = true;
state = true;
try {
final remoteDeleted = await _deleteRemoteAssets(deleteAssets, force);
if (remoteDeleted.isNotEmpty) {
final assetsToUpdate = force
/// If force, only update merged only assets and remove remote assets
? remoteDeleted
.where((e) => e.storage == AssetState.merged)
.map((e) {
e.remoteId = null;
return e;
})
// If not force, trash everything
: remoteDeleted.where((e) => e.isRemote).map((e) {
e.isTrashed = true;
return e;
});
await _db.writeTxn(() async {
if (assetsToUpdate.isNotEmpty) {
await _db.assets.putAll(assetsToUpdate.toList());
}
if (force) {
final remoteOnly = remoteDeleted
.where((e) => e.storage == AssetState.remote)
.map((e) => e.id)
.toList();
await _db.exifInfos.deleteAll(remoteOnly);
await _db.assets.deleteAll(remoteOnly);
}
});
return true;
}
await _assetService.deleteRemoteAssets(
deleteAssets,
shouldDeletePermanently: shouldDeletePermanently,
);
return true;
} catch (error) {
log.severe("Failed to delete remote assets", error);
return false;
} finally {
_deleteInProgress = false;
state = false;
}
return false;
}
Future<bool> deleteAssets(
@ -183,111 +144,18 @@ class AssetNotifier extends StateNotifier<bool> {
_deleteInProgress = true;
state = true;
try {
final hasLocal = deleteAssets.any((a) => a.storage != AssetState.remote);
final localDeleted = await _deleteLocalAssets(deleteAssets);
final remoteDeleted = (hasLocal && localDeleted.isNotEmpty) || !hasLocal
? await _deleteRemoteAssets(deleteAssets, force)
: [];
if (localDeleted.isNotEmpty || remoteDeleted.isNotEmpty) {
final dbIds = <int>[];
final dbUpdates = <Asset>[];
// Local assets are removed
if (localDeleted.isNotEmpty) {
// Permanently remove local only assets from isar
dbIds.addAll(
deleteAssets
.where((a) => a.storage == AssetState.local)
.map((e) => e.id),
);
if (remoteDeleted.any((e) => e.isLocal)) {
// Force delete: Add all local assets including merged assets
if (force) {
dbIds.addAll(remoteDeleted.map((e) => e.id));
// Soft delete: Remove local Id from asset and trash it
} else {
dbUpdates.addAll(
remoteDeleted.map((e) {
e.localId = null;
e.isTrashed = true;
return e;
}),
);
}
}
}
// Handle remote deletion
if (remoteDeleted.isNotEmpty) {
if (force) {
// Remove remote only assets
dbIds.addAll(
deleteAssets
.where((a) => a.storage == AssetState.remote)
.map((e) => e.id),
);
// Local assets are not removed and there are merged assets
final hasLocal = remoteDeleted.any((e) => e.isLocal);
if (localDeleted.isEmpty && hasLocal) {
// Remove remote Id from local assets
dbUpdates.addAll(
remoteDeleted.map((e) {
e.remoteId = null;
// Remove from trashed if remote asset is removed
e.isTrashed = false;
return e;
}),
);
}
} else {
dbUpdates.addAll(
remoteDeleted.map((e) {
e.isTrashed = true;
return e;
}),
);
}
}
await _db.writeTxn(() async {
await _db.assets.putAll(dbUpdates);
await _db.exifInfos.deleteAll(dbIds);
await _db.assets.deleteAll(dbIds);
});
return true;
}
await _assetService.deleteAssets(
deleteAssets,
shouldDeletePermanently: force,
);
return true;
} catch (error) {
log.severe("Failed to delete assets", error);
return false;
} finally {
_deleteInProgress = false;
state = false;
}
return false;
}
Future<List<String>> _deleteLocalAssets(
Iterable<Asset> assetsToDelete,
) async {
final List<String> local =
assetsToDelete.where((a) => a.isLocal).map((a) => a.localId!).toList();
// Delete asset from device
if (local.isNotEmpty) {
try {
return await _ref.read(assetMediaRepositoryProvider).deleteAll(local);
} catch (e, stack) {
log.severe("Failed to delete asset from device", e, stack);
}
}
return [];
}
Future<List<Asset>> _deleteRemoteAssets(
Iterable<Asset> assetsToDelete,
bool? force,
) async {
final Iterable<Asset> remote = assetsToDelete.where((e) => e.isRemote);
final isSuccess = await _assetService.deleteAssets(remote, force: force);
return isSuccess ? remote.toList() : [];
}
Future<void> toggleFavorite(List<Asset> assets, [bool? status]) {
@ -301,41 +169,40 @@ class AssetNotifier extends StateNotifier<bool> {
}
}
final assetProvider = StateNotifierProvider<AssetNotifier, bool>((ref) {
return AssetNotifier(
ref.watch(assetServiceProvider),
ref.watch(albumServiceProvider),
ref.watch(userServiceProvider),
ref.watch(syncServiceProvider),
ref.watch(dbProvider),
ref,
);
});
final assetDetailProvider =
StreamProvider.autoDispose.family<Asset, Asset>((ref, asset) async* {
yield await ref.watch(assetServiceProvider).loadExif(asset);
final db = ref.watch(dbProvider);
await for (final a in db.assets.watchObject(asset.id)) {
if (a != null) {
yield await ref.watch(assetServiceProvider).loadExif(a);
final assetService = ref.watch(assetServiceProvider);
yield await assetService.loadExif(asset);
await for (final asset in assetService.watchAsset(asset.id)) {
if (asset != null) {
yield await ref.watch(assetServiceProvider).loadExif(asset);
}
}
});
final assetWatcher =
StreamProvider.autoDispose.family<Asset?, Asset>((ref, asset) {
final db = ref.watch(dbProvider);
return db.assets.watchObject(asset.id, fireImmediately: true);
final assetService = ref.watch(assetServiceProvider);
return assetService.watchAsset(asset.id, fireImmediately: true);
});
final assetsProvider = StreamProvider.family<RenderList, int?>(
(ref, userId) {
if (userId == null) return const Stream.empty();
ref.watch(localeProvider);
final query = _commonFilterAndSort(
_assets(ref).where().ownerIdEqualToAnyChecksum(userId),
);
final query = ref
.watch(dbProvider)
.assets
.where()
.ownerIdEqualToAnyChecksum(userId)
.filter()
.isArchivedEqualTo(false)
.isTrashedEqualTo(false)
.stackPrimaryAssetIdIsNull()
.sortByFileCreatedAtDesc();
return renderListGenerator(query, ref);
},
dependencies: [localeProvider],
@ -345,11 +212,17 @@ final multiUserAssetsProvider = StreamProvider.family<RenderList, List<int>>(
(ref, userIds) {
if (userIds.isEmpty) return const Stream.empty();
ref.watch(localeProvider);
final query = _commonFilterAndSort(
_assets(ref)
.where()
.anyOf(userIds, (q, u) => q.ownerIdEqualToAnyChecksum(u)),
);
final query = ref
.watch(dbProvider)
.assets
.where()
.anyOf(userIds, (q, u) => q.ownerIdEqualToAnyChecksum(u))
.filter()
.isArchivedEqualTo(false)
.isTrashedEqualTo(false)
.stackPrimaryAssetIdIsNull()
.sortByFileCreatedAtDesc();
return renderListGenerator(query, ref);
},
dependencies: [localeProvider],
@ -371,17 +244,3 @@ QueryBuilder<Asset, Asset, QAfterSortBy>? getRemoteAssetQuery(WidgetRef ref) {
.stackPrimaryAssetIdIsNull()
.sortByFileCreatedAtDesc();
}
IsarCollection<Asset> _assets(StreamProviderRef<RenderList> ref) =>
ref.watch(dbProvider).assets;
QueryBuilder<Asset, Asset, QAfterSortBy> _commonFilterAndSort(
QueryBuilder<Asset, Asset, QAfterWhereClause> query,
) {
return query
.filter()
.isArchivedEqualTo(false)
.isTrashedEqualTo(false)
.stackPrimaryAssetIdIsNull()
.sortByFileCreatedAtDesc();
}

View File

@ -57,7 +57,7 @@ class TrashNotifier extends StateNotifier<bool> {
final isRemoved = await _ref
.read(assetProvider.notifier)
.deleteRemoteOnlyAssets(assetList, force: true);
.deleteRemoteAssets(assetList, shouldDeletePermanently: true);
if (isRemoved) {
final idsToRemove =

View File

@ -155,6 +155,13 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
return await query.findAll();
}
@override
Future<void> clearTable() async {
await txn(() async {
await db.albums.clear();
});
}
@override
Stream<List<Album>> watchRemoteAlbums() {
return db.albums.where().remoteIdIsNotNull().watch();

View File

@ -57,7 +57,7 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository {
}
@override
Future<void> deleteById(List<int> ids) => txn(() async {
Future<void> deleteByIds(List<int> ids) => txn(() async {
await db.assets.deleteAll(ids);
await db.exifInfos.deleteAll(ids);
});
@ -210,6 +210,18 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository {
.thenByFileCreatedAtDesc()
.findAll();
}
@override
Future<void> clearTable() async {
await txn(() async {
await db.assets.clear();
});
}
@override
Stream<Asset?> watchAsset(int id, {bool fireImmediately = false}) {
return db.assets.watchObject(id, fireImmediately: fireImmediately);
}
}
Future<List<Asset>> _getMatchesImpl(

View File

@ -26,4 +26,11 @@ class ETagRepository extends DatabaseRepository implements IETagRepository {
@override
Future<ETag?> getById(String id) => db.eTags.getById(id);
@override
Future<void> clearTable() async {
await txn(() async {
await db.eTags.clear();
});
}
}

View File

@ -28,4 +28,9 @@ class ExifInfoRepository extends DatabaseRepository
await txn(() => db.exifInfos.putAll(exifInfos));
return exifInfos;
}
@override
Future<void> clearTable() {
return txn(() => db.exifInfos.clear());
}
}

View File

@ -57,4 +57,11 @@ class UserRepository extends DatabaseRepository implements IUserRepository {
.or()
.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId)
.findAll();
@override
Future<void> clearTable() async {
await txn(() async {
await db.users.clear();
});
}
}

View File

@ -310,7 +310,7 @@ class AlbumService {
final List<int> idsToRemove =
_syncService.sharedAssetsToRemove(foreignAssets, existing);
if (idsToRemove.isNotEmpty) {
await _assetRepository.deleteById(idsToRemove);
await _assetRepository.deleteByIds(idsToRemove);
}
} else {
await _albumRepository.delete(album.id);
@ -491,4 +491,8 @@ class AlbumService {
}
return null;
}
Future<void> clearTable() async {
await _albumRepository.clearTable();
}
}

View File

@ -9,6 +9,7 @@ import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
import 'package:immich_mobile/interfaces/backup.interface.dart';
import 'package:immich_mobile/interfaces/etag.interface.dart';
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
@ -17,6 +18,7 @@ import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/repositories/backup.repository.dart';
import 'package:immich_mobile/repositories/etag.repository.dart';
import 'package:immich_mobile/repositories/exif_info.repository.dart';
@ -43,6 +45,7 @@ final assetServiceProvider = Provider(
ref.watch(userServiceProvider),
ref.watch(backupServiceProvider),
ref.watch(albumServiceProvider),
ref.watch(assetMediaRepositoryProvider),
),
);
@ -58,6 +61,7 @@ class AssetService {
final UserService _userService;
final BackupService _backupService;
final AlbumService _albumService;
final IAssetMediaRepository _assetMediaRepository;
final log = Logger('AssetService');
AssetService(
@ -72,6 +76,7 @@ class AssetService {
this._userService,
this._backupService,
this._albumService,
this._assetMediaRepository,
);
/// Checks the server for updated assets and updates the local database if
@ -158,30 +163,6 @@ class AssetService {
}
}
Future<bool> deleteAssets(
Iterable<Asset> deleteAssets, {
bool? force = false,
}) async {
try {
final List<String> payload = [];
for (final asset in deleteAssets) {
payload.add(asset.remoteId!);
}
await _apiService.assetsApi.deleteAssets(
AssetBulkDeleteDto(
ids: payload,
force: force,
),
);
return true;
} catch (error, stack) {
log.severe("Error while deleting assets", error, stack);
}
return false;
}
/// Loads the exif information from the database. If there is none, loads
/// the exif info from the server (remote assets only)
Future<Asset> loadExif(Asset a) async {
@ -432,4 +413,105 @@ class AssetService {
Future<List<Asset>> getStackAssets(String stackId) {
return _assetRepository.getStackAssets(stackId);
}
Future<void> clearTable() {
return _assetRepository.clearTable();
}
/// Delete assets from local file system and unreference from the database
Future<void> deleteLocalAssets(Iterable<Asset> assets) async {
// Delete files from local gallery
final candidates = assets.where((asset) => asset.isLocal);
final deletedIds = await _assetMediaRepository
.deleteAll(candidates.map((asset) => asset.localId!).toList());
// Modify local database by removing the reference to the local assets
if (deletedIds.isNotEmpty) {
// Delete records from local database
final isarIds = assets
.where((asset) => asset.storage == AssetState.local)
.map((asset) => asset.id)
.toList();
await _assetRepository.deleteByIds(isarIds);
// Modify Merged asset to be remote only
final updatedAssets = assets
.where((asset) => asset.storage == AssetState.merged)
.map((asset) {
asset.localId = null;
return asset;
}).toList();
await _assetRepository.updateAll(updatedAssets);
}
}
/// Delete assets from the server and unreference from the database
Future<void> deleteRemoteAssets(
Iterable<Asset> assets, {
bool shouldDeletePermanently = false,
}) async {
final candidates = assets.where((a) => a.isRemote);
if (candidates.isEmpty) {
return;
}
await _apiService.assetsApi.deleteAssets(
AssetBulkDeleteDto(
ids: candidates.map((a) => a.remoteId!).toList(),
force: shouldDeletePermanently,
),
);
/// Update asset info bassed on the deletion type.
final payload = shouldDeletePermanently
? assets
.where((asset) => asset.storage == AssetState.merged)
.map((asset) {
asset.remoteId = null;
return asset;
})
: assets.where((asset) => asset.isRemote).map((asset) {
asset.isTrashed = true;
return asset;
});
await _assetRepository.transaction(() async {
await _assetRepository.updateAll(payload.toList());
if (shouldDeletePermanently) {
final remoteAssetIds = assets
.where((asset) => asset.storage == AssetState.remote)
.map((asset) => asset.id)
.toList();
await _assetRepository.deleteByIds(remoteAssetIds);
}
});
}
/// Delete assets on both local file system and the server.
/// Unreference from the database.
Future<void> deleteAssets(
Iterable<Asset> assets, {
bool shouldDeletePermanently = false,
}) async {
final hasLocal = assets.any((asset) => asset.isLocal);
final hasRemote = assets.any((asset) => asset.isRemote);
if (hasLocal) {
await deleteLocalAssets(assets);
}
if (hasRemote) {
await deleteRemoteAssets(
assets,
shouldDeletePermanently: shouldDeletePermanently,
);
}
}
Stream<Asset?> watchAsset(int id, {bool fireImmediately = false}) {
return _assetRepository.watchAsset(id, fireImmediately: fireImmediately);
}
}

View File

@ -0,0 +1,16 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/etag.interface.dart';
import 'package:immich_mobile/repositories/etag.repository.dart';
final etagServiceProvider =
Provider((ref) => ETagService(ref.watch(etagRepositoryProvider)));
class ETagService {
final IETagRepository _eTagRepository;
ETagService(this._eTagRepository);
Future<void> clearTable() {
return _eTagRepository.clearTable();
}
}

View File

@ -0,0 +1,16 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
import 'package:immich_mobile/repositories/exif_info.repository.dart';
final exifServiceProvider =
Provider((ref) => ExifService(ref.watch(exifInfoRepositoryProvider)));
class ExifService {
final IExifInfoRepository _exifInfoRepository;
ExifService(this._exifInfoRepository);
Future<void> clearTable() {
return _exifInfoRepository.clearTable();
}
}

View File

@ -286,7 +286,7 @@ class SyncService {
}
final idsToDelete = toRemove.map((e) => e.id).toList();
try {
await _assetRepository.deleteById(idsToDelete);
await _assetRepository.deleteByIds(idsToDelete);
await upsertAssetsWithExif(toAdd + toUpdate);
} catch (e) {
_log.severe("Failed to sync remote assets to db", e);
@ -334,7 +334,7 @@ class SyncService {
if (toDelete.isNotEmpty) {
final List<int> idsToRemove = sharedAssetsToRemove(toDelete, existing);
if (idsToRemove.isNotEmpty) {
await _assetRepository.deleteById(idsToRemove);
await _assetRepository.deleteByIds(idsToRemove);
}
} else {
assert(toDelete.isEmpty);
@ -531,7 +531,7 @@ class SyncService {
);
if (toDelete.isNotEmpty || toUpdate.isNotEmpty) {
await _assetRepository.transaction(() async {
await _assetRepository.deleteById(toDelete);
await _assetRepository.deleteByIds(toDelete);
await _assetRepository.updateAll(toUpdate);
});
_log.info(
@ -826,7 +826,7 @@ class SyncService {
final (toDelete, toUpdate) =
_handleAssetRemoval(assets, [], remote: false);
await _assetRepository.transaction(() async {
await _assetRepository.deleteById(toDelete);
await _assetRepository.deleteByIds(toDelete);
await _assetRepository.updateAll(toUpdate);
await _albumRepository.deleteAllLocal();
});

View File

@ -103,4 +103,8 @@ class UserService {
if (users == null) return false;
return _syncService.syncUsersFromServer(users);
}
Future<void> clearTable() {
return _userRepository.clearTable();
}
}

View File

@ -1,18 +0,0 @@
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:isar/isar.dart';
Future<void> clearAssetsAndAlbums(Isar db) async {
await Store.delete(StoreKey.assetETag);
await db.writeTxn(() async {
await db.assets.clear();
await db.exifInfos.clear();
await db.albums.clear();
await db.eTags.clear();
await db.users.clear();
});
}

View File

@ -1,7 +1,11 @@
import 'dart:async';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/utils/db.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:isar/isar.dart';
const int targetVersion = 8;
@ -14,6 +18,13 @@ Future<void> migrateDatabaseIfNeeded(Isar db) async {
}
Future<void> _migrateTo(Isar db, int version) async {
await clearAssetsAndAlbums(db);
await Store.delete(StoreKey.assetETag);
await db.writeTxn(() async {
await db.assets.clear();
await db.exifInfos.clear();
await db.albums.clear();
await db.eTags.clear();
await db.users.clear();
});
await Store.put(StoreKey.version, version);
}

View File

@ -200,24 +200,26 @@ class MultiselectGrid extends HookConsumerWidget {
}
}
void onDeleteLocal(bool onlyBackedUp) async {
void onDeleteLocal(bool isMergedAsset) async {
processing.value = true;
try {
// Select only the local assets from the selection
final localIds = selection.value.where((a) => a.isLocal).toList();
final localAssets = selection.value.where((a) => a.isLocal).toList();
final toDelete = isMergedAsset
? localAssets.where((e) => e.storage == AssetState.merged)
: localAssets;
if (toDelete.isEmpty) {
return;
}
// Delete only the backed-up assets if 'onlyBackedUp' is true
final isDeleted = await ref
.read(assetProvider.notifier)
.deleteLocalOnlyAssets(localIds, onlyBackedUp: onlyBackedUp);
.deleteLocalAssets(toDelete.toList());
if (isDeleted) {
// Show a toast with the correct number of deleted assets
final deletedCount = localIds
.where(
(e) => !onlyBackedUp || e.isRemote,
) // Only count backed-up assets
.length;
final deletedCount =
localAssets.where((e) => !isMergedAsset || e.isRemote).length;
ImmichToast.show(
context: context,
@ -226,7 +228,6 @@ class MultiselectGrid extends HookConsumerWidget {
gravity: ToastGravity.BOTTOM,
);
// Reset the selection
selectionEnabledHook.value = false;
}
} finally {
@ -234,7 +235,7 @@ class MultiselectGrid extends HookConsumerWidget {
}
}
void onDeleteRemote([bool force = false]) async {
void onDeleteRemote([bool shouldDeletePermanently = false]) async {
processing.value = true;
try {
final toDelete = ownedRemoteSelection(
@ -242,13 +243,15 @@ class MultiselectGrid extends HookConsumerWidget {
ownerErrorMessage: 'home_page_delete_err_partner'.tr(),
).toList();
final isDeleted = await ref
.read(assetProvider.notifier)
.deleteRemoteOnlyAssets(toDelete, force: force);
final isDeleted =
await ref.read(assetProvider.notifier).deleteRemoteAssets(
toDelete,
shouldDeletePermanently: shouldDeletePermanently,
);
if (isDeleted) {
ImmichToast.show(
context: context,
msg: force
msg: shouldDeletePermanently
? 'assets_deleted_permanently_from_server'
.tr(args: ["${toDelete.length}"])
: 'assets_trashed_from_server'.tr(args: ["${toDelete.length}"]),

View File

@ -134,7 +134,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
ref.read(manualUploadProvider.notifier).cancelBackup();
ref.read(backupProvider.notifier).cancelBackup();
ref.read(assetProvider.notifier).clearAllAsset();
ref.read(assetProvider.notifier).clearAllAssets();
ref.read(websocketProvider.notifier).disconnect();
context.replaceRoute(const LoginRoute());
},

View File

@ -85,7 +85,7 @@ class ChangePasswordForm extends HookConsumerWidget {
ref.read(backupProvider.notifier).cancelBackup();
await ref
.read(assetProvider.notifier)
.clearAllAsset();
.clearAllAssets();
ref.read(websocketProvider.notifier).disconnect();
AutoRouter.of(context).back();

View File

@ -105,7 +105,7 @@ void main() {
when(() => assetRepository.getAllByOwnerIdChecksum(any(), any()))
.thenAnswer((_) async => [initialAssets[3], null, null]);
when(() => assetRepository.updateAll(any())).thenAnswer((_) async => []);
when(() => assetRepository.deleteById(any())).thenAnswer((_) async {});
when(() => assetRepository.deleteByIds(any())).thenAnswer((_) async {});
when(() => exifInfoRepository.updateAll(any()))
.thenAnswer((_) async => []);
when(() => assetRepository.transaction<void>(any())).thenAnswer(