feat(lightbox): medium-resolution preview tier (#492)
Adds an opt-in lightbox preview tier so guests open photos against an
aspect-preserved ~1920px JPEG (~200–500 KB) instead of the full original
(often 5–12 MB). Originals are still served on Download.
Backend:
- imageProcessor: generatePreviewImage / isPreviewValid / ensurePreviewImage
using fit:'inside' + withoutEnlargement (longEdge 1920, q85, mozjpeg)
- migration 104: photos.preview_path + lightbox_preview_enabled setting
(off by default, JSON-stringified for SQLite/Postgres parity)
- GET /api/gallery/:slug/preview/:photoId — gallery-auth, lazy generation,
ETag based on mtime+photoId+watermarkHash
- preview_url surfaced in the photo response only when the toggle is on
- admin /thumbnails/regenerate-previews mirrors regenerate-thumbnails,
skipping videos
- backup walk + archive cleanup + photo-delete now include previews/
Frontend:
- PhotoLightbox uses photo.preview_url ?? photo.url (null-safe fallback)
- ThumbnailsTab gets a Lightbox Preview Tier card: opt-in toggle +
Regenerate All Previews button (gated until the toggle is on)
- en/de locale strings; nl/pt/ru/fr fall back to en
Tested end-to-end: 11 MB / 4000×3000 source → 985 KB / 1920×1440 preview,
381 ms first call, 7 ms cached, ~91% byte reduction.
This commit is contained in:
@@ -719,7 +719,12 @@ export const PhotoLightbox: React.FC<PhotoLightboxProps> = ({
|
||||
/>
|
||||
) : (
|
||||
<AuthenticatedImage
|
||||
src={photo.url}
|
||||
// Prefer the lightbox preview tier when the admin
|
||||
// opted in (#492). Falls back to `url` (the
|
||||
// original) when preview_url is null — happens
|
||||
// when the toggle is off, when the photo is a
|
||||
// video, or briefly while lazy generation runs.
|
||||
src={photo.preview_url || photo.url}
|
||||
alt={photo.filename}
|
||||
fallbackSrc={photo.thumbnail_url || undefined}
|
||||
className="max-w-full max-h-full object-contain select-none pointer-events-none"
|
||||
@@ -742,7 +747,9 @@ export const PhotoLightbox: React.FC<PhotoLightboxProps> = ({
|
||||
onClick={handleImageClick}
|
||||
>
|
||||
<AuthenticatedImage
|
||||
src={photo.url}
|
||||
// Same preview-prefer-with-fallback logic as the
|
||||
// off-screen tile above (#492).
|
||||
src={photo.preview_url || photo.url}
|
||||
alt={photo.filename}
|
||||
fallbackSrc={photo.thumbnail_url || undefined}
|
||||
className="max-w-full max-h-full object-contain select-none"
|
||||
|
||||
Reference in New Issue
Block a user