import { hslToHex } from './converter.js'; const STORAGE_KEY = 'pigmento'; let editSchemaHandler = null; export function setEditSchemaHandler(fn) { editSchemaHandler = fn; } const HISTORY_MAX = 20; function load() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || { favoriten: [], history: [], schemata: [] }; } catch { return { favoriten: [], history: [], schemata: [] }; } } function save(data) { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } export function addFavorit(hsl) { const data = load(); const hex = hslToHex(hsl); if (!data.favoriten.some(f => hslToHex(f) === hex)) { data.favoriten.push(hsl); save(data); renderSammlung(); } } export function removeFavorit(hex) { const data = load(); data.favoriten = data.favoriten.filter(f => hslToHex(f) !== hex); save(data); renderSammlung(); } export function addToHistory(hsl) { const data = load(); const hex = hslToHex(hsl); data.history = data.history.filter(h => hslToHex(h) !== hex); data.history.unshift(hsl); if (data.history.length > HISTORY_MAX) data.history = data.history.slice(0, HISTORY_MAX); save(data); } export function addColorToSchema(hsl) { const name = prompt('Schema-Name (leer = letztes Schema):'); if (name === null) return; // user pressed Cancel — do nothing const data = load(); if (name) { const existing = data.schemata.find(s => s.name === name); if (existing) { if (existing.farben.length < 4) { existing.farben.push(hsl); save(data); renderSammlung(); } else { alert('Schema hat bereits 4 Farben.'); } } else { data.schemata.push({ name, farben: [hsl] }); save(data); renderSammlung(); } } else if (data.schemata.length > 0) { const last = data.schemata[data.schemata.length - 1]; if (last.farben.length < 4) { last.farben.push(hsl); save(data); renderSammlung(); } else { alert('Letztes Schema hat bereits 4 Farben.'); } } else { alert('Kein Schema vorhanden. Bitte Name eingeben.'); } } export function saveSchema(name, farben, bild, originalName) { const data = load(); if (originalName && originalName !== name) { const old = data.schemata.find(s => s.name === originalName); const preservedParent = old?.parent; // Kind-Referenzen auf neuen Namen umbiegen data.schemata.forEach(s => { if (s.parent === originalName) s.parent = name; }); data.schemata = data.schemata.filter(s => s.name !== originalName); const entry = { name, farben, bild: bild || null }; if (preservedParent) entry.parent = preservedParent; data.schemata.push(entry); } else { const existing = data.schemata.find(s => s.name === name); if (existing) { existing.farben = farben; if (bild !== undefined) existing.bild = bild || null; // parent bleibt erhalten } else { data.schemata.push({ name, farben, bild: bild || null }); } } save(data); renderSammlung(); } export function deleteSchema(name) { const data = load(); const schema = data.schemata.find(s => s.name === name); const parentOfDeleted = schema?.parent || null; // Kinder auf die Eltern-Ebene des gelöschten Schemas verschieben data.schemata.forEach(s => { if (s.parent === name) { if (parentOfDeleted) { s.parent = parentOfDeleted; } else { delete s.parent; } } }); data.schemata = data.schemata.filter(s => s.name !== name); save(data); renderSammlung(); } export function exportCollection() { const data = load(); const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'pigmento-sammlung.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } export function importCollection() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { try { const imported = JSON.parse(ev.target.result); const data = load(); const existingFavHexes = new Set(data.favoriten.map(hslToHex)); (imported.favoriten || []).forEach(hsl => { if (!hsl || typeof hsl.h !== 'number' || typeof hsl.s !== 'number' || typeof hsl.l !== 'number') return; if (!existingFavHexes.has(hslToHex(hsl))) data.favoriten.push(hsl); }); const existingSchemaNames = new Set(data.schemata.map(s => s.name)); (imported.schemata || []).forEach(s => { if (!s || typeof s.name !== 'string' || !Array.isArray(s.farben)) return; if (!existingSchemaNames.has(s.name)) data.schemata.push(s); }); const existingHistHexes = new Set(data.history.map(hslToHex)); (imported.history || []).forEach(hsl => { if (!hsl || typeof hsl.h !== 'number' || typeof hsl.s !== 'number' || typeof hsl.l !== 'number') return; if (!existingHistHexes.has(hslToHex(hsl))) data.history.push(hsl); }); if (data.history.length > 20) data.history = data.history.slice(0, 20); save(data); renderSammlung(); alert('Import erfolgreich.'); } catch { alert('Ungültige Datei.'); } }; reader.readAsText(file); }; input.click(); } // --- Schema-Hierarchie --- function buildTree(schemata) { const nodes = {}; schemata.forEach(s => { nodes[s.name] = { ...s, children: [] }; }); const roots = []; schemata.forEach(s => { if (s.parent && nodes[s.parent]) { nodes[s.parent].children.push(nodes[s.name]); } else { roots.push(nodes[s.name]); } }); return roots; } function isDescendant(schemata, ancestorName, checkName) { for (const s of schemata) { if (s.parent === ancestorName) { if (s.name === checkName || isDescendant(schemata, s.name, checkName)) return true; } } return false; } function setSchemaParent(childName, parentName) { const data = load(); const schema = data.schemata.find(s => s.name === childName); if (!schema) return; if (parentName) { schema.parent = parentName; } else { delete schema.parent; } save(data); renderSammlung(); } let draggedSchemaName = null; function renderSchemaCard(node, container, allSchemata, depth) { const card = document.createElement('div'); card.className = depth === 0 ? 'schema-card' : 'schema-card schema-card-nested'; card.dataset.schemaName = node.name; card.draggable = true; card.addEventListener('dragstart', (e) => { draggedSchemaName = node.name; e.dataTransfer.effectAllowed = 'move'; setTimeout(() => card.classList.add('schema-dragging'), 0); }); card.addEventListener('dragend', () => { card.classList.remove('schema-dragging'); draggedSchemaName = null; document.querySelectorAll('.schema-drag-over').forEach(el => el.classList.remove('schema-drag-over')); }); card.addEventListener('dragover', (e) => { if (!draggedSchemaName || draggedSchemaName === node.name) return; if (isDescendant(allSchemata, draggedSchemaName, node.name)) return; e.preventDefault(); e.stopPropagation(); document.querySelectorAll('.schema-drag-over').forEach(el => el.classList.remove('schema-drag-over')); card.classList.add('schema-drag-over'); }); card.addEventListener('dragleave', (e) => { if (!card.contains(e.relatedTarget)) card.classList.remove('schema-drag-over'); }); card.addEventListener('drop', (e) => { e.preventDefault(); e.stopPropagation(); card.classList.remove('schema-drag-over'); if (!draggedSchemaName || draggedSchemaName === node.name) return; if (isDescendant(allSchemata, draggedSchemaName, node.name)) return; setSchemaParent(draggedSchemaName, node.name); }); const header = document.createElement('div'); header.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem;gap:0.5rem'; const nameRow = document.createElement('div'); nameRow.style.cssText = 'display:flex;align-items:center;gap:0.5rem;min-width:0'; const handle = document.createElement('span'); handle.textContent = '⠿'; handle.title = 'Ziehen um Untergruppe zu setzen'; handle.style.cssText = 'color:#ccc;cursor:grab;font-size:1.1rem;user-select:none;flex-shrink:0'; nameRow.appendChild(handle); if (node.bild) { const thumb = document.createElement('div'); thumb.className = 'schema-thumb'; thumb.style.backgroundImage = 'url(' + node.bild + ')'; nameRow.appendChild(thumb); } const nameEl = document.createElement('strong'); nameEl.textContent = node.name; nameRow.appendChild(nameEl); const btnGroup = document.createElement('div'); btnGroup.style.cssText = 'display:flex;gap:0.4rem;flex-shrink:0'; if (depth > 0) { const unestBtn = document.createElement('button'); unestBtn.className = 'action-btn'; unestBtn.textContent = '↑ Entnesten'; unestBtn.style.fontSize = '0.75rem'; unestBtn.addEventListener('click', () => setSchemaParent(node.name, null)); btnGroup.appendChild(unestBtn); } const editBtn = document.createElement('button'); editBtn.className = 'action-btn'; editBtn.textContent = 'Bearbeiten'; editBtn.style.fontSize = '0.75rem'; editBtn.addEventListener('click', () => editSchemaHandler?.(node)); const delBtn = document.createElement('button'); delBtn.className = 'action-btn'; delBtn.textContent = 'Löschen'; delBtn.style.fontSize = '0.75rem'; delBtn.addEventListener('click', () => { if (!confirm('"' + node.name + '" wirklich löschen?')) return; deleteSchema(node.name); }); btnGroup.appendChild(editBtn); btnGroup.appendChild(delBtn); header.appendChild(nameRow); header.appendChild(btnGroup); const swatchesDiv = document.createElement('div'); swatchesDiv.style.cssText = 'display:flex;gap:0.5rem;flex-wrap:wrap'; node.farben.forEach(hsl => swatchesDiv.appendChild(makeSwatch(hsl))); card.appendChild(header); card.appendChild(swatchesDiv); if (node.children.length > 0) { const childrenDiv = document.createElement('div'); childrenDiv.className = 'schema-children'; node.children.forEach(child => renderSchemaCard(child, childrenDiv, allSchemata, depth + 1)); card.appendChild(childrenDiv); } container.appendChild(card); } function makeSwatch(hsl) { const hex = hslToHex(hsl); const wrapper = document.createElement('div'); wrapper.className = 'swatch-cell'; const swatch = document.createElement('div'); swatch.className = 'swatch'; swatch.style.background = hex; swatch.title = hex; const label = document.createElement('span'); label.style.cssText = 'font-size:0.75rem;font-family:monospace'; label.textContent = hex; wrapper.appendChild(swatch); wrapper.appendChild(label); return wrapper; } export function renderSammlung() { const data = load(); // Favoriten const favContainer = document.getElementById('sammlung-favoriten'); if (favContainer) { favContainer.textContent = ''; if (data.favoriten.length === 0) { const msg = document.createElement('p'); msg.style.cssText = 'color:#999;font-size:0.85rem'; msg.textContent = 'Noch keine Favoriten.'; favContainer.appendChild(msg); } else { data.favoriten.forEach(hsl => { const hex = hslToHex(hsl); const cell = makeSwatch(hsl); const del = document.createElement('button'); del.className = 'action-btn'; del.textContent = 'Löschen'; del.style.fontSize = '0.7rem'; del.addEventListener('click', () => { if (!confirm('Favorit wirklich löschen?')) return; removeFavorit(hex); }); cell.appendChild(del); favContainer.appendChild(cell); }); } } // Historie const histContainer = document.getElementById('sammlung-history'); if (histContainer) { histContainer.textContent = ''; if (data.history.length === 0) { const msg = document.createElement('p'); msg.style.cssText = 'color:#999;font-size:0.85rem'; msg.textContent = 'Noch kein Verlauf.'; histContainer.appendChild(msg); } else { const clearBtn = document.createElement('button'); clearBtn.className = 'action-btn'; clearBtn.textContent = 'Verlauf löschen'; clearBtn.style.cssText = 'font-size:0.75rem;margin-bottom:0.75rem;display:block'; clearBtn.addEventListener('click', () => { if (!confirm('Verlauf wirklich löschen?')) return; const d = load(); d.history = []; save(d); renderSammlung(); }); histContainer.appendChild(clearBtn); data.history.forEach(hsl => histContainer.appendChild(makeSwatch(hsl))); } } // Schemata const schemataContainer = document.getElementById('sammlung-schemata'); if (schemataContainer) { schemataContainer.textContent = ''; if (data.schemata.length === 0) { const msg = document.createElement('p'); msg.style.cssText = 'color:#999;font-size:0.85rem'; msg.textContent = 'Noch keine Schemata.'; schemataContainer.appendChild(msg); } else { const tree = buildTree(data.schemata); tree.forEach(node => renderSchemaCard(node, schemataContainer, data.schemata, 0)); const rootZone = document.createElement('div'); rootZone.className = 'schema-root-dropzone'; rootZone.textContent = 'Hierher ziehen um auf oberste Ebene zu verschieben'; rootZone.addEventListener('dragover', (e) => { if (!draggedSchemaName) return; e.preventDefault(); rootZone.classList.add('active'); }); rootZone.addEventListener('dragleave', () => rootZone.classList.remove('active')); rootZone.addEventListener('drop', (e) => { e.preventDefault(); rootZone.classList.remove('active'); if (draggedSchemaName) setSchemaParent(draggedSchemaName, null); }); schemataContainer.appendChild(rootZone); } } }