mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
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:
parent
70d08a2b2a
commit
9d4aee36e2
24 changed files with 326 additions and 299 deletions
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue