document.addEventListener('DOMContentLoaded', () => { // --- 0. CONSTANTS --- // Limits based on Version 40 (Byte Mode) const MAX_CAPACITY = { 'L': 2950, 'M': 2328, 'Q': 1660, 'H': 1270 }; // --- 1. DOM REFERENCES --- const qrContainer = document.getElementById('qr-container'); const downloadBtn = document.getElementById('btn-download'); const stickyWrapper = document.querySelector('.sticky-wrapper'); const clearBtn = document.getElementById('btn-clear'); // Gauge Elements const capMeter = document.getElementById('capacity-meter'); const capBar = document.getElementById('capacity-bar'); const capText = document.getElementById('capacity-text'); const capLabel = document.getElementById('capacity-label'); // Inputs const modeSelector = document.getElementById('mode-selector'); const optEcc = document.getElementById('opt-ecc'); const optSize = document.getElementById('opt-size'); const optFg = document.getElementById('opt-fg'); const optBg = document.getElementById('opt-bg'); const hexFg = document.getElementById('hex-fg'); const hexBg = document.getElementById('hex-bg'); const fmtRadios = document.getElementsByName('opt-fmt'); let debounceTimer; let renderSeq = 0; // --- 2. STATE GETTERS --- function getFormat() { for (const r of fmtRadios) { if (r.checked) return r.value; } return 'png'; } function getDataString() { const mode = modeSelector.value; if (mode === 'text') { return document.getElementById('inp-text').value; } else if (mode === 'wifi') { const ssid = document.getElementById('inp-wifi-ssid').value; const pass = document.getElementById('inp-wifi-pass').value; const type = document.getElementById('inp-wifi-type').value; if (!ssid) return ''; const cleanSSID = ssid.replace(/([\\;,:])/g, '\\$1'); const cleanPass = pass.replace(/([\\;,:])/g, '\\$1'); return `WIFI:S:${cleanSSID};T:${type};P:${cleanPass};;`; } else if (mode === 'email') { const to = document.getElementById('inp-email-to').value; const sub = document.getElementById('inp-email-sub').value; const body = document.getElementById('inp-email-body').value; if (!to) return ''; return `mailto:${to}?subject=${encodeURIComponent(sub)}&body=${encodeURIComponent(body)}`; } return ''; } // --- 3. RENDER HELPERS --- function generateSVGString(modules, size, fg, bg) { const count = modules.length; const modSize = size / count; let pathData = ''; for (let r = 0; r < count; r++) { for (let c = 0; c < count; c++) { if (modules[r][c]) { const x = c * modSize; const y = r * modSize; pathData += `M${x},${y}h${modSize}v${modSize}h-${modSize}z`; } } } return ` `; } function renderError(title, subtitle) { qrContainer.innerHTML = `
ERROR: ${title}
${subtitle}
`; downloadBtn.disabled = true; downloadBtn.textContent = "GENERATION FAILED"; stickyWrapper.classList.add('has-error'); } function updateCapacityUI(textData) { if (!textData) { capMeter.style.display = 'none'; return; } const blob = new Blob([textData]); const bytes = blob.size; const maxBytes = MAX_CAPACITY[optEcc.value] || 1270; const rawUsage = Math.round((bytes / maxBytes) * 100); const usage = Math.min(100, rawUsage); if (usage > 50) { capMeter.style.display = 'block'; capBar.style.width = `${usage}%`; capText.textContent = `${usage}% (${bytes} / ${maxBytes} B)`; if (bytes <= maxBytes) { capLabel.textContent = 'CAPACITY'; } else { capLabel.textContent = 'OVER CAPACITY'; } if (usage > 90) { // Critical Level: Force RED capBar.style.backgroundColor = 'red'; capText.style.color = 'red'; capLabel.style.color = 'red'; } else { // Normal Level: Let CSS variables handle it (Black/White) capBar.style.backgroundColor = ''; capText.style.color = ''; capLabel.style.color = ''; } } else { capMeter.style.display = 'none'; } } // --- 4. CORE RENDERER --- function renderQR() { const mySeq = ++renderSeq; // 1. Gather State const textData = getDataString(); const format = getFormat(); const ecc = QRCode.CorrectLevel[optEcc.value]; const colorDark = optFg.value; const colorLight = optBg.value; // 2. Validate Size & Labels let size = parseInt(optSize.value) || 256; if (size < 64) size = 64; if (size > 4000) size = 4000; hexFg.textContent = colorDark; hexBg.textContent = colorLight; const resDisplay = document.getElementById('resolution-display'); if (resDisplay) resDisplay.textContent = `${size} x ${size} px`; // 3. Reset Error State stickyWrapper.classList.remove('has-error'); // 4. Handle Empty State if (!textData || textData.trim() === '') { if (mySeq === renderSeq) { qrContainer.innerHTML = ''; downloadBtn.disabled = true; downloadBtn.textContent = `DOWNLOAD ${format.toUpperCase()}`; } return; } downloadBtn.textContent = `DOWNLOAD ${format.toUpperCase()}`; // 5. Generate (Off-screen) try { const tempContainer = document.createElement('div'); const instance = new QRCode(tempContainer, { text: textData, width: size, height: size, colorDark : colorDark, colorLight : colorLight, correctLevel : ecc }); if (format === 'svg') { const modules = instance._oQRCode.modules; const svgString = generateSVGString(modules, size, colorDark, colorLight); if (mySeq === renderSeq) { qrContainer.innerHTML = svgString; downloadBtn.disabled = false; } } else { // PNG Mode const canvas = tempContainer.querySelector('canvas'); if (canvas) { const dataUrl = canvas.toDataURL("image/png"); const newImg = new Image(); newImg.style.display = 'block'; newImg.style.width = '100%'; newImg.style.height = '100%'; newImg.style.imageRendering = 'pixelated'; newImg.src = dataUrl; const ready = newImg.decode ? newImg.decode() : new Promise((resolve, reject) => { newImg.onload = resolve; newImg.onerror = reject; }); ready.then(() => { if (mySeq !== renderSeq) return; qrContainer.replaceChildren(newImg); downloadBtn.disabled = false; }).catch(() => { if (mySeq !== renderSeq) return; qrContainer.replaceChildren(newImg); downloadBtn.disabled = false; }); } } } catch (e) { if (mySeq !== renderSeq) return; const blob = new Blob([textData]); const bytes = blob.size; const maxBytes = MAX_CAPACITY[optEcc.value] || 1270; if (bytes >= maxBytes) { renderError("CAPACITY EXCEEDED", "REDUCE TEXT OR LOWER ECC LEVEL"); } else { console.error(e); renderError("UNKNOWN ERROR", `CODE: ${e.name || 'Except'}`); } } } function handleUpdate(immediate = false) { const text = getDataString(); updateCapacityUI(text); clearTimeout(debounceTimer); if (immediate) { renderQR(); } else { debounceTimer = setTimeout(renderQR, 300); } } // --- 5. RESET LOGIC --- function resetApp() { const inputs = document.querySelectorAll('.input-form input, .input-form textarea'); inputs.forEach(el => el.value = ''); modeSelector.value = 'text'; optEcc.value = 'H'; optSize.value = '256'; optFg.value = '#000000'; hexFg.textContent = '#000000'; optBg.value = '#ffffff'; hexBg.textContent = '#FFFFFF'; for (const r of fmtRadios) { if (r.value === 'png') r.checked = true; } document.querySelectorAll('.input-form').forEach(f => f.classList.remove('active')); document.getElementById('form-text').classList.add('active'); handleUpdate(true); const firstField = document.querySelector('#form-text textarea'); if (firstField) firstField.focus(); } // --- 6. LISTENERS --- // (Listeners remain the same) [optEcc, optSize, optFg, optBg].forEach(el => { el.addEventListener('input', () => handleUpdate(false)); }); fmtRadios.forEach(r => r.addEventListener('change', () => handleUpdate(true))); modeSelector.addEventListener('change', (e) => { const newMode = e.target.value; document.querySelectorAll('.input-form').forEach(f => f.classList.remove('active')); document.getElementById(`form-${newMode}`).classList.add('active'); const newForm = document.getElementById(`form-${newMode}`); const firstField = newForm.querySelector('input, textarea'); if (firstField) firstField.focus(); handleUpdate(true); }); document.querySelectorAll('.input-form').forEach(form => { form.addEventListener('input', () => handleUpdate(false)); }); if (clearBtn) { clearBtn.addEventListener('click', resetApp); } downloadBtn.addEventListener('click', () => { const format = getFormat(); if (format === 'png') { const img = qrContainer.querySelector('img'); if (img && img.src) { const link = document.createElement('a'); link.href = img.src; link.download = `qrdamage-${Date.now()}.png`; document.body.appendChild(link); link.click(); link.remove(); } } else { const svgEl = qrContainer.querySelector('svg'); if (svgEl) { const serializer = new XMLSerializer(); const svgString = serializer.serializeToString(svgEl); const blob = new Blob([svgString], {type: 'image/svg+xml;charset=utf-8'}); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `qrdamage-${Date.now()}.svg`; document.body.appendChild(link); link.click(); link.remove(); } } }); // --- 7. STARTUP SEQUENCE --- const initialMode = modeSelector.value; document.querySelectorAll('.input-form').forEach(f => f.classList.remove('active')); const initialForm = document.getElementById(`form-${initialMode}`); if (initialForm) { initialForm.classList.add('active'); const startField = initialForm.querySelector('input, textarea'); if (startField) startField.focus(); } handleUpdate(true); });