feat(preview): inline split-pane preview + sidebar metadata pass
Replace the fullscreen PreviewOverlay with an inline top pane above
each grid surface. SplitGrid + InlinePreview render the focused
photo/video inside the host page; a resizableVertical action drives
the divider and the height persists via the view store. Applied to
the timeline, /tags drill-in, /review cause tabs, /photo/[uid], and
/map. selection.focused is now the single source of truth for both
the inline pane and the right sidebar — preview.svelte store and
PreviewOverlay are removed.
Sidebar: drop the thumb; lead with icon-led filename and folder
rows that match the date/place rhythm. Move dims+size to the top
(below date) and camera/lens/exposure into the collapsible File
section. Read-only spans share the input padding so the text column
aligns across rows. Folder row sits between date and dims+size.
VideoPlayer: stop forcing width/height: 100% so videos honour their
intrinsic aspect ratio inside the pane. Key the player on file hash
in InlinePreview so navigating between videos remounts the element
and autoplay fires again.
Sidebar (LeftSidebar): switch the labels badge to a dedicated
countPhotos('label:*') query so it reports photos with a label
rather than PhotoPrism's category roll-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user