Commit Graph

68 Commits

Author SHA1 Message Date
a9685f64c4 fix(people): keep "name new faces" reachable after naming the first one
Landing on /tags/people with no value in the URL auto-selects the
first named person (so there's always something to look at) — but
that same effect made the naming workflow unreachable the moment a
second person existed to redirect into: NewFacesPanel only rendered
in the "!selectedValue" branch, and there was no way back to a null
selection once one existed.

Added a pinned "Name new faces" row in the People sidebar (with a live
unnamed-cluster count) that sets a `?view=new-faces` query param
instead of clearing the `[[value]]` route param — deliberately
independent of the value-drives-selection model so it can't be
overwritten. The auto-select-first-tag effect also needed an explicit
guard for it: navigating to a bare /tags/people URL still clears
selectedValue, which re-triggers that same effect in the same tick and
would otherwise bounce straight back to the first person before the
panel ever rendered.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 10:12:56 +02:00
c6f31b5dfb feat(move): undoable moves + Lightroom-style move/copy dialog
Backend correctness was already sound (companion files travel as a
group, coordinated collision suffixes, EXDEV fallback, per-file scope
checks, blocking scoped reindex) — this pass adds reversibility and
brings the modal up to standard.

Sidecar:
- movePhotoFiles records per-file {from,to} pairs (move mode) —
  including siblings of photos that failed partway, since undo must
  restore whatever actually left its folder. Both POST /photos/move
  and POST /albums/:uid/convert return them as movedFiles.
- New POST /files/restore-moves plays those pairs backwards: both ends
  scope-checked (sources aren't quarantined like the duplicates
  restore), never clobbers an existing destination, EXDEV fallback,
  blocking reindex of affected parents so the client's refetch already
  sees the restored layout.

Dialog (all three subjects — photos, heap convert, folder reparent):
- Search field on top (autofocused) filtering the tree live: matches +
  ancestors, force-expanded without touching the sidebar's persisted
  open/collapse state (new FolderTree forceExpand prop).
- Arrow keys rove through visible rows with selection following focus
  (data-move-row attributes in FolderTree's readonly picker mode);
  Enter confirms from anywhere once a destination is set.
- Recent destinations as one-click chips (last 5, per library base).
- Live destination preview line and count-labeled confirm buttons
  ("Move 12 photos", "Move “2024”") with a disabled-reason tooltip.
- Client-side subfolder validation mirroring the sidecar's
  sanitizeFilename rules (inline error, aria-invalid, confirm gated).
- Pre-disables Move when every selected photo is already in the target.
- Undo everywhere it's safe: photo/heap moves restore via the new
  endpoint, folder moves invert to another folder move, copies stay
  toast-only (their inverse would be deletion). Success toasts carry
  an inline Undo action; ⌘Z works through the shared undo stack.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 09:56:21 +02:00
246d159d93 feat(metadata,people): richer metadata editing + face-naming flow
Metadata (sidebar):
- New editable fields: Title, Credits section (Artist/Copyright/
  License via Details), GPS Altitude.
- Video facts in the File section: Duration, FPS, Codec — required
  fixing videoFile(): PhotoPrism serializes MediaType as the bare word
  "video", so the old startsWith('video/') check never matched and the
  helper always fell back to the JPEG poster.
- Timezone correctness: buildTakenAtPatch no longer forces
  TakenAtLocal=UTC; it preserves the photo's existing UTC↔local offset
  (per-photo in bulk edits) so PhotoPrism can't clobber manual date
  edits when recomputing from TimeZone, and Year/Month/Day now derive
  from local wall-clock time.

People (was "disabled" — really: zero subjects because naming is what
creates a person, and the UI had no naming flow; prod has 40k face
markers in 790 unnamed clusters):
- Sidecar GET /api/sidecar/subjects — scoped people list via one
  markers→files→photos SQL pass (labels pattern), replacing the
  client-side probe-per-subject N+1 filter.
- Sidecar GET /api/sidecar/faces/unnamed — the caller's unnamed face
  clusters with count, crop thumb, and a representative marker UID.
- "Name new faces" panel on /tags/people: face-crop cards with inline
  name input; naming uses PhotoPrism's own flow (PUT /markers/:uid
  {Name, SubjSrc:manual}, verified against PP source) which creates
  the Subject and propagates across the cluster.
- Scoped proxy: marker PUT / subject-clear DELETE now allowed with
  per-marker ownership checks (was blanket-forbidden, which would have
  blocked naming for scoped users).
- Per-photo People chips in the sidebar from named Files[].Markers,
  linking to the person's page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 18:35:27 +02:00
75008f238a feat(review): fast keyboard-first Stacks & Duplicates resolve queue
Both tabs get a resolve-and-advance queue instead of independent
click-to-focus cards: ↑/↓ or j/k rove between groups, resolving a
group removes it optimistically and auto-advances focus, and a sticky
header tracks reclaimable bytes + a running "resolved this session"
tally. ⌘Z undoes via a new sidecar restore endpoint (gridKeyNav — the
usual ⌘Z owner — isn't mounted on these tabs, so DuplicatesView wires
its own).

Sidecar (handlers_dups.go, fs.go, main.go):
- POST /duplicates/restore — inverse of /duplicates/archive, moves
  quarantined files back to their original path with the same BasePath
  guards and async reindex-with-cleanup.
- Scan results now include each file's mtime so the UI can label
  older/newer copies.

Stack losers now go through the same sidecar quarantine as
cross-folder duplicates (setPrimary + archiveDuplicatePaths) instead
of a hard PhotoPrism DELETE, so both tabs share one recoverable,
undoable resolution path (services/duplicateActions.svelte.ts).

StackGroupCard: comparison-first — fact rows highlight the best
size/resolution per file, a "Suggested" badge appears when one file
wins outright, and Space opens a fullscreen CompareLightbox that flips
between candidates while preserving zoom/pan (extracted the zoom/pan
gesture handling from PreviewPane into a shared lib/actions/zoomPan.ts
action so both consumers share one implementation).

CrossFolderGroupCard: since every copy is byte-identical, the old grid
of N identical thumbnails told the user nothing — replaced with one
thumbnail plus a path list that highlights the differing folder
segment and flags the indexed/newest copy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 11:00:36 +02:00
0f65bfb94a feat(web): native favorites, lightbox zoom, EXIF jump links + perf fix
- 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>
2026-07-03 21:56:49 +02:00
9ba8d625bc feat(web): ⌘K command palette
bits-ui Command in a dialog: jump to sections, heaps, folders (from the
already-warm sidebar queries), plus dark-mode and shortcut-overlay
actions. Global ⌘K binding in the layout works from any route and while
inputs hold focus.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 13:22:31 +02:00
9ef1b4c2f9 feat(web): keyboard rating/color marks, search focus, shortcuts overlay
Lightroom-style keys in grid+preview: 0-5 rating (re-key toggles),
6-9 color labels, optimistic marks cache patch with rollback. / focuses
the search box, ? opens a new shortcut-reference overlay that inertly
swallows other keys while open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 13:15:00 +02:00
634abc2a95 feat(settings,index): per-user index sub-path, strip dead PP settings, scope duplicates
Lets a user pick a sub-folder under their library as a working index root,
stored server-side (new sidecar user_prefs table). The Library tree, reindex,
and both duplicate views (stacks + cross-folder scan) now re-root to it via a
single userLibraryBase() helper. Also fixes the cross-folder scan/archive
endpoints, which previously walked/touched the whole originals root instead
of being scoped per-user (archive now rejects out-of-scope paths, 403).

Removes PhotoPrism settings (Search/Maps/Server-UI/Features/Import) that only
steered PhotoPrism's own bundled SPA and were never read by mulimage's UI.

Also fixes the Library tree occasionally getting stuck on "Loading folders…"
by dropping gcTime:0 and gating the spinner on isLoading instead of isPending.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 22:41:33 +02:00
ba5684d120 feat(countries): replace Map view with browse-by-country (like Tags)
Removes the maplibre-gl Map view and adds "Countries" as a sixth
TagCategory, reusing the existing /tags/[category]/[[value]] browse
machinery instead of a bespoke map UI. Backed by a new self-contained
sidecar endpoint that aggregates photos.photo_country with BasePath
scoping, mirroring handleLabels/handleScopedCounts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 21:25:24 +02:00
400b215036 fix(sidebar,map,duplicates): flatten sidebar hierarchy, filter by user basePath, fix map coordinates
- Flatten sidebar: remove collapsible Tags and Review sections, place all items at level-0
  Notes, tag categories (Labels/Keywords/People/Colors/Ratings) now appear directly in Views
  Review tabs (Causes/Stacks/Duplicates) and Hidden appear directly in Manage
- Filter duplicates by user base path to ensure multi-tenant isolation
  listDuplicateGroups now accepts optional basePath parameter
  update review page and sidebar to pass userBasePath() for proper per-user caching
- Filter map geo data by user base path using path: query filter
  map page now only shows geotagged photos from current user's library
- Fix map coordinate positioning: PhotoPrism /geo endpoint returns [lat,lng]
  but GeoJSON and MapLibre expect [lng,lat]. Transform coordinates and bbox
  on data receive to fix photo placement and zoom behavior

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-30 21:04:56 +02:00
a52f171946 fix(folders): translate BasePath for create/rename/delete to fix "invalid path"
The sidebar shows user-relative paths (BasePath stripped) but the sidecar
operates on originals-relative paths. Folder create/rename/delete passed the
stripped path straight through, so a BasePath user's ops resolved to the wrong
directory and the sidecar returned "invalid path". Wrap outgoing paths with
toOriginalsPath and map returned paths back with toUserPath, matching the move
flow. Identity for admin accounts (empty BasePath).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 00:06:18 +02:00
ad6e733622 fix(move): close the dialog when a move starts so header progress shows
The move dialog held its full-screen overlay open for the whole operation,
hiding exactly the header reindex/status pill the user waits on. Snapshot the
draft state, closeMove() up front, and run the move in the background with a
toast.loading→success/error — mirrors the archive flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 00:06:18 +02:00
6d9b236ef6 feat(library): reindex button, live grid, archive-reappear fix, bigger carets
- Add one-click "reindex new files" button to the Library sidebar header
  (RefreshCw, calls startIndex rescan:false), spins + disables while active.
- Refresh the photos grid from the indexer WS stream (throttled during the
  scan + once on completion) so newly indexed files appear live.
- Fix archived photos flashing back into the grid when archiving others:
  drop the per-action settle-driven clearRemoved and reconcile removedIds
  against the actual cache instead (clears an id only once it's gone from
  the deduped pages). Covers archive, delete, and bulk-bar removals.
- Replace the tiny Unicode caret triangles with a 16px Lucide ChevronRight
  that rotates 90deg on expand, across folder tree rows, root, Tags, Review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 23:48:18 +02:00
b2b6060872 feat(move): "move to folder" for grid selections, folders, and m shortcut
Extends the heap-only "move to folder" action to grid single/bulk
selections, sidebar folders, and an `m` keyboard shortcut — all through
one shared dialog driven by a moveDialog store.

Backend (sidecar):
- Extract the heap move/copy + reindex loop into a reusable movePhotoFiles
  helper plus resolveMoveTarget
- POST /photos/move: move/copy an arbitrary UID list into a folder
- POST /folders/:rel/move: reparent a folder dir (whole subtree) under a
  new parent, guarding against moving into itself/a descendant

Frontend:
- moveDialog store + generalized MoveToFolderDialog (heap | photos | folder
  subjects); mounted once in +layout.svelte. Replaces HeapConvertDialog
- movePhotosToFolder / moveFolder service fns
- Entry points: BulkActionBar button, gridKeyNav `m`, FolderTree kebab,
  heap kebab — all call openMove()

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 00:10:19 +02:00
5be6fd9047 feat(archive): red-cross flash + instant tile removal on archive/delete
Archive/delete now flash a red cross then drop tiles from the grid
immediately, instead of a green check that lingered until the slow
server-reconcile refetch landed. Keyboard `x` archive previously never
called markRemoved, so tiles only vanished on refetch — that lag is gone.

- Add 'removed' bulk state + removedBulk() helper (red cross overlay)
- gridKeyNav archive/delete: removedBulk -> 500ms flash -> markRemoved,
  clearRemoved once refetch settles; restore stays green check
- BulkActionBar: BulkConfig.removing routes archive/delete through the
  red flash; approve/restore/label/note unchanged

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 23:45:38 +02:00
259adb6a41 fix: tile overlay reactivity + facets showing all marked/noted photos
Problem 1 — per-photo progress overlay never rendered on the grid:
- bulkPhotoStates was `$state(new Map())`; a `.get(uid)` read in PhotoTile
  didn't reliably re-run when the entry flipped, so the spinner/check/X
  overlay never appeared. Switch to SvelteMap (svelte/reactivity).

Problem 2 — Notes / Colors / Ratings only showed the newest ~1000 photos:
- All three derived from `listPhotos({ count: 1000 })`, silently hiding
  older marked/noted photos.
- listPhotosWithNotes now pages the whole library.
- Add listPhotosByUids() and resolve the Colors/Ratings marks-pool from the
  complete marked-UID set (from getAllMarks) instead of the newest slice;
  wire it into the TagsBrowserSidebar panel and the tag drill page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 00:37:30 +02:00
ccf2c6b7c7 fix: bulk label apply, instant archive removal, per-photo state, drop sidebar counts
Issue 1 — colors/labels not applying in bulk:
- sidecar validColors only accepted 4 of the 8 UI swatches, so teal/blue/
  purple/pink returned "invalid color" and rolled back the whole bulk txn.
  Add teal, blue, purple, pink to validColors.
- Add invalidateFacets() and call it on the success path of bulk marks,
  patchTargets, and single-photo edits so the Colors/Ratings/Notes facet
  sections refresh immediately instead of waiting out staleTime.

Issue 2 — archived photos linger in the grid:
- Add a UI-only removedIds set to the bulkAction store; archive/delete/
  restore/keep call markRemoved() on success so tiles vanish instantly,
  cleared once the server-reconcile refetch lands (no cache eviction).

Issue 3 — per-photo progress state:
- Wire startBulk/doneBulk/failBulk into all metadata applies, bulk
  (BulkMetadataSidebar) and single (RightSidebar), so colors/ratings/
  notes/dates/keywords show the spinner -> check -> X overlay.

Issue 4 — remove Left-sidebar count badges:
- Drop count badges from root folder, Archive, heaps, Notes, and the
  folder tree, plus the now-dead count queries and unused imports. Facet
  drill-panel counts are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 00:21:04 +02:00
a13e171295 bump post-action delay to 1s for PhotoPrism indexer to catch up
200ms was too short - PhotoPrism's batch archive marks photos in the DB
but the search index (used by /api/v1/photos) updates asynchronously.
1s gives the indexer time to reflect the change before we invalidate
and refetch the timeline.
2026-06-07 22:43:24 +02:00
73c36b4817 revert evictFromCache, add back delay + marks invalidation
Remove evictFromCache entirely - the cache-manipulation approach was
brittle and broke both archive (photos not removed) and tags
(colors/ratings showing empty). Replace with: 200ms delay before
invalidation to let PhotoPrism's indexer process the change, plus
invalidateQueries(['marks']) so tag caches (colors, ratings, notes)
refresh alongside the photo timeline.
2026-06-07 22:31:39 +02:00
82f2a40269 fix: folder loading gets error state + gcTime=0 for fresh refetch
Add a proper error branch to the folder tree so a failed sidecar request
shows an error state rather than a perpetual loading spinner. Also sets
gcTime: 0 so the query re-fetches fresh data when the sidebar remounts
instead of holding onto stale cache across navigations.
2026-06-07 22:10:44 +02:00
1df16a6142 perf: evict archived/deleted photos from cache immediately
Remove archived/restored/deleted/approved UIDs from all cached photo-list
pages right after the API confirms, so the grid updates on the same tick
instead of waiting for a network round-trip. Also removes the 400ms
doneBulk animation delay (now unnecessary since tiles vanish instantly).
2026-06-07 21:53:16 +02:00
5da1022ed1 feat: loading toasts for all photo actions
Add loading→success/error toast transition to every bulk operation
(archive, restore, delete, approve, add-to-heap, metadata patch).
Also wires gridKeyNav + CauseGroupCard into the bulkAction store so
keyboard-triggered actions show the same per-tile pending/done/error
feedback as BulkActionBar buttons.
2026-06-07 21:40:18 +02:00
dtoro
da63ad769a feat: bulk action progress — header pill + per-thumbnail states
- New bulkAction store: tracks active/label/detail state for the pill
  and a Map<uid, pending|done|error> for per-tile overlays
- Extract StatusPill.svelte from IndexerStatusPill (generic active/label/detail
  props); IndexerStatusPill becomes a one-line wrapper
- +layout.svelte: render a second StatusPill driven by bulkAction store,
  alongside the indexer pill in the AnimatedMule header
- BulkActionBar: extend withBusy with optional BulkConfig (ids/label/doneLabel);
  pending tiles dim + spinner on start, green checkmark flashes for 400ms before
  cache invalidation removes them; red overlay on error, auto-clears after 2s
- onApprove/batchEdit: wire onProgress callback to setDetail so the pill shows
  the filename currently being processed during fan-out keep operations
- batch.ts: add completedId as third arg to onProgress (backwards-compatible)
- PhotoTile: derive bulkState from store; pending/done/error overlays sit above
  the selection tint; hover-video guarded against pending tiles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 09:43:12 +02:00
9fc650fb12 web(sidebar): auto-expand folder tree ancestors of the active folder
When the timeline navigates to a nested folder (RightSidebar's
open-folder icon, URL hydration, back/forward), the LeftSidebar
already highlighted the matching row via filters.folderPath — but
if the parent folder was collapsed in the persisted openSet, the
highlighted row wasn't visible at all.

Each FolderTree instance now runs an effect that adds every
ancestor of the active path to its openSet on filter change. The
root instance expands the top-level ancestor first, which mounts
the next-depth FolderTree instance — and the same effect runs
there, cascading down to the leaf. Persisted to localStorage so
the expansion sticks across reloads.

Skipped in `readonly` mode (heap-convert picker has its own
selectedPath and shouldn't drive the sidebar state).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:12:36 +02:00
29f7ad7073 web: deep-link from RightSidebar to folder/map + anchor-mode timeline + fix root count
RightSidebar:
- Folder + Location rows gain a small ArrowUpRight icon button that
  deep-links into the timeline / map view focused on the photo. OSM
  external link removed; the in-app map nav covers the same job.

Folder navigation:
- New navigateToFolder(path, { focusUid, focusTakenAt }) helper in the
  filters store; LeftSidebar's pickFolder collapses to a one-liner that
  reuses it.
- One-shot pending-focus stash carries both UID and TakenAt across the
  goto. URL-watch effect on the timeline consumes the stash so even
  same-folder navigations (where the filter doesn't change) get
  picked up.

Anchor-mode timeline query:
- listPhotosAround(q, takenAt, after, before) issues two parallel
  PhotoPrism calls (`after:<day-1>` oldest-first + `before:<day+1>`
  newest-first), merges + dedupes newest-first. Uses PhotoPrism's
  existing date-only DSL clauses — no server changes.
- When a deep-link stashes a TakenAt, page 0 of the photosQuery uses
  the merged window so the target photo is loaded even for photos
  buried past the standard newest-first cursor. Pages 1+ are disabled
  in anchor mode (PhotoPrism's day-precision cursor would infinite-loop
  on dense days; users see 120 around the target, refresh to drop the
  anchor).
- After page 0 lands, the existing scrollToIndex(targetIdx) expands the
  windowed render set + scrolls the tile into view.

Map view:
- /map honors `?lat=&lng=&zoom=&focus=` URL params, jumping to the
  photo's coordinates at zoom 17 instead of fitBounds-ing the full
  library. Params are stripped after first apply so a manual zoom-out
  + reload doesn't snap back.

LeftSidebar root count badge:
- Now matches what Cmd+A selects in the timeline. Old code used
  /config.count.all (library aggregate, includes archived/hidden/
  review). Switched to countPhotos('', { merged: true }) which counts
  the actual photo entries the timeline lists.
- countPhotos gains a `merged` option; with merged=true it returns the
  response body length instead of the X-Count header — PhotoPrism's
  X-Count is always the file-row count regardless of merged, so a
  HEIC + JPG companion pair inflated the badge to 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:04:23 +02:00
c134afe023 web(review): confidence-aware date guesser with combined filename + path signals
Refactor suggestDateFromPath to combine multiple signals instead of
trying patterns in priority order:

- Filename Y-M-D corroborated by path Y-M/Y-M-D → HIGH (filename-
  agrees-path). Fixes the case where a Samsung-style 20240226_xxx.jpg
  under 2024/02/ was returning the path-only 2024-02-01.
- Filename Y-M-D with no path signal → HIGH (filename-only).
- 10/13-digit Unix epoch in basename → HIGH (unix-timestamp) —
  covers WeChat (mmexport...) and FB saves.
- Path Y-M-D → HIGH (path-ymd).
- Path Y-M only → MEDIUM (path-ym-default-day, synthesised day=01).
  Sidebar row labels these "(estimated day)" so the user knows.

Filename parser now accepts `.` and space separators (covers macOS
screenshots, manual 2024.02.26 renames). Path parser accepts `.` too.
OriginalName participates as a secondary filename signal when present
and different from the on-disk basename.

Patterns we explicitly DO NOT parse, to avoid silent date flips:
DD-MM-YYYY / MM-DD-YYYY, 2-digit years, bare camera sequence numbers.

Add photoNameAndDir(p) helper next to primaryFile so RightSidebar,
BulkActionBar, photoActions, and gridKeyNav all derive {fileName,
path} the same way — fixes the bug where photo.FileName was
undefined on the single-photo detail endpoint and the basename branch
was being skipped entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:04:48 +02:00
a54d90a2d9 web(review): YYYY/MM path fallback + suggestion row below date + tighten 'a' gate
- suggestDateFromPath: when only year+month appear in the path
  (e.g. 2024/01/), synthesize day=01 so date-only foldering yields
  a usable suggestion instead of null.
- RightSidebar: move the suggestion row below the Taken-at input.
- BulkActionBar + gridKeyNav: show the "Accept date & Keep" button
  and fire the bare 'a' shortcut only when EVERY targeted photo has
  a path-derivable date — no more silent approve-without-fix for
  mixed selections.
- gridKeyNav: drop local cachedPhoto duplicate, use the shared one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:41:40 +02:00
97f51a05c4 web(review): scope date suggestion to EXIF Stripped tab + 'a' shortcut
- Gate the sidebar's "Suggested from path" row on /review?tab=stripped_exif
  instead of a per-photo TakenSrc heuristic — PhotoPrism stores a guessed
  TakenAt for stripped-EXIF photos too, so the heuristic was hiding the
  row even when a path-derived date was available.
- Same gate on the BulkActionBar's "Accept date & Keep" button.
- Extract acceptDateAndKeep() + cachedPhoto() into photoActions so the
  bar button and a new bare-'a' shortcut in gridKeyNav share one path.
- Show an 'A' kbd hint on the bar button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:31:04 +02:00
d1ddc48f81 web(review): switch cause tabs to PhotoGrid, add date-from-path suggestion
- CauseGroupCard now wraps PhotoGrid so selection, keyboard nav, and
  preview flow through the standard timeline plumbing. Per-tile hover
  Approve/Archive and the group-wide Approve all are gone — the bottom
  BulkActionBar's review-section Keep/Archive handle single + bulk.
- Low Resolution tab opts into a new PhotoTile dimensionBadge prop so
  WxH stays visible on each tile.
- New suggestDateFromPath util parses YYYY-MM-DD from filename or
  folder path. RightSidebar surfaces it as an amber Apply row above
  the Taken-at input whenever the photo lacks a trusted TakenAt.
- BulkActionBar gains a "Accept date & Keep" button (review section
  only) that patches each selected photo's TakenAt from its path
  suggestion when available, then approves.
- Drop the Same folder / Same camera / Same year strips and the
  RelatedStrip component from the metadata sidebar.

Also bundles in-progress Notes route + tile components and small
tweaks to LeftSidebar, DuplicatesView, CrossFolderGroupCard, and
photoprism.ts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:20:02 +02:00
55c870c155 web(sidebar): nest Hidden under Review submenu
Hidden is the resting place for photos dismissed during review, so it
groups naturally with the Review subitems. Stays a section-nav button
(keeping its scoped count badge); only Archive remains as a flat Manage
entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:44:43 +02:00
981328faff web(review): expandable Review group in sidebar, tabs become subitems
Mirrors the Tags affordance: chevron-only toggle, no /review landing
entry, navigation only via subitems (cause buckets + Stacks +
Cross-folder linked as /review?tab=<id>). Cause list reuses the
review-groups query so empty buckets stay hidden. The /review toolbar
drops the pill row and shows the active tab as a breadcrumb segment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:42:43 +02:00
e1707c314d preview: keep metadata sidebar mounted across photo changes
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>
2026-05-20 10:28:12 +02:00
64c0da794d web(sidebar): drop count badges from Map and Tags rows
Per-tag totals are already surfaced by the TagsBrowserSidebar, so the
main sidebar's Map/Tags rows stay as pure navigators. Also removes the
now-orphaned geo, marks, and keywords cache observers that only fed
those badges.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:24:53 +02:00
0f4e2e0b8f preview: LQIP + ±2 prefetch + bound action bar to its column
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>
2026-05-20 10:24:31 +02:00
ea1803ec2f web: 8-color label palette + VIDEO badge in carousel
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>
2026-05-20 09:56:05 +02:00
fc5f30fad1 fix(sidebar): use count.labels for Tags badge, drop bogus label:* query
PhotoPrism's q-DSL has no "has any label" predicate: `label:*` matches
every photo regardless of label, `label:<slug>` only matches that one
slug, and `keywords:*` behaves the same way. The prior `all:true label:*`
returned a 400 (and the earlier "drop all:true" follow-up made it return
the unfiltered library size, which then fed into tagsTotal and inflated
the parent Tags badge to ~library_size on admin sessions).

Switch labelsBadge to the precomputed `configQuery.count.labels` — the
same source the Tags sub-row's `tagCategoryCount('labels')` already
uses. The parent Tags badge now sums the exact same numbers the sub-rows
display: labels, keywords, people (distinct slugs/keywords/subjects)
plus ratings/colors (photos carrying each mark). Drop the wantScoped
short-circuit since the values are all library-wide now anyway.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 06:41:07 +00:00
a7b8a60473 web: admin surfaces so the PhotoPrism UI is never needed
- Account tab in General settings — self-service password change.
- UsersDialog (admin-only footer entry) — full /api/v1/users CRUD with
  admin-issued password reset.
- People as a fifth tag category alongside Labels/Keywords/Colors/Ratings,
  backed by /api/v1/subjects and the `person:` DSL clause.
- About tab in Library settings — version, library counts, feature chips,
  and a collapsible env-config help panel for the bits PP has no runtime
  API for (OIDC, TF, WebDAV).
- Library tab expanded with Indexer-advanced, extra Downloads checksums,
  and a Features grid that only renders keys PhotoPrism actually returns.
- Fix the SettingsDialog null-draft race the same way GeneralSettingsDialog
  already had: normalize on open, never null on close.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:26:12 +02:00
24dfa996b3 web: de-brand PhotoPrism references in user-facing copy
Replaces "PhotoPrism" in UI strings (empty states, tooltips, toasts,
log header, login screen) with neutral terms like "the indexer", "the
library", "the server" — accurate regardless of backend. The login
header becomes "Mulimage" and drops the explicit PhotoPrism mention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:26:12 +02:00
e364e4128f web: unify empty + loading states behind EmptyState/InlineLoader
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>
2026-05-20 08:26:12 +02:00
d2a76fa58c fix(sidebar): paginate folder counts; drop bogus all:true from labels query
Two distinct bugs were causing left-sidebar badges to under-report:

1. sidecar/folders/counts hard-capped each PP /photos call at count=1000
   and deduped UIDs from that single page. Any folder with >1000 file
   rows under it (typical for a multi-year root scan with HEIC sidecars)
   silently lost everything past row 1000. On this library the root
   badge reported 912 while the year subfolders summed to 1175. Loop
   offsets instead, breaking when PP returns a short page.

2. The Labels-badge query passed all:true label:* to PP, which 400s with
   "Unable to do that" - none of the other bucket queries prefix
   all:true. Drop it; the scoped() helper already injects the user's
   path clause when applicable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 22:44:43 +00:00
0d5f380948 preview: full-screen modal replaces inline split + tags route reorg
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>
2026-05-20 00:13:20 +02:00
680fa90cbe web: collapsible Tags section + file size on its own row
Groups score, color label, keywords, and auto-labels under one
collapsible "Tags" section on the metadata sidebar (open by default,
choice persists). Moves file size onto its own row with a HardDrive
icon so dimensions and weight read as independent facts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 23:34:01 +02:00
155e9bb126 web: single-pointer selection + post-action focus, video debounce, tags metadata sidebar
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>
2026-05-18 23:52:16 +02:00
c783f129cc 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>
2026-05-18 23:25:10 +02:00
24c449f475 feat(preview): fanned deck of cards on multi-select
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>
2026-05-18 22:50:40 +02:00
e36f1939c6 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>
2026-05-18 22:46:06 +02:00
Claudio
85847848c4 Sidebar: scope count badges to current user's library
PhotoPrism's /api/v1/config.count is library-wide and the same value
for every authenticated session. That made non-admins (and admins
with a non-empty BasePath) see badges that didn't match what the
timeline actually showed them.

Replaces the direct `configQuery.data?.count?.<bucket>` reads in
LeftSidebar with per-bucket queries against PhotoPrism's /photos
endpoint. The new `countPhotos(q)` helper sets `count=10000` and
reads the X-Count response header to get the true total in one round
-trip (PhotoPrism's ACL filter is what scopes the result, so the
header reflects "what this session can see").

Each bucket query appends `path:"<BasePath>*"` so admins-with-a-
BasePath stay scoped too; non-admins without a BasePath short-circuit
to `uid:none` (their effective visibility is zero, no point
querying). Admins without a BasePath skip the scoped queries
entirely and keep using the precomputed /config totals — same
network footprint as before for the common case.

Affected badges: Favorites, Hidden, Archive, Review, Tags (labels
component). Map already used `geoQuery` whose result is ACL-filtered
server-side, so its badge is per-user-correct without changes. The
`favorites` field was missing from PpClientConfig.count's TypeScript
type; added it.

Resolves the `test`-user complaint: sidebar showed the admin
library's totals next to Review / Hidden / Archive / Favorites
because those numbers came from /config, not from a user-scoped
query.
2026-05-18 20:11:52 +00:00
Claudio
b0c8c06b2b Sidebar: suppress global count badges for non-admin users
PhotoPrism's /api/v1/config.count returns library-wide aggregates to
any authenticated session, with no per-user scoping. The timeline
itself IS scoped (a guest sees zero photos), but the sidebar was
rendering admin-side totals next to Review / Hidden / Archive / Tags /
Map / root for non-admins — including a freshly-registered "test"
user with role=guest and BasePath="".

Until PhotoPrism gains per-user counters, the SPA now derives an
`isAdminUser` flag and gates every count that's drawn from
configQuery on it. Non-admin users see the labels without badges;
counts re-appear automatically when promoted. Per-folder counts from
the sidecar (which DO scope to BasePath) are unaffected.
2026-05-18 20:02:20 +00:00
829d7bed83 feat(sidebar,tags): per-tab counts and aggregated Tags badge
- /tags: each tab pill shows its own count (labels/keywords =
  distinct tags, ratings/colors = photos covered). Labels and marks
  queries become always-enabled on the route so every pill resolves
  immediately; keywords stays lazy.
- LeftSidebar: Map badge now reads from the shared `['geo']` cache so
  it matches /map's "N geotagged" footer instead of count.places
  (distinct locations). Tags badge sums the four inner counts;
  keywords contributes lazily once /tags?tab=keywords is visited.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:59:47 +02:00
d70244f17e feat(web): fold duplicates+inbox into /review; sidebar UX cleanup
- /duplicates and /inbox routes removed and folded into /review as
  additional tabs alongside cause tabs; /duplicates keeps a redirect
  for bookmarks.
- LeftSidebar: drop import/inbox tile and favorites; show per-user
  BasePath label at the folder root.
- RightSidebar: split file header into read-only path over editable
  basename (matches sidecar rename contract); date field switches to
  plain-text ISO YYYY-MM-DD (no native datetime picker) with strict
  validation and revert-on-invalid-blur; preserves original hour.
- BulkMetadataSidebar: same ISO-only date input with invalid-state
  styling and apply-button gating.
- BulkActionBar: drop redundant Restore and Undo buttons; ⌘Z still
  reachable via gridKeyNav.
- gridKeyNav: remove favorite toggle (F) alongside the favorites view
  retirement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:48:38 +02:00