preview: serve fit_1280 + tighten timeline grid padding
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>
This commit is contained in:
@@ -85,13 +85,13 @@
|
|||||||
{#key vf.Hash}
|
{#key vf.Hash}
|
||||||
<VideoPlayer
|
<VideoPlayer
|
||||||
src={videoUrl(vf.Hash)}
|
src={videoUrl(vf.Hash)}
|
||||||
poster={thumbUrl(pf.Hash, 'fit_1920')}
|
poster={thumbUrl(pf.Hash, 'fit_1280')}
|
||||||
title={photoQuery.data.OriginalName ?? pf.Name ?? ''}
|
title={photoQuery.data.OriginalName ?? pf.Name ?? ''}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
{:else}
|
{:else}
|
||||||
<img
|
<img
|
||||||
src={thumbUrl(pf.Hash, 'fit_1920')}
|
src={thumbUrl(pf.Hash, 'fit_1280')}
|
||||||
alt={photoQuery.data.OriginalName ?? pf.Name ?? 'Photo'}
|
alt={photoQuery.data.OriginalName ?? pf.Name ?? 'Photo'}
|
||||||
class="max-h-full max-w-full rounded-md object-contain shadow-2xl"
|
class="max-h-full max-w-full rounded-md object-contain shadow-2xl"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -811,122 +811,123 @@
|
|||||||
-->
|
-->
|
||||||
<div class="flex min-w-0 flex-1 flex-col overflow-hidden">
|
<div class="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||||
<SplitGrid>
|
<SplitGrid>
|
||||||
{#snippet preview()}
|
{#snippet preview()}
|
||||||
<InlinePreview uid={selection.focused} order={selection.order} />
|
<InlinePreview uid={selection.focused} order={selection.order} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet grid()}
|
{#snippet grid()}
|
||||||
<main
|
<main
|
||||||
bind:this={scrollRoot}
|
bind:this={scrollRoot}
|
||||||
class="flex-1 overflow-y-auto outline-none focus:outline-none"
|
class="flex-1 overflow-y-auto outline-none focus:outline-none"
|
||||||
use:gridKeyNav={{ scrollToIndex, onArrow }}
|
use:gridKeyNav={{ scrollToIndex, onArrow }}
|
||||||
use:visibleRange={{
|
use:visibleRange={{
|
||||||
onChange: (f, l) => {
|
onChange: (f, l) => {
|
||||||
visFirst = f;
|
visFirst = f;
|
||||||
visLast = l;
|
visLast = l;
|
||||||
},
|
},
|
||||||
sampleEvery: TILE_SAMPLE,
|
sampleEvery: TILE_SAMPLE,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="p-6 pb-24">
|
<div class="pr-2 pl-2 pb-2 overflow-x-hidden">
|
||||||
{#if photosQuery.isPending}
|
{#if photosQuery.isPending}
|
||||||
<SkeletonGrid />
|
<SkeletonGrid />
|
||||||
{:else if photosQuery.isError}
|
{:else if photosQuery.isError}
|
||||||
<p class="text-sm text-destructive">
|
<p class="text-sm text-destructive">
|
||||||
Failed to load photos: {photosQuery.error instanceof Error
|
Failed to load photos: {photosQuery.error instanceof Error
|
||||||
? photosQuery.error.message
|
? photosQuery.error.message
|
||||||
: "unknown error"}
|
: "unknown error"}
|
||||||
</p>
|
</p>
|
||||||
{:else if photos.length === 0}
|
{:else if photos.length === 0}
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
{#if filters.section === "archive"}
|
{#if filters.section === "archive"}
|
||||||
Archive is empty.
|
Archive is empty.
|
||||||
{:else if filters.section === "review"}
|
{:else if filters.section === "review"}
|
||||||
Nothing left to review. Photos PhotoPrism's indexer wasn't sure
|
Nothing left to review. Photos PhotoPrism's indexer wasn't
|
||||||
about land here — use Keep to accept them into the timeline or
|
sure about land here — use Keep to accept them into the
|
||||||
Archive to set them aside.
|
timeline or Archive to set them aside.
|
||||||
{:else if filters.section === "hidden"}
|
{:else if filters.section === "hidden"}
|
||||||
No hidden photos. PhotoPrism auto-hides files it can't index
|
No hidden photos. PhotoPrism auto-hides files it can't index
|
||||||
(broken files, very low quality); they only ever show up here.
|
(broken files, very low quality); they only ever show up here.
|
||||||
{:else if filters.section === "heap"}
|
{:else if filters.section === "heap"}
|
||||||
This heap has no photos yet. Select some photos and use the bulk
|
This heap has no photos yet. Select some photos and use the
|
||||||
bar's "+ Add to heap" button.
|
bulk bar's "+ Add to heap" button.
|
||||||
|
{:else}
|
||||||
|
No photos. Index a folder via PhotoPrism's reindex command.
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
No photos. Index a folder via PhotoPrism's reindex command.
|
<div
|
||||||
{/if}
|
data-photo-grid
|
||||||
</p>
|
use:trackGridCols
|
||||||
{:else}
|
class="grid gap-2"
|
||||||
<div
|
style="grid-template-columns: repeat(auto-fill, minmax({view.thumbnailSize}px, 1fr));"
|
||||||
data-photo-grid
|
>
|
||||||
use:trackGridCols
|
{#each rows as row (row.kind === "header" ? `h:${row.key}` : `t:${row.photo.UID}`)}
|
||||||
class="grid gap-2"
|
{#if row.kind === "header"}
|
||||||
style="grid-template-columns: repeat(auto-fill, minmax({view.thumbnailSize}px, 1fr));"
|
<!-- col-span-full + position:sticky pins the month label to
|
||||||
>
|
|
||||||
{#each rows as row (row.kind === "header" ? `h:${row.key}` : `t:${row.photo.UID}`)}
|
|
||||||
{#if row.kind === "header"}
|
|
||||||
<!-- col-span-full + position:sticky pins the month label to
|
|
||||||
the top of the scrolling main as the user passes through.
|
the top of the scrolling main as the user passes through.
|
||||||
-mx-6 stretches the bar past the wrapper padding so it
|
-mx-6 stretches the bar past the wrapper padding so it
|
||||||
reads edge-to-edge in the viewport. -->
|
reads edge-to-edge in the viewport. -->
|
||||||
<h2
|
<h2
|
||||||
class="sticky top-0 z-10 -mx-6 border-b border-border bg-background/95 px-6 py-2 text-xs font-semibold uppercase tracking-wide text-foreground/80 backdrop-blur"
|
class="sticky top-0 z-10 -mx-6 border-b border-border bg-background/95 px-6 py-2 text-xs font-semibold uppercase tracking-wide text-foreground/80 backdrop-blur"
|
||||||
style="grid-column: 1 / -1;"
|
style="grid-column: 1 / -1;"
|
||||||
>
|
>
|
||||||
{row.label}
|
{row.label}
|
||||||
<span
|
<span
|
||||||
class="ml-2 text-[10px] font-normal text-muted-foreground"
|
class="ml-2 text-[10px] font-normal text-muted-foreground"
|
||||||
>
|
>
|
||||||
{row.count}
|
{row.count}
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
{:else}
|
{:else}
|
||||||
{@const photo = row.photo}
|
{@const photo = row.photo}
|
||||||
{@const i = row.tileIndex}
|
{@const i = row.tileIndex}
|
||||||
{@const inWindow = i >= renderFirst && i <= renderLast}
|
{@const inWindow = i >= renderFirst && i <= renderLast}
|
||||||
<!-- Shell: always rendered. Holds grid-cell space + the
|
<!-- Shell: always rendered. Holds grid-cell space + the
|
||||||
stable `data-uid-shell` anchor that gridKeyNav.scrollToIndex
|
stable `data-uid-shell` anchor that gridKeyNav.scrollToIndex
|
||||||
can query even when the inner button is windowed out. -->
|
can query even when the inner button is windowed out. -->
|
||||||
<div
|
<div
|
||||||
data-uid-shell={photo.UID}
|
data-uid-shell={photo.UID}
|
||||||
class="aspect-square"
|
class="aspect-square"
|
||||||
use:tileRegister={i}
|
use:tileRegister={i}
|
||||||
>
|
>
|
||||||
{#if inWindow}
|
{#if inWindow}
|
||||||
{@const sel =
|
{@const sel =
|
||||||
selectedIds.has(photo.UID) || focusedUid === photo.UID}
|
selectedIds.has(photo.UID) ||
|
||||||
<PhotoTile
|
focusedUid === photo.UID}
|
||||||
{photo}
|
<PhotoTile
|
||||||
selected={sel}
|
{photo}
|
||||||
onClick={(e) => onTileClick(e, photo.UID)}
|
selected={sel}
|
||||||
onDblclick={(e) => onTileDblclick(e, photo.UID)}
|
onClick={(e) => onTileClick(e, photo.UID)}
|
||||||
/>
|
onDblclick={(e) => onTileDblclick(e, photo.UID)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
{/each}
|
||||||
{/if}
|
</div>
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sentinel: when this div nears the viewport we kick the next
|
<!-- Sentinel: when this div nears the viewport we kick the next
|
||||||
page. The 4-viewport rootMargin (handled by the action) is
|
page. The 4-viewport rootMargin (handled by the action) is
|
||||||
PhotoPrism's preload distance from page/photos.vue. -->
|
PhotoPrism's preload distance from page/photos.vue. -->
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="h-4"
|
class="h-4"
|
||||||
use:nearBottom={{
|
use:nearBottom={{
|
||||||
onHit: () => photosQuery.fetchNextPage(),
|
onHit: () => photosQuery.fetchNextPage(),
|
||||||
enabled:
|
enabled:
|
||||||
photosQuery.hasNextPage && !photosQuery.isFetchingNextPage,
|
photosQuery.hasNextPage && !photosQuery.isFetchingNextPage,
|
||||||
root: scrollRoot,
|
root: scrollRoot,
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
{#if photosQuery.isFetchingNextPage}
|
{#if photosQuery.isFetchingNextPage}
|
||||||
<p class="py-3 text-center text-xs text-muted-foreground">
|
<p class="py-3 text-center text-xs text-muted-foreground">
|
||||||
Loading more…
|
Loading more…
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</SplitGrid>
|
</SplitGrid>
|
||||||
<BulkActionBar />
|
<BulkActionBar />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user