InlinePreview was requesting fit_1920 for both the still image and the video poster — roughly 3× the pixel count of what the pane actually needs. Drop to fit_1280: still sharp inside the inline pane (which the user resizes around 300–500px tall in practice) while cutting payload by ~⅔. Timeline: pull the grid wrapper padding in from `p-6 pb-24` to `pr-2 pl-2 pb-2 overflow-x-hidden` now that the SplitGrid preview pane sits above the grid — the old generous padding existed to breathe under a full-screen modal that no longer renders inline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
101 lines
3.6 KiB
Svelte
101 lines
3.6 KiB
Svelte
<!--
|
||
Inline preview pane: replaces the old fullscreen `PreviewOverlay`.
|
||
Renders the focused photo or video inside its host pane so the grid
|
||
stays visible below. Prev/next move `selection.focused` directly so
|
||
the right metadata sidebar tracks in lockstep.
|
||
-->
|
||
<script lang="ts">
|
||
import { createQuery } from '@tanstack/svelte-query';
|
||
import { getPhoto } from '$lib/services/photoprism';
|
||
import { thumbUrl, videoUrl } from '$lib/stores/session.svelte';
|
||
import { selection, setAnchor, setFocused } from '$lib/stores/selection.svelte';
|
||
import SelectionDeck from '$lib/components/preview/SelectionDeck.svelte';
|
||
import VideoPlayer from '$lib/components/preview/VideoPlayer.svelte';
|
||
import { isVideo, primaryFile, videoFile, type PpPhoto } from '$lib/types/photoprism';
|
||
|
||
interface Props {
|
||
uid: string | null;
|
||
/** Ordered uid list for prev/next chevrons. The host page mirrors
|
||
* whatever list the user is currently looking at into this prop so
|
||
* navigation stays in context (timeline order, drill-in order, etc). */
|
||
order: string[];
|
||
}
|
||
let { uid, order }: Props = $props();
|
||
|
||
// Multi-select swaps the single-photo pane for a fanned deck of the
|
||
// selected thumbnails. Reads `selection.ids` directly (already in
|
||
// scope via the import) so the host doesn't need to thread it
|
||
// through as a prop. Spreading SvelteSet preserves insertion order,
|
||
// so the most recently picked card sits on top of the fan.
|
||
const selectedUids = $derived([...selection.ids]);
|
||
const multiSelect = $derived(selectedUids.length >= 2);
|
||
|
||
const photoQuery = createQuery<PpPhoto>(() => ({
|
||
queryKey: ['photo', uid ?? ''],
|
||
queryFn: () => getPhoto(uid as string),
|
||
enabled: Boolean(uid)
|
||
}));
|
||
|
||
const currentIndex = $derived(uid ? order.indexOf(uid) : -1);
|
||
|
||
function focusAt(i: number) {
|
||
const next = order[i];
|
||
if (!next) return;
|
||
setFocused(next);
|
||
setAnchor(next);
|
||
}
|
||
</script>
|
||
|
||
<div class="relative flex h-full w-full items-center justify-center bg-black/30 p-4">
|
||
{#if multiSelect}
|
||
<SelectionDeck uids={selectedUids} />
|
||
{:else if uid === null}
|
||
<p class="text-sm text-muted-foreground">Select a photo to preview.</p>
|
||
{:else if photoQuery.isPending}
|
||
<p class="text-sm text-muted-foreground">Loading…</p>
|
||
{:else if photoQuery.isError}
|
||
<p class="text-sm text-destructive">Failed to load photo.</p>
|
||
{:else if photoQuery.data}
|
||
{@const pf = primaryFile(photoQuery.data)}
|
||
{#if currentIndex > 0}
|
||
<button
|
||
class="absolute left-2 top-1/2 z-10 -translate-y-1/2 rounded-full bg-background/80 px-3 py-2 text-lg hover:bg-background"
|
||
onclick={() => focusAt(currentIndex - 1)}
|
||
aria-label="Previous photo"
|
||
>
|
||
‹
|
||
</button>
|
||
{/if}
|
||
{#if currentIndex >= 0 && currentIndex < order.length - 1}
|
||
<button
|
||
class="absolute right-2 top-1/2 z-10 -translate-y-1/2 rounded-full bg-background/80 px-3 py-2 text-lg hover:bg-background"
|
||
onclick={() => focusAt(currentIndex + 1)}
|
||
aria-label="Next photo"
|
||
>
|
||
›
|
||
</button>
|
||
{/if}
|
||
|
||
{#if isVideo(photoQuery.data)}
|
||
{@const vf = videoFile(photoQuery.data)}
|
||
<!-- Key on the video hash so navigating to a new video remounts the
|
||
player. Without this the <media-player> element keeps the
|
||
previous src bound and `autoplay` doesn't re-fire — clicking
|
||
a video tile would leave the pane idle on its poster. -->
|
||
{#key vf.Hash}
|
||
<VideoPlayer
|
||
src={videoUrl(vf.Hash)}
|
||
poster={thumbUrl(pf.Hash, 'fit_1280')}
|
||
title={photoQuery.data.OriginalName ?? pf.Name ?? ''}
|
||
/>
|
||
{/key}
|
||
{:else}
|
||
<img
|
||
src={thumbUrl(pf.Hash, 'fit_1280')}
|
||
alt={photoQuery.data.OriginalName ?? pf.Name ?? 'Photo'}
|
||
class="max-h-full max-w-full rounded-md object-contain shadow-2xl"
|
||
/>
|
||
{/if}
|
||
{/if}
|
||
</div>
|