import React, { useState, useEffect, useCallback } from 'react'; interface Item { id: string; filename: string; output_px: string; has_alpha: boolean; folder_id: string | null; by_name: string; tasks: string[]; delivery_status: string; nas_status: string; model_used: string; variant_of: string | null; mode: string; thumb_path: string | null; prompt_used: string | null; color_tag: string | null; created_at: string; } const TAGS = [ { id: 'red', label: 'Rot', color: '#E5484D' }, { id: 'orange', label: 'Orange', color: '#E8830C' }, { id: 'green', label: 'Grün', color: '#46A758' }, { id: 'final', label: 'Final', color: '#3A3733' }, ]; const tagColor = (t: string | null) => TAGS.find((x) => x.id === t)?.color || null; export default function LibraryApp() { const [items, setItems] = useState([]); const [folders, setFolders] = useState([]); const [models, setModels] = useState([]); const [folderFilter, setFolderFilter] = useState(null); const [pick, setPick] = useState>(new Set()); const [variantSel, setVariantSel] = useState>({}); const [compare, setCompare] = useState(null); const [promptOf, setPromptOf] = useState(null); const [saveView, setSaveView] = useState(null); const [tagMenu, setTagMenu] = useState(null); const [tagFilter, setTagFilter] = useState(null); const [toast, setToast] = useState(null); const notify = (t: string) => { setToast(t); setTimeout(() => setToast(null), 2400); }; // Modellname: Admin-Label, sonst lesbarer Rest der Modell-ID. const modelName = (id: string | null) => { if (!id) return null; const m = models.find((x) => x.model_id === id); if (m) return m.label; return id.split('/').pop()!.replace(/-/g, ' '); }; const load = useCallback(async () => { const [i, f, m] = await Promise.all([ fetch('/api/items').then((r) => r.json()).catch(() => ({ items: [] })), fetch('/api/folders').then((r) => r.json()).catch(() => ({ folders: [] })), fetch('/api/models').then((r) => r.json()).catch(() => ({ models: [] })), ]); setItems(i.items || []); setFolders(f.folders || []); setModels(m.models || []); }, []); useEffect(() => { load(); }, [load]); const rename = async (id: string, filename: string) => fetch(`/api/items/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename }) }); const setTag = async (id: string, color_tag: string | null) => { setItems((x) => x.map((y) => y.id === id ? { ...y, color_tag } : y)); setTagMenu(null); await fetch(`/api/items/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ color_tag }) }); }; const setFolder = async (id: string, folder_id: string | null) => { await fetch(`/api/items/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ folder_id }) }); load(); }; const del = async (id: string) => { await fetch(`/api/items/${id}`, { method: 'DELETE' }); setItems((x) => x.filter((y) => y.id !== id)); }; const delSelected = async () => { for (const id of pick) await fetch(`/api/items/${id}`, { method: 'DELETE' }); setPick(new Set()); load(); notify('Gelöscht.'); }; const deliverSelected = async () => { notify('Sende an Picdrop …'); const r = await fetch('/api/deliver', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ itemIds: [...pick] }) }).then((x) => x.json()); setPick(new Set()); load(); notify(`${r.delivered}/${r.total} nach Picdrop ausgeliefert.`); }; const toggle = (id: string) => setPick((s) => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; }); const moveSelected = async (folder_id: string | null) => { for (const id of pick) await fetch(`/api/items/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ folder_id }) }); setPick(new Set()); load(); notify(folder_id ? 'In Ordner verschoben.' : 'Aus Ordner entfernt.'); }; const zipSelected = async () => { notify('Packe ZIP …'); const res = await fetch('/api/items/zip', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ itemIds: [...pick] }) }); if (!res.ok) { notify('ZIP fehlgeschlagen.'); return; } const blob = await res.blob(); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `klarbild_${new Date().toISOString().slice(0, 10)}.zip`; a.click(); URL.revokeObjectURL(a.href); notify('ZIP geladen.'); }; const alternative = async (id: string) => { notify('Erzeuge Alternative …'); const r = await fetch(`/api/items/${id}/alternative`, { method: 'POST' }).then((x) => x.json()); if (r.ok) notify('Alternative wird erzeugt — erscheint gleich hier.'); else notify(r.error || 'Fehler.'); setTimeout(load, 2500); }; // Ergebnis als neue Vorlage ins Studio übernehmen und weiterbearbeiten. const reuse = (id: string) => { window.location.href = `/?reuse=${id}`; }; const newFolder = async () => { const name = prompt('Name des Ordners?'); if (!name) return; const gallery = prompt('Picdrop-Galerie für diesen Ordner? (leer = Standard)') || ''; await fetch('/api/folders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, picdrop_gallery: gallery.trim() || null }) }); load(); }; const delFolder = async (id: string) => { if (!confirm('Ordner löschen? Die Bilder bleiben erhalten.')) return; await fetch(`/api/folders?id=${id}`, { method: 'DELETE' }); if (folderFilter === id) setFolderFilter(null); load(); }; const renameFolder = async (o: any) => { const name = prompt('Ordner umbenennen:', o.name); if (name == null) return; const gallery = prompt('Picdrop-Galerie (leer = Standard):', o.picdrop_gallery || ''); await fetch('/api/folders', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: o.id, name: name.trim() || o.name, picdrop_gallery: (gallery || '').trim() || null }) }); load(); }; // Nach Ordner + Markierung filtern, dann Alternativen unter ihrem Ursprung gruppieren. const filtered = items .filter((v) => !folderFilter || v.folder_id === folderFilter) .filter((v) => !tagFilter || v.color_tag === tagFilter); const groupMap = new Map(); for (const it of filtered) { const root = it.variant_of || it.id; if (!groupMap.has(root)) groupMap.set(root, []); groupMap.get(root)!.push(it); } const groups = [...groupMap.values()].map((g) => g.slice().sort((a, b) => +new Date(a.created_at) - +new Date(b.created_at))); groups.sort((a, b) => +new Date(b[b.length - 1].created_at) - +new Date(a[a.length - 1].created_at)); const shown = (g: Item[]) => g.find((x) => x.id === variantSel[g[0].id]) || g[g.length - 1]; return (
Bibliothek{filtered.length ? ` · ${filtered.length}` : ''}
{pick.size > 0 && <> {pick.size} gewählt }
{/* Ordner-Filter */}
{folders.map((o) => ( {folderFilter === o.id && <> } ))}
Ordner gruppieren die Bibliothek und können je Ordner eine eigene Picdrop-Galerie bekommen (Pfeil = eigene Galerie hinterlegt).
{/* Farbmarkierungen filtern */}
{TAGS.map((t) => ( ))}
{filtered.length === 0 ? (

{folderFilter ? 'Ordner ist leer' : 'Noch nichts erstellt'}

{folderFilter ? 'Weise Bildern über das Ordner-Menü diesen Ordner zu.' : 'Lege im Studio los — die Ergebnisse sammeln sich hier.'}

) : (
{groups.map((g) => { const v = shown(g); const multi = g.length > 1; return (
toggle(v.id)}> {pick.has(v.id) ? '✓' : ''} {(v.mode === 'compose' || v.mode === 'generate') && {v.mode === 'compose' ? 'kombiniert' : 'erzeugt'}} {v.color_tag && } {multi && Fassung {g.indexOf(v) + 1}/{g.length}}
{multi && (
{g.map((x, idx) => ( ))}
)} rename(v.id, e.target.value)} />
{(v.tasks || []).join(' · ')}{v.output_px ? ` · ${v.output_px}px` : ''}{v.has_alpha ? ' · transparent' : ''}{modelName(v.model_used) ? ` · ${modelName(v.model_used)}` : ''} {v.delivery_status === 'delivered' && Picdrop ✓} {v.delivery_status === 'pending' && Picdrop …} {v.delivery_status === 'failed' && Picdrop ✕} {v.nas_status === 'mirrored' && NAS ✓} {v.nas_status === 'failed' && NAS ✕}
{tagMenu === v.id && (
setTagMenu(null)}> {TAGS.map((t) => ( ))}
)}
{v.prompt_used && } {v.mode !== 'generate' && }
); })}
)} {compare && (
setCompare(null)}>
e.stopPropagation()}> vorher nachher { const o = document.getElementById('ov'); if (o) o.style.clipPath = `inset(0 0 0 ${e.target.value}%)`; }} />
{compare.filename} · {compare.output_px}px — Regler: links {compare.mode === 'compose' ? 'Vorlage' : 'Original'}, rechts Ergebnis
)} {saveView && (
setSaveView(null)}>
e.stopPropagation()}> {saveView.filename
e.stopPropagation()}> 📱 iPhone/iPad: lange auf das Bild tippen → „In Fotos sichern". Original als Datei (.png)
)} {promptOf && (
setPromptOf(null)}>
e.stopPropagation()}>
{promptOf.filename}
{promptOf.prompt_used}
{promptOf.model_used ? `Modell: ${modelName(promptOf.model_used)} · ` : ''}{promptOf.output_px}px
)} {toast &&
{toast}
}
); } function LibStyles() { return ; }