feat(mobile): delete assets from device only
This commit is contained in:
parent
219f99e516
commit
1679973caf
6 changed files with 140 additions and 54 deletions
|
@ -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",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in a new issue