From 1679973caf8e61edcb788823bee7d275782be497 Mon Sep 17 00:00:00 2001 From: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Date: Tue, 17 Oct 2023 07:29:36 +0530 Subject: [PATCH] feat(mobile): delete assets from device only --- mobile/assets/i18n/en-US.json | 2 + .../home/ui/control_bottom_app_bar.dart | 125 +++++++++++------- mobile/lib/modules/home/ui/delete_dialog.dart | 4 +- mobile/lib/modules/home/views/home_page.dart | 30 ++++- .../lib/shared/providers/asset.provider.dart | 31 +++++ mobile/lib/shared/ui/drag_sheet.dart | 2 + 6 files changed, 140 insertions(+), 54 deletions(-) diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 7b1770ccf..bcc835a86 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -128,6 +128,7 @@ "control_bottom_app_bar_archive": "Archive", "control_bottom_app_bar_create_new_album": "Create new album", "control_bottom_app_bar_delete": "Delete", + "control_bottom_app_bar_delete_from_local": "Delete from device", "control_bottom_app_bar_favorite": "Favorite", "control_bottom_app_bar_share": "Share", "control_bottom_app_bar_unarchive": "Unarchive", @@ -142,6 +143,7 @@ "daily_title_text_date_year": "E, MMM dd, yyyy", "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_local": "These items will be permanently deleted from your device", "delete_dialog_cancel": "Cancel", "delete_dialog_ok": "Delete", "delete_dialog_title": "Delete Permanently", diff --git a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart b/mobile/lib/modules/home/ui/control_bottom_app_bar.dart index 6315cf1d4..440fb1523 100644 --- a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart +++ b/mobile/lib/modules/home/ui/control_bottom_app_bar.dart @@ -15,7 +15,7 @@ class ControlBottomAppBar extends ConsumerWidget { final void Function() onShare; final void Function() onFavorite; final void Function() onArchive; - final void Function() onDelete; + final void Function(bool localOnly) onDelete; final Function(Album album) onAddToAlbum; final void Function() onCreateNewAlbum; final void Function() onUpload; @@ -43,69 +43,90 @@ class ControlBottomAppBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { var isDarkMode = Theme.of(context).brightness == Brightness.dark; - var hasRemote = selectionAssetState == AssetState.remote; + var hasRemote = selectionAssetState == AssetState.remote || + selectionAssetState == AssetState.merged; + var hasMerged = selectionAssetState == AssetState.merged; final trashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); - Widget renderActionButtons() { - return Row( - children: [ + List renderActionButtons() { + return [ + ControlBoxButton( + iconData: Platform.isAndroid + ? Icons.share_rounded + : Icons.ios_share_rounded, + label: "control_bottom_app_bar_share".tr(), + onPressed: enabled ? onShare : null, + ), + if (hasRemote) ControlBoxButton( - iconData: Platform.isAndroid - ? Icons.share_rounded - : Icons.ios_share_rounded, - label: "control_bottom_app_bar_share".tr(), - onPressed: enabled ? onShare : null, + iconData: Icons.archive_outlined, + label: "control_bottom_app_bar_archive".tr(), + onPressed: enabled ? onArchive : null, ), - if (hasRemote) - ControlBoxButton( - iconData: Icons.archive, - label: "control_bottom_app_bar_archive".tr(), - onPressed: enabled ? onArchive : null, - ), - if (hasRemote) - ControlBoxButton( - iconData: Icons.favorite_border_rounded, - label: "control_bottom_app_bar_favorite".tr(), - onPressed: enabled ? onFavorite : null, - ), + if (hasRemote) ControlBoxButton( - iconData: Icons.delete_outline_rounded, - label: "control_bottom_app_bar_delete".tr(), - onPressed: enabled - ? () { - if (!trashEnabled) { + iconData: Icons.favorite_border_rounded, + label: "control_bottom_app_bar_favorite".tr(), + onPressed: enabled ? onFavorite : null, + ), + ControlBoxButton( + iconData: Icons.delete_outline_rounded, + label: "control_bottom_app_bar_delete".tr(), + onPressed: enabled + ? () { + if (!trashEnabled) { + showDialog( + context: context, + builder: (BuildContext context) { + return DeleteDialog( + onDelete: () => onDelete(false), + ); + }, + ); + } else { + onDelete(false); + } + } + : null, + ), + if (hasMerged) + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 100), + child: ControlBoxButton( + iconData: Icons.no_cell_rounded, + label: "control_bottom_app_bar_delete_from_local".tr(), + onPressed: enabled + ? () { showDialog( context: context, builder: (BuildContext context) { return DeleteDialog( - onDelete: onDelete, + content: 'delete_dialog_alert_local', + onDelete: () => onDelete(true), ); }, ); - } else { - onDelete(); } - } - : null, - ), - if (!hasRemote) - ControlBoxButton( - iconData: Icons.backup_outlined, - label: "Upload", - onPressed: enabled - ? () => showDialog( - context: context, - builder: (BuildContext context) { - return UploadDialog( - onUpload: onUpload, - ); - }, - ) : null, ), - ], - ); + ), + if (!hasRemote) + ControlBoxButton( + iconData: Icons.backup_outlined, + label: "Upload", + onPressed: enabled + ? () => showDialog( + context: context, + builder: (BuildContext context) { + return UploadDialog( + onUpload: onUpload, + ); + }, + ) + : null, + ), + ]; } return DraggableScrollableSheet( @@ -137,7 +158,13 @@ class ControlBottomAppBar extends ConsumerWidget { const SizedBox(height: 12), const CustomDraggingHandle(), const SizedBox(height: 12), - renderActionButtons(), + SizedBox( + height: hasMerged ? 90 : 75, + child: ListView( + scrollDirection: Axis.horizontal, + children: renderActionButtons(), + ), + ), if (hasRemote) const Divider( indent: 16, diff --git a/mobile/lib/modules/home/ui/delete_dialog.dart b/mobile/lib/modules/home/ui/delete_dialog.dart index 7d290cd1a..a7d95ad69 100644 --- a/mobile/lib/modules/home/ui/delete_dialog.dart +++ b/mobile/lib/modules/home/ui/delete_dialog.dart @@ -4,11 +4,11 @@ import 'package:immich_mobile/shared/ui/confirm_dialog.dart'; class DeleteDialog extends ConfirmDialog { final Function onDelete; - const DeleteDialog({Key? key, required this.onDelete}) + const DeleteDialog({Key? key, required this.onDelete, String? content}) : super( key: key, title: "delete_dialog_title", - content: "delete_dialog_alert", + content: content ?? "delete_dialog_alert", cancel: "delete_dialog_cancel", ok: "delete_dialog_ok", onOk: onDelete, diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index 5dfc8ed91..dfec89e47 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -84,7 +84,9 @@ class HomePage extends HookConsumerWidget { selectionEnabledHook.value = multiselect; selection.value = selectedAssets; selectionAssetState.value = selectedAssets.any((e) => e.isRemote) - ? AssetState.remote + ? selectedAssets.any((e) => e.isLocal) + ? AssetState.merged + : AssetState.remote : AssetState.local; } @@ -144,7 +146,6 @@ class HomePage extends HookConsumerWidget { await ref .read(assetProvider.notifier) .deleteAssets(selection.value, force: !trashEnabled); - final hasRemote = selection.value.any((a) => a.isRemote); final assetOrAssets = selection.value.length > 1 ? 'assets' : 'asset'; final trashOrRemoved = @@ -162,6 +163,28 @@ class HomePage extends HookConsumerWidget { } } + void onDeleteLocal() async { + processing.value = true; + try { + final isDeleted = await ref + .read(assetProvider.notifier) + .deleteLocalAssets(selection.value); + if (isDeleted) { + final assetOrAssets = + selection.value.length > 1 ? 'assets' : 'asset'; + ImmichToast.show( + context: context, + msg: + '${selection.value.length} $assetOrAssets deleted permanently from your device', + gravity: ToastGravity.BOTTOM, + ); + } + selectionEnabledHook.value = false; + } finally { + processing.value = false; + } + } + void onUpload() { processing.value = true; selectionEnabledHook.value = false; @@ -331,7 +354,8 @@ class HomePage extends HookConsumerWidget { onShare: onShareAssets, onFavorite: onFavoriteAssets, onArchive: onArchiveAsset, - onDelete: onDelete, + onDelete: (bool localOnly) => + localOnly ? onDeleteLocal() : onDelete(), onAddToAlbum: onAddToAlbum, albums: albums, sharedAlbums: sharedAlbums, diff --git a/mobile/lib/shared/providers/asset.provider.dart b/mobile/lib/shared/providers/asset.provider.dart index d17c95355..8838de5e0 100644 --- a/mobile/lib/shared/providers/asset.provider.dart +++ b/mobile/lib/shared/providers/asset.provider.dart @@ -91,6 +91,37 @@ class AssetNotifier extends StateNotifier { await _syncService.syncNewAssetToDb(newAsset); } + Future deleteLocalAssets(Iterable deleteAssets) async { + _deleteInProgress = true; + state = true; + try { + final localDeleted = await _deleteLocalAssets(deleteAssets); + if (localDeleted.isNotEmpty) { + final localOnlyIds = deleteAssets + .where((e) => e.storage == AssetState.local) + .map((e) => e.id) + .toList(); + 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 deleteAssets( Iterable deleteAssets, { bool? force = false, diff --git a/mobile/lib/shared/ui/drag_sheet.dart b/mobile/lib/shared/ui/drag_sheet.dart index 574962cc0..8b0840f23 100644 --- a/mobile/lib/shared/ui/drag_sheet.dart +++ b/mobile/lib/shared/ui/drag_sheet.dart @@ -43,6 +43,8 @@ class ControlBoxButton extends StatelessWidget { Text( label, style: const TextStyle(fontSize: 12.0), + maxLines: 2, + textAlign: TextAlign.center, ), ], ),