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>
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>
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>