Skip to content

Commit

Permalink
feat(web): Add stacking option to deduplication utilities (#11114)
Browse files Browse the repository at this point in the history
* feat(web): Add stacking option to deduplication utilities

* Update web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte

Co-authored-by: Alex <[email protected]>

* 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 <[email protected]>
  • Loading branch information
i-am-a-teapot and alextran1502 authored Aug 6, 2024
1 parent 9f4fad2 commit 65f5118
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,6 +14,7 @@
$: isFromExternalLibrary = !!asset.libraryId;
$: assetData = JSON.stringify(asset, null, 2);
$: stackCount = asset.stackCount;
</script>

<div
Expand Down Expand Up @@ -54,12 +55,22 @@
{isSelected ? $t('keep') : $t('to_trash')}
</div>

<!-- EXTERNAL LIBRARY CHIP-->
{#if isFromExternalLibrary}
<div class="absolute top-2 right-3 bg-immich-primary/90 px-4 py-1 rounded-xl text-xs text-white">
{$t('external')}
</div>
{/if}
<!-- EXTERNAL LIBRARY / STACK COUNT CHIP-->
<div class="absolute top-2 right-3">
{#if isFromExternalLibrary}
<div class="bg-immich-primary/90 px-2 py-1 rounded-xl text-xs text-white">
{$t('external')}
</div>
{/if}
{#if stackCount != null && stackCount != 0}
<div class="bg-immich-primary/90 px-2 py-1 my-0.5 rounded-xl text-xs text-white">
<div class="flex items-center justify-center">
<div class="mr-1">{stackCount}</div>
<Icon path={mdiImageMultipleOutline} size="18" />
</div>
</div>
{/if}
</div>
</button>

<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
import { suggestDuplicateByFileSize } from '$lib/utils';
import { shortcuts } from '$lib/actions/shortcut';
import { type AssetResponseDto } from '@immich/sdk';
import { mdiCheck, mdiTrashCanOutline } from '@mdi/js';
import { mdiCheck, mdiTrashCanOutline, mdiImageMultipleOutline } from '@mdi/js';
import { onDestroy, onMount } from 'svelte';
import { t } from 'svelte-i18n';
export let assets: AssetResponseDto[];
export let onResolve: (duplicateAssetIds: string[], trashIds: string[]) => void;
export let onStack: (assets: AssetResponseDto[]) => void;
const { isViewing: showAssetViewer, asset: viewingAsset, setAsset } = assetViewingStore;
const getAssetIndex = (id: string) => assets.findIndex((asset) => asset.id === id);
Expand Down Expand Up @@ -60,6 +60,10 @@
const duplicateAssetIds = assets.map((asset) => asset.id);
onResolve(duplicateAssetIds, trashIds);
};
const handleStack = () => {
onStack(assets);
};
</script>

<svelte:window
Expand All @@ -73,6 +77,7 @@
},
{ shortcut: { key: 'd' }, onShortcut: onSelectNone },
{ shortcut: { key: 'c', shift: true }, onShortcut: handleResolve },
{ shortcut: { key: 's', shift: true }, onShortcut: handleStack },
]}
/>

Expand Down Expand Up @@ -104,17 +109,38 @@
</div>

<!-- CONFIRM BUTTONS -->
{#if trashCount === 0}
<Button size="sm" color="primary" class="flex place-items-center gap-2" on:click={handleResolve}>
<Icon path={mdiCheck} size="20" />{$t('keep_all')}
</Button>
{:else}
<Button size="sm" color="red" class="flex place-items-center gap-2 py-3" on:click={handleResolve}>
<Icon path={mdiTrashCanOutline} size="20" />{trashCount === assets.length
? $t('trash_all')
: $t('trash_count', { values: { count: trashCount } })}
<div class="flex text-xs text-black">
{#if trashCount === 0}
<Button
size="sm"
color="primary"
class="flex place-items-center rounded-tl-full rounded-bl-full gap-2"
on:click={handleResolve}
>
<Icon path={mdiCheck} size="20" />{$t('keep_all')}
</Button>
{:else}
<Button
size="sm"
color="red"
class="flex place-items-center rounded-tl-full rounded-bl-full gap-2 py-3"
on:click={handleResolve}
>
<Icon path={mdiTrashCanOutline} size="20" />{trashCount === assets.length
? $t('trash_all')
: $t('trash_count', { values: { count: trashCount } })}
</Button>
{/if}
<Button
size="sm"
color="primary"
class="flex place-items-center rounded-tr-full rounded-br-full gap-2"
on:click={handleStack}
disabled={selectedAssetIds.size !== 1}
>
<Icon path={mdiImageMultipleOutline} size="20" />{$t('stack')}
</Button>
{/if}
</div>
</div>
</div>

Expand Down
2 changes: 2 additions & 0 deletions web/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
22 changes: 12 additions & 10 deletions web/src/lib/utils/asset-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ export const getSelectedAssets = (assets: Set<AssetResponseDto>, user: UserRespo
return ids;
};

export const stackAssets = async (assets: AssetResponseDto[]) => {
export const stackAssets = async (assets: AssetResponseDto[], showNotification = true) => {
if (assets.length < 2) {
return false;
}
Expand Down Expand Up @@ -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;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@
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';
import { t } from 'svelte-i18n';
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';
Expand All @@ -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') },
],
};
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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}
Expand Down

0 comments on commit 65f5118

Please sign in to comment.