4a8887f37b
* refactor(server): delete assets endpoint * fix: formatting * chore: cleanup * chore: open api * chore(mobile): replace DeleteAssetDTO with BulkIdsDTOs * feat: trash an asset * chore(server): formatting * chore: open api * chore: wording * chore: open-api * feat(server): add withDeleted to getAssets queries * WIP: mobile-recycle-bin * feat(server): recycle-bin to system config * feat(web): use recycle-bin system config * chore(server): domain assetcore removed * chore(server): rename recycle-bin to trash * chore(web): rename recycle-bin to trash * chore(server): always send soft deleted assets for getAllByUserId * chore(web): formatting * feat(server): permanent delete assets older than trashed period * feat(web): trash empty placeholder image * feat(server): empty trash * feat(web): empty trash * WIP: mobile-recycle-bin * refactor(server): empty / restore trash to separate endpoint * test(server): handle failures * test(server): fix e2e server-info test * test(server): deletion test refactor * feat(mobile): use map settings from server-config to enable / disable map * feat(mobile): trash asset * fix(server): operations on assets in trash * feat(web): show trash statistics * fix(web): handle trash enabled * fix(mobile): restore updates from trash * fix(server): ignore trashed assets for person * fix(server): add / remove search index when trashed / restored * chore(web): format * fix(server): asset service test * fix(server): include trashed assts for duplicates from uploads * feat(mobile): no dialog for trash, always dialog for permanent delete * refactor(mobile): use isar where instead of dart filter * refactor(mobile): asset provide - handle deletes in single db txn * chore(mobile): review changes * feat(web): confirmation before empty trash * server: review changes * fix(server): handle library changes * fix: filter external assets from getting trashed / deleted * fix(server): empty-bin * feat: broadcast config update events through ws * change order of trash button on mobile * styling * fix(mobile): do not show trashed toast for local only assets --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
173 lines
5.5 KiB
Dart
173 lines
5.5 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:immich_mobile/shared/models/asset.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/api.provider.dart';
|
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
|
import 'package:immich_mobile/shared/services/sync.service.dart';
|
|
import 'package:isar/isar.dart';
|
|
import 'package:logging/logging.dart';
|
|
import 'package:openapi/api.dart';
|
|
|
|
final assetServiceProvider = Provider(
|
|
(ref) => AssetService(
|
|
ref.watch(apiServiceProvider),
|
|
ref.watch(syncServiceProvider),
|
|
ref.watch(dbProvider),
|
|
),
|
|
);
|
|
|
|
class AssetService {
|
|
final ApiService _apiService;
|
|
final SyncService _syncService;
|
|
final log = Logger('AssetService');
|
|
final Isar _db;
|
|
|
|
AssetService(
|
|
this._apiService,
|
|
this._syncService,
|
|
this._db,
|
|
);
|
|
|
|
/// Checks the server for updated assets and updates the local database if
|
|
/// required. Returns `true` if there were any changes.
|
|
Future<bool> refreshRemoteAssets([User? user]) async {
|
|
user ??= Store.get<User>(StoreKey.currentUser);
|
|
final Stopwatch sw = Stopwatch()..start();
|
|
final bool changes = await _syncService.syncRemoteAssetsToDb(
|
|
user,
|
|
_getRemoteAssetChanges,
|
|
_getRemoteAssets,
|
|
);
|
|
debugPrint("refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms");
|
|
return changes;
|
|
}
|
|
|
|
/// Returns `(null, null)` if changes are invalid -> requires full sync
|
|
Future<(List<Asset>? toUpsert, List<String>? toDelete)>
|
|
_getRemoteAssetChanges(User user, DateTime since) async {
|
|
final deleted = await _apiService.auditApi
|
|
.getAuditDeletes(EntityType.ASSET, since, userId: user.id);
|
|
if (deleted == null || deleted.needsFullSync) return (null, null);
|
|
final assetDto = await _apiService.assetApi
|
|
.getAllAssets(userId: user.id, updatedAfter: since);
|
|
if (assetDto == null) return (null, null);
|
|
return (assetDto.map(Asset.remote).toList(), deleted.ids);
|
|
}
|
|
|
|
/// Returns `null` if the server state did not change, else list of assets
|
|
Future<List<Asset>?> _getRemoteAssets(User user) async {
|
|
try {
|
|
final List<AssetResponseDto>? assets =
|
|
await _apiService.assetApi.getAllAssets(
|
|
userId: user.id,
|
|
);
|
|
if (assets == null) {
|
|
return null;
|
|
} else if (assets.isNotEmpty && assets.first.ownerId != user.id) {
|
|
log.warning("Make sure that server and app versions match!"
|
|
" The server returned assets for user ${assets.first.ownerId}"
|
|
" while requesting assets of user ${user.id}");
|
|
return null;
|
|
}
|
|
return assets.map(Asset.remote).toList();
|
|
} catch (error, stack) {
|
|
log.severe(
|
|
'Error while getting remote assets: ${error.toString()}',
|
|
error,
|
|
stack,
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
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.assetApi.deleteAssets(
|
|
AssetBulkDeleteDto(
|
|
ids: payload,
|
|
force: force,
|
|
),
|
|
);
|
|
return true;
|
|
} catch (error, stack) {
|
|
log.severe("Error deleteAssets ${error.toString()}", 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 {
|
|
a.exifInfo ??= await _db.exifInfos.get(a.id);
|
|
// fileSize is always filled on the server but not set on client
|
|
if (a.exifInfo?.fileSize == null) {
|
|
if (a.isRemote) {
|
|
final dto = await _apiService.assetApi.getAssetById(a.remoteId!);
|
|
if (dto != null && dto.exifInfo != null) {
|
|
final newExif = Asset.remote(dto).exifInfo!.copyWith(id: a.id);
|
|
if (newExif != a.exifInfo) {
|
|
if (a.isInDb) {
|
|
_db.writeTxn(() => a.put(_db));
|
|
} else {
|
|
debugPrint("[loadExif] parameter Asset is not from DB!");
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// TODO implement local exif info parsing
|
|
}
|
|
}
|
|
return a;
|
|
}
|
|
|
|
Future<List<Asset?>> updateAssets(
|
|
List<Asset> assets,
|
|
UpdateAssetDto updateAssetDto,
|
|
) async {
|
|
final List<AssetResponseDto?> dtos = await Future.wait(
|
|
assets.map(
|
|
(a) => _apiService.assetApi.updateAsset(a.remoteId!, updateAssetDto),
|
|
),
|
|
);
|
|
bool allInDb = true;
|
|
for (int i = 0; i < assets.length; i++) {
|
|
final dto = dtos[i], old = assets[i];
|
|
if (dto != null) {
|
|
final remote = Asset.remote(dto);
|
|
if (old.canUpdate(remote)) {
|
|
assets[i] = old.updatedCopy(remote);
|
|
}
|
|
allInDb &= assets[i].isInDb;
|
|
}
|
|
}
|
|
final toUpdate = allInDb ? assets : assets.where((e) => e.isInDb).toList();
|
|
await _syncService.upsertAssetsWithExif(toUpdate);
|
|
return assets;
|
|
}
|
|
|
|
Future<List<Asset?>> changeFavoriteStatus(
|
|
List<Asset> assets,
|
|
bool isFavorite,
|
|
) {
|
|
return updateAssets(assets, UpdateAssetDto(isFavorite: isFavorite));
|
|
}
|
|
|
|
Future<List<Asset?>> changeArchiveStatus(List<Asset> assets, bool isArchive) {
|
|
return updateAssets(assets, UpdateAssetDto(isArchived: isArchive));
|
|
}
|
|
}
|