- Favorites use PhotoPrism's own like/unlike endpoint (not a mule-only
mark) so they sync to third-party gallery apps, with heart controls
in the tile, sidebar, and an `f` shortcut.
- Lightbox: wheel-zoom around cursor, double-click to 2.5x, drag-to-pan,
auto-upgrades to the fit_2048 tile past 1.25x zoom.
- Sidebar: copy-EXIF button, clickable Camera/Lens values that jump to
a filtered timeline (camera:/lens: DSL), matching the existing
Country link.
- Fix filtersToQ() quoting the entire search string whenever it
contained a colon, which silently turned any raw DSL operator
(camera:, taken:2024, etc.) into a literal phrase search — discovered
while verifying the new jump-links against production.
- Disable TanStack Query's refetchOnWindowFocus: the indexer WebSocket
already invalidates photo queries on real changes, so the focus
refetch was just a redundant full-timeline re-render on tab-switch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Previously the modal's aside was gated on `focusedPhotoQuery.data`, so
each arrow-skim unmounted the sidebar until the next photo's metadata
arrived — which reflowed the preview pane sideways. Now the aside is
always mounted while the modal is open; its contents swap between the
metadata panel and a small InlineLoader the same way the timeline's
right-aside does.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Preview pane paints instantly: a blurred copy of the same thumbnail the
grid loaded (cache hit) rides beneath the sharp fit_1280, which now
carries fetchpriority=high and decoding=async. A $effect prefetches
fit_1280 for the ±2 neighbours so arrow-skim hits the HTTP cache.
Carousel thumbs drop to fetchpriority=low so they yield to the main
image. Skeleton grid gains an mt-2 to breathe against the toolbar.
BulkActionBar moves inside the main column in both PreviewModal and the
/tags drill-in so it no longer stretches under the right sidebar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expand color labels from the 4-swatch Lightroom culling palette to 8
neutral colors (red/orange/yellow/green/teal/blue/purple/pink) with no
attached semantics, rendered as outlines that fill in when picked.
Carousel tiles now show a VIDEO badge, and the folder row in the right
sidebar always renders ("/" for root) instead of disappearing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces ad-hoc "Loading…" text and bare empty messages with two
shared feedback primitives that carry subtle lucide icons, consistent
muted-foreground/destructive tones, and a11y signaling (role=status,
aria-busy, role=alert on destructive empties). Loading copy gains
context ("Loading photos/folders/heaps/metadata…") and the right-
sidebar idle state moves from a "ⓘ" glyph to a MousePointerClick
icon. SkeletonGrid stays as the initial-grid loader.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The old SplitGrid + InlinePreview pane is replaced by a full-screen
PreviewModal mounted once at the layout root. Open via Space on the
focused tile or double-click; close on Esc (or X / Space again).
Inside, PreviewPane renders the focused photo, RightSidebar carries
the metadata, BulkActionBar reuses the existing per-photo actions,
and PreviewCarousel windows ±50 thumbs around the focused index.
Selection contract matches the grid: plain click reduces, shift
extends the range, ⌘/Ctrl toggles, plain arrow drops the multi-
selection, shift-arrow extends. New clearBulkToFirst() helper makes
Esc / Clear collapse a bulk back to single-focus on its first member
before the next press fully dismisses (modal closes, grid clears
focus).
Tags route reorganised into /tags/[category]/[[value]] with its own
+layout and TagsBrowserSidebar; the old monolithic /tags/+page is
trimmed to a legacy redirect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Selection: plain arrow nav now clears prior multi-selection so exactly
one tile is ringed at a time; shift-extend still grows from the anchor.
onApprove / onRestore / onDelete advance focus via focusAfter(ids)
before clearing selection, matching onArchive.
Preview: defer mounting <VideoPlayer> by 250ms so arrow-skim across
video tiles doesn't open and immediately cancel range requests; hard-
abort the underlying <video> on unmount so the connection releases.
Tags drill view: right-sidebar metadata wired in (single-photo
RightSidebar, BulkMetadataSidebar for >=2 selected), resizable edge
mirrors the timeline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
When `selection.ids.size >= 2`, the inline preview swaps the single-
photo view for a SelectionDeck: each selected thumbnail renders as
an absolutely-positioned card with a translate + rotate computed
from its index in the deck, so the spread reads as a fan. CSS
transition-transform handles the reflow as the deck grows or
shrinks; `in:fly` lands new cards from above, `out:scale` shrinks
removals into the stack. Hash resolution walks the existing
TanStack caches (per-photo + photos-infinite envelope) so the deck
is side-effect-free — no fetches just to render thumbs.
Drop the now-redundant Maximize hover affordance on PhotoTile and
the `onOpenPreview` plumbing through PhotoGrid / +page.svelte:
single-click already places a tile into the inline preview pane,
so the dedicated "open preview" button no longer has a job.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
PhotoPrism plays a silent preview of the actual video when you hover
its grid tile; this mirrors that. After a 250ms debounce the tile
mounts a muted, looping <video> over the thumbnail and cross-fades it
in on first decoded frame, so cursor-skimming doesn't fire N requests
and the tile never blanks mid-fetch. The byte-prefetch helper added
in af96922 is now redundant — the hover <video> warms the same caches
on its own.
Also tells Vidstack the playback URL is video/mp4 via a nested
<source>: our /api/v1/videos/.../avc URL has no extension, so
Vidstack's suffix sniff was failing, falling back to a HEAD probe,
and picking the wrong loader (which surfaced as
NS_ERROR_DOM_MEDIA_METADATA_ERR in Firefox).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace PreviewOverlay's bare <video controls> with a vidstack-driven
player wrapping the same source URL. Vidstack's default video layout
provides a polished chrome (gradient bottom bar, large play overlay,
hover-revealed scrubber) and registers <media-player>/<media-provider>
custom elements that vendor the browser quirks.
To keep first-frame latency low, PhotoTile starts a hover-warm fetch
of the playback URL after a short (120 ms) delay — a single Range
request of the first 512 KB pages the backend's pre-transcoded MP4
cache file into the OS page cache and lands in the browser's HTTP
cache, so when the player mounts and issues its own bytes=0- request
the response is satisfied from disk. Each hash is warmed at most once
per session; AbortController cancels hovers that don't commit.
The vidstack modules are dynamically imported on mount so they never
run during SvelteKit's static prerender — they side-effect
customElements.define() calls which would crash under SSR.
Sidebar
- New `/` root-folder entry at the top of the Folders group. Active
when the timeline is scoped to root; the photo grid post-filters to
`Path === ''` because PhotoPrism's `path:` operator can't express an
exact-root match. Collapsible chevron, persisted to its own
localStorage key, and a kebab carrying just "New subfolder".
- Per-folder count badges. `/api/v1/photos?q=path:X&count=1000` per
folder in parallel via `listFolderCounts`; root count derived from
`config.count.all − Σ subfolder counts`.
- Folder tree starts at depth=1 under the root so nested rows indent
visually relative to `/`.
- Footer matches the Toolbar / action-bar h-9 height.
Timeline interaction
- Single click on a tile selects only that tile (clears others); the
preview now lives on dblclick. Modifier clicks still go through
`gridKeyNav`'s document handler (shift = range, cmd/ctrl = toggle).
- `x` (archive) now actually archives — PhotoPrism's photo PUT
silently drops the Archived field, so we route through
/batch/photos/{archive,restore} the same way the BulkActionBar
already did. Mirror for `u`.
- Preview close restores the timeline focus + scrolls the last-shown
photo into view via `forcedExpand`+`scrollTileIntoView` so it
actually mounts (selection ring would otherwise stay invisible when
the user navigated far in preview).
- `applyFolderScope` only narrows the timeline to root when the active
view is a folder view (no heap / search / non-default section), so
label clicks / heap views / favorites no longer drop subfolder
photos.
Action bar
- Inline `h-9` row at the bottom of the main column (not `fixed`),
matching the Toolbar's visual language. Right sidebar stays full
height — the bar only spans the timeline width.
- Approve action wired for the review pile.
Colors / Tags / Ratings drill-ins
- New shared `PhotoGrid` component owning tile rendering, selection
styling, single-click-selects + dblclick-previews, and `setOrder`
for arrow-key nav.
- Each route's drill-in `<main>` carries `use:gridKeyNav` and a
trailing `<BulkActionBar />` so shift/cmd/ctrl click, arrow keys,
and the keyboard shortcuts work the same as the timeline.
- Tags switches from `goto('/?q=label:…')` to an in-place drill-in
with a back button, mirroring `/colors`'s flow.
- Category cards + drill-in photo cards honour the global
`view.thumbnailSize` (XS–XL) so the timeline's size selector now
reaches into all four grids.
Settings
- General-settings dialog merges Appearance into UI and switches free
text inputs to selects for the PhotoPrism theme / language / start
page / map style (the value-from-server prepends if it's outside
the curated list so we never silently rewrite a custom value). Time
zone uses `<datalist>` with `Intl.supportedValuesOf('timeZone')`.
Sidecar
- Heap convert runs reindex synchronously per source path so the
client's invalidate-and-refetch sees the moved files.
Inbox
- New /inbox route stub for the upcoming import workflow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switch the preview overlay from a still <img> to a <video> tag when the
focused photo's Type is "video". Uses PhotoPrism's /api/v1/videos/:hash
endpoint with the existing previewToken, falls back to a still thumb as
the poster, and autoplays muted so the controls reveal without
clobbering whatever else is on the page.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the legacy mule-image backend with PhotoPrism plus a thin
SvelteKit client and a Node sidecar for endpoints PhotoPrism doesn't
expose (file rename), and add a two-phase migrator (metadata via PUT,
heaps → albums) for the existing Postgres library.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>