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:
2026-05-18 22:46:02 +02:00
parent 2a75896274
commit e36f1939c6
19 changed files with 701 additions and 630 deletions

View File

@@ -24,6 +24,7 @@ interface Persisted {
thumbnailSize?: ThumbnailSize;
leftSidebarWidth?: number;
rightSidebarWidth?: number;
previewPaneHeight?: number;
/**
* Per-section expanded state for the right-sidebar metadata panel
* (GPS, Credits, File). Keyed by section id; missing entries use a
@@ -39,6 +40,14 @@ export const DEFAULT_LEFT_WIDTH = 224;
export const MIN_RIGHT_WIDTH = 220;
export const MAX_RIGHT_WIDTH = 480;
export const DEFAULT_RIGHT_WIDTH = 280;
export const MIN_PREVIEW_HEIGHT = 160;
export const DEFAULT_PREVIEW_HEIGHT = 360;
/**
* Cap the preview pane at 70 % of the viewport so the grid is always
* visible underneath. Resolved against `window.innerHeight` at set-time
* (the localStorage load happens before any viewport size is known).
*/
export const MAX_PREVIEW_HEIGHT_FRAC = 0.7;
function clamp(n: number, lo: number, hi: number): number {
return Math.min(hi, Math.max(lo, n));
@@ -69,6 +78,7 @@ export const view = $state<{
thumbnailSize: ThumbnailSize;
leftSidebarWidth: number;
rightSidebarWidth: number;
previewPaneHeight: number;
metadataSections: Record<string, boolean>;
}>({
rightSidebarCollapsed: initial.rightSidebarCollapsed ?? false,
@@ -86,6 +96,12 @@ export const view = $state<{
MIN_RIGHT_WIDTH,
MAX_RIGHT_WIDTH
),
previewPaneHeight: Math.max(
MIN_PREVIEW_HEIGHT,
typeof initial.previewPaneHeight === 'number'
? initial.previewPaneHeight
: DEFAULT_PREVIEW_HEIGHT
),
metadataSections:
initial.metadataSections && typeof initial.metadataSections === 'object'
? { ...initial.metadataSections }
@@ -100,6 +116,7 @@ function persist(): void {
thumbnailSize: view.thumbnailSize,
leftSidebarWidth: view.leftSidebarWidth,
rightSidebarWidth: view.rightSidebarWidth,
previewPaneHeight: view.previewPaneHeight,
metadataSections: view.metadataSections
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
@@ -115,6 +132,14 @@ export function setRightSidebarWidth(px: number): void {
persist();
}
export function setPreviewPaneHeight(px: number): void {
const maxH = browser
? Math.max(MIN_PREVIEW_HEIGHT + 1, Math.floor(window.innerHeight * MAX_PREVIEW_HEIGHT_FRAC))
: 1024;
view.previewPaneHeight = clamp(Math.round(px), MIN_PREVIEW_HEIGHT, maxH);
persist();
}
export function setThumbnailSize(size: ThumbnailSize): void {
view.thumbnailSize = size;
persist();