diff --git a/web/eslint.config.js b/web/eslint.config.js index f8e6cdd9c6..f139634f11 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -130,6 +130,7 @@ export default typescriptEslint.config( '@typescript-eslint/require-await': 'error', 'object-shorthand': ['error', 'always'], 'svelte/no-navigation-without-resolve': 'off', + 'svelte/no-unnecessary-state-wrap': ['error', { allowReassign: true }], }, }, { diff --git a/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte b/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte index e5f3b00429..06ad51e4a3 100644 --- a/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte +++ b/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte @@ -4,16 +4,16 @@ import Portal from '$lib/elements/Portal.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; + import { duplicateTiePreference } from '$lib/stores/duplicate-tie-preferences-manager.svelte'; import { handlePromiseError } from '$lib/utils'; + import { suggestBestDuplicate } from '$lib/utils/duplicate-utils'; import { navigate } from '$lib/utils/navigation'; import { getAssetInfo, type AssetResponseDto } from '@immich/sdk'; import { Button } from '@immich/ui'; import { mdiCheck, mdiImageMultipleOutline, mdiTrashCanOutline } from '@mdi/js'; - import { onDestroy, onMount } from 'svelte'; + import { onDestroy } from 'svelte'; import { t } from 'svelte-i18n'; import { SvelteSet } from 'svelte/reactivity'; - import { duplicateTiePreference } from '$lib/stores/duplicate-tie-preferences.svelte'; - import { suggestBestDuplicate } from '$lib/utils/duplicate-utils'; interface Props { assets: AssetResponseDto[]; onResolve: (duplicateAssetIds: string[], trashIds: string[]) => void; @@ -116,7 +116,7 @@ ]} /> -
+
diff --git a/web/src/lib/modals/DuplicatesSettingsModal.svelte b/web/src/lib/modals/DuplicatesSettingsModal.svelte index 8e89795698..2e48d3da80 100644 --- a/web/src/lib/modals/DuplicatesSettingsModal.svelte +++ b/web/src/lib/modals/DuplicatesSettingsModal.svelte @@ -1,99 +1,86 @@ - - -
-
-

{$t('deduplicate_source_preference')}

+ (confirmed ? handleConfirm() : onClose())} +> + {#snippet promptSnippet()} + + {$t('deduplicate_source_preference')} -
+ - - -
-
- -
- + + - -
- - -
-
-
-
+ + {/snippet} + diff --git a/web/src/lib/stores/duplicate-tie-preferences.svelte.ts b/web/src/lib/stores/duplicate-tie-preferences-manager.svelte.ts similarity index 66% rename from web/src/lib/stores/duplicate-tie-preferences.svelte.ts rename to web/src/lib/stores/duplicate-tie-preferences-manager.svelte.ts index b4ce342fc1..075af36328 100644 --- a/web/src/lib/stores/duplicate-tie-preferences.svelte.ts +++ b/web/src/lib/stores/duplicate-tie-preferences-manager.svelte.ts @@ -10,20 +10,15 @@ export type DuplicateTiePreferencesSvelte = PreferenceDuplicateTieItem[]; export type PreferenceDuplicateTieItem = SourcePreference; -export let duplicateTiePreference = $state<{ +export const duplicateTiePreference = $state<{ value: DuplicateTiePreferencesSvelte | undefined; }>({ value: undefined }); export const findDuplicateTiePreference = ( preference: DuplicateTiePreferencesSvelte | undefined, variant: T, -): Extract | undefined => - preference?.find( - (preference): preference is Extract => preference.variant === variant, - ); +) => preference?.find((preference) => preference.variant === variant); -export function setDuplicateTiePreference( - nextDuplicateTiePreferences: DuplicateTiePreferencesSvelte | undefined, -): void { +export function setDuplicateTiePreference(nextDuplicateTiePreferences: DuplicateTiePreferencesSvelte | undefined) { duplicateTiePreference.value = nextDuplicateTiePreferences; } diff --git a/web/src/lib/utils/duplicate-utils.spec.ts b/web/src/lib/utils/duplicate-utils.spec.ts index 5c6aad1cac..8774aa63ca 100644 --- a/web/src/lib/utils/duplicate-utils.spec.ts +++ b/web/src/lib/utils/duplicate-utils.spec.ts @@ -1,4 +1,4 @@ -import type { SourcePreference } from '$lib/stores/duplicate-tie-preferences.svelte'; +import type { SourcePreference } from '$lib/stores/duplicate-tie-preferences-manager.svelte'; import { suggestBestDuplicate } from '$lib/utils/duplicate-utils'; import type { AssetResponseDto } from '@immich/sdk'; diff --git a/web/src/lib/utils/duplicate-utils.ts b/web/src/lib/utils/duplicate-utils.ts index d139dfaf56..a619174c67 100644 --- a/web/src/lib/utils/duplicate-utils.ts +++ b/web/src/lib/utils/duplicate-utils.ts @@ -1,12 +1,11 @@ import { type DuplicateTiePreferencesSvelte, findDuplicateTiePreference, -} from '$lib/stores/duplicate-tie-preferences.svelte'; +} from '$lib/stores/duplicate-tie-preferences-manager.svelte'; import { getExifCount } from '$lib/utils/exif-utils'; import type { AssetResponseDto } from '@immich/sdk'; const sizeOf = (asset: AssetResponseDto) => asset.exifInfo?.fileSizeInByte ?? 0; -const isExternal = (asset: AssetResponseDto) => Boolean(asset.libraryId); /** * Suggests the best duplicate asset to keep from a list of duplicates. @@ -16,16 +15,12 @@ const isExternal = (asset: AssetResponseDto) => Boolean(asset.libraryId); * - Largest count of exif data * - Optional source preference (internal vs external) * - * @param assets List of duplicate assets - * @param preference Preference for selecting duplicates - * @returns The best asset to keep - * */ export function suggestBestDuplicate( assets: AssetResponseDto[], preference: DuplicateTiePreferencesSvelte | undefined, ): AssetResponseDto | undefined { - if (!assets.length) { + if (assets.length === 0) { return; } let candidates = filterBySizeAndExif(assets); @@ -38,11 +33,11 @@ export function suggestBestDuplicate( } const filterBySizeAndExif = (assets: AssetResponseDto[]): AssetResponseDto[] => { - const maxSize = Math.max(...assets.map(sizeOf)); - const sizeFiltered = assets.filter((assets) => sizeOf(assets) === maxSize); + const maxSize = Math.max(...assets.map((asset) => sizeOf(asset))); + const sizeFilteredAssets = assets.filter((assets) => sizeOf(assets) === maxSize); - const maxExif = Math.max(...sizeFiltered.map(getExifCount)); - return sizeFiltered.filter((assets) => getExifCount(assets) === maxExif); + const maxExif = Math.max(...sizeFilteredAssets.map((asset) => getExifCount(asset))); + return sizeFilteredAssets.filter((assets) => getExifCount(assets) === maxExif); }; const filterBySource = (assets: AssetResponseDto[], priority: 'internal' | 'external'): AssetResponseDto[] => { 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 3625b324c9..0bef930983 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 @@ -7,30 +7,30 @@ import { AppRoute } from '$lib/constants'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; import DuplicatesInformationModal from '$lib/modals/DuplicatesInformationModal.svelte'; + import DuplicatesSettingsModal from '$lib/modals/DuplicatesSettingsModal.svelte'; import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; + import { duplicateTiePreference } from '$lib/stores/duplicate-tie-preferences-manager.svelte'; import { locale } from '$lib/stores/preferences.store'; import { stackAssets } from '$lib/utils/asset-utils'; + import { suggestBestDuplicate } from '$lib/utils/duplicate-utils'; import { handleError } from '$lib/utils/handle-error'; import type { AssetResponseDto } from '@immich/sdk'; import { deleteAssets, deleteDuplicates, updateAssets } from '@immich/sdk'; - import DuplicatesSettingsModal from '$lib/modals/DuplicatesSettingsModal.svelte'; import { Button, HStack, IconButton, modalManager, Text, toastManager } from '@immich/ui'; import { mdiCheckOutline, mdiChevronLeft, mdiChevronRight, + mdiCogOutline, mdiInformationOutline, mdiKeyboard, mdiPageFirst, mdiPageLast, mdiTrashCanOutline, - mdiCogOutline, } from '@mdi/js'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; - import { duplicateTiePreference } from '$lib/stores/duplicate-tie-preferences.svelte'; - import { suggestBestDuplicate } from '$lib/utils/duplicate-utils'; interface Props { data: PageData; @@ -127,16 +127,12 @@ }; const handleDeduplicateAll = async () => { - const keepCandidates = duplicates.map((group) => suggestBestDuplicate(group.assets, duplicateTiePreference.value)); - - const idsToKeep: (string | undefined)[] = keepCandidates.map((assets) => assets?.id); + const idsToKeep = duplicates.map((group) => suggestBestDuplicate(group.assets, duplicateTiePreference.value)?.id); const idsToDelete = duplicates.flatMap((group, i) => - group.assets.map((asset) => asset.id).filter((id) => id !== idsToKeep[i]), + group.assets.map(({ id }) => id).filter((id) => id !== idsToKeep[i]), ); - const keptIds = idsToKeep.filter((id): id is string => id !== undefined); - let prompt, confirmText; if (featureFlagsManager.value.trash) { prompt = $t('bulk_trash_duplicates_confirmation', { values: { count: idsToDelete.length } }); @@ -151,7 +147,7 @@ await deleteAssets({ assetBulkDeleteDto: { ids: idsToDelete, force: !featureFlagsManager.value.trash } }); await updateAssets({ assetBulkUpdateDto: { - ids: [...idsToDelete, ...keptIds], + ids: [...idsToDelete, ...idsToKeep.filter((id) => id !== undefined)], duplicateId: null, }, });