refactor: remove EXIF metadata from photo lightbox

EXIF was only available for Immich photos and inconsistent for local
uploads. Removed entirely for now — cleaner lightbox with just photo,
nav, counter, and caption. Nav buttons now show on hover (desktop)
and always on mobile.
This commit is contained in:
Maurice
2026-04-12 02:31:07 +02:00
parent f323952012
commit 133676d05b
+58 -119
View File
@@ -1,6 +1,5 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import { ChevronLeft, ChevronRight, X, Camera, Aperture } from 'lucide-react'
import apiClient from '../../api/client'
import { ChevronLeft, ChevronRight, X } from 'lucide-react'
interface LightboxPhoto {
id: string
@@ -11,16 +10,6 @@ interface LightboxPhoto {
owner_id?: number | null
}
interface ExifData {
camera?: string
lens?: string
focalLength?: string
aperture?: string
shutter?: string
iso?: number
fileName?: string
}
interface Props {
photos: LightboxPhoto[]
startIndex?: number
@@ -29,8 +18,6 @@ interface Props {
export default function PhotoLightbox({ photos, startIndex = 0, onClose }: Props) {
const [idx, setIdx] = useState(startIndex)
const [exif, setExif] = useState<ExifData | null>(null)
const [exifLoading, setExifLoading] = useState(false)
const touchStart = useRef<{ x: number; y: number } | null>(null)
const photo = photos[idx]
@@ -50,32 +37,6 @@ export default function PhotoLightbox({ photos, startIndex = 0, onClose }: Props
return () => window.removeEventListener('keydown', onKey)
}, [prev, next, onClose])
// Fetch EXIF data for Immich photos
useEffect(() => {
setExif(null)
if (!photo || photo.provider !== 'immich' || !photo.asset_id || !photo.owner_id) return
let cancelled = false
setExifLoading(true)
apiClient.get(`/integrations/memories/immich/assets/0/${photo.asset_id}/${photo.owner_id}/info`)
.then(r => {
if (!cancelled && r.data) {
const d = r.data
const parts: Partial<ExifData> = {}
if (d.camera && d.camera.trim() && d.camera !== 'undefined undefined') parts.camera = d.camera
if (d.lens) parts.lens = d.lens
if (d.focalLength) parts.focalLength = d.focalLength
if (d.aperture) parts.aperture = d.aperture
if (d.shutter) parts.shutter = d.shutter
if (d.iso) parts.iso = d.iso
if (d.fileName) parts.fileName = d.fileName
if (Object.keys(parts).length > 0) setExif(parts)
}
})
.catch(() => {})
.finally(() => { if (!cancelled) setExifLoading(false) })
return () => { cancelled = true }
}, [photo])
const onTouchStart = (e: React.TouchEvent) => {
const t = e.touches[0]
touchStart.current = { x: t.clientX, y: t.clientY }
@@ -112,99 +73,77 @@ export default function PhotoLightbox({ photos, startIndex = 0, onClose }: Props
onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd}
>
{/* Top bar */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 16px', flexShrink: 0 }}>
<span style={{ color: 'rgba(255,255,255,0.5)', fontSize: 13 }}>
{idx + 1} / {photos.length}
</span>
<button onClick={onClose} style={{
background: 'rgba(255,255,255,0.1)', border: 'none', borderRadius: '50%',
width: 36, height: 36, display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', cursor: 'pointer',
}}>
<X size={18} />
</button>
</div>
{/* Photo area — centered with nav overlays */}
<div
className="group/lightbox"
style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative', overflow: 'hidden' }}
>
{/* Top bar */}
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 10, display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 20px' }}>
<span style={{ color: 'rgba(255,255,255,0.5)', fontSize: 13, fontWeight: 500 }}>
{idx + 1} / {photos.length}
</span>
<button onClick={onClose} style={{
background: 'rgba(255,255,255,0.1)', border: 'none', borderRadius: '50%',
width: 36, height: 36, display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', cursor: 'pointer',
}}>
<X size={18} />
</button>
</div>
{/* Photo */}
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative', overflow: 'hidden' }}>
{/* Prev button — visible on hover (desktop), always visible (mobile) */}
{hasPrev && (
<button onClick={prev} className="hidden sm:flex" style={{
position: 'absolute', left: 12, zIndex: 2,
width: 40, height: 40, borderRadius: '50%',
background: 'rgba(255,255,255,0.1)', border: 'none',
display: 'flex', alignItems: 'center', justifyContent: 'center',
<button onClick={prev} className="flex sm:opacity-0 sm:group-hover/lightbox:opacity-100 transition-opacity" style={{
position: 'absolute', left: 16, zIndex: 5,
width: 44, height: 44, borderRadius: '50%',
background: 'rgba(0,0,0,0.4)', backdropFilter: 'blur(8px)',
border: '1px solid rgba(255,255,255,0.1)',
alignItems: 'center', justifyContent: 'center',
color: '#fff', cursor: 'pointer',
}}>
<ChevronLeft size={20} />
<ChevronLeft size={22} />
</button>
)}
<div style={{ position: 'relative', display: 'inline-flex' }}>
<img
key={photo.id}
src={photo.src}
alt={photo.caption || ''}
style={{
maxWidth: '90vw', maxHeight: 'calc(100vh - 140px)',
objectFit: 'contain', borderRadius: 4,
animation: 'fadeIn 0.15s ease',
}}
/>
{/* EXIF metadata overlay */}
{exif && !exifLoading && (
<div style={{
position: 'absolute', bottom: 12, right: 12,
background: 'rgba(0,0,0,0.45)', backdropFilter: 'blur(16px)',
borderRadius: 12, padding: '10px 14px',
color: 'rgba(255,255,255,0.85)', fontSize: 11,
display: 'flex', flexDirection: 'column', gap: 4,
maxWidth: 220, border: '1px solid rgba(255,255,255,0.08)',
}}>
{exif.camera && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<Camera size={11} style={{ opacity: 0.6, flexShrink: 0 }} />
<span style={{ fontWeight: 500 }}>{exif.camera}</span>
</div>
)}
{exif.lens && (
<div style={{ fontSize: 10, color: 'rgba(255,255,255,0.55)', paddingLeft: 17 }}>{exif.lens}</div>
)}
{(exif.focalLength || exif.aperture || exif.shutter || exif.iso) && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 2 }}>
<Aperture size={11} style={{ opacity: 0.6, flexShrink: 0 }} />
<span style={{ fontWeight: 400, letterSpacing: '0.02em' }}>
{[exif.focalLength, exif.aperture, exif.shutter, exif.iso ? `ISO ${exif.iso}` : ''].filter(Boolean).join(' · ')}
</span>
</div>
)}
</div>
)}
</div>
{/* Photo */}
<img
key={photo.id}
src={photo.src}
alt={photo.caption || ''}
style={{
maxWidth: '92vw', maxHeight: '92vh',
objectFit: 'contain', borderRadius: 4,
animation: 'fadeIn 0.15s ease',
}}
/>
{/* Next button */}
{hasNext && (
<button onClick={next} className="hidden sm:flex" style={{
position: 'absolute', right: 12, zIndex: 2,
width: 40, height: 40, borderRadius: '50%',
background: 'rgba(255,255,255,0.1)', border: 'none',
display: 'flex', alignItems: 'center', justifyContent: 'center',
<button onClick={next} className="flex sm:opacity-0 sm:group-hover/lightbox:opacity-100 transition-opacity" style={{
position: 'absolute', right: 16, zIndex: 5,
width: 44, height: 44, borderRadius: '50%',
background: 'rgba(0,0,0,0.4)', backdropFilter: 'blur(8px)',
border: '1px solid rgba(255,255,255,0.1)',
alignItems: 'center', justifyContent: 'center',
color: '#fff', cursor: 'pointer',
}}>
<ChevronRight size={20} />
<ChevronRight size={22} />
</button>
)}
{/* Caption — bottom center overlay */}
{photo.caption && (
<div style={{ position: 'absolute', bottom: 20, left: '50%', transform: 'translateX(-50%)', zIndex: 5, maxWidth: '70%', textAlign: 'center' }}>
<p style={{
fontSize: 14, fontStyle: 'italic',
color: 'rgba(255,255,255,0.75)', margin: 0, lineHeight: 1.5,
background: 'rgba(0,0,0,0.3)', backdropFilter: 'blur(8px)',
padding: '6px 14px', borderRadius: 10,
}}>{photo.caption}</p>
</div>
)}
</div>
{/* Caption */}
{photo.caption && (
<div style={{ textAlign: 'center', padding: '12px 24px 20px', flexShrink: 0 }}>
<p style={{
fontFamily: 'var(--font-system)', fontSize: 14, fontStyle: 'italic',
color: 'rgba(255,255,255,0.7)', margin: 0, lineHeight: 1.5,
}}>{photo.caption}</p>
</div>
)}
</div>
)
}