Mulimage 2.0 #1

Merged
dtoro merged 64 commits from new into main 2026-05-21 22:48:55 +02:00
2 changed files with 109 additions and 108 deletions
Showing only changes of commit c783f129cc - Show all commits

View File

@@ -85,13 +85,13 @@
{#key vf.Hash}
<VideoPlayer
src={videoUrl(vf.Hash)}
poster={thumbUrl(pf.Hash, 'fit_1920')}
poster={thumbUrl(pf.Hash, 'fit_1280')}
title={photoQuery.data.OriginalName ?? pf.Name ?? ''}
/>
{/key}
{:else}
<img
src={thumbUrl(pf.Hash, 'fit_1920')}
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"
/>

View File

@@ -811,122 +811,123 @@
-->
<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"
use:gridKeyNav={{ scrollToIndex, onArrow }}
use:visibleRange={{
onChange: (f, l) => {
visFirst = f;
visLast = l;
},
sampleEvery: TILE_SAMPLE,
}}
>
<div class="p-6 pb-24">
{#if photosQuery.isPending}
<SkeletonGrid />
{:else if photosQuery.isError}
<p class="text-sm text-destructive">
Failed to load photos: {photosQuery.error instanceof Error
? photosQuery.error.message
: "unknown error"}
</p>
{:else if photos.length === 0}
<p class="text-sm text-muted-foreground">
{#if filters.section === "archive"}
Archive is empty.
{:else if filters.section === "review"}
Nothing left to review. Photos PhotoPrism's indexer wasn't sure
about land here — use Keep to accept them into the timeline or
Archive to set them aside.
{:else if filters.section === "hidden"}
No hidden photos. PhotoPrism auto-hides files it can't index
(broken files, very low quality); they only ever show up here.
{:else if filters.section === "heap"}
This heap has no photos yet. Select some photos and use the bulk
bar's " Add to heap" button.
{#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"
use:gridKeyNav={{ scrollToIndex, onArrow }}
use:visibleRange={{
onChange: (f, l) => {
visFirst = f;
visLast = l;
},
sampleEvery: TILE_SAMPLE,
}}
>
<div class="pr-2 pl-2 pb-2 overflow-x-hidden">
{#if photosQuery.isPending}
<SkeletonGrid />
{:else if photosQuery.isError}
<p class="text-sm text-destructive">
Failed to load photos: {photosQuery.error instanceof Error
? photosQuery.error.message
: "unknown error"}
</p>
{:else if photos.length === 0}
<p class="text-sm text-muted-foreground">
{#if filters.section === "archive"}
Archive is empty.
{:else if filters.section === "review"}
Nothing left to review. Photos PhotoPrism's indexer wasn't
sure about land here — use Keep to accept them into the
timeline or Archive to set them aside.
{:else if filters.section === "hidden"}
No hidden photos. PhotoPrism auto-hides files it can't index
(broken files, very low quality); they only ever show up here.
{:else if filters.section === "heap"}
This heap has no photos yet. Select some photos and use the
bulk bar's " Add to heap" button.
{:else}
No photos. Index a folder via PhotoPrism's reindex command.
{/if}
</p>
{:else}
No photos. Index a folder via PhotoPrism's reindex command.
{/if}
</p>
{:else}
<div
data-photo-grid
use:trackGridCols
class="grid gap-2"
style="grid-template-columns: repeat(auto-fill, minmax({view.thumbnailSize}px, 1fr));"
>
{#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
<div
data-photo-grid
use:trackGridCols
class="grid gap-2"
style="grid-template-columns: repeat(auto-fill, minmax({view.thumbnailSize}px, 1fr));"
>
{#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.
-mx-6 stretches the bar past the wrapper padding so it
reads edge-to-edge in the viewport. -->
<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"
style="grid-column: 1 / -1;"
>
{row.label}
<span
class="ml-2 text-[10px] font-normal text-muted-foreground"
>
{row.count}
</span>
</h2>
{:else}
{@const photo = row.photo}
{@const i = row.tileIndex}
{@const inWindow = i >= renderFirst && i <= renderLast}
<!-- Shell: always rendered. Holds grid-cell space + the
<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"
style="grid-column: 1 / -1;"
>
{row.label}
<span
class="ml-2 text-[10px] font-normal text-muted-foreground"
>
{row.count}
</span>
</h2>
{:else}
{@const photo = row.photo}
{@const i = row.tileIndex}
{@const inWindow = i >= renderFirst && i <= renderLast}
<!-- Shell: always rendered. Holds grid-cell space + the
stable `data-uid-shell` anchor that gridKeyNav.scrollToIndex
can query even when the inner button is windowed out. -->
<div
data-uid-shell={photo.UID}
class="aspect-square"
use:tileRegister={i}
>
{#if inWindow}
{@const sel =
selectedIds.has(photo.UID) || focusedUid === photo.UID}
<PhotoTile
{photo}
selected={sel}
onClick={(e) => onTileClick(e, photo.UID)}
onDblclick={(e) => onTileDblclick(e, photo.UID)}
/>
<div
data-uid-shell={photo.UID}
class="aspect-square"
use:tileRegister={i}
>
{#if inWindow}
{@const sel =
selectedIds.has(photo.UID) ||
focusedUid === photo.UID}
<PhotoTile
{photo}
selected={sel}
onClick={(e) => onTileClick(e, photo.UID)}
onDblclick={(e) => onTileDblclick(e, photo.UID)}
/>
{/if}
</div>
{/if}
</div>
{/if}
{/each}
</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
PhotoPrism's preload distance from page/photos.vue. -->
<div
aria-hidden="true"
class="h-4"
use:nearBottom={{
onHit: () => photosQuery.fetchNextPage(),
enabled:
photosQuery.hasNextPage && !photosQuery.isFetchingNextPage,
root: scrollRoot,
}}
></div>
{#if photosQuery.isFetchingNextPage}
<p class="py-3 text-center text-xs text-muted-foreground">
Loading more
</p>
{/if}
{/if}
</div>
</main>
{/snippet}
<div
aria-hidden="true"
class="h-4"
use:nearBottom={{
onHit: () => photosQuery.fetchNextPage(),
enabled:
photosQuery.hasNextPage && !photosQuery.isFetchingNextPage,
root: scrollRoot,
}}
></div>
{#if photosQuery.isFetchingNextPage}
<p class="py-3 text-center text-xs text-muted-foreground">
Loading more
</p>
{/if}
{/if}
</div>
</main>
{/snippet}
</SplitGrid>
<BulkActionBar />
</div>