mirror of
https://github.com/immich-app/immich
synced 2026-08-22 13:13:05 +00:00
Some checks failed
CLI Build / CLI Publish (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
CodeQL / Analyze-1 (push) Has been cancelled
Docker / pre-job (push) Has been cancelled
Docs build / pre-job (push) Has been cancelled
Zizmor / Zizmor (push) Has been cancelled
Static Code Analysis / pre-job (push) Has been cancelled
Test / pre-job (push) Has been cancelled
Test / ShellCheck (push) Has been cancelled
Test / OpenAPI Clients (push) Has been cancelled
Test / SQL Schema Checks (push) Has been cancelled
CLI Build / Docker (push) Has been cancelled
Docker / Re-Tag ML (push) Has been cancelled
Docker / Re-Tag ML-1 (push) Has been cancelled
Docker / Re-Tag ML-2 (push) Has been cancelled
Docker / Re-Tag ML-3 (push) Has been cancelled
Docker / Re-Tag ML-4 (push) Has been cancelled
Docker / Re-Tag ML-5 (push) Has been cancelled
Docker / Re-Tag Server (push) Has been cancelled
Docker / Build and Push ML (push) Has been cancelled
Docker / Build and Push ML-1 (push) Has been cancelled
Docker / Build and Push ML-2 (push) Has been cancelled
Docker / Build and Push ML-3 (push) Has been cancelled
Docker / Build and Push ML-4 (push) Has been cancelled
Docker / Build and Push ML-5 (push) Has been cancelled
Docker / Build and Push Server (push) Has been cancelled
Docker / Mirror to Docker Hub (push) Has been cancelled
Docker / Mirror to Docker Hub-1 (push) Has been cancelled
Docker / Mirror to Docker Hub-2 (push) Has been cancelled
Docker / Mirror to Docker Hub-3 (push) Has been cancelled
Docker / Mirror to Docker Hub-4 (push) Has been cancelled
Docker / Mirror to Docker Hub-5 (push) Has been cancelled
Docker / Mirror to Docker Hub-6 (push) Has been cancelled
Docker / Docker Build & Push Server Success (push) Has been cancelled
Docker / Docker Build & Push ML Success (push) Has been cancelled
Docs build / Docs Build (push) Has been cancelled
Static Code Analysis / Run Dart Code Analysis (push) Has been cancelled
Test / Scripts unit tests (push) Has been cancelled
Test / Test & Lint Server (push) Has been cancelled
Test / Unit Test CLI (push) Has been cancelled
Test / Unit Test CLI (Windows) (push) Has been cancelled
Test / Lint Web (push) Has been cancelled
Test / Test Web (push) Has been cancelled
Test / Test i18n (push) Has been cancelled
Test / End-to-End Lint (push) Has been cancelled
Test / Medium Tests (Server) (push) Has been cancelled
Test / End-to-End Tests (Server & CLI) (push) Has been cancelled
Test / End-to-End Tests (Server & CLI)-1 (push) Has been cancelled
Test / End-to-End Tests (Web) (push) Has been cancelled
Test / End-to-End Tests (Web)-1 (push) Has been cancelled
Test / End-to-End Tests Success (push) Has been cancelled
Test / Unit Test Mobile (push) Has been cancelled
Test / Unit Test ML (push) Has been cancelled
Test / .github Files Formatting (push) Has been cancelled
* permanently delete local copies when moving to the locked folder * simplify the delete path and move the stubs into the mock defaults * drop the kept count from the partial message and log the mismatch instead
106 lines
4.2 KiB
Dart
106 lines
4.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:immich_mobile/constants/enums.dart';
|
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
|
import 'package:immich_mobile/generated/translations.g.dart';
|
|
import 'package:immich_mobile/presentation/actions/action.dart';
|
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
|
import 'package:immich_mobile/providers/infrastructure/toast.provider.dart';
|
|
import 'package:immich_mobile/services/toast.service.dart';
|
|
import 'package:immich_mobile/utils/error_handler.dart';
|
|
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
|
import 'package:logging/logging.dart';
|
|
|
|
typedef _State = ({bool shouldLock, List<String> assetIds, List<String> localIds});
|
|
|
|
final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, source) {
|
|
final assets = ref.watch(ownedAssetsActionProvider(source));
|
|
if (assets.isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
final shouldLock = assets.locked(isLocked: false).isNotEmpty;
|
|
final targets = assets.locked(isLocked: !shouldLock);
|
|
return (
|
|
shouldLock: shouldLock,
|
|
assetIds: targets.map((asset) => asset.id).toList(growable: false),
|
|
// Only locking has an on-device copy to clean up; unlocking leaves the device alone.
|
|
localIds: shouldLock ? targets.map((asset) => asset.localId).nonNulls.toList(growable: false) : const [],
|
|
);
|
|
}, dependencies: [ownedAssetsActionProvider]);
|
|
|
|
class LockAction extends AssetActionBuilder {
|
|
const LockAction({required super.source});
|
|
|
|
static final Logger _log = Logger('LockAction');
|
|
|
|
@override
|
|
ActionItem? create(BuildContext context, WidgetRef ref) {
|
|
final shouldLock = ref.watch(_stateProvider(source).select((state) => state?.shouldLock));
|
|
if (shouldLock == null) {
|
|
return null;
|
|
}
|
|
|
|
return .new(
|
|
icon: shouldLock ? Icons.lock_rounded : Icons.lock_open_rounded,
|
|
label: shouldLock ? context.t.move_to_locked_folder : context.t.remove_from_locked_folder,
|
|
onAction: () => _lock(context, ref),
|
|
);
|
|
}
|
|
|
|
Future<void> _lock(BuildContext context, WidgetRef ref) async {
|
|
final state = ref.read(_stateProvider(source));
|
|
if (state == null) {
|
|
return;
|
|
}
|
|
|
|
final (:shouldLock, :assetIds, :localIds) = state;
|
|
if (shouldLock && localIds.isNotEmpty) {
|
|
final confirmed = await showDialog<bool>(
|
|
context: context,
|
|
builder: (_) => ConfirmDialog(
|
|
title: context.t.move_to_locked_folder,
|
|
content: CurrentPlatform.isAndroid
|
|
? context.t.delete_dialog_alert_local
|
|
: context.t.delete_dialog_alert_local_ios,
|
|
ok: context.t.confirm,
|
|
),
|
|
);
|
|
if (confirmed != true || !context.mounted) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
final assetService = ref.read(assetServiceProvider);
|
|
final toastService = ref.read(toastServiceProvider);
|
|
final clearSelection = ref.read(clearSelectionProvider(source));
|
|
|
|
try {
|
|
await assetService.update(assetIds, visibility: .some(shouldLock ? .locked : .timeline));
|
|
var keptCount = 0;
|
|
if (localIds.isNotEmpty) {
|
|
// A locked asset still sits in the device gallery, so offer to remove the local copy.
|
|
keptCount = localIds.length - await assetService.deleteLocal(localIds, trash: false);
|
|
if (keptCount != 0) {
|
|
_log.warning('Only deleted ${localIds.length - keptCount} of ${localIds.length} local copies');
|
|
}
|
|
}
|
|
if (!context.mounted) {
|
|
return;
|
|
}
|
|
final message = shouldLock
|
|
? keptCount > 0
|
|
? context.t.move_to_lock_folder_partial_prompt(count: assetIds.length)
|
|
: context.t.move_to_lock_folder_action_prompt(count: assetIds.length)
|
|
: context.t.remove_from_lock_folder_action_prompt(count: assetIds.length);
|
|
// Unlocking is a sensitive action and requires an elevated session
|
|
final toast = shouldLock
|
|
? null
|
|
: ToastOption(onUndo: () => assetService.update(assetIds, visibility: const .some(.locked)));
|
|
toastService.success(message, toast: toast);
|
|
clearSelection();
|
|
} catch (error, stack) {
|
|
handleError(error, stack: stack, description: "Failed to update the locked folder for assets");
|
|
}
|
|
}
|
|
}
|