// Develop panel — Lightroom-style sliders, wired to /api/edit backend const DEV_SECTIONS = [ { id: 'light', label: 'Light', sliders: [ { key: 'exposure', label: 'Exposure', min: -5, max: 5, step: 0.05 }, { key: 'contrast', label: 'Contrast', min: -100, max: 100, step: 1 }, { key: 'highlights', label: 'Highlights', min: -100, max: 100, step: 1 }, { key: 'shadows', label: 'Shadows', min: -100, max: 100, step: 1 }, { key: 'whites', label: 'Whites', min: -100, max: 100, step: 1 }, { key: 'blacks', label: 'Blacks', min: -100, max: 100, step: 1 }, ]}, { id: 'color', label: 'Color', sliders: [ { key: 'temperature', label: 'Temp', min: -100, max: 100, step: 1 }, { key: 'tint', label: 'Tint', min: -100, max: 100, step: 1 }, { key: 'vibrance', label: 'Vibrance', min: -100, max: 100, step: 1 }, { key: 'saturation', label: 'Saturation', min: -100, max: 100, step: 1 }, ]}, { id: 'detail', label: 'Detail', sliders: [ { key: 'sharpness', label: 'Sharpness', min: 0, max: 100, step: 1 }, { key: 'noise', label: 'Noise Reduction', min: 0, max: 100, step: 1 }, { key: 'clarity', label: 'Clarity', min: -100, max: 100, step: 1 }, ]}, ]; const PRESETS = { 'None': {}, 'Portra 400': { exposure: 0.3, contrast: -10, highlights: -20, shadows: 15, temperature: 15, saturation: 10, vibrance: 15 }, 'Neutral': { contrast: -5, highlights: -10, shadows: 5 }, 'Moody': { exposure: -0.4, contrast: 20, highlights: -30, shadows: -20, saturation: -15, temperature: -10 }, 'B&W': { saturation: -100, contrast: 15, clarity: 20 }, 'Flash': { exposure: 0.6, contrast: -15, highlights: -40, temperature: -5, vibrance: 20 }, }; const XMP_FIELD_MAP = { Exposure2012: 'exposure', Contrast2012: 'contrast', Highlights2012: 'highlights', Shadows2012: 'shadows', Whites2012: 'whites', Blacks2012: 'blacks', Tint: 'tint', Vibrance: 'vibrance', Saturation: 'saturation', Clarity2012: 'clarity', Sharpness: 'sharpness', }; function parseXMPPreset(text) { const parser = new DOMParser(); const doc = parser.parseFromString(text, 'text/xml'); const desc = doc.querySelector('Description'); if (!desc) return null; const vals = {}; for (const [xmpKey, sliderKey] of Object.entries(XMP_FIELD_MAP)) { const raw = desc.getAttribute(`crs:${xmpKey}`); if (raw != null) { const n = parseFloat(raw); if (!isNaN(n)) vals[sliderKey] = n; } } // Temperature: XMP stores absolute Kelvin; map to -100..100 around 5500K neutral const rawTemp = desc.getAttribute('crs:Temperature'); if (rawTemp != null) { const k = parseFloat(rawTemp); if (!isNaN(k)) vals.temperature = Math.max(-100, Math.min(100, (k - 5500) / 30)); } const nameEl = desc.getAttribute('crs:PresetName') || desc.getAttribute('crs:Name') || null; return { name: nameEl, vals }; } function DevelopPanel({ open, filename, edits, onChange, onReset, onResetAll, onSave, onApplyToAll, setOpen }) { const [openSections, setOpenSections] = React.useState({ light: true, color: true, detail: false }); const [preset, setPreset] = React.useState('None'); const [saving, setSaving] = React.useState(false); const [saved, setSaved] = React.useState(false); const [customPresets, setCustomPresets] = React.useState({}); const fileInputRef = React.useRef(null); const allPresets = { ...PRESETS, ...customPresets }; const applyPreset = (name) => { setPreset(name); onResetAll(); const vals = allPresets[name] || {}; Object.entries(vals).forEach(([k, v]) => onChange(k, v)); }; const handleXMPUpload = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { const result = parseXMPPreset(ev.target.result); if (!result || !Object.keys(result.vals).length) { alert('Could not read preset — make sure it is a valid Lightroom .xmp file.'); return; } const name = result.name || file.name.replace(/\.[^.]+$/, ''); setCustomPresets(prev => ({ ...prev, [name]: result.vals })); setPreset(name); onResetAll(); Object.entries(result.vals).forEach(([k, v]) => onChange(k, v)); }; reader.readAsText(file); e.target.value = ''; }; const handleSave = async () => { setSaving(true); await onSave(); setSaving(false); setSaved(true); setTimeout(() => setSaved(false), 1500); }; const handleExport = async () => { if (!filename) return; try { const r = await fetch(`/api/edit/${encodeURIComponent(filename)}/export`, { method: 'POST' }); if (!r.ok) return; const blob = await r.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `edit_${filename.replace(/\.[^.]+$/, '')}.jpg`; a.click(); URL.revokeObjectURL(url); } catch {} }; return ( ); } function Slider({ label, min, max, step, value, onChange, onDblClick }) { const isZero = value === 0 || value === undefined; const pct = ((value - min) / (max - min)) * 100; const zeroPct = ((0 - min) / (max - min)) * 100; return (
{label}
onChange(parseFloat(e.target.value))} onDoubleClick={onDblClick} style={{ position: 'absolute', inset: 0, width: '100%', opacity: 0, cursor: 'pointer', margin: 0 }}/>
{isZero ? '0' : (value > 0 ? '+' : '') + (step < 1 ? value.toFixed(2) : Math.round(value))}
); } Object.assign(window, { DevelopPanel });