mirror of
https://github.com/immich-app/immich.git
synced 2025-08-30 23:02:39 -04:00
feat(web): Skip duplicates (#20880)
* - add skip button to duplicates-compare-control * - cleanup * - change to next/previous - move buttons to duplicates page, intead of compareControl - add param based control/position * - remove index param on keep/dedupe all * - cleanup * - cleanup index corrections * - add left/right arrow keyboard shortcuts for previous/next - cleanup * - cleanup
This commit is contained in:
parent
257b0c74af
commit
a313e4338e
@ -1056,6 +1056,7 @@
|
||||
"filter_people": "Filter people",
|
||||
"filter_places": "Filter places",
|
||||
"find_them_fast": "Find them fast by name with search",
|
||||
"first": "First",
|
||||
"fix_incorrect_match": "Fix incorrect match",
|
||||
"folder": "Folder",
|
||||
"folder_not_found": "Folder not found",
|
||||
@ -1177,6 +1178,7 @@
|
||||
"language_search_hint": "Search languages...",
|
||||
"language_setting_description": "Select your preferred language",
|
||||
"large_files": "Large Files",
|
||||
"last": "Last",
|
||||
"last_seen": "Last seen",
|
||||
"latest_version": "Latest Version",
|
||||
"latitude": "Latitude",
|
||||
|
@ -112,7 +112,7 @@
|
||||
]}
|
||||
/>
|
||||
|
||||
<div class="pt-4 rounded-3xl border dark:border-2 border-gray-300 dark:border-gray-700 max-w-216 mx-auto mb-16">
|
||||
<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">
|
||||
|
@ -1,10 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { shortcuts } from '$lib/actions/shortcut';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import DuplicatesCompareControl from '$lib/components/utilities-page/duplicates/duplicates-compare-control.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import DuplicatesInformationModal from '$lib/modals/DuplicatesInformationModal.svelte';
|
||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
@ -15,7 +19,16 @@
|
||||
import type { AssetResponseDto } from '@immich/sdk';
|
||||
import { deleteAssets, deleteDuplicates, updateAssets } from '@immich/sdk';
|
||||
import { Button, HStack, IconButton, modalManager, Text } from '@immich/ui';
|
||||
import { mdiCheckOutline, mdiInformationOutline, mdiKeyboard, mdiTrashCanOutline } from '@mdi/js';
|
||||
import {
|
||||
mdiCheckOutline,
|
||||
mdiChevronLeft,
|
||||
mdiChevronRight,
|
||||
mdiInformationOutline,
|
||||
mdiKeyboard,
|
||||
mdiPageFirst,
|
||||
mdiPageLast,
|
||||
mdiTrashCanOutline,
|
||||
} from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
@ -47,6 +60,19 @@
|
||||
};
|
||||
|
||||
let duplicates = $state(data.duplicates);
|
||||
|
||||
const correctDuplicatesIndex = (index: number) => {
|
||||
return Math.max(0, Math.min(index, duplicates.length - 1));
|
||||
};
|
||||
|
||||
let duplicatesIndex = $derived(
|
||||
(() => {
|
||||
const indexParam = page.url.searchParams.get('index') ?? '0';
|
||||
const parsedIndex = Number.parseInt(indexParam, 10);
|
||||
return correctDuplicatesIndex(Number.isNaN(parsedIndex) ? 0 : parsedIndex);
|
||||
})(),
|
||||
);
|
||||
|
||||
let hasDuplicates = $derived(duplicates.length > 0);
|
||||
const withConfirmation = async (callback: () => Promise<void>, prompt?: string, confirmText?: string) => {
|
||||
if (prompt && confirmText) {
|
||||
@ -85,6 +111,7 @@
|
||||
duplicates = duplicates.filter((duplicate) => duplicate.duplicateId !== duplicateId);
|
||||
|
||||
deletedNotification(trashIds.length);
|
||||
await correctDuplicatesIndexAndGo(duplicatesIndex);
|
||||
},
|
||||
trashIds.length > 0 && !$featureFlags.trash ? $t('delete_duplicates_confirmation') : undefined,
|
||||
trashIds.length > 0 && !$featureFlags.trash ? $t('permanently_delete') : undefined,
|
||||
@ -96,6 +123,7 @@
|
||||
const duplicateAssetIds = assets.map((asset) => asset.id);
|
||||
await updateAssets({ assetBulkUpdateDto: { ids: duplicateAssetIds, duplicateId: null } });
|
||||
duplicates = duplicates.filter((duplicate) => duplicate.duplicateId !== duplicateId);
|
||||
await correctDuplicatesIndexAndGo(duplicatesIndex);
|
||||
};
|
||||
|
||||
const handleDeduplicateAll = async () => {
|
||||
@ -126,6 +154,9 @@
|
||||
duplicates = [];
|
||||
|
||||
deletedNotification(idsToDelete.length);
|
||||
|
||||
page.url.searchParams.delete('index');
|
||||
await goto(`${AppRoute.DUPLICATES}`);
|
||||
},
|
||||
prompt,
|
||||
confirmText,
|
||||
@ -144,13 +175,39 @@
|
||||
message: $t('resolved_all_duplicates'),
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
page.url.searchParams.delete('index');
|
||||
await goto(`${AppRoute.DUPLICATES}`);
|
||||
},
|
||||
$t('bulk_keep_duplicates_confirmation', { values: { count: ids.length } }),
|
||||
$t('confirm'),
|
||||
);
|
||||
};
|
||||
|
||||
const handleFirst = async () => {
|
||||
await correctDuplicatesIndexAndGo(0);
|
||||
};
|
||||
const handlePrevious = async () => {
|
||||
await correctDuplicatesIndexAndGo(Math.max(duplicatesIndex - 1, 0));
|
||||
};
|
||||
const handleNext = async () => {
|
||||
await correctDuplicatesIndexAndGo(Math.min(duplicatesIndex + 1, duplicates.length - 1));
|
||||
};
|
||||
const handleLast = async () => {
|
||||
await correctDuplicatesIndexAndGo(duplicates.length - 1);
|
||||
};
|
||||
const correctDuplicatesIndexAndGo = async (index: number) => {
|
||||
page.url.searchParams.set('index', correctDuplicatesIndex(index).toString());
|
||||
await goto(`${AppRoute.DUPLICATES}?${page.url.searchParams.toString()}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:document
|
||||
use:shortcuts={[
|
||||
{ shortcut: { key: 'ArrowLeft' }, onShortcut: handlePrevious },
|
||||
{ shortcut: { key: 'ArrowRight' }, onShortcut: handleNext },
|
||||
]}
|
||||
/>
|
||||
|
||||
<UserPageLayout title={data.meta.title + ` (${duplicates.length.toLocaleString($locale)})`} scrollbar={true}>
|
||||
{#snippet buttons()}
|
||||
<HStack gap={0}>
|
||||
@ -203,13 +260,61 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#key duplicates[0].duplicateId}
|
||||
{#key duplicates[duplicatesIndex].duplicateId}
|
||||
<DuplicatesCompareControl
|
||||
assets={duplicates[0].assets}
|
||||
assets={duplicates[duplicatesIndex].assets}
|
||||
onResolve={(duplicateAssetIds, trashIds) =>
|
||||
handleResolve(duplicates[0].duplicateId, duplicateAssetIds, trashIds)}
|
||||
onStack={(assets) => handleStack(duplicates[0].duplicateId, assets)}
|
||||
handleResolve(duplicates[duplicatesIndex].duplicateId, duplicateAssetIds, trashIds)}
|
||||
onStack={(assets) => handleStack(duplicates[duplicatesIndex].duplicateId, assets)}
|
||||
/>
|
||||
<div class="max-w-216 mx-auto mb-16">
|
||||
<div class="flex flex-wrap gap-y-6 mb-4 px-6 w-full place-content-end justify-between">
|
||||
<div class="flex text-xs text-black">
|
||||
<Button
|
||||
size="small"
|
||||
leadingIcon={mdiPageFirst}
|
||||
color="primary"
|
||||
class="flex place-items-center rounded-s-full gap-2 px-2 sm:px-4"
|
||||
onclick={handleFirst}
|
||||
disabled={duplicatesIndex === 0}
|
||||
>
|
||||
{$t('first')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
leadingIcon={mdiChevronLeft}
|
||||
color="primary"
|
||||
class="flex place-items-center rounded-e-full gap-2 px-2 sm:px-4"
|
||||
onclick={handlePrevious}
|
||||
disabled={duplicatesIndex === 0}
|
||||
>
|
||||
{$t('previous')}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex text-xs text-black">
|
||||
<Button
|
||||
size="small"
|
||||
trailingIcon={mdiChevronRight}
|
||||
color="primary"
|
||||
class="flex place-items-center rounded-s-full gap-2 px-2 sm:px-4"
|
||||
onclick={handleNext}
|
||||
disabled={duplicatesIndex === duplicates.length - 1}
|
||||
>
|
||||
{$t('next')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
trailingIcon={mdiPageLast}
|
||||
color="primary"
|
||||
class="flex place-items-center rounded-e-full gap-2 px-2 sm:px-4"
|
||||
onclick={handleLast}
|
||||
disabled={duplicatesIndex === duplicates.length - 1}
|
||||
>
|
||||
{$t('last')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
{:else}
|
||||
<p class="text-center text-lg dark:text-white flex place-items-center place-content-center">
|
||||
|
Loading…
x
Reference in New Issue
Block a user