mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
refactor: delete confirm modal (#25135)
This commit is contained in:
parent
6997ed83c4
commit
471fab0591
8 changed files with 106 additions and 159 deletions
|
|
@ -7,6 +7,13 @@ import DeleteAction from './delete-action.svelte';
|
||||||
let asset: AssetResponseDto;
|
let asset: AssetResponseDto;
|
||||||
|
|
||||||
describe('DeleteAction component', () => {
|
describe('DeleteAction component', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.mock(import('$lib/managers/feature-flags-manager.svelte'), () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return { featureFlagsManager: { init: vi.fn(), loadFeatureFlags: vi.fn(), value: { trash: true } } as any };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('given an asset which is not trashed yet', () => {
|
describe('given an asset which is not trashed yet', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
asset = assetFactory.build({ isTrashed: false });
|
asset = assetFactory.build({ isTrashed: false });
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { shortcuts } from '$lib/actions/shortcut';
|
import { shortcuts } from '$lib/actions/shortcut';
|
||||||
import DeleteAssetDialog from '$lib/components/photos-page/delete-asset-dialog.svelte';
|
|
||||||
import { AssetAction } from '$lib/constants';
|
import { AssetAction } from '$lib/constants';
|
||||||
import Portal from '$lib/elements/Portal.svelte';
|
|
||||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||||
|
import AssetDeleteConfirmModal from '$lib/modals/AssetDeleteConfirmModal.svelte';
|
||||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||||
import { deleteAssets as deleteAssetsUtil, type OnUndoDelete } from '$lib/utils/actions';
|
import { deleteAssets as deleteAssetsUtil, type OnUndoDelete } from '$lib/utils/actions';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { deleteAssets, type AssetResponseDto } from '@immich/sdk';
|
import { deleteAssets, type AssetResponseDto } from '@immich/sdk';
|
||||||
import { IconButton, toastManager } from '@immich/ui';
|
import { IconButton, modalManager, toastManager } from '@immich/ui';
|
||||||
import { mdiDeleteForeverOutline, mdiDeleteOutline } from '@mdi/js';
|
import { mdiDeleteForeverOutline, mdiDeleteOutline } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { OnAction, PreAction } from './action';
|
import type { OnAction, PreAction } from './action';
|
||||||
|
|
@ -23,24 +22,32 @@
|
||||||
|
|
||||||
let { asset, onAction, preAction, onUndoDelete = undefined }: Props = $props();
|
let { asset, onAction, preAction, onUndoDelete = undefined }: Props = $props();
|
||||||
|
|
||||||
let showConfirmModal = $state(false);
|
const forceDefault = $derived(asset.isTrashed || !featureFlagsManager.value.trash);
|
||||||
|
|
||||||
const trashOrDelete = async (force = false) => {
|
const trashOrDelete = async (forceRequest?: boolean) => {
|
||||||
if (force || !featureFlagsManager.value.trash) {
|
const timelineAsset = toTimelineAsset(asset);
|
||||||
|
const force = forceDefault || forceRequest;
|
||||||
|
|
||||||
|
if (force) {
|
||||||
if ($showDeleteModal) {
|
if ($showDeleteModal) {
|
||||||
showConfirmModal = true;
|
const confirmed = await modalManager.show(AssetDeleteConfirmModal, { size: 1 });
|
||||||
return;
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await deleteAsset();
|
|
||||||
|
try {
|
||||||
|
preAction({ type: AssetAction.DELETE, asset: timelineAsset });
|
||||||
|
await deleteAssets({ assetBulkDeleteDto: { ids: [asset.id], force: true } });
|
||||||
|
onAction({ type: AssetAction.DELETE, asset: timelineAsset });
|
||||||
|
toastManager.success($t('permanently_deleted_asset'));
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.unable_to_delete_asset'));
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await trashAsset();
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
const trashAsset = async () => {
|
|
||||||
const timelineAsset = toTimelineAsset(asset);
|
|
||||||
preAction({ type: AssetAction.TRASH, asset: timelineAsset });
|
preAction({ type: AssetAction.TRASH, asset: timelineAsset });
|
||||||
await deleteAssetsUtil(
|
await deleteAssetsUtil(
|
||||||
false,
|
false,
|
||||||
|
|
@ -49,24 +56,11 @@
|
||||||
onUndoDelete,
|
onUndoDelete,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteAsset = async () => {
|
|
||||||
try {
|
|
||||||
preAction({ type: AssetAction.DELETE, asset: toTimelineAsset(asset) });
|
|
||||||
await deleteAssets({ assetBulkDeleteDto: { ids: [asset.id], force: true } });
|
|
||||||
onAction({ type: AssetAction.DELETE, asset: toTimelineAsset(asset) });
|
|
||||||
toastManager.success($t('permanently_deleted_asset'));
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.unable_to_delete_asset'));
|
|
||||||
} finally {
|
|
||||||
showConfirmModal = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:document
|
<svelte:document
|
||||||
use:shortcuts={[
|
use:shortcuts={[
|
||||||
{ shortcut: { key: 'Delete' }, onShortcut: () => trashOrDelete(asset.isTrashed) },
|
{ shortcut: { key: 'Delete' }, onShortcut: () => trashOrDelete() },
|
||||||
{ shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) },
|
{ shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
@ -75,13 +69,7 @@
|
||||||
color="secondary"
|
color="secondary"
|
||||||
shape="round"
|
shape="round"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon={asset.isTrashed ? mdiDeleteForeverOutline : mdiDeleteOutline}
|
icon={forceDefault ? mdiDeleteForeverOutline : mdiDeleteOutline}
|
||||||
aria-label={asset.isTrashed ? $t('permanently_delete') : $t('delete')}
|
aria-label={forceDefault ? $t('permanently_delete') : $t('delete')}
|
||||||
onclick={() => trashOrDelete(asset.isTrashed)}
|
onclick={() => trashOrDelete()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if showConfirmModal}
|
|
||||||
<Portal target="body">
|
|
||||||
<DeleteAssetDialog size={1} onCancel={() => (showConfirmModal = false)} onConfirm={deleteAsset} />
|
|
||||||
</Portal>
|
|
||||||
{/if}
|
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,14 @@ describe('AssetViewerNavBar component', () => {
|
||||||
vi.fn(() => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn() })),
|
vi.fn(() => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn() })),
|
||||||
);
|
);
|
||||||
vi.mock(import('$lib/managers/feature-flags-manager.svelte'), () => {
|
vi.mock(import('$lib/managers/feature-flags-manager.svelte'), () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
return {
|
||||||
return { featureFlagsManager: { init: vi.fn(), loadFeatureFlags: vi.fn(), value: { smartSearch: true } } as any };
|
featureFlagsManager: {
|
||||||
|
init: vi.fn(),
|
||||||
|
loadFeatureFlags: vi.fn(),
|
||||||
|
value: { trash: true, smartSearch: true },
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} as any,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import Portal from '$lib/elements/Portal.svelte';
|
import Portal from '$lib/elements/Portal.svelte';
|
||||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||||
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
|
||||||
|
import AssetDeleteConfirmModal from '$lib/modals/AssetDeleteConfirmModal.svelte';
|
||||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
|
|
@ -23,9 +24,8 @@
|
||||||
import { modalManager } from '@immich/ui';
|
import { modalManager } from '@immich/ui';
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import DeleteAssetDialog from '../../photos-page/delete-asset-dialog.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
type Props = {
|
||||||
initialAssetId?: string;
|
initialAssetId?: string;
|
||||||
assets: AssetResponseDto[];
|
assets: AssetResponseDto[];
|
||||||
assetInteraction: AssetInteraction;
|
assetInteraction: AssetInteraction;
|
||||||
|
|
@ -34,7 +34,6 @@
|
||||||
viewport: Viewport;
|
viewport: Viewport;
|
||||||
onIntersected?: (() => void) | undefined;
|
onIntersected?: (() => void) | undefined;
|
||||||
showAssetName?: boolean;
|
showAssetName?: boolean;
|
||||||
isShowDeleteConfirmation?: boolean;
|
|
||||||
onPrevious?: (() => Promise<{ id: string } | undefined>) | undefined;
|
onPrevious?: (() => Promise<{ id: string } | undefined>) | undefined;
|
||||||
onNext?: (() => Promise<{ id: string } | undefined>) | undefined;
|
onNext?: (() => Promise<{ id: string } | undefined>) | undefined;
|
||||||
onRandom?: (() => Promise<{ id: string } | undefined>) | undefined;
|
onRandom?: (() => Promise<{ id: string } | undefined>) | undefined;
|
||||||
|
|
@ -42,7 +41,7 @@
|
||||||
pageHeaderOffset?: number;
|
pageHeaderOffset?: number;
|
||||||
slidingWindowOffset?: number;
|
slidingWindowOffset?: number;
|
||||||
arrowNavigation?: boolean;
|
arrowNavigation?: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
let {
|
let {
|
||||||
initialAssetId = undefined,
|
initialAssetId = undefined,
|
||||||
|
|
@ -53,7 +52,6 @@
|
||||||
viewport,
|
viewport,
|
||||||
onIntersected = undefined,
|
onIntersected = undefined,
|
||||||
showAssetName = false,
|
showAssetName = false,
|
||||||
isShowDeleteConfirmation = $bindable(false),
|
|
||||||
onPrevious = undefined,
|
onPrevious = undefined,
|
||||||
onNext = undefined,
|
onNext = undefined,
|
||||||
onRandom = undefined,
|
onRandom = undefined,
|
||||||
|
|
@ -209,30 +207,27 @@
|
||||||
|
|
||||||
const onDelete = () => {
|
const onDelete = () => {
|
||||||
const hasTrashedAsset = assetInteraction.selectedAssets.some((asset) => asset.isTrashed);
|
const hasTrashedAsset = assetInteraction.selectedAssets.some((asset) => asset.isTrashed);
|
||||||
|
|
||||||
if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) {
|
|
||||||
isShowDeleteConfirmation = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handlePromiseError(trashOrDelete(hasTrashedAsset));
|
handlePromiseError(trashOrDelete(hasTrashedAsset));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onForceDelete = () => {
|
|
||||||
if ($showDeleteModal) {
|
|
||||||
isShowDeleteConfirmation = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handlePromiseError(trashOrDelete(true));
|
|
||||||
};
|
|
||||||
|
|
||||||
const trashOrDelete = async (force: boolean = false) => {
|
const trashOrDelete = async (force: boolean = false) => {
|
||||||
isShowDeleteConfirmation = false;
|
const forceOrNoTrash = force || !featureFlagsManager.value.trash;
|
||||||
|
const selectedAssets = assetInteraction.selectedAssets;
|
||||||
|
|
||||||
|
if ($showDeleteModal && forceOrNoTrash) {
|
||||||
|
const confirmed = await modalManager.show(AssetDeleteConfirmModal, { size: selectedAssets.length });
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await deleteAssets(
|
await deleteAssets(
|
||||||
!(isTrashEnabled && !force),
|
forceOrNoTrash,
|
||||||
(assetIds) => (assets = assets.filter((asset) => !assetIds.includes(asset.id))),
|
(assetIds) => (assets = assets.filter((asset) => !assetIds.includes(asset.id))),
|
||||||
assetInteraction.selectedAssets,
|
selectedAssets,
|
||||||
onReload,
|
onReload,
|
||||||
);
|
);
|
||||||
|
|
||||||
assetInteraction.clearMultiselect();
|
assetInteraction.clearMultiselect();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -285,7 +280,7 @@
|
||||||
shortcuts.push(
|
shortcuts.push(
|
||||||
{ shortcut: { key: 'Escape' }, onShortcut: deselectAllAssets },
|
{ shortcut: { key: 'Escape' }, onShortcut: deselectAllAssets },
|
||||||
{ shortcut: { key: 'Delete' }, onShortcut: onDelete },
|
{ shortcut: { key: 'Delete' }, onShortcut: onDelete },
|
||||||
{ shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete },
|
{ shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) },
|
||||||
{ shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() },
|
{ shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() },
|
||||||
{ shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
|
{ shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
|
||||||
);
|
);
|
||||||
|
|
@ -405,8 +400,6 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let isTrashEnabled = $derived(featureFlagsManager.value.trash);
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!lastAssetMouseEvent) {
|
if (!lastAssetMouseEvent) {
|
||||||
assetInteraction.clearAssetSelectionCandidates();
|
assetInteraction.clearAssetSelectionCandidates();
|
||||||
|
|
@ -440,14 +433,6 @@
|
||||||
onscroll={() => updateSlidingWindow()}
|
onscroll={() => updateSlidingWindow()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if isShowDeleteConfirmation}
|
|
||||||
<DeleteAssetDialog
|
|
||||||
size={assetInteraction.selectedAssets.length}
|
|
||||||
onCancel={() => (isShowDeleteConfirmation = false)}
|
|
||||||
onConfirm={() => handlePromiseError(trashOrDelete(true))}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if assets.length > 0}
|
{#if assets.length > 0}
|
||||||
<div
|
<div
|
||||||
style:position="relative"
|
style:position="relative"
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@
|
||||||
album?: AlbumResponseDto;
|
album?: AlbumResponseDto;
|
||||||
albumUsers?: UserResponseDto[];
|
albumUsers?: UserResponseDto[];
|
||||||
person?: PersonResponseDto;
|
person?: PersonResponseDto;
|
||||||
isShowDeleteConfirmation?: boolean;
|
|
||||||
onSelect?: (asset: TimelineAsset) => void;
|
onSelect?: (asset: TimelineAsset) => void;
|
||||||
onEscape?: () => void;
|
onEscape?: () => void;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
|
|
@ -79,7 +78,6 @@
|
||||||
album,
|
album,
|
||||||
albumUsers = [],
|
albumUsers = [],
|
||||||
person,
|
person,
|
||||||
isShowDeleteConfirmation = $bindable(false),
|
|
||||||
onSelect = () => {},
|
onSelect = () => {},
|
||||||
onEscape = () => {},
|
onEscape = () => {},
|
||||||
children,
|
children,
|
||||||
|
|
@ -600,7 +598,6 @@
|
||||||
scrollToAsset={(asset) => scrollToAsset(asset) ?? false}
|
scrollToAsset={(asset) => scrollToAsset(asset) ?? false}
|
||||||
{timelineManager}
|
{timelineManager}
|
||||||
{assetInteraction}
|
{assetInteraction}
|
||||||
bind:isShowDeleteConfirmation
|
|
||||||
{onEscape}
|
{onEscape}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,48 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DeleteAssetDialog from '$lib/components/photos-page/delete-asset-dialog.svelte';
|
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||||
|
import AssetDeleteConfirmModal from '$lib/modals/AssetDeleteConfirmModal.svelte';
|
||||||
|
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||||
import { type OnDelete, type OnUndoDelete, deleteAssets } from '$lib/utils/actions';
|
import { type OnDelete, type OnUndoDelete, deleteAssets } from '$lib/utils/actions';
|
||||||
import { IconButton } from '@immich/ui';
|
import { IconButton, modalManager } from '@immich/ui';
|
||||||
import { mdiDeleteForeverOutline, mdiDeleteOutline, mdiTimerSand } from '@mdi/js';
|
import { mdiDeleteForeverOutline, mdiDeleteOutline, mdiTimerSand } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
type Props = {
|
||||||
onAssetDelete: OnDelete;
|
onAssetDelete: OnDelete;
|
||||||
onUndoDelete?: OnUndoDelete | undefined;
|
onUndoDelete?: OnUndoDelete | undefined;
|
||||||
menuItem?: boolean;
|
menuItem?: boolean;
|
||||||
force?: boolean;
|
force?: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
let {
|
let { onAssetDelete, onUndoDelete = undefined, menuItem = false, force: forceRequested }: Props = $props();
|
||||||
onAssetDelete,
|
|
||||||
onUndoDelete = undefined,
|
const force = $derived(forceRequested || !featureFlagsManager.value.trash);
|
||||||
menuItem = false,
|
let label = $derived(force ? $t('permanently_delete') : $t('delete'));
|
||||||
force = !featureFlagsManager.value.trash,
|
let loading = $state(false);
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||||
|
|
||||||
let isShowConfirmation = $state(false);
|
const onAction = async () => {
|
||||||
let loading = $state(false);
|
const assets = getOwnedAssets();
|
||||||
|
|
||||||
let label = $derived(force ? $t('permanently_delete') : $t('delete'));
|
if (force && $showDeleteModal) {
|
||||||
|
const confirmed = await modalManager.show(AssetDeleteConfirmModal, { size: assets.length });
|
||||||
const handleTrash = async () => {
|
if (!confirmed) {
|
||||||
if (force) {
|
return;
|
||||||
isShowConfirmation = true;
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await handleDelete();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
loading = true;
|
loading = true;
|
||||||
const assets = [...getOwnedAssets()];
|
|
||||||
await deleteAssets(force, onAssetDelete, assets, onUndoDelete);
|
await deleteAssets(force, onAssetDelete, assets, onUndoDelete);
|
||||||
clearSelect();
|
clearSelect();
|
||||||
isShowConfirmation = false;
|
|
||||||
loading = false;
|
loading = false;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if menuItem}
|
{#if menuItem}
|
||||||
<MenuOption text={label} icon={mdiDeleteOutline} onClick={handleTrash} />
|
<MenuOption text={label} icon={mdiDeleteOutline} onClick={onAction} />
|
||||||
{:else if loading}
|
{:else if loading}
|
||||||
<IconButton
|
<IconButton
|
||||||
shape="round"
|
shape="round"
|
||||||
|
|
@ -66,14 +59,6 @@
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
icon={mdiDeleteForeverOutline}
|
icon={mdiDeleteForeverOutline}
|
||||||
onclick={handleTrash}
|
onclick={onAction}
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if isShowConfirmation}
|
|
||||||
<DeleteAssetDialog
|
|
||||||
size={getOwnedAssets().length}
|
|
||||||
onConfirm={handleDelete}
|
|
||||||
onCancel={() => (isShowConfirmation = false)}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut';
|
import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut';
|
||||||
import DeleteAssetDialog from '$lib/components/photos-page/delete-asset-dialog.svelte';
|
|
||||||
import {
|
import {
|
||||||
setFocusToAsset as setFocusAssetInit,
|
setFocusToAsset as setFocusAssetInit,
|
||||||
setFocusTo as setFocusToInit,
|
setFocusTo as setFocusToInit,
|
||||||
|
|
@ -10,6 +9,7 @@
|
||||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
|
import AssetDeleteConfirmModal from '$lib/modals/AssetDeleteConfirmModal.svelte';
|
||||||
import NavigateToDateModal from '$lib/modals/NavigateToDateModal.svelte';
|
import NavigateToDateModal from '$lib/modals/NavigateToDateModal.svelte';
|
||||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
|
|
@ -22,53 +22,42 @@
|
||||||
import { AssetVisibility } from '@immich/sdk';
|
import { AssetVisibility } from '@immich/sdk';
|
||||||
import { modalManager } from '@immich/ui';
|
import { modalManager } from '@immich/ui';
|
||||||
|
|
||||||
interface Props {
|
type Props = {
|
||||||
timelineManager: TimelineManager;
|
timelineManager: TimelineManager;
|
||||||
assetInteraction: AssetInteraction;
|
assetInteraction: AssetInteraction;
|
||||||
isShowDeleteConfirmation: boolean;
|
|
||||||
onEscape?: () => void;
|
onEscape?: () => void;
|
||||||
scrollToAsset: (asset: TimelineAsset) => boolean;
|
scrollToAsset: (asset: TimelineAsset) => boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
let {
|
let { timelineManager = $bindable(), assetInteraction, onEscape, scrollToAsset }: Props = $props();
|
||||||
timelineManager = $bindable(),
|
|
||||||
assetInteraction,
|
|
||||||
isShowDeleteConfirmation = $bindable(false),
|
|
||||||
onEscape,
|
|
||||||
scrollToAsset,
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
const { isViewing: showAssetViewer } = assetViewingStore;
|
const { isViewing: showAssetViewer } = assetViewingStore;
|
||||||
|
|
||||||
const trashOrDelete = async (force: boolean = false) => {
|
const trashOrDelete = async (forceRequested?: boolean) => {
|
||||||
isShowDeleteConfirmation = false;
|
const force = forceRequested || !featureFlagsManager.value.trash;
|
||||||
|
const selectedAssets = assetInteraction.selectedAssets;
|
||||||
|
|
||||||
|
if ($showDeleteModal && force) {
|
||||||
|
const confirmed = await modalManager.show(AssetDeleteConfirmModal, { size: selectedAssets.length });
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await deleteAssets(
|
await deleteAssets(
|
||||||
!(isTrashEnabled && !force),
|
force,
|
||||||
(assetIds) => timelineManager.removeAssets(assetIds),
|
(assetIds) => timelineManager.removeAssets(assetIds),
|
||||||
assetInteraction.selectedAssets,
|
selectedAssets,
|
||||||
!isTrashEnabled || force ? undefined : (assets) => timelineManager.upsertAssets(assets),
|
force ? undefined : (assets) => timelineManager.upsertAssets(assets),
|
||||||
);
|
);
|
||||||
assetInteraction.clearMultiselect();
|
assetInteraction.clearMultiselect();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDelete = () => {
|
const onDelete = () => {
|
||||||
const hasTrashedAsset = assetInteraction.selectedAssets.some((asset) => asset.isTrashed);
|
const hasTrashedAsset = assetInteraction.selectedAssets.some((asset) => asset.isTrashed);
|
||||||
|
|
||||||
if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) {
|
|
||||||
isShowDeleteConfirmation = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handlePromiseError(trashOrDelete(hasTrashedAsset));
|
handlePromiseError(trashOrDelete(hasTrashedAsset));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onForceDelete = () => {
|
|
||||||
if ($showDeleteModal) {
|
|
||||||
isShowDeleteConfirmation = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handlePromiseError(trashOrDelete(true));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onStackAssets = async () => {
|
const onStackAssets = async () => {
|
||||||
const result = await stackAssets(assetInteraction.selectedAssets);
|
const result = await stackAssets(assetInteraction.selectedAssets);
|
||||||
|
|
||||||
|
|
@ -118,9 +107,7 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isTrashEnabled = $derived(featureFlagsManager.value.trash);
|
|
||||||
const isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
|
const isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
|
||||||
const idsSelectedAssets = $derived(assetInteraction.selectedAssets.map(({ id }) => id));
|
|
||||||
let isShortcutModalOpen = false;
|
let isShortcutModalOpen = false;
|
||||||
|
|
||||||
const handleOpenShortcutModal = async () => {
|
const handleOpenShortcutModal = async () => {
|
||||||
|
|
@ -176,7 +163,7 @@
|
||||||
if (assetInteraction.selectionActive) {
|
if (assetInteraction.selectionActive) {
|
||||||
shortcuts.push(
|
shortcuts.push(
|
||||||
{ shortcut: { key: 'Delete' }, onShortcut: onDelete },
|
{ shortcut: { key: 'Delete' }, onShortcut: onDelete },
|
||||||
{ shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete },
|
{ shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) },
|
||||||
{ shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() },
|
{ shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() },
|
||||||
{ shortcut: { key: 's' }, onShortcut: () => onStackAssets() },
|
{ shortcut: { key: 's' }, onShortcut: () => onStackAssets() },
|
||||||
{ shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
|
{ shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
|
||||||
|
|
@ -189,11 +176,3 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} onselectstart={onSelectStart} use:shortcuts={shortcutList} />
|
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} onselectstart={onSelectStart} use:shortcuts={shortcutList} />
|
||||||
|
|
||||||
{#if isShowDeleteConfirmation}
|
|
||||||
<DeleteAssetDialog
|
|
||||||
size={idsSelectedAssets.length}
|
|
||||||
onCancel={() => (isShowDeleteConfirmation = false)}
|
|
||||||
onConfirm={() => handlePromiseError(trashOrDelete(true))}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,21 @@
|
||||||
import { mdiDeleteForeverOutline } from '@mdi/js';
|
import { mdiDeleteForeverOutline } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
interface Props {
|
type Props = {
|
||||||
size: number;
|
size: number;
|
||||||
onConfirm: () => void;
|
onClose: (confirmed?: boolean) => void;
|
||||||
onCancel: () => void;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
let { size, onConfirm, onCancel }: Props = $props();
|
let { size, onClose: onCloseParent }: Props = $props();
|
||||||
|
|
||||||
let checked = $state(false);
|
let checked = $state(false);
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const onClose = (confirmed: boolean) => {
|
||||||
if (checked) {
|
if (confirmed && checked) {
|
||||||
$showDeleteModal = false;
|
$showDeleteModal = false;
|
||||||
}
|
}
|
||||||
onConfirm();
|
|
||||||
|
onCloseParent(confirmed);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
title={$t('permanently_delete_assets_count', { values: { count: size } })}
|
title={$t('permanently_delete_assets_count', { values: { count: size } })}
|
||||||
confirmText={$t('delete')}
|
confirmText={$t('delete')}
|
||||||
icon={mdiDeleteForeverOutline}
|
icon={mdiDeleteForeverOutline}
|
||||||
onClose={(confirmed) => (confirmed ? handleConfirm() : onCancel())}
|
{onClose}
|
||||||
>
|
>
|
||||||
{#snippet promptSnippet()}
|
{#snippet promptSnippet()}
|
||||||
<p>
|
<p>
|
||||||
Loading…
Reference in a new issue