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:
@@ -33,7 +33,6 @@
|
||||
setFocused,
|
||||
setOrder,
|
||||
} from "$lib/stores/selection.svelte";
|
||||
import { openPreview, preview } from "$lib/stores/preview.svelte";
|
||||
import {
|
||||
setRightSidebarWidth,
|
||||
setThumbnailSize,
|
||||
@@ -51,9 +50,11 @@
|
||||
} from "$lib/actions/visibleRange";
|
||||
import BulkActionBar from "$lib/components/timeline/BulkActionBar.svelte";
|
||||
import BulkMetadataSidebar from "$lib/components/sidebar/BulkMetadataSidebar.svelte";
|
||||
import InlinePreview from "$lib/components/preview/InlinePreview.svelte";
|
||||
import PhotoTile from "$lib/components/timeline/PhotoTile.svelte";
|
||||
import RightSidebar from "$lib/components/sidebar/RightSidebar.svelte";
|
||||
import SkeletonGrid from "$lib/components/timeline/SkeletonGrid.svelte";
|
||||
import SplitGrid from "$lib/components/preview/SplitGrid.svelte";
|
||||
import Toolbar from "$lib/components/layout/Toolbar.svelte";
|
||||
import { type PpPhoto } from "$lib/types/photoprism";
|
||||
|
||||
@@ -408,25 +409,6 @@
|
||||
if (el) scrollTileIntoView(el);
|
||||
}
|
||||
|
||||
// When the preview overlay closes, scroll the just-shown photo back
|
||||
// into the timeline window. PreviewOverlay already keeps
|
||||
// selection.focused in lockstep with preview.uid, so we just need to
|
||||
// make sure that tile is mounted (forcedExpand) and visible — the
|
||||
// blue ring renders itself once the inner button is in the DOM.
|
||||
let wasPreviewOpen = $state(false);
|
||||
$effect(() => {
|
||||
const open = preview.uid !== null;
|
||||
const closing = wasPreviewOpen && !open;
|
||||
wasPreviewOpen = open;
|
||||
if (!closing) return;
|
||||
const uid = selection.focused;
|
||||
if (!uid) return;
|
||||
untrack(() => {
|
||||
const i = photos.findIndex((p) => p.UID === uid);
|
||||
if (i >= 0) void scrollToIndex(i);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Visual rows for keyboard navigation ──────────────────────────────────
|
||||
// The CSS Grid lays each photo into a cell with column count derived from
|
||||
// `repeat(auto-fill, minmax(thumbnailSize, 1fr))`. Month headers span the
|
||||
@@ -675,24 +657,19 @@
|
||||
}
|
||||
|
||||
function onTileDblclick(e: MouseEvent, uid: string) {
|
||||
// Modifier-modified dblclicks shouldn't open the preview either —
|
||||
// gridKeyNav already handled the underlying click.
|
||||
if (e.shiftKey || e.metaKey || e.ctrlKey) return;
|
||||
e.preventDefault();
|
||||
openPreview(
|
||||
uid,
|
||||
photos.map((p) => p.UID),
|
||||
);
|
||||
selection.ids.clear();
|
||||
selection.ids.add(uid);
|
||||
setFocused(uid);
|
||||
setAnchor(uid);
|
||||
}
|
||||
|
||||
// Single-click fallback for the dblclick preview gesture. Wired to the
|
||||
// hover-only Maximize icon in PhotoTile so users who haven't discovered
|
||||
// dblclick can still get to the preview.
|
||||
function onTileOpenPreview(uid: string) {
|
||||
openPreview(
|
||||
uid,
|
||||
photos.map((p) => p.UID),
|
||||
);
|
||||
selection.ids.clear();
|
||||
selection.ids.add(uid);
|
||||
setFocused(uid);
|
||||
setAnchor(uid);
|
||||
}
|
||||
|
||||
// Scroll root for the infinite-scroll IntersectionObserver. Bound by
|
||||
@@ -840,6 +817,11 @@
|
||||
sibling at row level and stays full height when the bar appears.
|
||||
-->
|
||||
<div class="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<SplitGrid>
|
||||
{#snippet preview()}
|
||||
<InlinePreview uid={selection.focused} order={selection.order} />
|
||||
{/snippet}
|
||||
{#snippet grid()}
|
||||
<main
|
||||
bind:this={scrollRoot}
|
||||
class="flex-1 overflow-y-auto outline-none focus:outline-none"
|
||||
@@ -952,6 +934,8 @@
|
||||
{/if}
|
||||
</div>
|
||||
</main>
|
||||
{/snippet}
|
||||
</SplitGrid>
|
||||
<BulkActionBar />
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user