mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
feat(mobile): separate delete buttons (#4505)
* feat(mobile): delete assets from device only * mobile: add backed up only toggle for delete device only * remove toggle inside alert and show different content * mobile: change content color for local only * mobile: delete local only button to dialog * style: display bottom action in two lines * feat: separate delete buttons * fix: incorrect error message for ownedRemoteSelection * fix: handle remoteOnly from delete everywhere * request confirmation for local only only when non-backed assets are in selection --------- Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
78de4f1312
commit
4c2befc68c
@ -141,7 +141,9 @@
|
|||||||
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
|
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
|
||||||
"control_bottom_app_bar_archive": "Archive",
|
"control_bottom_app_bar_archive": "Archive",
|
||||||
"control_bottom_app_bar_create_new_album": "Create new album",
|
"control_bottom_app_bar_create_new_album": "Create new album",
|
||||||
"control_bottom_app_bar_delete": "Delete",
|
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
|
||||||
|
"control_bottom_app_bar_delete_from_local": "Delete from device",
|
||||||
|
"control_bottom_app_bar_delete": "Delete Everywhere",
|
||||||
"control_bottom_app_bar_edit_location": "Edit Location",
|
"control_bottom_app_bar_edit_location": "Edit Location",
|
||||||
"control_bottom_app_bar_edit_time": "Edit Date & Time",
|
"control_bottom_app_bar_edit_time": "Edit Date & Time",
|
||||||
"control_bottom_app_bar_favorite": "Favorite",
|
"control_bottom_app_bar_favorite": "Favorite",
|
||||||
@ -162,8 +164,14 @@
|
|||||||
"daily_title_text_date_year": "E, MMM dd, yyyy",
|
"daily_title_text_date_year": "E, MMM dd, yyyy",
|
||||||
"date_format": "E, LLL d, y • h:mm a",
|
"date_format": "E, LLL d, y • h:mm a",
|
||||||
"delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
|
"delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
|
||||||
|
"delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
|
||||||
|
"delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server",
|
||||||
|
"delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
|
||||||
"delete_dialog_cancel": "Cancel",
|
"delete_dialog_cancel": "Cancel",
|
||||||
"delete_dialog_ok": "Delete",
|
"delete_dialog_ok": "Delete",
|
||||||
|
"delete_dialog_ok_force": "Delete Anyway",
|
||||||
|
"delete_local_dialog_ok_force": "Delete Anyway",
|
||||||
|
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
|
||||||
"delete_dialog_title": "Delete Permanently",
|
"delete_dialog_title": "Delete Permanently",
|
||||||
"delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?",
|
"delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?",
|
||||||
"delete_shared_link_dialog_title": "Delete Shared Link",
|
"delete_shared_link_dialog_title": "Delete Shared Link",
|
||||||
@ -190,6 +198,7 @@
|
|||||||
"home_page_archive_err_partner": "Can not archive partner assets, skipping",
|
"home_page_archive_err_partner": "Can not archive partner assets, skipping",
|
||||||
"home_page_building_timeline": "Building the timeline",
|
"home_page_building_timeline": "Building the timeline",
|
||||||
"home_page_delete_err_partner": "Can not delete partner assets, skipping",
|
"home_page_delete_err_partner": "Can not delete partner assets, skipping",
|
||||||
|
"home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
|
||||||
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
|
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
|
||||||
"home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping",
|
"home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping",
|
||||||
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
|
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
|
||||||
|
@ -16,7 +16,9 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||||||
final void Function(bool shareLocal) onShare;
|
final void Function(bool shareLocal) onShare;
|
||||||
final void Function()? onFavorite;
|
final void Function()? onFavorite;
|
||||||
final void Function()? onArchive;
|
final void Function()? onArchive;
|
||||||
final void Function()? onDelete;
|
final void Function([bool force])? onDelete;
|
||||||
|
final void Function([bool force])? onDeleteServer;
|
||||||
|
final void Function(bool onlyBackedUp)? onDeleteLocal;
|
||||||
final Function(Album album) onAddToAlbum;
|
final Function(Album album) onAddToAlbum;
|
||||||
final void Function() onCreateNewAlbum;
|
final void Function() onCreateNewAlbum;
|
||||||
final void Function() onUpload;
|
final void Function() onUpload;
|
||||||
@ -36,6 +38,8 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||||||
this.onFavorite,
|
this.onFavorite,
|
||||||
this.onArchive,
|
this.onArchive,
|
||||||
this.onDelete,
|
this.onDelete,
|
||||||
|
this.onDeleteServer,
|
||||||
|
this.onDeleteLocal,
|
||||||
required this.onAddToAlbum,
|
required this.onAddToAlbum,
|
||||||
required this.onCreateNewAlbum,
|
required this.onCreateNewAlbum,
|
||||||
required this.onUpload,
|
required this.onUpload,
|
||||||
@ -51,14 +55,35 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var hasRemote =
|
final hasRemote =
|
||||||
selectionAssetState.hasRemote || selectionAssetState.hasMerged;
|
selectionAssetState.hasRemote || selectionAssetState.hasMerged;
|
||||||
var hasLocal = selectionAssetState.hasLocal;
|
final hasLocal =
|
||||||
|
selectionAssetState.hasLocal || selectionAssetState.hasMerged;
|
||||||
final trashEnabled =
|
final trashEnabled =
|
||||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
||||||
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
||||||
final sharedAlbums = ref.watch(sharedAlbumProvider);
|
final sharedAlbums = ref.watch(sharedAlbumProvider);
|
||||||
|
|
||||||
|
void handleRemoteDelete(
|
||||||
|
bool force,
|
||||||
|
Function(bool) deleteCb, {
|
||||||
|
String? alertMsg,
|
||||||
|
}) {
|
||||||
|
if (!force) {
|
||||||
|
deleteCb(force);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return DeleteDialog(
|
||||||
|
alert: alertMsg,
|
||||||
|
onDelete: () => deleteCb(force),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
List<Widget> renderActionButtons() {
|
List<Widget> renderActionButtons() {
|
||||||
return [
|
return [
|
||||||
if (hasRemote)
|
if (hasRemote)
|
||||||
@ -92,26 +117,55 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||||||
.tr(),
|
.tr(),
|
||||||
onPressed: enabled ? onFavorite : null,
|
onPressed: enabled ? onFavorite : null,
|
||||||
),
|
),
|
||||||
if (onDelete != null)
|
if (hasRemote && onDeleteServer != null)
|
||||||
ControlBoxButton(
|
ConstrainedBox(
|
||||||
iconData: Icons.delete_outline_rounded,
|
constraints: const BoxConstraints(maxWidth: 85),
|
||||||
label: "control_bottom_app_bar_delete".tr(),
|
child: ControlBoxButton(
|
||||||
onPressed: enabled
|
iconData: Icons.cloud_off_outlined,
|
||||||
? () {
|
label: "control_bottom_app_bar_delete_from_immich".tr(),
|
||||||
if (!trashEnabled) {
|
onPressed: enabled
|
||||||
|
? () => handleRemoteDelete(
|
||||||
|
!trashEnabled,
|
||||||
|
onDeleteServer!,
|
||||||
|
alertMsg: "delete_dialog_alert_remote",
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (hasLocal && onDeleteLocal != null)
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 85),
|
||||||
|
child: ControlBoxButton(
|
||||||
|
iconData: Icons.no_cell_rounded,
|
||||||
|
label: "control_bottom_app_bar_delete_from_local".tr(),
|
||||||
|
onPressed: enabled
|
||||||
|
? () {
|
||||||
|
if (!selectionAssetState.hasLocal) {
|
||||||
|
return onDeleteLocal?.call(true);
|
||||||
|
}
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return DeleteDialog(
|
return DeleteLocalOnlyDialog(
|
||||||
onDelete: onDelete!,
|
onDeleteLocal: onDeleteLocal!,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
onDelete!();
|
|
||||||
}
|
}
|
||||||
}
|
: null,
|
||||||
: null,
|
),
|
||||||
|
),
|
||||||
|
if (hasLocal && hasRemote && onDelete != null)
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 90),
|
||||||
|
child: ControlBoxButton(
|
||||||
|
iconData: Icons.delete_sweep_outlined,
|
||||||
|
label: "control_bottom_app_bar_delete".tr(),
|
||||||
|
onPressed: enabled
|
||||||
|
? () => handleRemoteDelete(!trashEnabled, onDelete!)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (hasRemote && onEditTime != null)
|
if (hasRemote && onEditTime != null)
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
@ -185,9 +239,9 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const CustomDraggingHandle(),
|
const CustomDraggingHandle(),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 24),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 70,
|
height: 90,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
@ -201,7 +255,7 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||||||
thickness: 1,
|
thickness: 1,
|
||||||
),
|
),
|
||||||
if (hasRemote)
|
if (hasRemote)
|
||||||
AddToAlbumTitleRow(
|
_AddToAlbumTitleRow(
|
||||||
onCreateNewAlbum: enabled ? onCreateNewAlbum : null,
|
onCreateNewAlbum: enabled ? onCreateNewAlbum : null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -225,9 +279,8 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AddToAlbumTitleRow extends StatelessWidget {
|
class _AddToAlbumTitleRow extends StatelessWidget {
|
||||||
const AddToAlbumTitleRow({
|
const _AddToAlbumTitleRow({
|
||||||
super.key,
|
|
||||||
required this.onCreateNewAlbum,
|
required this.onCreateNewAlbum,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,16 +1,80 @@
|
|||||||
|
// ignore_for_file: prefer-single-widget-per-file
|
||||||
|
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
|
import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
|
||||||
|
|
||||||
class DeleteDialog extends ConfirmDialog {
|
class DeleteDialog extends ConfirmDialog {
|
||||||
final Function onDelete;
|
const DeleteDialog({Key? key, String? alert, required Function onDelete})
|
||||||
|
|
||||||
const DeleteDialog({Key? key, required this.onDelete})
|
|
||||||
: super(
|
: super(
|
||||||
key: key,
|
key: key,
|
||||||
title: "delete_dialog_title",
|
title: "delete_dialog_title",
|
||||||
content: "delete_dialog_alert",
|
content: alert ?? "delete_dialog_alert",
|
||||||
cancel: "delete_dialog_cancel",
|
cancel: "delete_dialog_cancel",
|
||||||
ok: "delete_dialog_ok",
|
ok: "delete_dialog_ok",
|
||||||
onOk: onDelete,
|
onOk: onDelete,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DeleteLocalOnlyDialog extends StatelessWidget {
|
||||||
|
final void Function(bool onlyMerged) onDeleteLocal;
|
||||||
|
|
||||||
|
const DeleteLocalOnlyDialog({
|
||||||
|
super.key,
|
||||||
|
required this.onDeleteLocal,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
void onDeleteBackedUpOnly() {
|
||||||
|
context.pop();
|
||||||
|
onDeleteLocal(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onForceDelete() {
|
||||||
|
context.pop();
|
||||||
|
onDeleteLocal(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||||
|
),
|
||||||
|
title: const Text("delete_dialog_title").tr(),
|
||||||
|
content: const Text("delete_dialog_alert_local_non_backed_up").tr(),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => context.pop(),
|
||||||
|
child: Text(
|
||||||
|
"delete_dialog_cancel",
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.primaryColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: onDeleteBackedUpOnly,
|
||||||
|
child: Text(
|
||||||
|
"delete_local_dialog_ok_backed_up_only",
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.colorScheme.tertiary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: onForceDelete,
|
||||||
|
child: Text(
|
||||||
|
"delete_local_dialog_ok_force",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red[400],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -91,6 +91,90 @@ class AssetNotifier extends StateNotifier<bool> {
|
|||||||
await _syncService.syncNewAssetToDb(newAsset);
|
await _syncService.syncNewAssetToDb(newAsset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> deleteLocalOnlyAssets(
|
||||||
|
Iterable<Asset> deleteAssets, {
|
||||||
|
bool onlyBackedUp = false,
|
||||||
|
}) async {
|
||||||
|
_deleteInProgress = true;
|
||||||
|
state = true;
|
||||||
|
try {
|
||||||
|
final assets = onlyBackedUp
|
||||||
|
? deleteAssets.where((e) => e.storage == AssetState.merged)
|
||||||
|
: deleteAssets;
|
||||||
|
final localDeleted = await _deleteLocalAssets(assets);
|
||||||
|
if (localDeleted.isNotEmpty) {
|
||||||
|
final localOnlyIds = deleteAssets
|
||||||
|
.where((e) => e.storage == AssetState.local)
|
||||||
|
.map((e) => e.id)
|
||||||
|
.toList();
|
||||||
|
// Update merged assets to remote only
|
||||||
|
final mergedAssets =
|
||||||
|
deleteAssets.where((e) => e.storage == AssetState.merged).map((e) {
|
||||||
|
e.localId = null;
|
||||||
|
return e;
|
||||||
|
}).toList();
|
||||||
|
await _db.writeTxn(() async {
|
||||||
|
if (mergedAssets.isNotEmpty) {
|
||||||
|
await _db.assets.putAll(mergedAssets);
|
||||||
|
}
|
||||||
|
await _db.exifInfos.deleteAll(localOnlyIds);
|
||||||
|
await _db.assets.deleteAll(localOnlyIds);
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_deleteInProgress = false;
|
||||||
|
state = false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> deleteRemoteOnlyAssets(
|
||||||
|
Iterable<Asset> deleteAssets, {
|
||||||
|
bool force = 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;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_deleteInProgress = false;
|
||||||
|
state = false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> deleteAssets(
|
Future<bool> deleteAssets(
|
||||||
Iterable<Asset> deleteAssets, {
|
Iterable<Asset> deleteAssets, {
|
||||||
bool force = false,
|
bool force = false,
|
||||||
@ -98,8 +182,11 @@ class AssetNotifier extends StateNotifier<bool> {
|
|||||||
_deleteInProgress = true;
|
_deleteInProgress = true;
|
||||||
state = true;
|
state = true;
|
||||||
try {
|
try {
|
||||||
|
final hasLocal = deleteAssets.any((a) => a.storage != AssetState.remote);
|
||||||
final localDeleted = await _deleteLocalAssets(deleteAssets);
|
final localDeleted = await _deleteLocalAssets(deleteAssets);
|
||||||
final remoteDeleted = await _deleteRemoteAssets(deleteAssets, force);
|
final remoteDeleted = (hasLocal && localDeleted.isNotEmpty) || !hasLocal
|
||||||
|
? await _deleteRemoteAssets(deleteAssets, force)
|
||||||
|
: [];
|
||||||
if (localDeleted.isNotEmpty || remoteDeleted.isNotEmpty) {
|
if (localDeleted.isNotEmpty || remoteDeleted.isNotEmpty) {
|
||||||
final dbIds = <int>[];
|
final dbIds = <int>[];
|
||||||
final dbUpdates = <Asset>[];
|
final dbUpdates = <Asset>[];
|
||||||
@ -192,14 +279,14 @@ class AssetNotifier extends StateNotifier<bool> {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Iterable<Asset>> _deleteRemoteAssets(
|
Future<List<Asset>> _deleteRemoteAssets(
|
||||||
Iterable<Asset> assetsToDelete,
|
Iterable<Asset> assetsToDelete,
|
||||||
bool? force,
|
bool? force,
|
||||||
) async {
|
) async {
|
||||||
final Iterable<Asset> remote = assetsToDelete.where((e) => e.isRemote);
|
final Iterable<Asset> remote = assetsToDelete.where((e) => e.isRemote);
|
||||||
|
|
||||||
final isSuccess = await _assetService.deleteAssets(remote, force: force);
|
final isSuccess = await _assetService.deleteAssets(remote, force: force);
|
||||||
return isSuccess ? remote : [];
|
return isSuccess ? remote.toList() : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleFavorite(List<Asset> assets, [bool? status]) async {
|
Future<void> toggleFavorite(List<Asset> assets, [bool? status]) async {
|
||||||
|
@ -22,7 +22,6 @@ import 'package:immich_mobile/routing/router.dart';
|
|||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
|
||||||
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
@ -115,10 +114,10 @@ class MultiselectGrid extends HookConsumerWidget {
|
|||||||
}) {
|
}) {
|
||||||
final assets = selection.value;
|
final assets = selection.value;
|
||||||
return assets
|
return assets
|
||||||
.remoteOnly(errorCallback: errorBuilder(ownerErrorMessage))
|
.remoteOnly(errorCallback: errorBuilder(localErrorMessage))
|
||||||
.ownedOnly(
|
.ownedOnly(
|
||||||
currentUser,
|
currentUser,
|
||||||
errorCallback: errorBuilder(localErrorMessage),
|
errorCallback: errorBuilder(ownerErrorMessage),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,11 +175,9 @@ class MultiselectGrid extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDelete() async {
|
void onDelete([bool force = false]) async {
|
||||||
processing.value = true;
|
processing.value = true;
|
||||||
try {
|
try {
|
||||||
final trashEnabled =
|
|
||||||
ref.read(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
|
||||||
final toDelete = selection.value
|
final toDelete = selection.value
|
||||||
.ownedOnly(
|
.ownedOnly(
|
||||||
currentUser,
|
currentUser,
|
||||||
@ -192,23 +189,77 @@ class MultiselectGrid extends HookConsumerWidget {
|
|||||||
errorBuilder('asset_action_delete_err_read_only'.tr()),
|
errorBuilder('asset_action_delete_err_read_only'.tr()),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
await ref
|
final isDeleted = await ref
|
||||||
.read(assetProvider.notifier)
|
.read(assetProvider.notifier)
|
||||||
.deleteAssets(toDelete, force: !trashEnabled);
|
.deleteAssets(toDelete, force: force);
|
||||||
|
|
||||||
final hasRemote = toDelete.any((a) => a.isRemote);
|
if (isDeleted) {
|
||||||
final assetOrAssets = toDelete.length > 1 ? 'assets' : 'asset';
|
final assetOrAssets = toDelete.length > 1 ? 'assets' : 'asset';
|
||||||
final trashOrRemoved =
|
final trashOrRemoved = force ? 'deleted permanently' : 'trashed';
|
||||||
!trashEnabled ? 'deleted permanently' : 'trashed';
|
|
||||||
if (hasRemote) {
|
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: '${selection.value.length} $assetOrAssets $trashOrRemoved',
|
msg: '${selection.value.length} $assetOrAssets $trashOrRemoved',
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
selectionEnabledHook.value = false;
|
|
||||||
} finally {
|
} finally {
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
processing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDeleteLocal(bool onlyBackedUp) async {
|
||||||
|
processing.value = true;
|
||||||
|
try {
|
||||||
|
final localIds = selection.value.where((a) => a.isLocal).toList();
|
||||||
|
|
||||||
|
final isDeleted = await ref
|
||||||
|
.read(assetProvider.notifier)
|
||||||
|
.deleteLocalOnlyAssets(localIds, onlyBackedUp: onlyBackedUp);
|
||||||
|
if (isDeleted) {
|
||||||
|
final assetOrAssets = localIds.length > 1 ? 'assets' : 'asset';
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg:
|
||||||
|
'${localIds.length} $assetOrAssets removed permanently from your device',
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
processing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDeleteRemote([bool force = false]) async {
|
||||||
|
processing.value = true;
|
||||||
|
try {
|
||||||
|
final toDelete = ownedRemoteSelection(
|
||||||
|
localErrorMessage: 'home_page_delete_remote_err_local'.tr(),
|
||||||
|
ownerErrorMessage: 'home_page_delete_err_partner'.tr(),
|
||||||
|
)
|
||||||
|
// Cannot delete readOnly / external assets. They are handled through library offline jobs
|
||||||
|
.writableOnly(
|
||||||
|
errorCallback:
|
||||||
|
errorBuilder('asset_action_delete_err_read_only'.tr()),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final isDeleted = await ref
|
||||||
|
.read(assetProvider.notifier)
|
||||||
|
.deleteRemoteOnlyAssets(toDelete, force: force);
|
||||||
|
if (isDeleted) {
|
||||||
|
final assetOrAssets = toDelete.length > 1 ? 'assets' : 'asset';
|
||||||
|
final trashOrRemoved = force ? 'deleted permanently' : 'trashed';
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg:
|
||||||
|
'${toDelete.length} $assetOrAssets $trashOrRemoved from the Immich server',
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
processing.value = false;
|
processing.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -401,6 +452,11 @@ class MultiselectGrid extends HookConsumerWidget {
|
|||||||
onFavorite: favoriteEnabled ? onFavoriteAssets : null,
|
onFavorite: favoriteEnabled ? onFavoriteAssets : null,
|
||||||
onArchive: archiveEnabled ? onArchiveAsset : null,
|
onArchive: archiveEnabled ? onArchiveAsset : null,
|
||||||
onDelete: deleteEnabled ? onDelete : null,
|
onDelete: deleteEnabled ? onDelete : null,
|
||||||
|
onDeleteServer: deleteEnabled ? onDeleteRemote : null,
|
||||||
|
|
||||||
|
/// local file deletion is allowed irrespective of [deleteEnabled] since it has
|
||||||
|
/// nothing to do with the state of the asset in the Immich server
|
||||||
|
onDeleteLocal: onDeleteLocal,
|
||||||
onAddToAlbum: onAddToAlbum,
|
onAddToAlbum: onAddToAlbum,
|
||||||
onCreateNewAlbum: onCreateNewAlbum,
|
onCreateNewAlbum: onCreateNewAlbum,
|
||||||
onUpload: onUpload,
|
onUpload: onUpload,
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
|
||||||
class ConfirmDialog extends ConsumerWidget {
|
class ConfirmDialog extends StatelessWidget {
|
||||||
final Function onOk;
|
final Function onOk;
|
||||||
final String title;
|
final String title;
|
||||||
final String content;
|
final String content;
|
||||||
@ -20,9 +19,16 @@ class ConfirmDialog extends ConsumerWidget {
|
|||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context) {
|
||||||
|
void onOkPressed() {
|
||||||
|
onOk();
|
||||||
|
context.pop(true);
|
||||||
|
}
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||||
|
),
|
||||||
title: Text(title).tr(),
|
title: Text(title).tr(),
|
||||||
content: Text(content).tr(),
|
content: Text(content).tr(),
|
||||||
actions: [
|
actions: [
|
||||||
@ -37,10 +43,7 @@ class ConfirmDialog extends ConsumerWidget {
|
|||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: onOkPressed,
|
||||||
onOk();
|
|
||||||
context.pop(true);
|
|
||||||
},
|
|
||||||
child: Text(
|
child: Text(
|
||||||
ok,
|
ok,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
@ -37,14 +37,16 @@ class ControlBoxButton extends StatelessWidget {
|
|||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
minWidth: 75.0,
|
minWidth: 75.0,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(iconData, size: 24),
|
Icon(iconData, size: 24),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: const TextStyle(fontSize: 12.0),
|
style: const TextStyle(fontSize: 12.0),
|
||||||
|
maxLines: 2,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Loading…
Reference in New Issue
Block a user