diff --git a/backend/src/routes/gallery.js b/backend/src/routes/gallery.js index d954c2c3..e155da0a 100644 --- a/backend/src/routes/gallery.js +++ b/backend/src/routes/gallery.js @@ -430,6 +430,13 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => { // keeps working with the original. logger.debug to avoid noise. logger.debug('lightbox_preview_enabled lookup failed, treating as off', { error: e?.message }); } + + // #508: when the admin has flipped the "use original camera filenames" + // toggle (#493), the lightbox surfaces each photo's original_filename + // alongside the position counter so the photographer can map a guest's + // selection back to source files. Tied to the same toggle as downloads — + // one switch controls both surfaces. + const useOriginalFilenames = await getUseOriginalFilenames(); res.json({ @@ -458,6 +465,9 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => { hero_image_anchor: req.event.hero_image_anchor || 'center', default_photo_sort: req.event.default_photo_sort || 'upload_date_desc', download_zip_ready: !!(req.event.download_zip_path && req.event.download_zip_generated_at), + // Mirror of the admin-side toggle so the lightbox can decide + // whether to surface original camera filenames (#508). + use_original_filenames: useOriginalFilenames, ...protectionSettings }, categories: categories, @@ -472,6 +482,9 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => { return { id: photo.id, filename: photo.filename, + // Raw camera filename (or null for pre-migration-062 uploads). + // The lightbox renders it when `use_original_filenames` is on. + original_filename: photo.original_filename || null, url: photoUrl, thumbnail_url: photo.thumbnail_path ? `/api/gallery/${req.params.slug}/thumbnail/${photo.id}${wmQuery}` : null, // Hero-optimized image URL (1920x1080) for full-width hero sections diff --git a/frontend/src/components/gallery/GalleryView.tsx b/frontend/src/components/gallery/GalleryView.tsx index 22bc4c6f..6d6c1c09 100644 --- a/frontend/src/components/gallery/GalleryView.tsx +++ b/frontend/src/components/gallery/GalleryView.tsx @@ -143,6 +143,9 @@ export const GalleryView: React.FC = ({ slug, event }) => { const disableRightClick = data?.event?.disable_right_click === true; const enableDevtoolsProtection = data?.event?.enable_devtools_protection === true; const useCanvasRendering = data?.event?.use_canvas_rendering === true; + // #508 — surface original camera filenames in the lightbox when the + // admin has flipped the same toggle that drives original-name downloads. + const showOriginalFilename = data?.event?.use_original_filenames === true; // DevTools protection - enabled by individual setting OR legacy protection level const devToolsEnabled = enableDevtoolsProtection || protectionLevel === 'enhanced' || protectionLevel === 'maximum'; @@ -677,6 +680,7 @@ export const GalleryView: React.FC = ({ slug, event }) => { heroImageAnchor={data?.event?.hero_image_anchor || 'center'} welcomeMessage={event.welcome_message} onLogout={logout} + showOriginalFilename={showOriginalFilename} /> {/* Upload Modal for full-page layouts */} @@ -918,6 +922,7 @@ export const GalleryView: React.FC = ({ slug, event }) => { welcomeMessage={event.welcome_message} isClient={isClient} onToggleVisibility={isClient ? handleToggleVisibility : undefined} + showOriginalFilename={showOriginalFilename} /> diff --git a/frontend/src/components/gallery/PhotoGridWithLayouts.tsx b/frontend/src/components/gallery/PhotoGridWithLayouts.tsx index 15ee1c96..214b70ba 100644 --- a/frontend/src/components/gallery/PhotoGridWithLayouts.tsx +++ b/frontend/src/components/gallery/PhotoGridWithLayouts.tsx @@ -71,6 +71,9 @@ interface PhotoGridWithLayoutsProps { // Client visibility controls (#172) isClient?: boolean; onToggleVisibility?: (photoId: number, currentVisibility: string) => void; + // Mirror of the admin original-filename toggle (#508). When true, the + // lightbox bottom toolbar surfaces each photo's original camera name. + showOriginalFilename?: boolean; } export const PhotoGridWithLayouts: React.FC = ({ @@ -105,7 +108,8 @@ export const PhotoGridWithLayouts: React.FC = ({ welcomeMessage, onLogout, isClient = false, - onToggleVisibility + onToggleVisibility, + showOriginalFilename = false, }) => { const { t } = useTranslation(); const { theme } = useTheme(); @@ -238,6 +242,7 @@ export const PhotoGridWithLayouts: React.FC = ({ onLogout, isClient, onToggleVisibility, + showOriginalFilename, }; // Determine if we should show hero header (decoupled from layout) @@ -379,6 +384,7 @@ export const PhotoGridWithLayouts: React.FC = ({ enableDevtoolsProtection={enableDevtoolsProtection} initialShowFeedback={openFeedbackInitially} onFeedbackChange={onFeedbackChange} + showOriginalFilename={showOriginalFilename} /> )} diff --git a/frontend/src/components/gallery/PhotoLightbox.tsx b/frontend/src/components/gallery/PhotoLightbox.tsx index efdd606d..141c0244 100644 --- a/frontend/src/components/gallery/PhotoLightbox.tsx +++ b/frontend/src/components/gallery/PhotoLightbox.tsx @@ -24,6 +24,11 @@ interface PhotoLightboxProps { onFeedbackChange?: () => void; disableRightClick?: boolean; enableDevtoolsProtection?: boolean; + // When true, surface each photo's original camera filename in the + // bottom toolbar — useful for photographers matching guest selections + // back to source files (#508). Tied to the admin-side toggle that + // also drives original-filename downloads (#493). + showOriginalFilename?: boolean; } export const PhotoLightbox: React.FC = ({ @@ -40,6 +45,7 @@ export const PhotoLightbox: React.FC = ({ onFeedbackChange, disableRightClick = false, enableDevtoolsProtection = false, + showOriginalFilename = false, }) => { const [currentIndex, setCurrentIndex] = useState(initialIndex); const [zoom, setZoom] = useState(1); @@ -593,10 +599,23 @@ export const PhotoLightbox: React.FC = ({ }} >
-
+

{currentIndex + 1} / {photos.length}

+ {/* #508 — original camera filename next to the counter when + the admin has flipped the matching toggle. Falls back to + the storage filename only if `original_filename` is null + (pre-migration-062 uploads). truncate + max-w keep long + names from pushing the action row to another line. */} + {showOriginalFilename && (currentPhoto.original_filename || currentPhoto.filename) && ( +

+ {currentPhoto.original_filename || currentPhoto.filename} +

+ )}
diff --git a/frontend/src/components/gallery/layouts/BaseGalleryLayout.tsx b/frontend/src/components/gallery/layouts/BaseGalleryLayout.tsx index 2326dd9f..e0bff52d 100644 --- a/frontend/src/components/gallery/layouts/BaseGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/BaseGalleryLayout.tsx @@ -36,6 +36,9 @@ export interface BaseGalleryLayoutProps { // Client visibility controls (#172) isClient?: boolean; onToggleVisibility?: (photoId: number, currentVisibility: string) => void; + // Mirror of the admin original-filename toggle (#508). Forwarded to the + // lightbox by layouts that mount their own (story/premium). + showOriginalFilename?: boolean; } export abstract class BaseGalleryLayout extends React.Component { diff --git a/frontend/src/components/gallery/layouts/GalleryPremiumLayout.tsx b/frontend/src/components/gallery/layouts/GalleryPremiumLayout.tsx index ce045359..9bc02f12 100644 --- a/frontend/src/components/gallery/layouts/GalleryPremiumLayout.tsx +++ b/frontend/src/components/gallery/layouts/GalleryPremiumLayout.tsx @@ -6,8 +6,10 @@ import Thumbnails from 'yet-another-react-lightbox/plugins/thumbnails'; import Zoom from 'yet-another-react-lightbox/plugins/zoom'; import Fullscreen from 'yet-another-react-lightbox/plugins/fullscreen'; import Download from 'yet-another-react-lightbox/plugins/download'; +import Captions from 'yet-another-react-lightbox/plugins/captions'; import 'yet-another-react-lightbox/styles.css'; import 'yet-another-react-lightbox/plugins/thumbnails.css'; +import 'yet-another-react-lightbox/plugins/captions.css'; import { motion, AnimatePresence } from 'framer-motion'; import { Download as DownloadIcon, Heart, Check, Star, MessageSquare, Package, LogOut } from 'lucide-react'; import { useTranslation } from 'react-i18next'; @@ -185,7 +187,8 @@ export const GalleryPremiumLayout: React.FC = ({ feedbackEnabled = false, feedbackOptions, heroPhotoOverride, - onLogout + onLogout, + showOriginalFilename = false, }) => { // These props are passed by parent but we use our own lightbox, so mark as intentionally unused void _onPhotoClick; @@ -233,16 +236,20 @@ export const GalleryPremiumLayout: React.FC = ({ })); }, [filteredPhotos]); - // Lightbox slides + // Lightbox slides. `title` powers the Captions plugin — only emitted + // when the admin has flipped the original-filenames toggle (#508). const slides = useMemo(() => { return filteredPhotos.map(photo => ({ src: photo.url, alt: photo.filename, width: photo.width || 1200, height: photo.height || 800, - download: allowDownloads ? photo.url : undefined + download: allowDownloads ? photo.url : undefined, + title: showOriginalFilename + ? (photo.original_filename || photo.filename) + : undefined, })); - }, [filteredPhotos, allowDownloads]); + }, [filteredPhotos, allowDownloads, showOriginalFilename]); const handleLike = useCallback(async (photo: Photo, e: React.MouseEvent) => { e.stopPropagation(); @@ -537,7 +544,13 @@ export const GalleryPremiumLayout: React.FC = ({ close={() => setLightboxIndex(-1)} index={lightboxIndex} slides={slides} - plugins={allowDownloads ? [Thumbnails, Zoom, Fullscreen, Download] : [Thumbnails, Zoom, Fullscreen]} + plugins={[ + Thumbnails, + Zoom, + Fullscreen, + ...(allowDownloads ? [Download] : []), + ...(showOriginalFilename ? [Captions] : []), + ]} animation={{ fade: 300, swipe: 250 }} styles={{ container: { backgroundColor: 'rgba(0, 0, 0, 0.95)' }, diff --git a/frontend/src/components/gallery/layouts/GalleryStoryLayout.tsx b/frontend/src/components/gallery/layouts/GalleryStoryLayout.tsx index 07a69887..8397ec25 100644 --- a/frontend/src/components/gallery/layouts/GalleryStoryLayout.tsx +++ b/frontend/src/components/gallery/layouts/GalleryStoryLayout.tsx @@ -58,7 +58,8 @@ export const GalleryStoryLayout: React.FC = ({ feedbackOptions, heroPhotoOverride, welcomeMessage, - onLogout + onLogout, + showOriginalFilename = false, }) => { // These props are passed by parent but we use our own feedback system, so mark as intentionally unused void _onPhotoClick; @@ -379,6 +380,7 @@ export const GalleryStoryLayout: React.FC = ({ useEnhancedProtection={useEnhancedProtection} useCanvasRendering={useCanvasRendering} onFeedbackChange={onFeedbackChange} + showOriginalFilename={showOriginalFilename} /> )} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 216fafcd..3a0a42fc 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -88,6 +88,10 @@ export interface GalleryInfo { export interface Photo { id: number; filename: string; + // Original camera filename (e.g. DSC_1234.jpg) — populated for uploads + // post migration 062. Null for legacy rows. Surfaced in the lightbox + // when the admin toggles `use_original_filenames` on (#508). + original_filename?: string | null; url: string; thumbnail_url?: string; hero_url?: string; // Hero-optimized image URL (1920x1080) for full-width hero sections @@ -168,6 +172,10 @@ export interface GalleryData { hero_image_anchor?: string; // Default photo sort order default_photo_sort?: string; + // Mirror of admin's `general_use_original_filenames_for_downloads`. + // When true, the lightbox surfaces each photo's `original_filename` + // alongside the position counter (#508). + use_original_filenames?: boolean; }; categories?: PhotoCategory[]; photos: Photo[];