diff --git a/frontend/src/tools/devtools.jsx b/frontend/src/tools/devtools.jsx index 1c9f6d5..f736fe4 100644 --- a/frontend/src/tools/devtools.jsx +++ b/frontend/src/tools/devtools.jsx @@ -2699,97 +2699,53 @@ const CAP_POS = [ { id:'none', label:'Keine' }, ]; -function drawQrToCanvas(canvas, data, opts) { - const { size, fgColor, bgColor, margin, dotStyle, cornerStyle, caption, captionPos, captionColor, captionSize } = opts; +function drawQrToCanvas(targetCanvas, data, opts) { + const { size, fgColor, bgColor, margin, caption, captionPos, captionColor, captionSize } = opts; const capH = caption && captionPos !== 'none' ? (Number(captionSize) + 16) : 0; - const totalH = size + capH; - canvas.width = size; - canvas.height = totalH; - const ctx = canvas.getContext('2d'); + const totalH = Number(size) + capH; - // Step 1: generate QR to temporary canvas (black on white, no margin) + // Step 1: generate QR to a temp canvas via qrcode library (browser-compatible) const tmp = document.createElement('canvas'); return QRCode.toCanvas(tmp, data, { - width: size, - margin: 0, - color: { dark: '#000000ff', light: '#ffffffff' }, + width: Number(size), + margin: Number(margin) || 1, + color: { dark: fgColor, light: bgColor }, errorCorrectionLevel: 'M', }).then(() => { - // Step 2: read pixel data to detect module grid - const tctx = tmp.getContext('2d'); - const pixels = tctx.getImageData(0, 0, size, size).data; + // Step 2: compose onto target canvas (with optional caption) + targetCanvas.width = Number(size); + targetCanvas.height = totalH; + const ctx = targetCanvas.getContext('2d'); - // Find module size by scanning first dark run - let firstDark = -1, runEnd = -1; - for (let x = 0; x < size; x++) { - const idx = x * 4; - const dark = pixels[idx] < 128; - if (dark && firstDark < 0) firstDark = x; - if (!dark && firstDark >= 0 && runEnd < 0) { runEnd = x; break; } - } - const moduleSize = (runEnd > 0 && firstDark >= 0) ? (runEnd - firstDark) : Math.round(size / 25); - const n = Math.round(size / moduleSize); - - // Step 3: draw background + // Background ctx.fillStyle = bgColor; - ctx.fillRect(0, 0, size, totalH); + ctx.fillRect(0, 0, Number(size), totalH); - const qrTop = captionPos === 'top' ? capH : 0; - const pad = Math.round(margin * moduleSize); - const cellSize = (size - pad * 2) / n; + // QR image + const qrTop = captionPos === 'top' ? capH : 0; + ctx.drawImage(tmp, 0, qrTop, Number(size), Number(size)); - ctx.fillStyle = fgColor; - - for (let row = 0; row < n; row++) { - for (let col = 0; col < n; col++) { - // Sample the center pixel of this module from the temp canvas - const px = Math.round((col + 0.5) * moduleSize); - const py = Math.round((row + 0.5) * moduleSize); - const idx = (py * size + px) * 4; - if (pixels[idx] > 128) continue; // light module - - const x = pad + col * cellSize; - const y = qrTop + pad + row * cellSize; - const w = cellSize; - - // Finder pattern corners: rows/cols 0-6 and last 6 - const isCorner = (row < 7 && col < 7) || (row < 7 && col >= n - 7) || (row >= n - 7 && col < 7); - - const style = isCorner ? cornerStyle : dotStyle; - - if (style === 'dots') { - const r = w * 0.45; - ctx.beginPath(); ctx.arc(x + w/2, y + w/2, r, 0, Math.PI * 2); ctx.fill(); - } else if (style === 'rounded' || style === 'classy') { - const r = style === 'classy' ? w * 0.42 : w * 0.32; - ctx.beginPath(); ctx.roundRect(x + 0.5, y + 0.5, w - 1, w - 1, r); ctx.fill(); - } else if (style === 'dot') { - const r = w * 0.45; - ctx.beginPath(); ctx.arc(x + w/2, y + w/2, r, 0, Math.PI * 2); ctx.fill(); - } else { - ctx.fillRect(x, y, w, w); - } - } - } - - // Step 4: caption + // Caption if (caption && captionPos !== 'none') { - ctx.fillStyle = captionColor; - ctx.font = `${captionSize}px sans-serif`; - ctx.textAlign = 'center'; + ctx.fillStyle = captionColor; + ctx.font = `${captionSize}px sans-serif`; + ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - const cy = captionPos === 'top' ? capH / 2 : size + capH / 2; - ctx.fillText(caption, size / 2, cy, size - 16); + const cy = captionPos === 'top' ? capH / 2 : Number(size) + capH / 2; + ctx.fillText(caption, Number(size) / 2, cy, Number(size) - 16); } - }).catch(err => { - // Show error message on canvas + }).catch(() => { + // Show error on canvas + targetCanvas.width = Number(size); + targetCanvas.height = Number(size); + const ctx = targetCanvas.getContext('2d'); ctx.fillStyle = bgColor; - ctx.fillRect(0, 0, size, totalH); + ctx.fillRect(0, 0, Number(size), Number(size)); ctx.fillStyle = '#ff6b9d'; ctx.font = '13px monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - ctx.fillText('Ungültige URL', size/2, size/2); + ctx.fillText('Ungültige URL', Number(size)/2, Number(size)/2); }); } @@ -2797,9 +2753,9 @@ function QrGenerator({ toast, mobile }) { const canvasRef = useRef(null); const [tab, setTab] = useState('create'); const [saved, setSaved] = useState([]); - const [editing, setEditing] = useState(null); // saved qr being edited + const [editing, setEditing] = useState(null); + const [generated, setGenerated] = useState(false); // whether QR is shown - // Form state const DEFAULTS = { label:'', url:'', size:256, fgColor:'#000000', bgColor:'#ffffff', margin:4, dotStyle:'square', cornerStyle:'square', @@ -2808,24 +2764,25 @@ function QrGenerator({ toast, mobile }) { const [form, setForm] = useState(DEFAULTS); const [busy, setBusy] = useState(false); - const set = (k, v) => setForm(p => ({...p, [k]: v})); + const set = (k, v) => { setForm(p => ({...p, [k]: v})); setGenerated(false); }; const loadSaved = () => api('/tools/qrcodes').then(setSaved).catch(()=>{}); useEffect(()=>{ loadSaved(); },[]); - // Re-render QR whenever form changes - useEffect(()=>{ - if (!canvasRef.current || !form.url.trim()) return; - drawQrToCanvas(canvasRef.current, form.url, { + const generate = async () => { + if (!form.url.trim()) { toast('URL erforderlich','error'); return; } + if (!canvasRef.current) return; + await drawQrToCanvas(canvasRef.current, form.url, { size:form.size, fgColor:form.fgColor, bgColor:form.bgColor, margin:form.margin, dotStyle:form.dotStyle, cornerStyle:form.cornerStyle, caption:form.caption, captionPos:form.captionPos, captionColor:form.captionColor, captionSize:form.captionSize, }); - }, [form]); + setGenerated(true); + }; const download = () => { - if (!canvasRef.current) return; + if (!canvasRef.current || !generated) return; const a = document.createElement('a'); a.href = canvasRef.current.toDataURL('image/png'); a.download = `qrcode-${form.label||'export'}.png`; @@ -2987,31 +2944,46 @@ function QrGenerator({ toast, mobile }) { )}
- - - {editing && }
+ {/* Rechte Spalte: Vorschau */}
VORSCHAU
- {form.url.trim() - ? - :
- URL eingeben → -
- } + + {!generated && ( +
+ + + Einstellungen wählen{String.fromCharCode(10)}dann „Generieren" + +
+ )} + {generated && ( +
+ + +
+ )}
- QR-Code wird lokal generiert
Nichts wird ohne Speichern gespeichert + Lokal generiert · Nichts wird ohne Speichern gespeichert
@@ -3123,25 +3095,48 @@ export default function DevTools({ toast, mobile }) { const active = TOOL_DEFS.find(t=>t.id===activeTool); const ActiveComp = active?.tabs ? active.components[activeTab] : active?.component; + // Emoji/Icon aus Label extrahieren + const getIcon = lbl => { + const m = lbl.match(/^([\u{1F300}-\u{1FFFF}\u{2600}-\u{27BF}⏱⚡📅🎨🔐🔍🌐◻±{}]/u); + return m ? m[0] : lbl[0]; + }; + const getText = lbl => lbl.replace(/^[\u{1F300}-\u{1FFFF}\u{2600}-\u{27BF}⏱⚡📅🎨🔐🔍🌐◻±{}]\s*/u,'').trim() || lbl; + return (
-
+

Dev-Tools

Konverter · Formatter · Helfer
-
+ + {/* Tool-Auswahl: 2-spaltiges Grid */} +
{TOOL_DEFS.map(t=>( ))}
+
{active?.label}