diff --git a/i18n/en.json b/i18n/en.json
index a1c79bbec4..d56595c826 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -465,6 +465,7 @@
"confirm": "Confirm",
"confirm_admin_password": "Confirm Admin Password",
"confirm_delete_shared_link": "Are you sure you want to delete this shared link?",
+ "confirm_keep_this_delete_others": "All other assets in the stack will be deleted except for this asset. Are you sure you want to continue?",
"confirm_password": "Confirm password",
"contain": "Contain",
"context": "Context",
@@ -514,6 +515,7 @@
"delete_key": "Delete key",
"delete_library": "Delete Library",
"delete_link": "Delete link",
+ "delete_others": "Delete others",
"delete_shared_link": "Delete shared link",
"delete_tag": "Delete tag",
"delete_tag_confirmation_prompt": "Are you sure you want to delete {tagName} tag?",
@@ -610,6 +612,7 @@
"failed_to_remove_product_key": "Failed to remove product key",
"failed_to_stack_assets": "Failed to stack assets",
"failed_to_unstack_assets": "Failed to un-stack assets",
+ "failed_to_keep_this_delete_others": "Failed to keep this asset and delete the other assets",
"import_path_already_exists": "This import path already exists.",
"incorrect_email_or_password": "Incorrect email or password",
"paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation",
@@ -1253,8 +1256,10 @@
"unselect_all_duplicates": "Unselect all duplicates",
"unstack": "Un-stack",
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
+ "keep_this_delete_others": "Keep this, delete others",
"untracked_files": "Untracked files",
"untracked_files_decription": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug",
+ "kept_this_deleted_others": "Kept this asset and deleted {count, plural, one {# asset} other {# assets}}",
"up_next": "Up next",
"updated_password": "Updated password",
"upload": "Upload",
diff --git a/web/src/lib/components/asset-viewer/actions/action.ts b/web/src/lib/components/asset-viewer/actions/action.ts
index d6136f2d18..f8cfd447f0 100644
--- a/web/src/lib/components/asset-viewer/actions/action.ts
+++ b/web/src/lib/components/asset-viewer/actions/action.ts
@@ -12,6 +12,7 @@ type ActionMap = {
[AssetAction.ADD]: { asset: AssetResponseDto };
[AssetAction.ADD_TO_ALBUM]: { asset: AssetResponseDto; album: AlbumResponseDto };
[AssetAction.UNSTACK]: { assets: AssetResponseDto[] };
+ [AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: AssetResponseDto };
};
export type Action = {
diff --git a/web/src/lib/components/asset-viewer/actions/keep-this-delete-others.svelte b/web/src/lib/components/asset-viewer/actions/keep-this-delete-others.svelte
new file mode 100644
index 0000000000..3d52b5f28d
--- /dev/null
+++ b/web/src/lib/components/asset-viewer/actions/keep-this-delete-others.svelte
@@ -0,0 +1,33 @@
+
+
+
diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte
index 7972ff6c72..8bea15e2a7 100644
--- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte
+++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte
@@ -13,6 +13,7 @@
import ShareAction from '$lib/components/asset-viewer/actions/share-action.svelte';
import ShowDetailAction from '$lib/components/asset-viewer/actions/show-detail-action.svelte';
import UnstackAction from '$lib/components/asset-viewer/actions/unstack-action.svelte';
+ import KeepThisDeleteOthersAction from '$lib/components/asset-viewer/actions/keep-this-delete-others.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
@@ -166,6 +167,7 @@
{#if isOwner}
{#if stack}
+
{/if}
{#if album}
diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte
index 32894ad908..5e7d1a699b 100644
--- a/web/src/lib/components/asset-viewer/asset-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte
@@ -379,6 +379,7 @@
break;
}
+ case AssetAction.KEEP_THIS_DELETE_OTHERS:
case AssetAction.UNSTACK: {
closeViewer();
}
diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts
index f6cd8aa8ab..f47d4a8c87 100644
--- a/web/src/lib/constants.ts
+++ b/web/src/lib/constants.ts
@@ -9,6 +9,7 @@ export enum AssetAction {
ADD = 'add',
ADD_TO_ALBUM = 'add-to-album',
UNSTACK = 'unstack',
+ KEEP_THIS_DELETE_OTHERS = 'keep-this-delete-others',
}
export enum AppRoute {
diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts
index 77aa7b1ecd..37041ecbc4 100644
--- a/web/src/lib/utils/asset-utils.ts
+++ b/web/src/lib/utils/asset-utils.ts
@@ -14,6 +14,7 @@ import { getFormatter } from '$lib/utils/i18n';
import {
addAssetsToAlbum as addAssets,
createStack,
+ deleteAssets,
deleteStacks,
getAssetInfo,
getBaseUrl,
@@ -27,6 +28,7 @@ import {
type AssetResponseDto,
type AssetTypeEnum,
type DownloadInfoDto,
+ type StackResponseDto,
type UserPreferencesResponseDto,
type UserResponseDto,
} from '@immich/sdk';
@@ -438,6 +440,26 @@ export const deleteStack = async (stackIds: string[]) => {
}
};
+export const keepThisDeleteOthers = async (keepAsset: AssetResponseDto, stack: StackResponseDto) => {
+ const $t = get(t);
+
+ try {
+ const assetsToDeleteIds = stack.assets.filter((asset) => asset.id !== keepAsset.id).map((asset) => asset.id);
+ await deleteAssets({ assetBulkDeleteDto: { ids: assetsToDeleteIds } });
+ await deleteStacks({ bulkIdsDto: { ids: [stack.id] } });
+
+ notificationController.show({
+ type: NotificationType.Info,
+ message: $t('kept_this_deleted_others', { values: { count: assetsToDeleteIds.length } }),
+ });
+
+ keepAsset.stack = null;
+ return keepAsset;
+ } catch (error) {
+ handleError(error, $t('errors.failed_to_keep_this_delete_others'));
+ }
+};
+
export const selectAllAssets = async (assetStore: AssetStore, assetInteractionStore: AssetInteractionStore) => {
if (get(isSelectingAllAssets)) {
// Selection is already ongoing