// Lightbox — full-screen photo viewer w/ zoom, before/after, EXIF, develop panel trigger function Lightbox({ items, index, onClose, onNav, onPick, onReject, onRate, onColor, devOpen, setDevOpen, edits, onEditChange, onEditReset, showExif, setShowExif, baMode, setBaMode, zoom, setZoom, exifCache, readOnly }) { const f = items[index]; const [screenW, setScreenW] = React.useState(window.innerWidth); React.useEffect(() => { const fn = () => setScreenW(window.innerWidth); window.addEventListener('resize', fn); return () => window.removeEventListener('resize', fn); }, []); const isMobile = screenW < 768; React.useEffect(() => { if (!f) return; const onKey = (e) => { if (e.key === 'Escape') { onClose(); return; } if (e.key === 'ArrowRight') onNav(1); if (e.key === 'ArrowLeft') onNav(-1); if (!readOnly) { if (e.key === 'p' || e.key === 'P') onPick(); if (e.key === 'x' || e.key === 'X') onReject(); if (e.key >= '1' && e.key <= '5') onRate(parseInt(e.key)); if (e.key === '0') onRate(0); if (e.key === 'e' || e.key === 'E') setDevOpen(!devOpen); } if (e.key === 'i' || e.key === 'I') setShowExif(!showExif); if (e.key === 'z' || e.key === 'Z') setZoom(!zoom); if (e.key === '\\') setBaMode(!baMode); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [f, index, onNav, showExif, zoom, devOpen, baMode, readOnly]); if (!f) return null; const hasEdits = edits && Object.values(edits).some(v => v !== 0); const exif = exifCache && exifCache[f.filename]; // Bottom bar height: filmstrip (48px) + mobile actions (52px on mobile) const bottomPad = isMobile ? (readOnly ? 56 : 108) : 80; return (
{/* ── Top bar ── */}
e.stopPropagation()}> {/* Counter */} {index+1} / {items.length} {/* Filename + pills */} {f.filename} {f.file_type} {f.color && } {hasEdits && Edited} {/* Desktop-only controls */} {!isMobile && <>
{!readOnly && <> Pick Reject } setShowExif(!showExif)} icon={} title="EXIF (I)"/> setZoom(!zoom)} title="Zoom (Z)"> {zoom ? '100%' : 'FIT'} setBaMode(!baMode)} icon={} title="Before/After (\)"/> {!readOnly && ( setDevOpen(!devOpen)} icon={} title="Develop (E)">Develop )} } title="Download" onClick={() => window.open(f.download_url || `/download/${encodeURIComponent(f.filename)}`, '_blank')}/> } }/>
{/* ── Image area ── */}
e.stopPropagation()}> {baMode ? ( ) : ( {f.filename} setZoom(!zoom)} /> )}
{/* ── EXIF overlay ── */} {showExif && (
e.stopPropagation()} style={{ position: 'absolute', bottom: bottomPad + 8, left: '50%', transform: 'translateX(-50%)', background: 'rgba(10,10,12,0.88)', backdropFilter: 'blur(14px)', border: '1px solid var(--line-2)', borderRadius: 8, padding: '10px 20px', display: 'flex', gap: isMobile ? 16 : 28, animation: 'fade-up 180ms ease', maxWidth: 'calc(100vw - 40px)', flexWrap: 'wrap', }}> {exif ? <> {exif.camera && } {exif.shutter && } {exif.aperture && } {exif.iso && } {exif.focal_length && } {exif.width && exif.height && } {exif.datetime && } : ( Loading EXIF… )}
)} {/* ── Mobile action bar (above filmstrip) ── */} {isMobile && !readOnly && (
e.stopPropagation()} style={{ position: 'absolute', bottom: 52, left: 0, right: 0, zIndex: 5, padding: '8px 14px', background: 'rgba(5,5,7,0.85)', display: 'flex', alignItems: 'center', gap: 8, }}> {/* Pick / Reject */} Pick Reject {/* Stars */} {/* Download */} } title="Download" onClick={() => window.open(f.download_url || `/download/${encodeURIComponent(f.filename)}`, '_blank')}/> {/* EXIF toggle */} setShowExif(!showExif)} icon={}/>
)} {/* ── Filmstrip ── */}
e.stopPropagation()} style={{ position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 5, height: 52, padding: '8px 16px 10px', overflowX: 'auto', background: 'linear-gradient(to top, rgba(5,5,7,0.9), transparent)', display: 'flex', gap: 4, alignItems: 'flex-end', }}> {items.map((it, i) => ( ))}
); } function navArrowStyle(side, isMobile) { return { position: 'absolute', [side]: isMobile ? 8 : 16, top: '50%', transform: 'translateY(-50%)', width: isMobile ? 34 : 38, height: isMobile ? 34 : 38, borderRadius: '50%', background: 'rgba(10,10,12,0.7)', color: 'var(--ink-1)', border: '1px solid var(--line-2)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', backdropFilter: 'blur(8px)', zIndex: 4, }; } function ExifItem({ label, val, mono }) { return (
{label} {val}
); } function ColorPicker({ color, onColor }) { const colors = [null, 'red', 'yellow', 'green', 'blue', 'purple']; return (
{colors.map(c => ( onColor(c)}/> ))}
); } function BeforeAfterView({ file, edits }) { const [pos, setPos] = React.useState(50); const ref = React.useRef(null); const dragging = React.useRef(false); const imgStyle = { position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'contain' }; const calcPos = (clientX) => { if (!ref.current) return; const r = ref.current.getBoundingClientRect(); setPos(Math.max(0, Math.min(100, ((clientX - r.left) / r.width) * 100))); }; React.useEffect(() => { const onMouseMove = (e) => { if (dragging.current) calcPos(e.clientX); }; const onMouseUp = () => { dragging.current = false; }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); return () => { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); }; }, []); return (
{ e.preventDefault(); dragging.current = true; calcPos(e.clientX); }} onTouchStart={(e) => { e.preventDefault(); calcPos(e.touches[0].clientX); }} onTouchMove={(e) => { e.preventDefault(); calcPos(e.touches[0].clientX); }} > {/* AFTER — with edits applied, full underneath */} {/* BEFORE — original, clipped to left of divider */}
{/* Labels */} BEFORE AFTER {/* Divider */}
◁▷
); } function developFilter(edits) { if (!edits) return 'none'; const { exposure=0, contrast=0, highlights=0, shadows=0, temperature=0, tint=0, vibrance=0, saturation=0, sharpness=0 } = edits; const brightness = 1 + exposure * 0.14; const c = 1 + contrast * 0.005; const sat = 1 + saturation * 0.006 + vibrance * 0.004; const hue = temperature * 0.15; return `brightness(${brightness}) contrast(${c}) saturate(${sat}) hue-rotate(${hue}deg)`; } Object.assign(window, { Lightbox, developFilter });