DevTools: QR-Code fix (toCanvas statt create), Generieren-Button, 2-Spalten-Nav, CAD-Skizzen Schild zurueck
This commit is contained in:
@@ -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 }) {
|
||||
)}
|
||||
</div>
|
||||
<div style={{display:'flex',gap:8}}>
|
||||
<button onClick={download} disabled={!form.url.trim()}
|
||||
style={{...S.btn('#ffe66d',true),flex:1,textAlign:'center',padding:'8px 0',opacity:form.url.trim()?1:0.4}}>
|
||||
↓ PNG laden
|
||||
<button onClick={generate}
|
||||
style={{...S.btn('#4ecdc4'),flex:1,textAlign:'center',padding:'10px 0',fontWeight:700}}>
|
||||
▶ Generieren
|
||||
</button>
|
||||
<button onClick={save} disabled={busy||!form.url.trim()}
|
||||
style={{...S.btn('#4ecdc4'),flex:1,textAlign:'center',padding:'8px 0',opacity:form.url.trim()?1:0.4}}>
|
||||
{busy?'…':editing?'✓ Aktualisieren':'💾 Speichern'}
|
||||
</button>
|
||||
{editing && <button onClick={()=>{setEditing(null);setForm(DEFAULTS);}}
|
||||
{editing && <button onClick={()=>{setEditing(null);setForm(DEFAULTS);setGenerated(false);}}
|
||||
style={{...S.btn('#ff6b9d',true),padding:'0 12px'}}>✕</button>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rechte Spalte: Vorschau */}
|
||||
<div style={{display:'flex',flexDirection:'column',alignItems:'center',gap:12}}>
|
||||
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10}}>VORSCHAU</div>
|
||||
{form.url.trim()
|
||||
? <canvas ref={canvasRef} style={{borderRadius:8,maxWidth:'100%',
|
||||
boxShadow:'0 4px 24px rgba(0,0,0,0.4)',border:'1px solid rgba(255,255,255,0.08)'}}/>
|
||||
: <div style={{width:200,height:200,borderRadius:8,background:'rgba(255,255,255,0.04)',
|
||||
border:'1px solid rgba(255,255,255,0.08)',display:'flex',alignItems:'center',justifyContent:'center'}}>
|
||||
<span style={{color:'rgba(255,255,255,0.2)',fontFamily:'monospace',fontSize:11}}>URL eingeben →</span>
|
||||
</div>
|
||||
}
|
||||
<canvas ref={canvasRef} style={{
|
||||
borderRadius:8, maxWidth:'100%',
|
||||
boxShadow:'0 4px 24px rgba(0,0,0,0.4)',
|
||||
border:'1px solid rgba(255,255,255,0.08)',
|
||||
display: generated ? 'block' : 'none'}}/>
|
||||
{!generated && (
|
||||
<div style={{width:200,height:200,borderRadius:8,background:'rgba(255,255,255,0.04)',
|
||||
border:'1px solid rgba(255,255,255,0.08)',display:'flex',flexDirection:'column',
|
||||
alignItems:'center',justifyContent:'center',gap:10}}>
|
||||
<span style={{fontSize:36,opacity:0.2}}>◻</span>
|
||||
<span style={{color:'rgba(255,255,255,0.2)',fontFamily:'monospace',fontSize:10,textAlign:'center'}}>
|
||||
Einstellungen wählen{String.fromCharCode(10)}dann „Generieren"
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{generated && (
|
||||
<div style={{display:'flex',gap:8}}>
|
||||
<button onClick={download} style={{...S.btn('#ffe66d',true),fontSize:11,padding:'6px 14px'}}>
|
||||
↓ PNG laden
|
||||
</button>
|
||||
<button onClick={save} disabled={busy}
|
||||
style={{...S.btn('#4ecdc4'),fontSize:11,padding:'6px 14px',opacity:busy?0.5:1}}>
|
||||
{busy?'…':editing?'✓ Aktualisieren':'💾 Speichern'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:9,textAlign:'center',lineHeight:1.6}}>
|
||||
QR-Code wird lokal generiert<br/>Nichts wird ohne Speichern gespeichert
|
||||
Lokal generiert · Nichts wird ohne Speichern gespeichert
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 (
|
||||
<div style={{padding:mobile?'14px 14px 90px':'36px 44px',maxWidth:860}}>
|
||||
<div style={{display:'flex',alignItems:'center',gap:12,marginBottom:20,flexWrap:'wrap'}}>
|
||||
<div style={{display:'flex',alignItems:'center',gap:12,marginBottom:16,flexWrap:'wrap'}}>
|
||||
<h1 style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:mobile?17:22,margin:0}}>Dev-Tools</h1>
|
||||
<span style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:10}}>Konverter · Formatter · Helfer</span>
|
||||
</div>
|
||||
<div style={{display:'flex',gap:7,flexWrap:'wrap',marginBottom:20}}>
|
||||
|
||||
{/* Tool-Auswahl: 2-spaltiges Grid */}
|
||||
<div style={{display:'grid',gridTemplateColumns:'repeat(2,1fr)',gap:5,marginBottom:16}}>
|
||||
{TOOL_DEFS.map(t=>(
|
||||
<button key={t.id} title={t.desc}
|
||||
onClick={()=>{ if(t.ready){setActiveTool(t.id);setActiveTab(t.tabs?.[0]?.id||'');} else toast('Kommt bald 🚧'); }}
|
||||
style={{padding:'8px 14px',borderRadius:10,fontFamily:'monospace',fontSize:11,cursor:t.ready?'pointer':'default',
|
||||
background:activeTool===t.id?'#4ecdc4':t.ready?'rgba(255,255,255,0.06)':'rgba(255,255,255,0.02)',
|
||||
color: activeTool===t.id?'#0d0d0f':t.ready?'rgba(255,255,255,0.65)':'rgba(255,255,255,0.4)',
|
||||
border: activeTool===t.id?'none':t.ready?'1px solid rgba(255,255,255,0.1)':'1px solid rgba(255,255,255,0.05)',
|
||||
fontWeight: activeTool===t.id?700:400}}>
|
||||
{t.label}{!t.ready&&<span style={{marginLeft:4,fontSize:8,opacity:0.4}}>bald</span>}
|
||||
style={{
|
||||
display:'flex', alignItems:'center', gap:9,
|
||||
padding:'8px 11px', borderRadius:9, fontFamily:'monospace',
|
||||
cursor:t.ready?'pointer':'default', textAlign:'left', border:'none',
|
||||
background: activeTool===t.id ? 'rgba(78,205,196,0.15)' : t.ready ? 'rgba(255,255,255,0.04)' : 'rgba(255,255,255,0.02)',
|
||||
outline: activeTool===t.id ? '1px solid #4ecdc4' : 'none',
|
||||
}}>
|
||||
<span style={{fontSize:17,flexShrink:0,lineHeight:1,minWidth:22,textAlign:'center'}}>{getIcon(t.label)}</span>
|
||||
<div style={{minWidth:0,flex:1}}>
|
||||
<div style={{
|
||||
color: activeTool===t.id ? '#4ecdc4' : t.ready ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.3)',
|
||||
fontSize:12, fontWeight: activeTool===t.id?700:500,
|
||||
overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>
|
||||
{getText(t.label)}
|
||||
</div>
|
||||
<div style={{color:'rgba(255,255,255,0.28)',fontSize:9,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',marginTop:1}}>
|
||||
{t.desc}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{...S.card}}>
|
||||
<div style={{display:'flex',alignItems:'center',gap:10,marginBottom:14,flexWrap:'wrap'}}>
|
||||
<div style={{...S.head,marginBottom:0}}>{active?.label}</div>
|
||||
|
||||
Reference in New Issue
Block a user