Files
Immich/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte
T
Mees FrenselandCodex f96816b55f refactor(web): bulk (un)archive actions (#32189)
(cherry picked from commit d26c0eec10f1bb49117b71b667c88a8a715b2489)
2026-10-27 23:12:21 +03:00

139 lines
5.5 KiB
Svelte

<script lang="ts">
import { goto } from '$app/navigation';
import { shortcuts, type ShortcutOptions } from '#lib/actions/shortcut.ts';
import {
setFocusToAsset as setFocusAssetInit,
setFocusTo as setFocusToInit,
} from '#lib/components/timeline/actions/focus-actions.ts';
import type { AssetMultiSelectManager } from '#lib/managers/asset-multi-select-manager.svelte.ts';
import { assetViewerManager } from '#lib/managers/asset-viewer-manager.svelte.ts';
import { featureFlagsManager } from '#lib/managers/feature-flags-manager.svelte.ts';
import { TimelineManager } from '#lib/managers/timeline-manager/timeline-manager.svelte.ts';
import type { TimelineAsset } from '#lib/managers/timeline-manager/types.ts';
import AssetDeleteConfirmModal from '#lib/modals/AssetDeleteConfirmModal.svelte';
import NavigateToDateModal from '#lib/modals/NavigateToDateModal.svelte';
import ShortcutsModal from '#lib/modals/ShortcutsModal.svelte';
import { Route } from '#lib/route.ts';
import { handleStack } from '#lib/services/stack.service.ts';
import { keyboardManager } from '#lib/stores/keyboard-manager.svelte.ts';
import { showDeleteModal } from '#lib/stores/preferences.store.ts';
import { searchStore } from '#lib/stores/search.svelte.ts';
import { handlePromiseError } from '#lib/utils.ts';
import { deleteAssets } from '#lib/utils/actions.ts';
import { selectAllAssets } from '#lib/utils/asset-utils.ts';
import { isModalOpen, modalManager } from '@immich/ui';
type Props = {
timelineManager: TimelineManager;
assetInteraction: AssetMultiSelectManager;
onEscape?: () => void;
scrollToAsset: (asset: TimelineAsset) => boolean;
};
let { timelineManager = $bindable(), assetInteraction, onEscape, scrollToAsset }: Props = $props();
const trashOrDelete = async (forceRequested?: boolean) => {
const force = forceRequested || !featureFlagsManager.value.trash;
const selectedAssets = assetInteraction.assets;
if ($showDeleteModal && force) {
const confirmed = await modalManager.show(AssetDeleteConfirmModal, { size: selectedAssets.length });
if (!confirmed) {
return;
}
}
await deleteAssets(
force,
(assetIds) => timelineManager.removeAssets(assetIds),
selectedAssets,
force ? undefined : (assets) => timelineManager.upsertAssets(assets),
);
assetInteraction.clear();
};
const onDelete = () => {
const hasTrashedAsset = assetInteraction.assets.some((asset) => asset.isTrashed);
handlePromiseError(trashOrDelete(hasTrashedAsset));
};
const onStackAssets = async () => {
await handleStack(assetInteraction.assets.map((asset) => asset.id));
onEscape?.();
};
const onSelectStart = (e: Event) => {
if (!searchStore.isSearchEnabled && assetInteraction.selectionActive && keyboardManager.shift) {
e.preventDefault();
}
};
const isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
let isShortcutModalOpen = false;
const handleOpenShortcutModal = async () => {
if (isShortcutModalOpen) {
return;
}
isShortcutModalOpen = true;
await modalManager.show(ShortcutsModal, {});
isShortcutModalOpen = false;
};
$effect(() => {
if (isEmpty) {
assetInteraction.clear();
}
});
const setFocusTo = setFocusToInit.bind(undefined, scrollToAsset, timelineManager);
const setFocusAsset = setFocusAssetInit.bind(undefined, scrollToAsset);
const handleOpenDateModal = async () => {
const asset = await modalManager.show(NavigateToDateModal, { timelineManager });
if (asset) {
setFocusAsset(asset);
}
};
const shortcutList = $derived.by(() => {
if (searchStore.isSearchEnabled || assetViewerManager.isViewing || isModalOpen()) {
return [];
}
const shortcuts: ShortcutOptions[] = [
{ shortcut: { key: '?', shift: true }, onShortcut: handleOpenShortcutModal },
{ shortcut: { key: '/' }, onShortcut: () => goto(Route.explore()) },
{ shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets(timelineManager, assetInteraction) },
{ shortcut: { key: 'ArrowRight' }, onShortcut: () => setFocusTo('earlier', 'asset') },
{ shortcut: { key: 'ArrowLeft' }, onShortcut: () => setFocusTo('later', 'asset') },
{ shortcut: { key: 'D' }, onShortcut: () => setFocusTo('earlier', 'day') },
{ shortcut: { key: 'M' }, onShortcut: () => setFocusTo('earlier', 'month') },
{ shortcut: { key: 'M', shift: true }, onShortcut: () => setFocusTo('later', 'month') },
{ shortcut: { key: 'Y' }, onShortcut: () => setFocusTo('earlier', 'year') },
{ shortcut: { key: 'Y', shift: true }, onShortcut: () => setFocusTo('later', 'year') },
{ shortcut: { key: 'G' }, onShortcut: handleOpenDateModal },
];
if (onEscape) {
shortcuts.push({ shortcut: { key: 'Escape' }, onShortcut: onEscape });
}
if (assetInteraction.selectionActive) {
shortcuts.push(
{ shortcut: { key: 'Delete' }, onShortcut: onDelete },
{ shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) },
{ shortcut: { key: 'D', ctrl: true }, onShortcut: () => assetInteraction.clear() },
{ shortcut: { key: 's' }, onShortcut: () => onStackAssets() },
);
} else {
// conflicting shortcuts
shortcuts.push({ shortcut: { key: 'D', shift: true }, onShortcut: () => setFocusTo('later', 'day') });
}
return shortcuts;
});
</script>
<svelte:document onselectstart={onSelectStart} use:shortcuts={shortcutList} />