diff --git a/web/src/lib/actions/gridKeyNav.ts b/web/src/lib/actions/gridKeyNav.ts index 2ea24ff..8d21680 100644 --- a/web/src/lib/actions/gridKeyNav.ts +++ b/web/src/lib/actions/gridKeyNav.ts @@ -12,7 +12,6 @@ import { } from '$lib/services/photoprism'; import { queryClient } from '$lib/queryClient'; import { filters } from '$lib/stores/filters.svelte'; -import { closePreview, openPreview, preview } from '$lib/stores/preview.svelte'; import { clearSelection, focusAfter, @@ -55,16 +54,14 @@ export interface GridKeyNavParams { * - Window-level shortcuts mirroring mule-image's keyboard layer: * x archive-toggle, u restore, s + (1–9) add to * heap N (bare s adds to the currently-viewed heap), b/Tab toggles - * left sidebar, i toggles right sidebar, space/enter opens preview, - * esc clears, ⌘Z undoes, ⌘A selects all visible. + * left sidebar, i toggles right sidebar, esc clears, ⌘Z undoes, + * ⌘A selects all visible. * Rating + color labels are mouse-driven via the metadata sidebar — no * keyboard shortcuts. * * Archive / restore target a synthesized "cull target list" — in priority: - * 1. preview overlay uid (when open) — applies to the visible preview - * photo even if the grid still shows a stale selection - * 2. multi-selection set - * 3. focused tile + * 1. multi-selection set + * 2. focused tile */ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) { let scrollToIndex = params.scrollToIndex; @@ -151,9 +148,8 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) { } } - /** Synthesize a target list. Preview wins, then multi, then focused. */ + /** Synthesize a target list. Multi-selection wins, then focused. */ function cullTargets(): string[] { - if (preview.uid) return [preview.uid]; if (selection.ids.size > 0) return Array.from(selection.ids); if (selection.focused) return [selection.focused]; return []; @@ -376,17 +372,6 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) { await addCullTargetsToHeap(heap); } - function openPreviewFromGrid() { - const id = selection.focused ?? selection.order[0]; - if (!id) return; - openPreview(id, selection.order); - } - - function togglePreview() { - if (preview.uid) closePreview(); - else openPreviewFromGrid(); - } - async function onKey(e: KeyboardEvent) { // Don't hijack typing inside form fields. const tag = (e.target as HTMLElement | null)?.tagName?.toLowerCase(); @@ -408,47 +393,35 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) { const meta = e.metaKey || e.ctrlKey; const shift = e.shiftKey; - const inPreview = preview.uid !== null; - // ── Grid-only nav keys (preview owns its own Arrow/Esc) ────────────── - if (!inPreview) { - switch (e.key) { - case 'ArrowLeft': - case 'ArrowRight': - case 'ArrowUp': - case 'ArrowDown': - e.preventDefault(); - if (onArrow) { - // Host owns the visual-row map (needed for grids with - // interleaved headers). The host calls setFocused + - // scrollToIndex + selectRange-on-shift itself. - onArrow(e.key, shift); - } else { - const delta = - e.key === 'ArrowLeft' - ? -1 - : e.key === 'ArrowRight' - ? 1 - : e.key === 'ArrowUp' - ? -tilesPerRow() - : tilesPerRow(); - moveFocus(delta, shift); - if (shift && selection.focused) selectRange(selection.focused); - } - return; - case 'Escape': - clearSelection(); - setFocused(null); - return; - } - } - - // ── Mode-aware shortcuts (work in grid AND preview) ────────────────── + // ── Grid nav keys ──────────────────────────────────────────────────── switch (e.key) { - case ' ': - case 'Enter': + case 'ArrowLeft': + case 'ArrowRight': + case 'ArrowUp': + case 'ArrowDown': e.preventDefault(); - togglePreview(); + if (onArrow) { + // Host owns the visual-row map (needed for grids with + // interleaved headers). The host calls setFocused + + // scrollToIndex + selectRange-on-shift itself. + onArrow(e.key, shift); + } else { + const delta = + e.key === 'ArrowLeft' + ? -1 + : e.key === 'ArrowRight' + ? 1 + : e.key === 'ArrowUp' + ? -tilesPerRow() + : tilesPerRow(); + moveFocus(delta, shift); + if (shift && selection.focused) selectRange(selection.focused); + } + return; + case 'Escape': + clearSelection(); + setFocused(null); return; case 'Tab': // Tab in the grid context = mule-image's left-sidebar toggle. @@ -459,7 +432,7 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) { return; case 'i': case 'I': - if (!meta && !shift && !inPreview) { + if (!meta && !shift) { e.preventDefault(); toggleRightSidebar(); } @@ -482,7 +455,7 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) { return; case 'a': case 'A': - if (meta && !inPreview) { + if (meta) { e.preventDefault(); for (const id of selection.order) selection.ids.add(id); } @@ -560,8 +533,7 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) { node.addEventListener('click', onClick); // Keydown lives on the window so arrow keys, Esc, ⌘Z, etc. work // immediately on page load regardless of which element holds focus. - // The filters inside `onKey` keep form-field typing and preview mode - // safe (preview owns its own Arrow/Esc). + // The filter inside `onKey` keeps form-field typing safe. window.addEventListener('keydown', onKey); return { diff --git a/web/src/lib/actions/resizableVertical.ts b/web/src/lib/actions/resizableVertical.ts new file mode 100644 index 0000000..31dfeff --- /dev/null +++ b/web/src/lib/actions/resizableVertical.ts @@ -0,0 +1,75 @@ +/** + * Drag-to-resize Svelte action for vertical splits. Sibling of `resizable` + * (which handles left/right edges); kept as its own file so each action's + * surface stays small and the call sites read obviously. + * + * edge: 'bottom' — handle on the bottom edge of the pane; drag down enlarges + * edge: 'top' — handle on the top edge of the pane; drag up enlarges + * + * Usage (handle on the bottom edge of the top preview pane): + *
{MULIMAGO_ASCII}
- {MULIMAGO_ASCII}
+ Select a photo to preview.
+ {:else if photoQuery.isPending} +Loading…
+ {:else if photoQuery.isError} +Failed to load photo.
+ {:else if photoQuery.data} + {@const pf = primaryFile(photoQuery.data)} + {#if currentIndex > 0} + + {/if} + {#if currentIndex >= 0 && currentIndex < order.length - 1} + + {/if} + + {#if isVideo(photoQuery.data)} + {@const vf = videoFile(photoQuery.data)} + + {#key vf.Hash} +Loading…
- {:else if photoQuery.isError} -Failed to load photo.
- {:else if photoQuery.data} - {@const pf = primaryFile(photoQuery.data)} - {#if isVideo(photoQuery.data)} - {@const vf = videoFile(photoQuery.data)} -