mirror of
https://github.com/immich-app/immich.git
synced 2026-07-08 13:27:46 -07:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 49714a3b30 | |||
| d2a46ade78 |
@@ -1462,6 +1462,7 @@
|
|||||||
"move_to_lock_folder_action_prompt": "{count} added to the locked folder",
|
"move_to_lock_folder_action_prompt": "{count} added to the locked folder",
|
||||||
"move_to_locked_folder": "Move to locked folder",
|
"move_to_locked_folder": "Move to locked folder",
|
||||||
"move_to_locked_folder_confirmation": "These photos and video will be removed from all albums, and only viewable from the locked folder",
|
"move_to_locked_folder_confirmation": "These photos and video will be removed from all albums, and only viewable from the locked folder",
|
||||||
|
"move_to_locked_folder_local_ios": "These items will be moved to Recently Deleted but still be available on the Immich server",
|
||||||
"moved_to_trash": "Moved to trash",
|
"moved_to_trash": "Moved to trash",
|
||||||
"mute_memories": "Mute Memories",
|
"mute_memories": "Mute Memories",
|
||||||
"my_albums": "My albums",
|
"my_albums": "My albums",
|
||||||
|
|||||||
+5
-1
@@ -16,11 +16,15 @@ Future<void> performMoveToLockFolderAction(BuildContext context, WidgetRef ref,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final result = await ref.read(actionProvider.notifier).moveToLockFolder(source, context);
|
||||||
|
if (result == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (source == ActionSource.viewer) {
|
if (source == ActionSource.viewer) {
|
||||||
EventStream.shared.emit(const ViewerReloadAssetEvent());
|
EventStream.shared.emit(const ViewerReloadAssetEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).moveToLockFolder(source);
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
final successMessage = 'move_to_lock_folder_action_prompt'.t(
|
final successMessage = 'move_to_lock_folder_action_prompt'.t(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|||||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/asset.service.dart';
|
import 'package:immich_mobile/domain/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
||||||
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
|
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
|
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
|
||||||
@@ -27,6 +28,7 @@ import 'package:immich_mobile/services/download.service.dart';
|
|||||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||||
import 'package:immich_mobile/utils/semver.dart';
|
import 'package:immich_mobile/utils/semver.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
@@ -200,9 +202,24 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ActionResult> moveToLockFolder(ActionSource source) async {
|
Future<ActionResult?> moveToLockFolder(ActionSource source, BuildContext context) async {
|
||||||
final ids = _getOwnedRemoteIdsForSource(source);
|
final ids = _getOwnedRemoteIdsForSource(source);
|
||||||
final localIds = _getLocalIdsForSource(source, ignoreLocalOnly: true);
|
final localIds = _getLocalIdsForSource(source, ignoreLocalOnly: true);
|
||||||
|
|
||||||
|
if (localIds.isNotEmpty) {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => ConfirmDialog(
|
||||||
|
title: "move_to_locked_folder",
|
||||||
|
content: CurrentPlatform.isAndroid ? "delete_dialog_alert_local" : "move_to_locked_folder_local_ios",
|
||||||
|
ok: "confirm",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (confirmed != true) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _service.moveToLockFolder(ids, localIds);
|
await _service.moveToLockFolder(ids, localIds);
|
||||||
return ActionResult(count: ids.length, success: true);
|
return ActionResult(count: ids.length, success: true);
|
||||||
|
|||||||
@@ -45,9 +45,9 @@ class AssetMediaRepository {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>> deleteAll(List<String> ids) async {
|
Future<List<String>> deleteAll(List<String> ids, {bool trash = true}) async {
|
||||||
if (CurrentPlatform.isAndroid) {
|
if (CurrentPlatform.isAndroid) {
|
||||||
if (await _androidSupportsTrash()) {
|
if (trash && await _androidSupportsTrash()) {
|
||||||
return PhotoManager.editor.android.moveToTrash(
|
return PhotoManager.editor.android.moveToTrash(
|
||||||
ids.map((e) => AssetEntity(id: e, width: 1, height: 1, typeInt: 0)).toList(),
|
ids.map((e) => AssetEntity(id: e, width: 1, height: 1, typeInt: 0)).toList(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -92,9 +92,9 @@ class ActionService {
|
|||||||
await _assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked);
|
await _assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked);
|
||||||
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked);
|
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked);
|
||||||
|
|
||||||
// Ask user if they want to delete local copies
|
// Locked assets stay on the server, so permanently delete the local copies instead of trashing them
|
||||||
if (localIds.isNotEmpty) {
|
if (localIds.isNotEmpty) {
|
||||||
await _deleteLocalAssets(localIds);
|
await _deleteLocalAssets(localIds, trash: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,12 +313,12 @@ class ActionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> _deleteLocalAssets(List<String> localIds) async {
|
Future<int> _deleteLocalAssets(List<String> localIds, {bool trash = true}) async {
|
||||||
final deletedIds = await _assetMediaRepository.deleteAll(localIds);
|
final deletedIds = await _assetMediaRepository.deleteAll(localIds, trash: trash);
|
||||||
if (deletedIds.isEmpty) {
|
if (deletedIds.isEmpty) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
|
if (trash && CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
|
||||||
await _trashedLocalAssetRepository.applyTrashedAssets(deletedIds);
|
await _trashedLocalAssetRepository.applyTrashedAssets(deletedIds);
|
||||||
} else {
|
} else {
|
||||||
await _localAssetRepository.delete(deletedIds);
|
await _localAssetRepository.delete(deletedIds);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'package:drift/drift.dart' as drift;
|
|||||||
import 'package:drift/native.dart';
|
import 'package:drift/native.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
@@ -147,13 +149,13 @@ void main() {
|
|||||||
await Store.put(StoreKey.manageLocalMediaAndroid, true);
|
await Store.put(StoreKey.manageLocalMediaAndroid, true);
|
||||||
const ids = ['a', 'b'];
|
const ids = ['a', 'b'];
|
||||||
|
|
||||||
when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => ids);
|
when(() => assetMediaRepository.deleteAll(ids, trash: true)).thenAnswer((_) async => ids);
|
||||||
when(() => trashedLocalAssetRepository.applyTrashedAssets(ids)).thenAnswer((_) async {});
|
when(() => trashedLocalAssetRepository.applyTrashedAssets(ids)).thenAnswer((_) async {});
|
||||||
|
|
||||||
final result = await sut.deleteLocal(ids);
|
final result = await sut.deleteLocal(ids);
|
||||||
|
|
||||||
expect(result, ids.length);
|
expect(result, ids.length);
|
||||||
verify(() => assetMediaRepository.deleteAll(ids)).called(1);
|
verify(() => assetMediaRepository.deleteAll(ids, trash: true)).called(1);
|
||||||
verify(() => trashedLocalAssetRepository.applyTrashedAssets(ids)).called(1);
|
verify(() => trashedLocalAssetRepository.applyTrashedAssets(ids)).called(1);
|
||||||
verifyNever(() => localAssetRepository.delete(any()));
|
verifyNever(() => localAssetRepository.delete(any()));
|
||||||
});
|
});
|
||||||
@@ -162,13 +164,13 @@ void main() {
|
|||||||
await Store.put(StoreKey.manageLocalMediaAndroid, false);
|
await Store.put(StoreKey.manageLocalMediaAndroid, false);
|
||||||
const ids = ['c'];
|
const ids = ['c'];
|
||||||
|
|
||||||
when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => ids);
|
when(() => assetMediaRepository.deleteAll(ids, trash: true)).thenAnswer((_) async => ids);
|
||||||
when(() => localAssetRepository.delete(ids)).thenAnswer((_) async {});
|
when(() => localAssetRepository.delete(ids)).thenAnswer((_) async {});
|
||||||
|
|
||||||
final result = await sut.deleteLocal(ids);
|
final result = await sut.deleteLocal(ids);
|
||||||
|
|
||||||
expect(result, ids.length);
|
expect(result, ids.length);
|
||||||
verify(() => assetMediaRepository.deleteAll(ids)).called(1);
|
verify(() => assetMediaRepository.deleteAll(ids, trash: true)).called(1);
|
||||||
verify(() => localAssetRepository.delete(ids)).called(1);
|
verify(() => localAssetRepository.delete(ids)).called(1);
|
||||||
verifyNever(() => trashedLocalAssetRepository.applyTrashedAssets(any()));
|
verifyNever(() => trashedLocalAssetRepository.applyTrashedAssets(any()));
|
||||||
});
|
});
|
||||||
@@ -177,14 +179,46 @@ void main() {
|
|||||||
await Store.put(StoreKey.manageLocalMediaAndroid, true);
|
await Store.put(StoreKey.manageLocalMediaAndroid, true);
|
||||||
const ids = ['x'];
|
const ids = ['x'];
|
||||||
|
|
||||||
when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => <String>[]);
|
when(() => assetMediaRepository.deleteAll(ids, trash: true)).thenAnswer((_) async => <String>[]);
|
||||||
|
|
||||||
final result = await sut.deleteLocal(ids);
|
final result = await sut.deleteLocal(ids);
|
||||||
|
|
||||||
expect(result, 0);
|
expect(result, 0);
|
||||||
verify(() => assetMediaRepository.deleteAll(ids)).called(1);
|
verify(() => assetMediaRepository.deleteAll(ids, trash: true)).called(1);
|
||||||
verifyNever(() => trashedLocalAssetRepository.applyTrashedAssets(any()));
|
verifyNever(() => trashedLocalAssetRepository.applyTrashedAssets(any()));
|
||||||
verifyNever(() => localAssetRepository.delete(any()));
|
verifyNever(() => localAssetRepository.delete(any()));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('ActionService.moveToLockFolder', () {
|
||||||
|
const remoteIds = ['r1', 'r2'];
|
||||||
|
const localIds = ['l1', 'l2'];
|
||||||
|
|
||||||
|
test('permanently deletes local copies without trashing, even when Android trash handling is on', () async {
|
||||||
|
await Store.put(StoreKey.manageLocalMediaAndroid, true);
|
||||||
|
|
||||||
|
when(() => assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked)).thenAnswer((_) async {});
|
||||||
|
when(() => remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked)).thenAnswer((_) async {});
|
||||||
|
when(() => assetMediaRepository.deleteAll(localIds, trash: false)).thenAnswer((_) async => localIds);
|
||||||
|
when(() => localAssetRepository.delete(localIds)).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await sut.moveToLockFolder(remoteIds, localIds);
|
||||||
|
|
||||||
|
verify(() => assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked)).called(1);
|
||||||
|
verify(() => remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked)).called(1);
|
||||||
|
verify(() => assetMediaRepository.deleteAll(localIds, trash: false)).called(1);
|
||||||
|
verify(() => localAssetRepository.delete(localIds)).called(1);
|
||||||
|
verifyNever(() => trashedLocalAssetRepository.applyTrashedAssets(any()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('locks remote assets without touching local media when there are no local copies', () async {
|
||||||
|
when(() => assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked)).thenAnswer((_) async {});
|
||||||
|
when(() => remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked)).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await sut.moveToLockFolder(remoteIds, const []);
|
||||||
|
|
||||||
|
verify(() => assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked)).called(1);
|
||||||
|
verifyNever(() => assetMediaRepository.deleteAll(any(), trash: any(named: 'trash')));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,14 +151,14 @@ const methods = wrapper<Manifest>({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
webhook: ({ config, data, functions, type, trigger }) => {
|
webhook: ({ config, data, functions, type, trigger }) => {
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {};
|
||||||
'Content-Type': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (config.headerName && config.headerValue) {
|
if (config.headerName && config.headerValue) {
|
||||||
headers[config.headerName] = config.headerValue;
|
headers[config.headerName] = config.headerValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
|
||||||
functions.httpRequest(config.url, {
|
functions.httpRequest(config.url, {
|
||||||
method: config.method ?? 'POST',
|
method: config.method ?? 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
Reference in New Issue
Block a user