From 65f5118bdd4349f46f4b40b29a82c10f1016a374 Mon Sep 17 00:00:00 2001 From: i-am-a-teapot <75271959+i-am-a-teapot@users.noreply.github.com> Date: Tue, 6 Aug 2024 19:06:30 +0200 Subject: [PATCH] feat(web): Add stacking option to deduplication utilities (#11114) * feat(web): Add stacking option to deduplication utilities * Update web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte Co-authored-by: Alex * Fix prettier * Draft for server side modifications. Endpoint for stacks (PUT,DELETE) * Fix error * Disable stakc button if less or more than one asset selected * Remove unnecesarry log * Revert to first commit * Further Revert * Actually Revert to Origin * Only one stack button * Update +page.svelte * Fix optional arguments * Fix Prettier * Fix Linting * Add stack information to asset view * clean up --------- Co-authored-by: Alex --- .../duplicates/duplicate-asset.svelte | 25 +++++++--- .../duplicates-compare-control.svelte | 50 ++++++++++++++----- web/src/lib/i18n/en.json | 2 + web/src/lib/utils/asset-utils.ts | 22 ++++---- .../[[assetId=id]]/+page.svelte | 13 ++++- 5 files changed, 82 insertions(+), 30 deletions(-) diff --git a/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte b/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte index 74d17c621d..5fc2177e88 100644 --- a/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte +++ b/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte @@ -4,7 +4,7 @@ import { getAssetResolution, getFileSize } from '$lib/utils/asset-utils'; import { getAltText } from '$lib/utils/thumbnail-util'; import { getAllAlbums, type AssetResponseDto } from '@immich/sdk'; - import { mdiHeart, mdiMagnifyPlus } from '@mdi/js'; + import { mdiHeart, mdiMagnifyPlus, mdiImageMultipleOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; export let asset: AssetResponseDto; @@ -14,6 +14,7 @@ $: isFromExternalLibrary = !!asset.libraryId; $: assetData = JSON.stringify(asset, null, 2); + $: stackCount = asset.stackCount;
- - {#if isFromExternalLibrary} -
- {$t('external')} -
- {/if} + +
+ {#if isFromExternalLibrary} +
+ {$t('external')} +
+ {/if} + {#if stackCount != null && stackCount != 0} +
+
+
{stackCount}
+ +
+
+ {/if} +
- {#if trashCount === 0} - + {:else} + + {/if} + - {:else} - - {/if} + diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 1149bc99b8..172b1b5d05 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -1116,6 +1116,8 @@ "sort_title": "Title", "source": "Source", "stack": "Stack", + "stack_duplicates": "Stack duplicates", + "stack_select_one_photo": "Select one main photo for the stack", "stack_selected_photos": "Stack selected photos", "stacked_assets_count": "Stacked {count, plural, one {# asset} other {# assets}}", "stacktrace": "Stacktrace", diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 476d910523..a23c369009 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -324,7 +324,7 @@ export const getSelectedAssets = (assets: Set, user: UserRespo return ids; }; -export const stackAssets = async (assets: AssetResponseDto[]) => { +export const stackAssets = async (assets: AssetResponseDto[], showNotification = true) => { if (assets.length < 2) { return false; } @@ -362,16 +362,18 @@ export const stackAssets = async (assets: AssetResponseDto[]) => { parent.stack = parent.stack.concat(children, grandChildren); parent.stackCount = parent.stack.length + 1; - notificationController.show({ - message: $t('stacked_assets_count', { values: { count: parent.stackCount } }), - type: NotificationType.Info, - button: { - text: $t('view_stack'), - onClick() { - return assetViewingStore.setAssetId(parent.id); + if (showNotification) { + notificationController.show({ + message: $t('stacked_assets_count', { values: { count: parent.stackCount } }), + type: NotificationType.Info, + button: { + text: $t('view_stack'), + onClick() { + return assetViewingStore.setAssetId(parent.id); + }, }, - }, - }); + }); + } return ids; }; diff --git a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte index 3a9bfbea7f..34889261d5 100644 --- a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -6,6 +6,7 @@ notificationController, } from '$lib/components/shared-components/notification/notification'; import DuplicatesCompareControl from '$lib/components/utilities-page/duplicates/duplicates-compare-control.svelte'; + import type { AssetResponseDto } from '@immich/sdk'; import { featureFlags } from '$lib/stores/server-config.store'; import { handleError } from '$lib/utils/handle-error'; import { deleteAssets, updateAssets } from '@immich/sdk'; @@ -13,10 +14,11 @@ import type { PageData } from './$types'; import { suggestDuplicateByFileSize } from '$lib/utils'; import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; + import { mdiCheckOutline, mdiTrashCanOutline } from '@mdi/js'; + import { stackAssets } from '$lib/utils/asset-utils'; import ShowShortcuts from '$lib/components/shared-components/show-shortcuts.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import { mdiKeyboard } from '@mdi/js'; - import { mdiCheckOutline, mdiTrashCanOutline } from '@mdi/js'; import Icon from '$lib/components/elements/icon.svelte'; import { locale } from '$lib/stores/preferences.store'; @@ -40,6 +42,7 @@ { key: ['s'], action: $t('view') }, { key: ['d'], action: $t('unselect_all_duplicates') }, { key: ['⇧', 'c'], action: $t('resolve_duplicates') }, + { key: ['⇧', 'c'], action: $t('stack_duplicates') }, ], }; @@ -88,6 +91,13 @@ ); }; + const handleStack = async (duplicateId: string, assets: AssetResponseDto[]) => { + await stackAssets(assets, false); + const duplicateAssetIds = assets.map((asset) => asset.id); + await updateAssets({ assetBulkUpdateDto: { ids: duplicateAssetIds, duplicateId: null } }); + data.duplicates = data.duplicates.filter((duplicate) => duplicate.duplicateId !== duplicateId); + }; + const handleDeduplicateAll = async () => { const idsToKeep = data.duplicates .map((group) => suggestDuplicateByFileSize(group.assets)) @@ -174,6 +184,7 @@ assets={data.duplicates[0].assets} onResolve={(duplicateAssetIds, trashIds) => handleResolve(data.duplicates[0].duplicateId, duplicateAssetIds, trashIds)} + onStack={(assets) => handleStack(data.duplicates[0].duplicateId, assets)} /> {/key} {:else}