Commit Graph

13 Commits

Author SHA1 Message Date
ce2cda0565 refactor: merge reject into trash as a single soft-trash concept
The Photo model previously had two near-identical "negative culling"
states: is_rejected (a flag) and is_trashed (a flag plus a file move).
Lightroom users typically use one or the other, never both, and the
file-move semantics of the old trash made it harder to undo. Merging
into a single soft is_trashed flag — file stays on disk, restore is a
flag flip, permanent deletion still happens via DELETE /trash/empty.

Backend
- Drop is_rejected from PhotoBase, PhotoResponse, PhotoUpdate, the
  list endpoint filter, and the bulk-action 'reject' branch.
- Add is_trashed to PhotoUpdate so the PATCH path can set it.
- Drop is_rejected Column declaration from the SQLAlchemy model. The
  legacy DB column may persist on existing installs but is no longer
  read or written; SQLAlchemy ignores extra columns.
- Rewrite DELETE /photos/{id} as a soft trash: just sets is_trashed=
  true and trashed_at=now, no shutil.move. Permanent deletion still
  goes through the trash router.

Frontend
- Photo TS type drops is_rejected, gains is_trashed.
- X keyboard shortcut now sets is_trashed=true (was is_rejected); U
  clears both is_picked and is_trashed.
- RightSidebar Reject button → Trash button (Trash2 icon).
- PhotoThumbnail flag overlay shows Trash2 icon for trashed photos
  instead of an X for rejected.
- KeyboardHints relabels X from "Reject" to "Trash".
- filterStore FlagFilter renames 'rejected' → 'trashed'; the params
  builder now sends is_trashed=true for the trashed filter (the list
  endpoint defaults to hiding trashed photos otherwise).
- FilterBar dropdown / URL sync allow-list updated accordingly.

No data migration: existing rejected photos remain as-is (flag stale)
and effectively become unflagged in the new model. Re-trash from the
UI to bring them into the new state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 22:16:10 +02:00
f7bf22db29 chore(docker): add proxies_data volume for /proxy endpoint cache
The /photos/{id}/proxy endpoint (added in 1096854) caches transcoded
RAW/HEIC WebPs at /data/proxies/{id}.webp, but the compose file had no
volume mount for that path — files would be lost on every container
restart, forcing repeated full-resolution decodes. Adding a named
volume to both backend and worker so the cache survives restarts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 22:15:43 +02:00
3e322576f2 chore: drop static shortcut drawer; inline contextual hints
The bottom-left KeyboardShortcuts drawer duplicated information that
the contextual KeyboardHints pill already shows for the current
selection state. Removing it in favor of the contextual hints alone.

KeyboardHints was previously a fixed top-14 overlay, which collided
with the FilterBar when it was opened — the hints panel covered the
filter controls. Refactored it to render inline in the App header
stack (TopBar / FilterBar / ActiveFilterChips / KeyboardHints /
Timeline) so it flows naturally and never overlaps.

Also:
- Hide hints in loupe mode (the loupe has its own context)
- Replace the deleted shortcuts (Ctrl+A, Trash) with the newly wired
  ones (\\ Filters, / Search, E Loupe) so the hints surface them

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 22:15:31 +02:00
6e6672f225 fix: use square cells in timeline grid
PhotoThumbnail computed cell height as size * min(aspectRatio, 1.5),
so portrait photos overflowed their row. The TanStack Virtual row
estimate is a single fixed value (thumbnailSize + gap), so any cell
taller than that pushed into the row below — visible as overlapping
thumbnails whenever a portrait shared a row with landscapes.

Switching to square cells (Lightroom Library default) means every row
is exactly the estimated height. The image still fills via object-cover,
just cropped on the long axis.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 22:15:19 +02:00
d2155d9dd2 feat: filter bar with search, URL sync, and active-filter chips
Adds the spec §6.7 filter bar to the top of the timeline with:
  - Date range (native date inputs)
  - Media type chips (Photo / Video / RAW / HEIC, multi-select)
  - Min star rating (click to set, click again to clear)
  - Color label dots (red/orange/yellow/green/blue/purple, single)
  - Flag (any / picked / rejected / unflagged)
  - Clear-all button

Active filters surface as removable chips below the bar so they're
visible whether the bar is collapsed or open. The TopBar search input
is now wired to the same filter store with a 300ms debounce, and shows
a clear button when populated.

Filter state is the source of truth in a Zustand store and round-trips
through the URL via history.replaceState — bookmarkable and shareable
per spec §6.7. Hydrate happens once on mount; subsequent store changes
write back to ?q=&date_from=&… without navigation.

Timeline reads filter state, builds the backend params via
filtersToParams, and includes them in the React Query key so the cache
invalidates on every filter change. Also fixes a latent bug: Timeline
was sending limit=1000&offset=0, which the backend silently ignores —
swapped to page=1&per_page=500 with explicit sort=taken_at&order=desc.

