mirror of
https://github.com/immich-app/immich.git
synced 2025-12-29 09:14:59 +03:00
feat(web): bulk deduplicate (#10448)
* bulk deduplicate * notification for keeping all duplicates * fix notification * remove unused text * pr feedback * wording * formatting
This commit is contained in:
@@ -11,42 +11,129 @@
|
||||
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 Icon from '$lib/components/elements/icon.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const handleResolve = async (duplicateId: string, duplicateAssetIds: string[], trashIds: string[]) => {
|
||||
try {
|
||||
if (!$featureFlags.trash && trashIds.length > 0) {
|
||||
const isConfirmed = await dialogController.show({
|
||||
prompt: $t('delete_duplicates_confirmation'),
|
||||
confirmText: $t('permanently_delete'),
|
||||
});
|
||||
$: hasDuplicates = data.duplicates.length > 0;
|
||||
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await updateAssets({ assetBulkUpdateDto: { ids: duplicateAssetIds, duplicateId: null } });
|
||||
await deleteAssets({ assetBulkDeleteDto: { ids: trashIds, force: !$featureFlags.trash } });
|
||||
|
||||
data.duplicates = data.duplicates.filter((duplicate) => duplicate.duplicateId !== duplicateId);
|
||||
|
||||
if (trashIds.length === 0) {
|
||||
const withConfirmation = async (callback: () => Promise<void>, prompt?: string, confirmText?: string) => {
|
||||
if (prompt && confirmText) {
|
||||
const isConfirmed = await dialogController.show({ prompt, confirmText });
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
notificationController.show({
|
||||
message: $t('assets_moved_to_trash', { values: { count: trashIds.length } }),
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
try {
|
||||
return await callback();
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_resolve_duplicate'));
|
||||
}
|
||||
};
|
||||
|
||||
const deletedNotification = (trashedCount: number) => {
|
||||
if (!trashedCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
notificationController.show({
|
||||
message: $featureFlags.trash
|
||||
? $t('assets_moved_to_trash', { values: { count: trashedCount } })
|
||||
: $t('permanently_deleted_assets', { values: { count: trashedCount } }),
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
};
|
||||
|
||||
const handleResolve = async (duplicateId: string, duplicateAssetIds: string[], trashIds: string[]) => {
|
||||
return withConfirmation(
|
||||
async () => {
|
||||
await deleteAssets({ assetBulkDeleteDto: { ids: trashIds, force: !$featureFlags.trash } });
|
||||
await updateAssets({ assetBulkUpdateDto: { ids: duplicateAssetIds, duplicateId: null } });
|
||||
|
||||
data.duplicates = data.duplicates.filter((duplicate) => duplicate.duplicateId !== duplicateId);
|
||||
|
||||
deletedNotification(trashIds.length);
|
||||
},
|
||||
trashIds.length > 0 && !$featureFlags.trash ? $t('delete_duplicates_confirmation') : undefined,
|
||||
trashIds.length > 0 && !$featureFlags.trash ? $t('permanently_delete') : undefined,
|
||||
);
|
||||
};
|
||||
|
||||
const handleDeduplicateAll = async () => {
|
||||
const idsToKeep = data.duplicates
|
||||
.map((group) => suggestDuplicateByFileSize(group.assets))
|
||||
.map((asset) => asset?.id);
|
||||
const idsToDelete = data.duplicates.flatMap((group, i) =>
|
||||
group.assets.map((asset) => asset.id).filter((asset) => asset !== idsToKeep[i]),
|
||||
);
|
||||
|
||||
let prompt, confirmText;
|
||||
if ($featureFlags.trash) {
|
||||
prompt = $t('bulk_trash_duplicates_confirmation', { values: { count: idsToDelete.length } });
|
||||
confirmText = $t('confirm');
|
||||
} else {
|
||||
prompt = $t('bulk_delete_duplicates_confirmation', { values: { count: idsToDelete.length } });
|
||||
confirmText = $t('permanently_delete');
|
||||
}
|
||||
|
||||
return withConfirmation(
|
||||
async () => {
|
||||
await deleteAssets({ assetBulkDeleteDto: { ids: idsToDelete, force: !$featureFlags.trash } });
|
||||
await updateAssets({
|
||||
assetBulkUpdateDto: {
|
||||
ids: [...idsToDelete, ...idsToKeep.filter((id): id is string => !!id)],
|
||||
duplicateId: null,
|
||||
},
|
||||
});
|
||||
|
||||
data.duplicates = [];
|
||||
|
||||
deletedNotification(idsToDelete.length);
|
||||
},
|
||||
prompt,
|
||||
confirmText,
|
||||
);
|
||||
};
|
||||
|
||||
const handleKeepAll = async () => {
|
||||
const ids = data.duplicates.flatMap((group) => group.assets.map((asset) => asset.id));
|
||||
return withConfirmation(
|
||||
async () => {
|
||||
await updateAssets({ assetBulkUpdateDto: { ids, duplicateId: null } });
|
||||
|
||||
data.duplicates = [];
|
||||
|
||||
notificationController.show({
|
||||
message: $t('resolved_all_duplicates'),
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
},
|
||||
$t('bulk_keep_duplicates_confirmation', { values: { count: ids.length } }),
|
||||
$t('confirm'),
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<UserPageLayout title={data.meta.title + ` (${data.duplicates.length})`} scrollbar={true}>
|
||||
<div class="flex place-items-center gap-2" slot="buttons">
|
||||
<LinkButton on:click={() => handleDeduplicateAll()} disabled={!hasDuplicates}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiTrashCanOutline} size="18" />
|
||||
{$t('deduplicate_all')}
|
||||
</div>
|
||||
</LinkButton>
|
||||
<LinkButton on:click={() => handleKeepAll()} disabled={!hasDuplicates}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiCheckOutline} size="18" />
|
||||
{$t('keep_all')}
|
||||
</div>
|
||||
</LinkButton>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
{#if data.duplicates && data.duplicates.length > 0}
|
||||
<div class="mb-4 text-sm dark:text-white">
|
||||
|
||||
Reference in New Issue
Block a user