feat(mobile): delete assets from device only

This commit is contained in:
shalong-tanwen 2023-10-17 07:29:36 +05:30
parent 219f99e516
commit 1679973caf
6 changed files with 140 additions and 54 deletions

View file

@ -128,6 +128,7 @@
"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": "Delete",
"control_bottom_app_bar_delete_from_local": "Delete from device",
"control_bottom_app_bar_favorite": "Favorite", "control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "Share", "control_bottom_app_bar_share": "Share",
"control_bottom_app_bar_unarchive": "Unarchive", "control_bottom_app_bar_unarchive": "Unarchive",
@ -142,6 +143,7 @@
"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_local": "These items will be permanently deleted from your device",
"delete_dialog_cancel": "Cancel", "delete_dialog_cancel": "Cancel",
"delete_dialog_ok": "Delete", "delete_dialog_ok": "Delete",
"delete_dialog_title": "Delete Permanently", "delete_dialog_title": "Delete Permanently",

View file

@ -15,7 +15,7 @@ class ControlBottomAppBar extends ConsumerWidget {
final void Function() onShare; final void Function() 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 localOnly) onDelete;
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;
@ -43,69 +43,90 @@ class ControlBottomAppBar extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
var isDarkMode = Theme.of(context).brightness == Brightness.dark; 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 = final trashEnabled =
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
Widget renderActionButtons() { List<Widget> renderActionButtons() {
return Row( return [
children: [ 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( ControlBoxButton(
iconData: Platform.isAndroid iconData: Icons.archive_outlined,
? Icons.share_rounded label: "control_bottom_app_bar_archive".tr(),
: Icons.ios_share_rounded, onPressed: enabled ? onArchive : null,
label: "control_bottom_app_bar_share".tr(),
onPressed: enabled ? onShare : null,
), ),
if (hasRemote) 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,
),
ControlBoxButton( ControlBoxButton(
iconData: Icons.delete_outline_rounded, iconData: Icons.favorite_border_rounded,
label: "control_bottom_app_bar_delete".tr(), label: "control_bottom_app_bar_favorite".tr(),
onPressed: enabled onPressed: enabled ? onFavorite : null,
? () { ),
if (!trashEnabled) { 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( showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return DeleteDialog( 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, : 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( return DraggableScrollableSheet(
@ -137,7 +158,13 @@ class ControlBottomAppBar extends ConsumerWidget {
const SizedBox(height: 12), const SizedBox(height: 12),
const CustomDraggingHandle(), const CustomDraggingHandle(),
const SizedBox(height: 12), const SizedBox(height: 12),
renderActionButtons(), SizedBox(
height: hasMerged ? 90 : 75,
child: ListView(
scrollDirection: Axis.horizontal,
children: renderActionButtons(),
),
),
if (hasRemote) if (hasRemote)
const Divider( const Divider(
indent: 16, indent: 16,

View file

@ -4,11 +4,11 @@ import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
class DeleteDialog extends ConfirmDialog { class DeleteDialog extends ConfirmDialog {
final Function onDelete; final Function onDelete;
const DeleteDialog({Key? key, required this.onDelete}) const DeleteDialog({Key? key, required this.onDelete, String? content})
: super( : super(
key: key, key: key,
title: "delete_dialog_title", title: "delete_dialog_title",
content: "delete_dialog_alert", content: content ?? "delete_dialog_alert",
cancel: "delete_dialog_cancel", cancel: "delete_dialog_cancel",
ok: "delete_dialog_ok", ok: "delete_dialog_ok",
onOk: onDelete, onOk: onDelete,

View file

@ -84,7 +84,9 @@ class HomePage extends HookConsumerWidget {
selectionEnabledHook.value = multiselect; selectionEnabledHook.value = multiselect;
selection.value = selectedAssets; selection.value = selectedAssets;
selectionAssetState.value = selectedAssets.any((e) => e.isRemote) selectionAssetState.value = selectedAssets.any((e) => e.isRemote)
? AssetState.remote ? selectedAssets.any((e) => e.isLocal)
? AssetState.merged
: AssetState.remote
: AssetState.local; : AssetState.local;
} }
@ -144,7 +146,6 @@ class HomePage extends HookConsumerWidget {
await ref await ref
.read(assetProvider.notifier) .read(assetProvider.notifier)
.deleteAssets(selection.value, force: !trashEnabled); .deleteAssets(selection.value, force: !trashEnabled);
final hasRemote = selection.value.any((a) => a.isRemote); final hasRemote = selection.value.any((a) => a.isRemote);
final assetOrAssets = selection.value.length > 1 ? 'assets' : 'asset'; final assetOrAssets = selection.value.length > 1 ? 'assets' : 'asset';
final trashOrRemoved = 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() { void onUpload() {
processing.value = true; processing.value = true;
selectionEnabledHook.value = false; selectionEnabledHook.value = false;
@ -331,7 +354,8 @@ class HomePage extends HookConsumerWidget {
onShare: onShareAssets, onShare: onShareAssets,
onFavorite: onFavoriteAssets, onFavorite: onFavoriteAssets,
onArchive: onArchiveAsset, onArchive: onArchiveAsset,
onDelete: onDelete, onDelete: (bool localOnly) =>
localOnly ? onDeleteLocal() : onDelete(),
onAddToAlbum: onAddToAlbum, onAddToAlbum: onAddToAlbum,
albums: albums, albums: albums,
sharedAlbums: sharedAlbums, sharedAlbums: sharedAlbums,

View file

@ -91,6 +91,37 @@ class AssetNotifier extends StateNotifier<bool> {
await _syncService.syncNewAssetToDb(newAsset); await _syncService.syncNewAssetToDb(newAsset);
} }
Future<bool> deleteLocalAssets(Iterable<Asset> 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<bool> deleteAssets( Future<bool> deleteAssets(
Iterable<Asset> deleteAssets, { Iterable<Asset> deleteAssets, {
bool? force = false, bool? force = false,

View file

@ -43,6 +43,8 @@ class ControlBoxButton extends StatelessWidget {
Text( Text(
label, label,
style: const TextStyle(fontSize: 12.0), style: const TextStyle(fontSize: 12.0),
maxLines: 2,
textAlign: TextAlign.center,
), ),
], ],
), ),