mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat(server): trash asset (#4015)
* 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>
This commit is contained in:
@@ -30,7 +30,8 @@ class Asset {
|
||||
exifInfo =
|
||||
remote.exifInfo != null ? ExifInfo.fromDto(remote.exifInfo!) : null,
|
||||
isFavorite = remote.isFavorite,
|
||||
isArchived = remote.isArchived;
|
||||
isArchived = remote.isArchived,
|
||||
isTrashed = remote.isTrashed;
|
||||
|
||||
Asset.local(AssetEntity local, List<int> hash)
|
||||
: localId = local.id,
|
||||
@@ -45,6 +46,7 @@ class Asset {
|
||||
updatedAt = local.modifiedDateTime,
|
||||
isFavorite = local.isFavorite,
|
||||
isArchived = false,
|
||||
isTrashed = false,
|
||||
fileCreatedAt = local.createDateTime {
|
||||
if (fileCreatedAt.year == 1970) {
|
||||
fileCreatedAt = fileModifiedAt;
|
||||
@@ -74,6 +76,7 @@ class Asset {
|
||||
this.exifInfo,
|
||||
required this.isFavorite,
|
||||
required this.isArchived,
|
||||
required this.isTrashed,
|
||||
});
|
||||
|
||||
@ignore
|
||||
@@ -138,6 +141,8 @@ class Asset {
|
||||
|
||||
bool isArchived;
|
||||
|
||||
bool isTrashed;
|
||||
|
||||
@ignore
|
||||
ExifInfo? exifInfo;
|
||||
|
||||
@@ -194,7 +199,8 @@ class Asset {
|
||||
livePhotoVideoId == other.livePhotoVideoId &&
|
||||
isFavorite == other.isFavorite &&
|
||||
isLocal == other.isLocal &&
|
||||
isArchived == other.isArchived;
|
||||
isArchived == other.isArchived &&
|
||||
isTrashed == other.isTrashed;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -216,7 +222,8 @@ class Asset {
|
||||
livePhotoVideoId.hashCode ^
|
||||
isFavorite.hashCode ^
|
||||
isLocal.hashCode ^
|
||||
isArchived.hashCode;
|
||||
isArchived.hashCode ^
|
||||
isTrashed.hashCode;
|
||||
|
||||
/// Returns `true` if this [Asset] can updated with values from parameter [a]
|
||||
bool canUpdate(Asset a) {
|
||||
@@ -229,8 +236,9 @@ class Asset {
|
||||
width == null && a.width != null ||
|
||||
height == null && a.height != null ||
|
||||
livePhotoVideoId == null && a.livePhotoVideoId != null ||
|
||||
!isRemote && a.isRemote && isFavorite != a.isFavorite ||
|
||||
!isRemote && a.isRemote && isArchived != a.isArchived;
|
||||
isFavorite != a.isFavorite ||
|
||||
isArchived != a.isArchived ||
|
||||
isTrashed != a.isTrashed;
|
||||
}
|
||||
|
||||
/// Returns a new [Asset] with values from this and merged & updated with [a]
|
||||
@@ -261,6 +269,7 @@ class Asset {
|
||||
livePhotoVideoId: livePhotoVideoId,
|
||||
isFavorite: isFavorite,
|
||||
isArchived: isArchived,
|
||||
isTrashed: isTrashed,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -275,6 +284,7 @@ class Asset {
|
||||
// isFavorite + isArchived are not set by device-only assets
|
||||
isFavorite: a.isFavorite,
|
||||
isArchived: a.isArchived,
|
||||
isTrashed: a.isTrashed,
|
||||
exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo,
|
||||
);
|
||||
} else {
|
||||
@@ -306,6 +316,7 @@ class Asset {
|
||||
String? livePhotoVideoId,
|
||||
bool? isFavorite,
|
||||
bool? isArchived,
|
||||
bool? isTrashed,
|
||||
ExifInfo? exifInfo,
|
||||
}) =>
|
||||
Asset(
|
||||
@@ -325,6 +336,7 @@ class Asset {
|
||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
isArchived: isArchived ?? this.isArchived,
|
||||
isTrashed: isTrashed ?? this.isTrashed,
|
||||
exifInfo: exifInfo ?? this.exifInfo,
|
||||
);
|
||||
|
||||
@@ -378,7 +390,8 @@ class Asset {
|
||||
"storage": "$storage",
|
||||
"width": ${width ?? "N/A"},
|
||||
"height": ${height ?? "N/A"},
|
||||
"isArchived": $isArchived
|
||||
"isArchived": $isArchived,
|
||||
"isTrashed": $isTrashed,
|
||||
}""";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,39 +57,44 @@ const AssetSchema = CollectionSchema(
|
||||
name: r'isFavorite',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'livePhotoVideoId': PropertySchema(
|
||||
r'isTrashed': PropertySchema(
|
||||
id: 8,
|
||||
name: r'isTrashed',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'livePhotoVideoId': PropertySchema(
|
||||
id: 9,
|
||||
name: r'livePhotoVideoId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'localId': PropertySchema(
|
||||
id: 9,
|
||||
id: 10,
|
||||
name: r'localId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'ownerId': PropertySchema(
|
||||
id: 10,
|
||||
id: 11,
|
||||
name: r'ownerId',
|
||||
type: IsarType.long,
|
||||
),
|
||||
r'remoteId': PropertySchema(
|
||||
id: 11,
|
||||
id: 12,
|
||||
name: r'remoteId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'type': PropertySchema(
|
||||
id: 12,
|
||||
id: 13,
|
||||
name: r'type',
|
||||
type: IsarType.byte,
|
||||
enumMap: _AssettypeEnumValueMap,
|
||||
),
|
||||
r'updatedAt': PropertySchema(
|
||||
id: 13,
|
||||
id: 14,
|
||||
name: r'updatedAt',
|
||||
type: IsarType.dateTime,
|
||||
),
|
||||
r'width': PropertySchema(
|
||||
id: 14,
|
||||
id: 15,
|
||||
name: r'width',
|
||||
type: IsarType.int,
|
||||
)
|
||||
@@ -196,13 +201,14 @@ void _assetSerialize(
|
||||
writer.writeInt(offsets[5], object.height);
|
||||
writer.writeBool(offsets[6], object.isArchived);
|
||||
writer.writeBool(offsets[7], object.isFavorite);
|
||||
writer.writeString(offsets[8], object.livePhotoVideoId);
|
||||
writer.writeString(offsets[9], object.localId);
|
||||
writer.writeLong(offsets[10], object.ownerId);
|
||||
writer.writeString(offsets[11], object.remoteId);
|
||||
writer.writeByte(offsets[12], object.type.index);
|
||||
writer.writeDateTime(offsets[13], object.updatedAt);
|
||||
writer.writeInt(offsets[14], object.width);
|
||||
writer.writeBool(offsets[8], object.isTrashed);
|
||||
writer.writeString(offsets[9], object.livePhotoVideoId);
|
||||
writer.writeString(offsets[10], object.localId);
|
||||
writer.writeLong(offsets[11], object.ownerId);
|
||||
writer.writeString(offsets[12], object.remoteId);
|
||||
writer.writeByte(offsets[13], object.type.index);
|
||||
writer.writeDateTime(offsets[14], object.updatedAt);
|
||||
writer.writeInt(offsets[15], object.width);
|
||||
}
|
||||
|
||||
Asset _assetDeserialize(
|
||||
@@ -221,14 +227,15 @@ Asset _assetDeserialize(
|
||||
id: id,
|
||||
isArchived: reader.readBool(offsets[6]),
|
||||
isFavorite: reader.readBool(offsets[7]),
|
||||
livePhotoVideoId: reader.readStringOrNull(offsets[8]),
|
||||
localId: reader.readStringOrNull(offsets[9]),
|
||||
ownerId: reader.readLong(offsets[10]),
|
||||
remoteId: reader.readStringOrNull(offsets[11]),
|
||||
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[12])] ??
|
||||
isTrashed: reader.readBool(offsets[8]),
|
||||
livePhotoVideoId: reader.readStringOrNull(offsets[9]),
|
||||
localId: reader.readStringOrNull(offsets[10]),
|
||||
ownerId: reader.readLong(offsets[11]),
|
||||
remoteId: reader.readStringOrNull(offsets[12]),
|
||||
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[13])] ??
|
||||
AssetType.other,
|
||||
updatedAt: reader.readDateTime(offsets[13]),
|
||||
width: reader.readIntOrNull(offsets[14]),
|
||||
updatedAt: reader.readDateTime(offsets[14]),
|
||||
width: reader.readIntOrNull(offsets[15]),
|
||||
);
|
||||
return object;
|
||||
}
|
||||
@@ -257,19 +264,21 @@ P _assetDeserializeProp<P>(
|
||||
case 7:
|
||||
return (reader.readBool(offset)) as P;
|
||||
case 8:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
return (reader.readBool(offset)) as P;
|
||||
case 9:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 10:
|
||||
return (reader.readLong(offset)) as P;
|
||||
case 11:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 11:
|
||||
return (reader.readLong(offset)) as P;
|
||||
case 12:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 13:
|
||||
return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
|
||||
AssetType.other) as P;
|
||||
case 13:
|
||||
return (reader.readDateTime(offset)) as P;
|
||||
case 14:
|
||||
return (reader.readDateTime(offset)) as P;
|
||||
case 15:
|
||||
return (reader.readIntOrNull(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
@@ -1290,6 +1299,16 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> isTrashedEqualTo(
|
||||
bool value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'isTrashed',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> livePhotoVideoIdIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
@@ -2058,6 +2077,18 @@ extension AssetQuerySortBy on QueryBuilder<Asset, Asset, QSortBy> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsTrashed() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isTrashed', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsTrashedDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isTrashed', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByLivePhotoVideoId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'livePhotoVideoId', Sort.asc);
|
||||
@@ -2252,6 +2283,18 @@ extension AssetQuerySortThenBy on QueryBuilder<Asset, Asset, QSortThenBy> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsTrashed() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isTrashed', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsTrashedDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isTrashed', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByLivePhotoVideoId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'livePhotoVideoId', Sort.asc);
|
||||
@@ -2388,6 +2431,12 @@ extension AssetQueryWhereDistinct on QueryBuilder<Asset, Asset, QDistinct> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QDistinct> distinctByIsTrashed() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'isTrashed');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QDistinct> distinctByLivePhotoVideoId(
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
@@ -2490,6 +2539,12 @@ extension AssetQueryProperty on QueryBuilder<Asset, Asset, QQueryProperty> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, bool, QQueryOperations> isTrashedProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'isTrashed');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, String?, QQueryOperations> livePhotoVideoIdProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'livePhotoVideoId');
|
||||
|
||||
@@ -15,7 +15,6 @@ import 'package:immich_mobile/shared/services/user.service.dart';
|
||||
import 'package:immich_mobile/utils/db.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
class AssetNotifier extends StateNotifier<bool> {
|
||||
@@ -92,23 +91,45 @@ class AssetNotifier extends StateNotifier<bool> {
|
||||
await _syncService.syncNewAssetToDb(newAsset);
|
||||
}
|
||||
|
||||
Future<void> deleteAssets(Iterable<Asset> deleteAssets) async {
|
||||
Future<bool> deleteAssets(
|
||||
Iterable<Asset> deleteAssets, {
|
||||
bool? force = false,
|
||||
}) async {
|
||||
_deleteInProgress = true;
|
||||
state = true;
|
||||
try {
|
||||
final localDeleted = await _deleteLocalAssets(deleteAssets);
|
||||
final remoteDeleted = await _deleteRemoteAssets(deleteAssets);
|
||||
final remoteDeleted = await _deleteRemoteAssets(deleteAssets, force);
|
||||
if (localDeleted.isNotEmpty || remoteDeleted.isNotEmpty) {
|
||||
final dbIds = deleteAssets.map((e) => e.id).toList();
|
||||
List<Asset>? assetsToUpdate;
|
||||
// Local only assets are permanently deleted for now. So always remove them from db
|
||||
final dbIds = deleteAssets
|
||||
.where((a) => a.isLocal && !a.isRemote)
|
||||
.map((e) => e.id)
|
||||
.toList();
|
||||
if (force == null || !force) {
|
||||
assetsToUpdate = remoteDeleted.map((e) {
|
||||
e.isTrashed = true;
|
||||
return e;
|
||||
}).toList();
|
||||
} else {
|
||||
// Add all remote assets to be deleted from isar as since they are permanently deleted
|
||||
dbIds.addAll(remoteDeleted.map((e) => e.id));
|
||||
}
|
||||
await _db.writeTxn(() async {
|
||||
if (assetsToUpdate != null) {
|
||||
await _db.assets.putAll(assetsToUpdate);
|
||||
}
|
||||
await _db.exifInfos.deleteAll(dbIds);
|
||||
await _db.assets.deleteAll(dbIds);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
} finally {
|
||||
_deleteInProgress = false;
|
||||
state = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<List<String>> _deleteLocalAssets(
|
||||
@@ -127,15 +148,14 @@ class AssetNotifier extends StateNotifier<bool> {
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<Iterable<String>> _deleteRemoteAssets(
|
||||
Future<Iterable<Asset>> _deleteRemoteAssets(
|
||||
Iterable<Asset> assetsToDelete,
|
||||
bool? force,
|
||||
) async {
|
||||
final Iterable<Asset> remote = assetsToDelete.where((e) => e.isRemote);
|
||||
final List<DeleteAssetResponseDto> deleteAssetResult =
|
||||
await _assetService.deleteAssets(remote) ?? [];
|
||||
return deleteAssetResult
|
||||
.where((a) => a.status == DeleteAssetStatus.SUCCESS)
|
||||
.map((a) => a.id);
|
||||
|
||||
final isSuccess = await _assetService.deleteAssets(remote, force: force);
|
||||
return isSuccess ? remote : [];
|
||||
}
|
||||
|
||||
Future<void> toggleFavorite(List<Asset> assets, bool status) async {
|
||||
@@ -190,6 +210,7 @@ final assetsProvider =
|
||||
.ownerIdEqualToAnyChecksum(userId)
|
||||
.filter()
|
||||
.isArchivedEqualTo(false)
|
||||
.isTrashedEqualTo(false)
|
||||
.sortByFileCreatedAtDesc();
|
||||
final settings = ref.watch(appSettingsServiceProvider);
|
||||
final groupBy =
|
||||
@@ -210,6 +231,7 @@ final remoteAssetsProvider =
|
||||
.remoteIdIsNotNull()
|
||||
.filter()
|
||||
.ownerIdEqualTo(userId)
|
||||
.isTrashedEqualTo(false)
|
||||
.sortByFileCreatedAtDesc();
|
||||
final settings = ref.watch(appSettingsServiceProvider);
|
||||
final groupBy =
|
||||
|
||||
@@ -26,12 +26,14 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
|
||||
search: true,
|
||||
sidecar: true,
|
||||
tagImage: true,
|
||||
trash: true,
|
||||
reverseGeocoding: true,
|
||||
),
|
||||
serverConfig: ServerConfigDto(
|
||||
loginPageMessage: "",
|
||||
mapTileUrl: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
oauthButtonText: "",
|
||||
trashDays: 30,
|
||||
),
|
||||
isVersionMismatch: false,
|
||||
versionMismatchErrorMessage: "",
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:immich_mobile/modules/login/providers/authentication.provider.da
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:socket_io_client/socket_io_client.dart';
|
||||
@@ -92,6 +93,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
});
|
||||
|
||||
socket.on('on_upload_success', _handleOnUploadSuccess);
|
||||
socket.on('on_config_update', _handleOnConfigUpdate);
|
||||
} catch (e) {
|
||||
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
|
||||
}
|
||||
@@ -126,6 +128,11 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
||||
}
|
||||
}
|
||||
|
||||
_handleOnConfigUpdate(dynamic data) {
|
||||
ref.read(serverInfoProvider.notifier).getServerFeatures();
|
||||
ref.read(serverInfoProvider.notifier).getServerConfig();
|
||||
}
|
||||
}
|
||||
|
||||
final websocketProvider =
|
||||
|
||||
@@ -64,7 +64,9 @@ class AssetService {
|
||||
Future<List<Asset>?> _getRemoteAssets(User user) async {
|
||||
try {
|
||||
final List<AssetResponseDto>? assets =
|
||||
await _apiService.assetApi.getAllAssets(userId: user.id);
|
||||
await _apiService.assetApi.getAllAssets(
|
||||
userId: user.id,
|
||||
);
|
||||
if (assets == null) {
|
||||
return null;
|
||||
} else if (assets.isNotEmpty && assets.first.ownerId != user.id) {
|
||||
@@ -84,9 +86,10 @@ class AssetService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<DeleteAssetResponseDto>?> deleteAssets(
|
||||
Iterable<Asset> deleteAssets,
|
||||
) async {
|
||||
Future<bool> deleteAssets(
|
||||
Iterable<Asset> deleteAssets, {
|
||||
bool? force = false,
|
||||
}) async {
|
||||
try {
|
||||
final List<String> payload = [];
|
||||
|
||||
@@ -94,12 +97,17 @@ class AssetService {
|
||||
payload.add(asset.remoteId!);
|
||||
}
|
||||
|
||||
return await _apiService.assetApi
|
||||
.deleteAsset(DeleteAssetDto(ids: payload));
|
||||
await _apiService.assetApi.deleteAssets(
|
||||
AssetBulkDeleteDto(
|
||||
ids: payload,
|
||||
force: force,
|
||||
),
|
||||
);
|
||||
return true;
|
||||
} catch (error, stack) {
|
||||
log.severe("Error deleteAssets ${error.toString()}", error, stack);
|
||||
return null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Loads the exif information from the database. If there is none, loads
|
||||
|
||||
Reference in New Issue
Block a user