Files
immich/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte

196 lines
6.0 KiB
Svelte
Raw Normal View History

2024-05-23 12:57:25 -05:00
<script lang="ts">
import { shortcuts } from '$lib/actions/shortcut';
import Portal from '$lib/components/shared-components/portal/portal.svelte';
import DuplicateAsset from '$lib/components/utilities-page/duplicates/duplicate-asset.svelte';
2025-09-10 16:08:15 -04:00
import { authManager } from '$lib/managers/auth-manager.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { handlePromiseError } from '$lib/utils';
import { suggestDuplicate } from '$lib/utils/duplicate-utils';
import { navigate } from '$lib/utils/navigation';
2025-09-10 16:08:15 -04:00
import { getAssetInfo, type AssetResponseDto } from '@immich/sdk';
import { Button } from '@immich/ui';
import { mdiCheck, mdiImageMultipleOutline, mdiTrashCanOutline } from '@mdi/js';
import { onDestroy, onMount } from 'svelte';
feat(web): translations (#9854) * First test * Added translation using Weblate (French) * Translated using Weblate (German) Currently translated at 100.0% (4 of 4 strings) Translation: immich/web Translate-URL: http://familie-mach.net/projects/immich/web/de/ * Translated using Weblate (French) Currently translated at 100.0% (4 of 4 strings) Translation: immich/web Translate-URL: http://familie-mach.net/projects/immich/web/fr/ * Further testing * Further testing * Translated using Weblate (German) Currently translated at 100.0% (18 of 18 strings) Translation: immich/web Translate-URL: http://familie-mach.net/projects/immich/web/de/ * Further work * Update string file. * More strings * Automatically changed strings * Add automatically translated german file for testing purposes * Fix merge-face-selector component * Make server stats strings uppercase * Fix uppercase string * Fix some strings in jobs-panel * Fix lower and uppercase strings. Add a few additional string. Fix a few unnecessary replacements * Update german test translations * Fix typo in locales file * Change string keys * Extract more strings * Extract and replace some more strings * Update testtranslationfile * Change translation keys * Fix rebase errors * Fix one more rebase error * Remove german translation file * Co-authored-by: Daniel Dietzler <danieldietzler@users.noreply.github.com> * chore: clean up translations * chore: add new line * fix formatting * chore: fixes * fix: loading and tests --------- Co-authored-by: root <root@Blacki> Co-authored-by: admin <admin@example.com> Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2024-06-04 21:53:00 +02:00
import { t } from 'svelte-i18n';
import { SvelteSet } from 'svelte/reactivity';
2024-05-23 12:57:25 -05:00
interface Props {
assets: AssetResponseDto[];
onResolve: (duplicateAssetIds: string[], trashIds: string[]) => void;
onStack: (assets: AssetResponseDto[]) => void;
}
let { assets, onResolve, onStack }: Props = $props();
const { isViewing: showAssetViewer, asset: viewingAsset, setAsset } = assetViewingStore;
const getAssetIndex = (id: string) => assets.findIndex((asset) => asset.id === id);
2024-05-23 12:57:25 -05:00
// eslint-disable-next-line svelte/no-unnecessary-state-wrap
let selectedAssetIds = $state(new SvelteSet<string>());
let trashCount = $derived(assets.length - selectedAssetIds.size);
2024-05-23 12:57:25 -05:00
onMount(() => {
const suggestedAsset = suggestDuplicate(assets);
2024-05-23 12:57:25 -05:00
if (!suggestedAsset) {
selectedAssetIds = new SvelteSet(assets[0].id);
2024-05-23 12:57:25 -05:00
return;
}
selectedAssetIds.add(suggestedAsset.id);
});
onDestroy(() => {
assetViewingStore.showAssetViewer(false);
});
2025-09-10 16:08:15 -04:00
const onNext = async () => {
const index = getAssetIndex($viewingAsset.id) + 1;
if (index >= assets.length) {
2025-09-10 16:08:15 -04:00
return false;
}
2025-09-10 16:08:15 -04:00
await onViewAsset(assets[index]);
return true;
};
2025-09-10 16:08:15 -04:00
const onPrevious = async () => {
const index = getAssetIndex($viewingAsset.id) - 1;
if (index < 0) {
2025-09-10 16:08:15 -04:00
return false;
}
2025-09-10 16:08:15 -04:00
await onViewAsset(assets[index]);
return true;
};
2025-09-10 16:08:15 -04:00
const onRandom = async () => {
if (assets.length <= 0) {
2025-09-10 16:08:15 -04:00
return;
}
const index = Math.floor(Math.random() * assets.length);
const asset = assets[index];
2025-09-10 16:08:15 -04:00
await onViewAsset(asset);
return { id: asset.id };
};
2024-05-23 12:57:25 -05:00
const onSelectAsset = (asset: AssetResponseDto) => {
if (selectedAssetIds.has(asset.id)) {
selectedAssetIds.delete(asset.id);
} else {
selectedAssetIds.add(asset.id);
}
};
const onSelectNone = () => {
selectedAssetIds.clear();
};
const onSelectAll = () => {
selectedAssetIds = new SvelteSet(assets.map((asset) => asset.id));
};
2025-09-10 16:08:15 -04:00
const onViewAsset = async ({ id }: AssetResponseDto) => {
const asset = await getAssetInfo({ ...authManager.params, id });
setAsset(asset);
await navigate({ targetRoute: 'current', assetId: asset.id });
};
2024-05-23 12:57:25 -05:00
const handleResolve = () => {
const trashIds = assets.map((asset) => asset.id).filter((id) => !selectedAssetIds.has(id));
const duplicateAssetIds = assets.map((asset) => asset.id);
2024-05-23 12:57:25 -05:00
onResolve(duplicateAssetIds, trashIds);
};
const handleStack = () => {
onStack(assets);
};
2024-05-23 12:57:25 -05:00
</script>
<svelte:document
use:shortcuts={[
{ shortcut: { key: 'a' }, onShortcut: onSelectAll },
{
shortcut: { key: 's' },
2025-09-10 16:08:15 -04:00
onShortcut: () => onViewAsset(assets[0]),
},
{ shortcut: { key: 'd' }, onShortcut: onSelectNone },
{ shortcut: { key: 'c', shift: true }, onShortcut: handleResolve },
{ shortcut: { key: 's', shift: true }, onShortcut: handleStack },
]}
/>
<div class="pt-4 rounded-3xl border dark:border-2 border-gray-300 dark:border-gray-700 max-w-216 mx-auto mb-4">
<div class="flex flex-wrap gap-y-6 mb-4 px-6 w-full place-content-end justify-between">
<!-- MARK ALL BUTTONS -->
<div class="flex text-xs text-black">
<Button class="rounded-s-full" size="small" color="primary" leadingIcon={mdiCheck} onclick={onSelectAll}
>{$t('select_keep_all')}</Button
>
<Button
class="rounded-e-full"
size="small"
color="secondary"
leadingIcon={mdiTrashCanOutline}
onclick={onSelectNone}>{$t('select_trash_all')}</Button
>
</div>
<!-- CONFIRM BUTTONS -->
<div class="flex text-xs text-black">
{#if trashCount === 0}
<Button
size="small"
leadingIcon={mdiCheck}
color="primary"
class="flex place-items-center rounded-s-full gap-2"
onclick={handleResolve}
>
{$t('keep_all')}
</Button>
{:else}
<Button
size="small"
color="danger"
leadingIcon={mdiTrashCanOutline}
class="rounded-s-full"
onclick={handleResolve}
>
{trashCount === assets.length ? $t('trash_all') : $t('trash_count', { values: { count: trashCount } })}
</Button>
{/if}
<Button
size="small"
color="primary"
leadingIcon={mdiImageMultipleOutline}
class="rounded-e-full"
onclick={handleStack}
disabled={selectedAssetIds.size !== 1}
>
{$t('stack')}
</Button>
</div>
2024-05-23 12:57:25 -05:00
</div>
<div class="flex flex-wrap gap-1 mb-4 place-items-center place-content-center px-4 pt-4">
{#each assets as asset (asset.id)}
2025-09-10 16:08:15 -04:00
<DuplicateAsset {asset} {onSelectAsset} isSelected={selectedAssetIds.has(asset.id)} {onViewAsset} />
{/each}
</div>
2024-05-23 12:57:25 -05:00
</div>
{#if $showAssetViewer}
{#await import('$lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
<Portal target="body">
<AssetViewer
asset={$viewingAsset}
showNavigation={assets.length > 1}
{onNext}
{onPrevious}
{onRandom}
onClose={() => {
assetViewingStore.showAssetViewer(false);
handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
}}
/>
</Portal>
{/await}
{/if}