mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:39:37 -05: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,27 +117,56 @@ 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(
 | 
				
			||||||
 | 
					              iconData: Icons.cloud_off_outlined,
 | 
				
			||||||
 | 
					              label: "control_bottom_app_bar_delete_from_immich".tr(),
 | 
				
			||||||
 | 
					              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
 | 
					              onPressed: enabled
 | 
				
			||||||
                  ? () {
 | 
					                  ? () {
 | 
				
			||||||
                    if (!trashEnabled) {
 | 
					                      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(
 | 
				
			||||||
            iconData: Icons.edit_calendar_outlined,
 | 
					            iconData: Icons.edit_calendar_outlined,
 | 
				
			||||||
@ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user