diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index 6d19d17931c9e..eeb8dd00f5bc5 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -164,6 +164,14 @@ class RemoteAssetRepository extends DriftDatabaseRepository { }); } + Future emptyTrash() async { + await _db.remoteAssetEntity.deleteWhere((t) => t.deletedAt.isNotNull()); + } + + Future restoreAllTrash() async { + await _db.remoteAssetEntity.update().write(const RemoteAssetEntityCompanion(deletedAt: Value(null))); + } + Future delete(List ids) { return _db.batch((batch) { for (final id in ids) { diff --git a/mobile/lib/presentation/pages/drift_trash.page.dart b/mobile/lib/presentation/pages/drift_trash.page.dart index a85f69a75e343..bc612af5c52e4 100644 --- a/mobile/lib/presentation/pages/drift_trash.page.dart +++ b/mobile/lib/presentation/pages/drift_trash.page.dart @@ -1,13 +1,18 @@ import 'package:auto_route/auto_route.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/translate_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; @RoutePage() class DriftTrashPage extends StatelessWidget { @@ -36,6 +41,7 @@ class DriftTrashPage extends StatelessWidget { pinned: true, centerTitle: true, elevation: 0, + actions: [const _TrashKebabMenu()], ), topSliverWidgetHeight: 24, topSliverWidget: Consumer( @@ -53,3 +59,83 @@ class DriftTrashPage extends StatelessWidget { ); } } + +class _TrashKebabMenu extends ConsumerWidget { + const _TrashKebabMenu(); + + Future _onEmptyTrash(BuildContext context, WidgetRef ref) async { + final confirmed = await showDialog( + context: context, + builder: (context) => + ConfirmDialog(title: context.t.empty_trash, content: context.t.empty_trash_confirmation, onOk: () {}), + ); + if (confirmed == true && context.mounted) { + final result = await ref.read(actionProvider.notifier).emptyTrash(); + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success + ? context.t.assets_permanently_deleted_count(count: result.count) + : context.t.scaffold_body_error_occurred, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } + } + + Future _onRestoreAll(BuildContext context, WidgetRef ref) async { + final confirmed = await showDialog( + context: context, + builder: (context) => + ConfirmDialog(title: context.t.restore_all, content: context.t.assets_restore_confirmation, onOk: () {}), + ); + if (confirmed == true && context.mounted) { + final result = await ref.read(actionProvider.notifier).restoreAllTrash(); + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success + ? context.t.assets_restored_count(count: result.count) + : context.t.scaffold_body_error_occurred, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return MenuAnchor( + consumeOutsideTap: true, + style: MenuStyle( + backgroundColor: WidgetStatePropertyAll(context.themeData.scaffoldBackgroundColor), + surfaceTintColor: const WidgetStatePropertyAll(Colors.grey), + elevation: const WidgetStatePropertyAll(4), + shape: const WidgetStatePropertyAll( + RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))), + ), + padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)), + ), + menuChildren: [ + BaseActionButton( + label: context.t.empty_trash, + iconData: Icons.delete_forever_outlined, + onPressed: () => _onEmptyTrash(context, ref), + menuItem: true, + ), + BaseActionButton( + label: context.t.restore_all, + iconData: Icons.restore_outlined, + onPressed: () => _onRestoreAll(context, ref), + menuItem: true, + ), + ], + builder: (context, controller, child) { + return IconButton( + icon: const Icon(Icons.more_vert_rounded), + onPressed: () => controller.isOpen ? controller.close() : controller.open(), + ); + }, + ); + } +} diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index d0d1d5d42480e..49a53f32aea47 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -239,6 +239,26 @@ class ActionNotifier extends Notifier { } } + Future emptyTrash() async { + try { + final count = await _service.emptyTrash(); + return ActionResult(count: count, success: true); + } catch (error, stack) { + _logger.severe('Failed to empty trash', error, stack); + return ActionResult(count: 0, success: false, error: error.toString()); + } + } + + Future restoreAllTrash() async { + try { + final count = await _service.restoreAllTrash(); + return ActionResult(count: count, success: true); + } catch (error, stack) { + _logger.severe('Failed to restore all trash assets', error, stack); + return ActionResult(count: 0, success: false, error: error.toString()); + } + } + Future trashRemoteAndDeleteLocal(ActionSource source) async { final ids = _getOwnedRemoteIdsForSource(source); final localIds = _getLocalIdsForSource(source); diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart index 2943177d60376..fdb4e3323bc64 100644 --- a/mobile/lib/repositories/asset_api.repository.dart +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -31,6 +31,16 @@ class AssetApiRepository extends ApiRepository { await _trashApi.restoreAssets(BulkIdsDto(ids: ids)); } + Future emptyTrash() async { + final response = await _trashApi.emptyTrash(); + return response?.count ?? 0; + } + + Future restoreAllTrash() async { + final response = await _trashApi.restoreTrash(); + return response?.count ?? 0; + } + Future updateVisibility(List ids, AssetVisibilityEnum visibility) async { return _api.updateAssets(AssetBulkUpdateDto(ids: ids, visibility: _mapVisibility(visibility))); } diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index 44b070e954f46..9fdb3f6712779 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -108,6 +108,18 @@ class ActionService { await _remoteAssetRepository.restoreTrash(ids); } + Future emptyTrash() async { + final count = await _assetApiRepository.emptyTrash(); + await _remoteAssetRepository.emptyTrash(); + return count; + } + + Future restoreAllTrash() async { + final count = await _assetApiRepository.restoreAllTrash(); + await _remoteAssetRepository.restoreAllTrash(); + return count; + } + Future trashRemoteAndDeleteLocal(List remoteIds, List localIds) async { await _assetApiRepository.delete(remoteIds, false); await _remoteAssetRepository.trash(remoteIds);