203 lines
7.1 KiB
Svelte
203 lines
7.1 KiB
Svelte
<script lang="ts">
|
|
import { goto } from '$app/navigation';
|
|
import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut';
|
|
import DeleteAssetDialog from '$lib/components/photos-page/delete-asset-dialog.svelte';
|
|
import {
|
|
setFocusToAsset as setFocusAssetInit,
|
|
setFocusTo as setFocusToInit,
|
|
} from '$lib/components/timeline/actions/focus-actions';
|
|
import { AppRoute } from '$lib/constants';
|
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
|
import NavigateToDateModal from '$lib/modals/NavigateToDateModal.svelte';
|
|
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
|
import { showDeleteModal } from '$lib/stores/preferences.store';
|
|
import { searchStore } from '$lib/stores/search.svelte';
|
|
import { featureFlags } from '$lib/stores/server-config.store';
|
|
import { handlePromiseError } from '$lib/utils';
|
|
import { deleteAssets, updateStackedAssetInTimeline } from '$lib/utils/actions';
|
|
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
|
import { AssetVisibility } from '@immich/sdk';
|
|
import { modalManager } from '@immich/ui';
|
|
|
|
interface Props {
|
|
timelineManager: TimelineManager;
|
|
assetInteraction: AssetInteraction;
|
|
isShowDeleteConfirmation: boolean;
|
|
onEscape?: () => void;
|
|
scrollToAsset: (asset: TimelineAsset) => boolean;
|
|
}
|
|
|
|
let {
|
|
timelineManager = $bindable(),
|
|
assetInteraction,
|
|
isShowDeleteConfirmation = $bindable(false),
|
|
onEscape,
|
|
scrollToAsset,
|
|
}: Props = $props();
|
|
|
|
const { isViewing: showAssetViewer } = assetViewingStore;
|
|
|
|
const trashOrDelete = async (force: boolean = false) => {
|
|
isShowDeleteConfirmation = false;
|
|
await deleteAssets(
|
|
!(isTrashEnabled && !force),
|
|
(assetIds) => timelineManager.removeAssets(assetIds),
|
|
assetInteraction.selectedAssets,
|
|
!isTrashEnabled || force ? undefined : (assets) => timelineManager.addAssets(assets),
|
|
);
|
|
assetInteraction.clearMultiselect();
|
|
};
|
|
|
|
const onDelete = () => {
|
|
const hasTrashedAsset = assetInteraction.selectedAssets.some((asset) => asset.isTrashed);
|
|
|
|
if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) {
|
|
isShowDeleteConfirmation = true;
|
|
return;
|
|
}
|
|
handlePromiseError(trashOrDelete(hasTrashedAsset));
|
|
};
|
|
|
|
const onForceDelete = () => {
|
|
if ($showDeleteModal) {
|
|
isShowDeleteConfirmation = true;
|
|
return;
|
|
}
|
|
handlePromiseError(trashOrDelete(true));
|
|
};
|
|
|
|
const onStackAssets = async () => {
|
|
const result = await stackAssets(assetInteraction.selectedAssets);
|
|
|
|
updateStackedAssetInTimeline(timelineManager, result);
|
|
|
|
onEscape?.();
|
|
};
|
|
|
|
const toggleArchive = async () => {
|
|
const visibility = assetInteraction.isAllArchived ? AssetVisibility.Timeline : AssetVisibility.Archive;
|
|
const ids = await archiveAssets(assetInteraction.selectedAssets, visibility);
|
|
timelineManager.updateAssetOperation(ids, (asset) => {
|
|
asset.visibility = visibility;
|
|
return { remove: false };
|
|
});
|
|
deselectAllAssets();
|
|
};
|
|
|
|
let shiftKeyIsDown = $state(false);
|
|
|
|
const deselectAllAssets = () => {
|
|
cancelMultiselect(assetInteraction);
|
|
};
|
|
|
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
if (searchStore.isSearchEnabled) {
|
|
return;
|
|
}
|
|
|
|
if (event.key === 'Shift') {
|
|
event.preventDefault();
|
|
shiftKeyIsDown = true;
|
|
}
|
|
};
|
|
|
|
const onKeyUp = (event: KeyboardEvent) => {
|
|
if (searchStore.isSearchEnabled) {
|
|
return;
|
|
}
|
|
|
|
if (event.key === 'Shift') {
|
|
event.preventDefault();
|
|
shiftKeyIsDown = false;
|
|
}
|
|
};
|
|
|
|
const onSelectStart = (e: Event) => {
|
|
if (assetInteraction.selectionActive && shiftKeyIsDown) {
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
|
|
const isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
|
|
const isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
|
|
const idsSelectedAssets = $derived(assetInteraction.selectedAssets.map(({ id }) => id));
|
|
let isShortcutModalOpen = false;
|
|
|
|
const handleOpenShortcutModal = async () => {
|
|
if (isShortcutModalOpen) {
|
|
return;
|
|
}
|
|
|
|
isShortcutModalOpen = true;
|
|
await modalManager.show(ShortcutsModal, {});
|
|
isShortcutModalOpen = false;
|
|
};
|
|
|
|
$effect(() => {
|
|
if (isEmpty) {
|
|
assetInteraction.clearMultiselect();
|
|
}
|
|
});
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
let shortcutList = $derived(
|
|
(() => {
|
|
if (searchStore.isSearchEnabled || $showAssetViewer) {
|
|
return [];
|
|
}
|
|
|
|
const shortcuts: ShortcutOptions[] = [
|
|
{ shortcut: { key: '?', shift: true }, onShortcut: handleOpenShortcutModal },
|
|
{ shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.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: 'D', shift: true }, onShortcut: () => setFocusTo('later', '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: onForceDelete },
|
|
{ shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() },
|
|
{ shortcut: { key: 's' }, onShortcut: () => onStackAssets() },
|
|
{ shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
|
|
);
|
|
}
|
|
|
|
return shortcuts;
|
|
})(),
|
|
);
|
|
</script>
|
|
|
|
<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}
|