2023-01-18 17:59:23 +02:00
|
|
|
import 'package:flutter/foundation.dart';
|
2022-02-03 18:06:44 +02:00
|
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
2023-03-04 00:38:30 +02:00
|
|
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
|
|
|
import 'package:immich_mobile/shared/models/album.dart';
|
|
|
|
import 'package:immich_mobile/shared/models/exif_info.dart';
|
2023-02-09 19:32:08 +02:00
|
|
|
import 'package:immich_mobile/shared/models/store.dart';
|
2023-03-04 00:38:30 +02:00
|
|
|
import 'package:immich_mobile/shared/models/user.dart';
|
|
|
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
2023-02-04 22:42:42 +02:00
|
|
|
import 'package:immich_mobile/shared/services/asset.service.dart';
|
2023-01-18 17:59:23 +02:00
|
|
|
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';
|
2022-11-08 19:00:24 +02:00
|
|
|
import 'package:immich_mobile/shared/models/asset.dart';
|
2022-02-07 04:31:32 +02:00
|
|
|
import 'package:collection/collection.dart';
|
2022-02-13 23:10:42 +02:00
|
|
|
import 'package:intl/intl.dart';
|
2023-03-04 00:38:30 +02:00
|
|
|
import 'package:isar/isar.dart';
|
2022-11-27 22:34:19 +02:00
|
|
|
import 'package:logging/logging.dart';
|
2022-07-13 14:23:48 +02:00
|
|
|
import 'package:openapi/api.dart';
|
2022-02-13 23:10:42 +02:00
|
|
|
import 'package:photo_manager/photo_manager.dart';
|
2022-02-03 18:06:44 +02:00
|
|
|
|
2023-01-18 17:59:23 +02:00
|
|
|
class AssetsState {
|
|
|
|
final List<Asset> allAssets;
|
|
|
|
final RenderList? renderList;
|
|
|
|
|
|
|
|
AssetsState(this.allAssets, {this.renderList});
|
|
|
|
|
2023-02-09 19:35:44 +02:00
|
|
|
Future<AssetsState> withRenderDataStructure(
|
|
|
|
AssetGridLayoutParameters layout,
|
|
|
|
) async {
|
2023-01-18 17:59:23 +02:00
|
|
|
return AssetsState(
|
|
|
|
allAssets,
|
2023-02-09 19:35:44 +02:00
|
|
|
renderList: await RenderList.fromAssets(
|
|
|
|
allAssets,
|
|
|
|
layout,
|
|
|
|
),
|
2023-01-18 17:59:23 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
AssetsState withAdditionalAssets(List<Asset> toAdd) {
|
|
|
|
return AssetsState([...allAssets, ...toAdd]);
|
|
|
|
}
|
|
|
|
|
2023-02-04 22:42:42 +02:00
|
|
|
static AssetsState fromAssetList(List<Asset> assets) {
|
2023-01-18 17:59:23 +02:00
|
|
|
return AssetsState(assets);
|
|
|
|
}
|
|
|
|
|
2023-02-04 22:42:42 +02:00
|
|
|
static AssetsState empty() {
|
2023-01-18 17:59:23 +02:00
|
|
|
return AssetsState([]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class AssetNotifier extends StateNotifier<AssetsState> {
|
2022-06-25 20:46:51 +02:00
|
|
|
final AssetService _assetService;
|
2023-01-18 17:59:23 +02:00
|
|
|
final AppSettingsService _settingsService;
|
2023-03-04 00:38:30 +02:00
|
|
|
final AlbumService _albumService;
|
|
|
|
final Isar _db;
|
2022-11-27 22:34:19 +02:00
|
|
|
final log = Logger('AssetNotifier');
|
2022-11-08 19:00:24 +02:00
|
|
|
bool _getAllAssetInProgress = false;
|
|
|
|
bool _deleteInProgress = false;
|
2022-02-03 18:06:44 +02:00
|
|
|
|
2023-01-18 17:59:23 +02:00
|
|
|
AssetNotifier(
|
|
|
|
this._assetService,
|
|
|
|
this._settingsService,
|
2023-03-04 00:38:30 +02:00
|
|
|
this._albumService,
|
|
|
|
this._db,
|
2023-01-18 17:59:23 +02:00
|
|
|
) : super(AssetsState.fromAssetList([]));
|
|
|
|
|
2023-03-04 00:38:30 +02:00
|
|
|
Future<void> _updateAssetsState(List<Asset> newAssetList) async {
|
2023-02-09 19:35:44 +02:00
|
|
|
final layout = AssetGridLayoutParameters(
|
2023-01-18 17:59:23 +02:00
|
|
|
_settingsService.getSetting(AppSettingsEnum.tilesPerRow),
|
2023-02-09 19:35:44 +02:00
|
|
|
_settingsService.getSetting(AppSettingsEnum.dynamicLayout),
|
2023-03-04 00:38:30 +02:00
|
|
|
GroupAssetsBy
|
|
|
|
.values[_settingsService.getSetting(AppSettingsEnum.groupAssetsBy)],
|
2023-01-18 17:59:23 +02:00
|
|
|
);
|
2023-02-09 19:35:44 +02:00
|
|
|
state = await AssetsState.fromAssetList(newAssetList)
|
|
|
|
.withRenderDataStructure(layout);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Just a little helper to trigger a rebuild of the state object
|
|
|
|
Future<void> rebuildAssetGridDataStructure() async {
|
2023-03-04 00:38:30 +02:00
|
|
|
await _updateAssetsState(state.allAssets);
|
2022-10-15 23:20:15 +02:00
|
|
|
}
|
|
|
|
|
2022-02-13 23:10:42 +02:00
|
|
|
getAllAsset() async {
|
2022-11-08 19:00:24 +02:00
|
|
|
if (_getAllAssetInProgress || _deleteInProgress) {
|
|
|
|
// guard against multiple calls to this method while it's still working
|
|
|
|
return;
|
|
|
|
}
|
2022-10-16 09:50:31 +02:00
|
|
|
final stopwatch = Stopwatch();
|
2022-11-08 19:00:24 +02:00
|
|
|
try {
|
|
|
|
_getAllAssetInProgress = true;
|
2023-03-04 00:38:30 +02:00
|
|
|
final User me = Store.get(StoreKey.currentUser);
|
|
|
|
final int cachedCount =
|
|
|
|
await _db.assets.filter().ownerIdEqualTo(me.isarId).count();
|
2022-11-26 18:16:02 +02:00
|
|
|
stopwatch.start();
|
2023-03-04 00:38:30 +02:00
|
|
|
if (cachedCount > 0 && cachedCount != state.allAssets.length) {
|
|
|
|
await _updateAssetsState(await _getUserAssets(me.isarId));
|
|
|
|
log.info(
|
|
|
|
"Reading assets ${state.allAssets.length} from DB: ${stopwatch.elapsedMilliseconds}ms",
|
|
|
|
);
|
2023-02-04 22:42:42 +02:00
|
|
|
stopwatch.reset();
|
|
|
|
}
|
2023-03-04 00:38:30 +02:00
|
|
|
final bool newRemote = await _assetService.refreshRemoteAssets();
|
|
|
|
final bool newLocal = await _albumService.refreshDeviceAlbums();
|
2022-11-27 22:34:19 +02:00
|
|
|
log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
|
2022-10-16 09:50:31 +02:00
|
|
|
stopwatch.reset();
|
2023-03-15 23:29:07 +02:00
|
|
|
if (!newRemote &&
|
|
|
|
!newLocal &&
|
|
|
|
state.allAssets.length ==
|
|
|
|
await _db.assets.filter().ownerIdEqualTo(me.isarId).count()) {
|
2022-11-27 22:34:19 +02:00
|
|
|
log.info("state is already up-to-date");
|
2022-11-26 18:16:02 +02:00
|
|
|
return;
|
|
|
|
}
|
2023-03-04 00:38:30 +02:00
|
|
|
stopwatch.reset();
|
|
|
|
final assets = await _getUserAssets(me.isarId);
|
|
|
|
if (!const ListEquality().equals(assets, state.allAssets)) {
|
|
|
|
log.info("setting new asset state");
|
|
|
|
await _updateAssetsState(assets);
|
|
|
|
}
|
2022-11-08 19:00:24 +02:00
|
|
|
} finally {
|
|
|
|
_getAllAssetInProgress = false;
|
2022-10-14 23:57:55 +02:00
|
|
|
}
|
2022-11-26 18:16:02 +02:00
|
|
|
}
|
|
|
|
|
2023-03-04 00:38:30 +02:00
|
|
|
Future<List<Asset>> _getUserAssets(int userId) => _db.assets
|
|
|
|
.filter()
|
|
|
|
.ownerIdEqualTo(userId)
|
|
|
|
.sortByFileCreatedAtDesc()
|
|
|
|
.findAll();
|
|
|
|
|
|
|
|
Future<void> clearAllAsset() {
|
|
|
|
state = AssetsState.empty();
|
|
|
|
return _db.writeTxn(() async {
|
|
|
|
await _db.assets.clear();
|
|
|
|
await _db.exifInfos.clear();
|
|
|
|
await _db.albums.clear();
|
|
|
|
});
|
2022-02-13 23:10:42 +02:00
|
|
|
}
|
2022-02-03 18:06:44 +02:00
|
|
|
|
2023-03-04 00:38:30 +02:00
|
|
|
Future<void> onNewAssetUploaded(Asset newAsset) async {
|
2023-01-18 17:59:23 +02:00
|
|
|
final int i = state.allAssets.indexWhere(
|
2022-11-08 19:00:24 +02:00
|
|
|
(a) =>
|
|
|
|
a.isRemote ||
|
2023-03-04 00:38:30 +02:00
|
|
|
(a.localId == newAsset.localId && a.deviceId == newAsset.deviceId),
|
2022-11-08 19:00:24 +02:00
|
|
|
);
|
|
|
|
|
2023-03-04 00:38:30 +02:00
|
|
|
if (i == -1 ||
|
|
|
|
state.allAssets[i].localId != newAsset.localId ||
|
|
|
|
state.allAssets[i].deviceId != newAsset.deviceId) {
|
|
|
|
await _updateAssetsState([...state.allAssets, newAsset]);
|
2022-11-08 19:00:24 +02:00
|
|
|
} else {
|
2023-03-04 00:38:30 +02:00
|
|
|
// unify local/remote assets by replacing the
|
|
|
|
// local-only asset in the DB with a local&remote asset
|
|
|
|
final Asset? inDb = await _db.assets
|
|
|
|
.where()
|
|
|
|
.localIdDeviceIdEqualTo(newAsset.localId, newAsset.deviceId)
|
|
|
|
.findFirst();
|
|
|
|
if (inDb != null) {
|
|
|
|
newAsset.id = inDb.id;
|
|
|
|
newAsset.isLocal = inDb.isLocal;
|
|
|
|
}
|
|
|
|
|
2022-11-08 19:00:24 +02:00
|
|
|
// order is important to keep all local-only assets at the beginning!
|
2023-03-04 00:38:30 +02:00
|
|
|
await _updateAssetsState([
|
2023-01-18 17:59:23 +02:00
|
|
|
...state.allAssets.slice(0, i),
|
|
|
|
...state.allAssets.slice(i + 1),
|
2023-02-04 22:42:42 +02:00
|
|
|
newAsset,
|
2023-01-18 17:59:23 +02:00
|
|
|
]);
|
2023-03-04 00:38:30 +02:00
|
|
|
}
|
|
|
|
try {
|
|
|
|
await _db.writeTxn(() => newAsset.put(_db));
|
|
|
|
} on IsarError catch (e) {
|
|
|
|
debugPrint(e.toString());
|
2022-11-08 19:00:24 +02:00
|
|
|
}
|
2022-02-14 18:40:41 +02:00
|
|
|
}
|
|
|
|
|
2023-03-04 00:38:30 +02:00
|
|
|
Future<void> deleteAssets(Set<Asset> deleteAssets) async {
|
2022-11-08 19:00:24 +02:00
|
|
|
_deleteInProgress = true;
|
|
|
|
try {
|
2023-03-04 00:38:30 +02:00
|
|
|
_updateAssetsState(
|
|
|
|
state.allAssets.whereNot(deleteAssets.contains).toList(),
|
|
|
|
);
|
2022-11-08 19:00:24 +02:00
|
|
|
final localDeleted = await _deleteLocalAssets(deleteAssets);
|
|
|
|
final remoteDeleted = await _deleteRemoteAssets(deleteAssets);
|
2023-03-04 00:38:30 +02:00
|
|
|
if (localDeleted.isNotEmpty || remoteDeleted.isNotEmpty) {
|
|
|
|
final dbIds = deleteAssets.map((e) => e.id).toList();
|
|
|
|
await _db.writeTxn(() async {
|
|
|
|
await _db.exifInfos.deleteAll(dbIds);
|
|
|
|
await _db.assets.deleteAll(dbIds);
|
|
|
|
});
|
2022-11-08 19:00:24 +02:00
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
_deleteInProgress = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<String>> _deleteLocalAssets(Set<Asset> assetsToDelete) async {
|
2023-03-04 00:38:30 +02:00
|
|
|
final int deviceId = Store.get(StoreKey.deviceIdHash);
|
2022-11-08 19:00:24 +02:00
|
|
|
final List<String> local = [];
|
2022-02-13 23:10:42 +02:00
|
|
|
// Delete asset from device
|
2022-11-08 19:00:24 +02:00
|
|
|
for (final Asset asset in assetsToDelete) {
|
|
|
|
if (asset.isLocal) {
|
2023-03-04 00:38:30 +02:00
|
|
|
local.add(asset.localId);
|
2022-11-08 19:00:24 +02:00
|
|
|
} else if (asset.deviceId == deviceId) {
|
|
|
|
// Delete asset on device if it is still present
|
2023-03-04 00:38:30 +02:00
|
|
|
var localAsset = await AssetEntity.fromId(asset.localId);
|
2022-02-13 23:10:42 +02:00
|
|
|
if (localAsset != null) {
|
2022-11-08 19:00:24 +02:00
|
|
|
local.add(localAsset.id);
|
2022-02-03 18:06:44 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-11-08 19:00:24 +02:00
|
|
|
if (local.isNotEmpty) {
|
|
|
|
try {
|
2023-03-04 00:38:30 +02:00
|
|
|
await PhotoManager.editor.deleteWithIds(local);
|
2022-11-27 22:34:19 +02:00
|
|
|
} catch (e, stack) {
|
|
|
|
log.severe("Failed to delete asset from device", e, stack);
|
2022-02-07 04:31:32 +02:00
|
|
|
}
|
|
|
|
}
|
2022-11-08 19:00:24 +02:00
|
|
|
return [];
|
|
|
|
}
|
2022-10-15 23:20:15 +02:00
|
|
|
|
2022-11-08 19:00:24 +02:00
|
|
|
Future<Iterable<String>> _deleteRemoteAssets(
|
|
|
|
Set<Asset> assetsToDelete,
|
|
|
|
) async {
|
2023-02-04 22:42:42 +02:00
|
|
|
final Iterable<Asset> remote = assetsToDelete.where((e) => e.isRemote);
|
2022-11-08 19:00:24 +02:00
|
|
|
final List<DeleteAssetResponseDto> deleteAssetResult =
|
|
|
|
await _assetService.deleteAssets(remote) ?? [];
|
|
|
|
return deleteAssetResult
|
|
|
|
.where((a) => a.status == DeleteAssetStatus.SUCCESS)
|
|
|
|
.map((a) => a.id);
|
2022-02-07 04:31:32 +02:00
|
|
|
}
|
2023-02-05 05:25:11 +02:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-02-06 14:59:56 +02:00
|
|
|
final index = state.allAssets.indexWhere((a) => asset.id == a.id);
|
|
|
|
if (index > 0) {
|
2023-02-09 19:32:08 +02:00
|
|
|
state.allAssets[index] = newAsset;
|
2023-02-06 14:59:56 +02:00
|
|
|
_updateAssetsState(state.allAssets);
|
|
|
|
}
|
2023-02-05 05:25:11 +02:00
|
|
|
|
|
|
|
return newAsset.isFavorite;
|
|
|
|
}
|
2022-02-03 18:06:44 +02:00
|
|
|
}
|
|
|
|
|
2023-01-18 17:59:23 +02:00
|
|
|
final assetProvider = StateNotifierProvider<AssetNotifier, AssetsState>((ref) {
|
2022-10-14 23:57:55 +02:00
|
|
|
return AssetNotifier(
|
2022-11-21 14:13:14 +02:00
|
|
|
ref.watch(assetServiceProvider),
|
2023-01-18 17:59:23 +02:00
|
|
|
ref.watch(appSettingsServiceProvider),
|
2023-03-04 00:38:30 +02:00
|
|
|
ref.watch(albumServiceProvider),
|
|
|
|
ref.watch(dbProvider),
|
2022-07-13 14:23:48 +02:00
|
|
|
);
|
2022-02-13 23:10:42 +02:00
|
|
|
});
|
2022-04-24 04:08:45 +02:00
|
|
|
|
|
|
|
final assetGroupByMonthYearProvider = StateProvider((ref) {
|
2022-11-08 19:00:24 +02:00
|
|
|
// 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
|
2023-01-18 17:59:23 +02:00
|
|
|
final assets =
|
|
|
|
ref.watch(assetProvider).allAssets.where((e) => e.isRemote).toList();
|
2022-04-24 04:08:45 +02:00
|
|
|
|
2022-06-22 07:23:35 +02:00
|
|
|
assets.sortByCompare<DateTime>(
|
2023-02-19 18:44:53 +02:00
|
|
|
(e) => e.fileCreatedAt,
|
2022-07-13 14:23:48 +02:00
|
|
|
(a, b) => b.compareTo(a),
|
|
|
|
);
|
|
|
|
|
|
|
|
return assets.groupListsBy(
|
2023-02-19 18:44:53 +02:00
|
|
|
(element) => DateFormat('MMMM, y').format(element.fileCreatedAt.toLocal()),
|
2022-07-13 14:23:48 +02:00
|
|
|
);
|
2022-04-24 04:08:45 +02:00
|
|
|
});
|