New keyboard shortcuts:
  - \\  toggles the filter bar
  - /   focuses the TopBar search input
  - Cmd/Ctrl+F  same as /

Filter button in the TopBar now lights up when filters are active or
the bar is open.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 21:50:58 +02:00
892e8e1da4 feat: wire culling keyboard shortcuts to photo mutations
Replaces the console.log stubs in useKeyboardShortcuts with real
PATCH /photos/{id} mutations against the active photo, so 1-5 / 0 / P /
X / U / 6-9 actually rate, flag, and color-label photos. Mutations
invalidate both the photo detail query and the timeline list query, so
the RightSidebar and grid update immediately.

Shortcuts now work in BOTH grid and loupe modes (the previous
{ enabled: isGrid } gate is removed) so the user can cull while
browsing in the loupe — the Lightroom workflow.

Color labels 6-9 are wired to red/orange/yellow/green per spec §6.4.

Active photo id is read fresh via usePhotoStore.getState() inside each
handler, so we don't re-bind hotkeys on every selection change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 21:44:47 +02:00
9089ad2f61 feat: wire RightSidebar to real photo metadata
Replaces the mock photo data with a TanStack Query fetch driven by
activePhotoId, so the metadata panel reflects the photo currently
selected (or being viewed in the loupe). Parses exif_json defensively
and renders ExifTool fields with sensible aliases (Make/Model,
LensModel/Lens, FNumber, ExposureTime/ShutterSpeedValue, FocalLength,
GPSLatitude/Longitude). Falls back to '—' for missing fields.

Rating stars and Pick/Reject buttons now fire useMutation against
PATCH /photos/{id} and invalidate both the photo detail query and the
['photos'] list query so the timeline grid reflects the change too.

Multi-select keeps its bulk-action footer and shows the selection
count instead of metadata.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:39:48 +02:00
f4fc15101e feat: loupe view with zoom, pan, video, and filmstrip
Adds the Lightroom-style full-screen single-photo viewer (spec §6.11).
Open with E / Enter / double-click; navigate with arrow keys; Esc returns
to grid. Filmstrip at the bottom auto-scrolls the active cell into view.

Display:
- Stills source from /photos/{id}/proxy so RAW/HEIC are decoded server-
  side; large thumbnail is the onError fallback only.
- Videos render in a <video controls> sourced from /original.
- Continuous wheel zoom (1×–8×, ~15% per tick) with click-drag pan when
  zoomed past fit. Z toggles between fit and natural-resolution
  (computed from naturalWidth / clientWidth); a second Z snaps back.
- Live percentage indicator in the bottom-center while zoomed.

Polish:
- Neighbor preloading via new Image() when currentIndex changes so arrow
  nav feels instant (skips videos).
- Focus trap with role=dialog, aria-modal, focus-on-mount, restore-on-
  unmount, and Tab cycling among focusable children.

Plumbing:
- New canonical Photo TS interface in types/photo.ts; removes the three
  duplicated definitions in PhotoThumbnail/Timeline/photoStore.
- photoStore gains viewMode + openLoupe/closeLoupe.
- useKeyboardShortcuts wires E/Enter/G to open/close, gates rating and
  flag stubs with { enabled: viewMode === 'grid' } so they're inert in
  loupe.
- App.tsx mounts <LoupeView/> as a z-40 overlay covering TopBar, gates
  the auto-open right sidebar effect on viewMode === 'grid' so leaving
  loupe doesn't fight the user's prior sidebar state.
- PhotoThumbnail gains an onDoubleClick prop wired to openLoupe.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:39:33 +02:00
1096854553 feat: backend /proxy endpoint for full-res RAW/HEIC display
Adds GET /photos/{id}/proxy that decodes RAW (rawpy), HEIC (pillow-heif),
and TIFF to a cached full-resolution WebP at /data/proxies/{id}.webp.
Web-safe formats (JPEG/PNG/WebP/GIF) pass through to the original to
avoid pointless transcoding. RAW failures fall back to extracting the
embedded JPEG preview. Mirrors the X-Accel-Redirect pattern from the
existing thumb endpoint.

Also fixes GET /photos/{id}/original to return the correct image/jpeg,
image/png, video/mp4, etc. content types instead of always serving
application/octet-stream, so <img> and <video> tags can render the
file inline rather than triggering a download.

Frontend: adds photos.getProxyUrl() helper in services/api.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:39:01 +02:00
72d301a9c7 feat: resilient thumbnail loading with backoff retries
Backend generates thumbnails on-demand via Celery, so the first request
often 404s while the worker runs. Auto-retry with 1.5s/3.5s/6s backoff,
manual retry fallback, and proper timer cleanup so fast-scrolling a
virtualized timeline doesn't setState on unmounted components.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:59:03 +02:00
78e12e8309 feat: timeline and folder import 2026-04-07 00:42:22 +02:00
6d1b227fb9 feat: structure 2 2026-04-07 00:15:00 +02:00
46a0d7aba8 feat: structure 2026-04-06 23:30:19 +02:00