mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
feat(mobile): lazy loading of assets (#2413)
This commit is contained in:
parent
93863b0629
commit
0dde76bbbc
54 changed files with 1494 additions and 2328 deletions
|
|
@ -3,18 +3,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/shared/services/asset.service.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:immich_mobile/shared/services/sync.service.dart';
|
||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||
import 'package:immich_mobile/utils/db.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
|
@ -22,72 +18,23 @@ import 'package:photo_manager/photo_manager.dart';
|
|||
|
||||
/// State does not contain archived assets.
|
||||
/// Use database provider if you want to access the isArchived assets
|
||||
class AssetsState {
|
||||
final List<Asset> allAssets;
|
||||
final RenderList? renderList;
|
||||
|
||||
AssetsState(this.allAssets, {this.renderList});
|
||||
|
||||
Future<AssetsState> withRenderDataStructure(
|
||||
AssetGridLayoutParameters layout,
|
||||
) async {
|
||||
return AssetsState(
|
||||
allAssets,
|
||||
renderList: await RenderList.fromAssets(
|
||||
allAssets,
|
||||
layout,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
AssetsState withAdditionalAssets(List<Asset> toAdd) {
|
||||
return AssetsState([...allAssets, ...toAdd]);
|
||||
}
|
||||
|
||||
static AssetsState fromAssetList(List<Asset> assets) {
|
||||
return AssetsState(assets);
|
||||
}
|
||||
|
||||
static AssetsState empty() {
|
||||
return AssetsState([]);
|
||||
}
|
||||
}
|
||||
class AssetsState {}
|
||||
|
||||
class AssetNotifier extends StateNotifier<AssetsState> {
|
||||
final AssetService _assetService;
|
||||
final AppSettingsService _settingsService;
|
||||
final AlbumService _albumService;
|
||||
final SyncService _syncService;
|
||||
final Isar _db;
|
||||
final log = Logger('AssetNotifier');
|
||||
bool _getAllAssetInProgress = false;
|
||||
bool _deleteInProgress = false;
|
||||
final AsyncMutex _stateUpdateLock = AsyncMutex();
|
||||
|
||||
AssetNotifier(
|
||||
this._assetService,
|
||||
this._settingsService,
|
||||
this._albumService,
|
||||
this._syncService,
|
||||
this._db,
|
||||
) : super(AssetsState.fromAssetList([]));
|
||||
|
||||
Future<void> _updateAssetsState(List<Asset> newAssetList) async {
|
||||
final layout = AssetGridLayoutParameters(
|
||||
_settingsService.getSetting(AppSettingsEnum.tilesPerRow),
|
||||
_settingsService.getSetting(AppSettingsEnum.dynamicLayout),
|
||||
GroupAssetsBy
|
||||
.values[_settingsService.getSetting(AppSettingsEnum.groupAssetsBy)],
|
||||
);
|
||||
|
||||
state = await AssetsState.fromAssetList(newAssetList)
|
||||
.withRenderDataStructure(layout);
|
||||
}
|
||||
|
||||
// Just a little helper to trigger a rebuild of the state object
|
||||
Future<void> rebuildAssetGridDataStructure() async {
|
||||
await _updateAssetsState(state.allAssets);
|
||||
}
|
||||
) : super(AssetsState());
|
||||
|
||||
Future<void> getAllAsset({bool clear = false}) async {
|
||||
if (_getAllAssetInProgress || _deleteInProgress) {
|
||||
|
|
@ -97,79 +44,32 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||
final stopwatch = Stopwatch()..start();
|
||||
try {
|
||||
_getAllAssetInProgress = true;
|
||||
final User me = Store.get(StoreKey.currentUser);
|
||||
if (clear) {
|
||||
await clearAssetsAndAlbums(_db);
|
||||
log.info("Manual refresh requested, cleared assets and albums from db");
|
||||
} else if (_stateUpdateLock.enqueued <= 1) {
|
||||
final int cachedCount = await _userAssetQuery(me.isarId).count();
|
||||
if (cachedCount > 0 && cachedCount != state.allAssets.length) {
|
||||
await _stateUpdateLock.run(
|
||||
() async => _updateAssetsState(await _getUserAssets(me.isarId)),
|
||||
);
|
||||
log.info(
|
||||
"Reading assets ${state.allAssets.length} from DB: ${stopwatch.elapsedMilliseconds}ms",
|
||||
);
|
||||
stopwatch.reset();
|
||||
}
|
||||
}
|
||||
final bool newRemote = await _assetService.refreshRemoteAssets();
|
||||
final bool newLocal = await _albumService.refreshDeviceAlbums();
|
||||
debugPrint("newRemote: $newRemote, newLocal: $newLocal");
|
||||
log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
|
||||
stopwatch.reset();
|
||||
if (!newRemote &&
|
||||
!newLocal &&
|
||||
state.allAssets.length == await _userAssetQuery(me.isarId).count()) {
|
||||
log.info("state is already up-to-date");
|
||||
return;
|
||||
}
|
||||
stopwatch.reset();
|
||||
if (_stateUpdateLock.enqueued <= 1) {
|
||||
_stateUpdateLock.run(() async {
|
||||
final assets = await _getUserAssets(me.isarId);
|
||||
if (!const ListEquality().equals(assets, state.allAssets)) {
|
||||
log.info("setting new asset state");
|
||||
await _updateAssetsState(assets);
|
||||
}
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
_getAllAssetInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Asset>> _getUserAssets(int userId) =>
|
||||
_userAssetQuery(userId).sortByFileCreatedAtDesc().findAll();
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> _userAssetQuery(
|
||||
int userId,
|
||||
) =>
|
||||
_db.assets.filter().ownerIdEqualTo(userId).isArchivedEqualTo(false);
|
||||
|
||||
Future<void> clearAllAsset() {
|
||||
state = AssetsState.empty();
|
||||
return clearAssetsAndAlbums(_db);
|
||||
}
|
||||
|
||||
Future<void> onNewAssetUploaded(Asset newAsset) async {
|
||||
final bool ok = await _syncService.syncNewAssetToDb(newAsset);
|
||||
if (ok && _stateUpdateLock.enqueued <= 1) {
|
||||
// run this sequentially if there is at most 1 other task waiting
|
||||
await _stateUpdateLock.run(() async {
|
||||
final userId = Store.get(StoreKey.currentUser).isarId;
|
||||
final assets = await _getUserAssets(userId);
|
||||
await _updateAssetsState(assets);
|
||||
});
|
||||
}
|
||||
// eTag on device is not valid after partially modifying the assets
|
||||
Store.delete(StoreKey.assetETag);
|
||||
await _syncService.syncNewAssetToDb(newAsset);
|
||||
}
|
||||
|
||||
Future<void> deleteAssets(Set<Asset> deleteAssets) async {
|
||||
_deleteInProgress = true;
|
||||
try {
|
||||
_updateAssetsState(
|
||||
state.allAssets.whereNot(deleteAssets.contains).toList(),
|
||||
);
|
||||
final localDeleted = await _deleteLocalAssets(deleteAssets);
|
||||
final remoteDeleted = await _deleteRemoteAssets(deleteAssets);
|
||||
if (localDeleted.isNotEmpty || remoteDeleted.isNotEmpty) {
|
||||
|
|
@ -201,7 +101,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||
}
|
||||
if (local.isNotEmpty) {
|
||||
try {
|
||||
await PhotoManager.editor.deleteWithIds(local);
|
||||
return await PhotoManager.editor.deleteWithIds(local);
|
||||
} catch (e, stack) {
|
||||
log.severe("Failed to delete asset from device", e, stack);
|
||||
}
|
||||
|
|
@ -220,53 +120,25 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||
.map((a) => a.id);
|
||||
}
|
||||
|
||||
Future<bool> toggleFavorite(Asset asset, bool status) async {
|
||||
final newAsset = await _assetService.changeFavoriteStatus(asset, status);
|
||||
|
||||
if (newAsset == null) {
|
||||
log.severe("Change favorite status failed for asset ${asset.id}");
|
||||
return asset.isFavorite;
|
||||
Future<void> toggleFavorite(List<Asset> assets, bool status) async {
|
||||
final newAssets = await _assetService.changeFavoriteStatus(assets, status);
|
||||
for (Asset? newAsset in newAssets) {
|
||||
if (newAsset == null) {
|
||||
log.severe("Change favorite status failed for asset");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
final index = state.allAssets.indexWhere((a) => asset.id == a.id);
|
||||
if (index != -1) {
|
||||
state.allAssets[index] = newAsset;
|
||||
_updateAssetsState(state.allAssets);
|
||||
}
|
||||
|
||||
return newAsset.isFavorite;
|
||||
}
|
||||
|
||||
Future<void> toggleArchive(Iterable<Asset> assets, bool status) async {
|
||||
final newAssets = await Future.wait(
|
||||
assets.map((a) => _assetService.changeArchiveStatus(a, status)),
|
||||
);
|
||||
Future<void> toggleArchive(List<Asset> assets, bool status) async {
|
||||
final newAssets = await _assetService.changeArchiveStatus(assets, status);
|
||||
int i = 0;
|
||||
bool unArchived = false;
|
||||
for (Asset oldAsset in assets) {
|
||||
final newAsset = newAssets[i++];
|
||||
if (newAsset == null) {
|
||||
log.severe("Change archive status failed for asset ${oldAsset.id}");
|
||||
continue;
|
||||
}
|
||||
final index = state.allAssets.indexWhere((a) => oldAsset.id == a.id);
|
||||
if (newAsset.isArchived) {
|
||||
// remove from state
|
||||
if (index != -1) {
|
||||
state.allAssets.removeAt(index);
|
||||
}
|
||||
} else {
|
||||
// add to state is difficult because the list is sorted
|
||||
unArchived = true;
|
||||
}
|
||||
}
|
||||
if (unArchived) {
|
||||
final User me = Store.get(StoreKey.currentUser);
|
||||
await _stateUpdateLock.run(
|
||||
() async => _updateAssetsState(await _getUserAssets(me.isarId)),
|
||||
);
|
||||
} else {
|
||||
_updateAssetsState(state.allAssets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -274,26 +146,53 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||
final assetProvider = StateNotifierProvider<AssetNotifier, AssetsState>((ref) {
|
||||
return AssetNotifier(
|
||||
ref.watch(assetServiceProvider),
|
||||
ref.watch(appSettingsServiceProvider),
|
||||
ref.watch(albumServiceProvider),
|
||||
ref.watch(syncServiceProvider),
|
||||
ref.watch(dbProvider),
|
||||
);
|
||||
});
|
||||
|
||||
final assetGroupByMonthYearProvider = StateProvider((ref) {
|
||||
// TODO: remove `where` once temporary workaround is no longer needed (to only
|
||||
// allow remote assets to be added to album). Keep `toList()` as to NOT sort
|
||||
// the original list/state
|
||||
final assets =
|
||||
ref.watch(assetProvider).allAssets.where((e) => e.isRemote).toList();
|
||||
|
||||
assets.sortByCompare<DateTime>(
|
||||
(e) => e.fileCreatedAt,
|
||||
(a, b) => b.compareTo(a),
|
||||
);
|
||||
|
||||
return assets.groupListsBy(
|
||||
(element) => DateFormat('MMMM, y').format(element.fileCreatedAt.toLocal()),
|
||||
);
|
||||
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 assetsProvider = StreamProvider.autoDispose<RenderList>((ref) async* {
|
||||
final query = ref
|
||||
.watch(dbProvider)
|
||||
.assets
|
||||
.filter()
|
||||
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
||||
.isArchivedEqualTo(false)
|
||||
.sortByFileCreatedAtDesc();
|
||||
final settings = ref.watch(appSettingsServiceProvider);
|
||||
final groupBy =
|
||||
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
|
||||
yield await RenderList.fromQuery(query, groupBy);
|
||||
await for (final _ in query.watchLazy()) {
|
||||
yield await RenderList.fromQuery(query, groupBy);
|
||||
}
|
||||
});
|
||||
|
||||
final remoteAssetsProvider =
|
||||
StreamProvider.autoDispose<RenderList>((ref) async* {
|
||||
final query = ref
|
||||
.watch(dbProvider)
|
||||
.assets
|
||||
.where()
|
||||
.remoteIdIsNotNull()
|
||||
.filter()
|
||||
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
||||
.sortByFileCreatedAt();
|
||||
final settings = ref.watch(appSettingsServiceProvider);
|
||||
final groupBy =
|
||||
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
|
||||
yield await RenderList.fromQuery(query, groupBy);
|
||||
await for (final _ in query.watchLazy()) {
|
||||
yield await RenderList.fromQuery(query, groupBy);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue