Skip to content

Commit d9f25a6

Browse files
committed
asset-grid actions split
1 parent 2180a22 commit d9f25a6

File tree

2 files changed

+221
-202
lines changed

2 files changed

+221
-202
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
<script lang="ts">
2+
import { goto } from '$app/navigation';
3+
import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut';
4+
import {
5+
setFocusToAsset as setFocusAssetInit,
6+
setFocusTo as setFocusToInit,
7+
} from '$lib/components/photos-page/actions/focus-actions';
8+
import ChangeDate from '$lib/components/shared-components/change-date.svelte';
9+
import { AppRoute } from '$lib/constants';
10+
import { modalManager } from '$lib/managers/modal-manager.svelte';
11+
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
12+
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
13+
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
14+
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
15+
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
16+
import { showDeleteModal } from '$lib/stores/preferences.store';
17+
import { searchStore } from '$lib/stores/search.svelte';
18+
import { featureFlags } from '$lib/stores/server-config.store';
19+
import { handlePromiseError } from '$lib/utils';
20+
import { deleteAssets, updateStackedAssetInTimeline } from '$lib/utils/actions';
21+
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
22+
import { AssetVisibility } from '@immich/sdk';
23+
import { DateTime } from 'luxon';
24+
import DeleteAssetDialog from './delete-asset-dialog.svelte';
25+
26+
let { isViewing: showAssetViewer } = assetViewingStore;
27+
28+
interface Props {
29+
timelineManager: TimelineManager;
30+
assetInteraction: AssetInteraction;
31+
isShowDeleteConfirmation: boolean;
32+
onEscape: () => void;
33+
scrollToAsset: (asset: TimelineAsset) => boolean;
34+
}
35+
36+
let {
37+
timelineManager = $bindable(),
38+
assetInteraction,
39+
isShowDeleteConfirmation = $bindable(false),
40+
onEscape,
41+
scrollToAsset,
42+
}: Props = $props();
43+
44+
let isShowSelectDate = $state(false);
45+
46+
const trashOrDelete = async (force: boolean = false) => {
47+
isShowDeleteConfirmation = false;
48+
await deleteAssets(
49+
!(isTrashEnabled && !force),
50+
(assetIds) => timelineManager.removeAssets(assetIds),
51+
assetInteraction.selectedAssets,
52+
!isTrashEnabled || force ? undefined : (assets) => timelineManager.addAssets(assets),
53+
);
54+
assetInteraction.clearMultiselect();
55+
};
56+
57+
const onDelete = () => {
58+
const hasTrashedAsset = assetInteraction.selectedAssets.some((asset) => asset.isTrashed);
59+
60+
if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) {
61+
isShowDeleteConfirmation = true;
62+
return;
63+
}
64+
handlePromiseError(trashOrDelete(hasTrashedAsset));
65+
};
66+
67+
const onForceDelete = () => {
68+
if ($showDeleteModal) {
69+
isShowDeleteConfirmation = true;
70+
return;
71+
}
72+
handlePromiseError(trashOrDelete(true));
73+
};
74+
75+
const onStackAssets = async () => {
76+
const result = await stackAssets(assetInteraction.selectedAssets);
77+
78+
updateStackedAssetInTimeline(timelineManager, result);
79+
80+
onEscape();
81+
};
82+
83+
const toggleArchive = async () => {
84+
const visibility = assetInteraction.isAllArchived ? AssetVisibility.Timeline : AssetVisibility.Archive;
85+
const ids = await archiveAssets(assetInteraction.selectedAssets, visibility);
86+
timelineManager.updateAssetOperation(ids, (asset) => {
87+
asset.visibility = visibility;
88+
return { remove: false };
89+
});
90+
deselectAllAssets();
91+
};
92+
93+
let shiftKeyIsDown = $state(false);
94+
95+
const deselectAllAssets = () => {
96+
cancelMultiselect(assetInteraction);
97+
};
98+
99+
const onKeyDown = (event: KeyboardEvent) => {
100+
if (searchStore.isSearchEnabled) {
101+
return;
102+
}
103+
104+
if (event.key === 'Shift') {
105+
event.preventDefault();
106+
shiftKeyIsDown = true;
107+
}
108+
};
109+
110+
const onKeyUp = (event: KeyboardEvent) => {
111+
if (searchStore.isSearchEnabled) {
112+
return;
113+
}
114+
115+
if (event.key === 'Shift') {
116+
event.preventDefault();
117+
shiftKeyIsDown = false;
118+
}
119+
};
120+
121+
const onSelectStart = (e: Event) => {
122+
if (assetInteraction.selectionActive && shiftKeyIsDown) {
123+
e.preventDefault();
124+
}
125+
};
126+
127+
let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
128+
let isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
129+
let idsSelectedAssets = $derived(assetInteraction.selectedAssets.map(({ id }) => id));
130+
let isShortcutModalOpen = false;
131+
132+
const handleOpenShortcutModal = async () => {
133+
if (isShortcutModalOpen) {
134+
return;
135+
}
136+
137+
isShortcutModalOpen = true;
138+
await modalManager.show(ShortcutsModal, {});
139+
isShortcutModalOpen = false;
140+
};
141+
142+
$effect(() => {
143+
if (isEmpty) {
144+
assetInteraction.clearMultiselect();
145+
}
146+
});
147+
148+
const setFocusTo = setFocusToInit.bind(undefined, scrollToAsset, timelineManager);
149+
const setFocusAsset = setFocusAssetInit.bind(undefined, scrollToAsset);
150+
151+
let shortcutList = $derived(
152+
(() => {
153+
if (searchStore.isSearchEnabled || $showAssetViewer) {
154+
return [];
155+
}
156+
157+
const shortcuts: ShortcutOptions[] = [
158+
{ shortcut: { key: 'Escape' }, onShortcut: onEscape },
159+
{ shortcut: { key: '?', shift: true }, onShortcut: handleOpenShortcutModal },
160+
{ shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
161+
{ shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets(timelineManager, assetInteraction) },
162+
{ shortcut: { key: 'ArrowRight' }, onShortcut: () => setFocusTo('earlier', 'asset') },
163+
{ shortcut: { key: 'ArrowLeft' }, onShortcut: () => setFocusTo('later', 'asset') },
164+
{ shortcut: { key: 'D' }, onShortcut: () => setFocusTo('earlier', 'day') },
165+
{ shortcut: { key: 'D', shift: true }, onShortcut: () => setFocusTo('later', 'day') },
166+
{ shortcut: { key: 'M' }, onShortcut: () => setFocusTo('earlier', 'month') },
167+
{ shortcut: { key: 'M', shift: true }, onShortcut: () => setFocusTo('later', 'month') },
168+
{ shortcut: { key: 'Y' }, onShortcut: () => setFocusTo('earlier', 'year') },
169+
{ shortcut: { key: 'Y', shift: true }, onShortcut: () => setFocusTo('later', 'year') },
170+
{ shortcut: { key: 'G' }, onShortcut: () => (isShowSelectDate = true) },
171+
];
172+
173+
if (assetInteraction.selectionActive) {
174+
shortcuts.push(
175+
{ shortcut: { key: 'Delete' }, onShortcut: onDelete },
176+
{ shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete },
177+
{ shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() },
178+
{ shortcut: { key: 's' }, onShortcut: () => onStackAssets() },
179+
{ shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
180+
);
181+
}
182+
183+
return shortcuts;
184+
})(),
185+
);
186+
</script>
187+
188+
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} onselectstart={onSelectStart} use:shortcuts={shortcutList} />
189+
190+
{#if isShowDeleteConfirmation}
191+
<DeleteAssetDialog
192+
size={idsSelectedAssets.length}
193+
onCancel={() => (isShowDeleteConfirmation = false)}
194+
onConfirm={() => handlePromiseError(trashOrDelete(true))}
195+
/>
196+
{/if}
197+
198+
{#if isShowSelectDate}
199+
<ChangeDate
200+
title="Navigate to Time"
201+
initialDate={DateTime.now()}
202+
timezoneInput={false}
203+
onConfirm={async (dateString: string) => {
204+
isShowSelectDate = false;
205+
const asset = await timelineManager.getClosestAssetToDate(
206+
(DateTime.fromISO(dateString) as DateTime<true>).toObject(),
207+
);
208+
if (asset) {
209+
setFocusAsset(asset);
210+
}
211+
}}
212+
onCancel={() => (isShowSelectDate = false)}
213+
/>
214+
{/if}

0 commit comments

Comments
 (0)