Files
qrdamage/script.js
Alexander Wainwright 205876a28f Add reset button
2025-12-18 18:30:58 +10:00

311 lines
11 KiB
JavaScript

document.addEventListener('DOMContentLoaded', () => {
// --- 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;
// --- 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 `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">
<rect width="100%" height="100%" fill="${bg}"/>
<path d="${pathData}" fill="${fg}" shape-rendering="crispEdges"/>
</svg>
`;
}
function renderError(title, subtitle) {
qrContainer.innerHTML = `
<div class="qr-error-msg">
ERROR: ${title}<br>
<span style="font-size:0.7em; opacity:0.8; display:block; margin-top:0.5rem;">
${subtitle}
</span>
</div>
`;
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;
// Limits based on Version 40 (Byte Mode)
const maxCapacityMap = { 'L': 2950, 'M': 2328, 'Q': 1660, 'H': 1270 };
const maxBytes = maxCapacityMap[optEcc.value] || 1270;
const raw_usage = Math.round((bytes / maxBytes) * 100);
const usage = Math.min(100, raw_usage);
if (usage > 50) {
capMeter.style.display = 'block';
capBar.style.width = `${usage}%`;
capText.textContent = `${usage}% (${bytes} / ${maxBytes} B)`;
if (raw_usage <= 100) {
capLabel.textContent = 'CAPACITY';
} else {
capLabel.textContent = 'OVER CAPACITY';
}
if (usage > 90) {
capBar.style.backgroundColor = 'red';
capText.style.color = 'red';
capLabel.style.color = 'red';
} else {
capBar.style.backgroundColor = '#000';
capText.style.color = '#000';
capLabel.style.color = '#000';
}
} else {
capMeter.style.display = 'none';
}
}
// --- 4. CORE RENDERER ---
function renderQR() {
const textData = getDataString();
const format = getFormat();
const ecc = QRCode.CorrectLevel[optEcc.value];
const colorDark = optFg.value;
const colorLight = optBg.value;
hexFg.textContent = colorDark;
hexBg.textContent = colorLight;
qrContainer.innerHTML = '';
stickyWrapper.classList.remove('has-error');
if (!textData || textData.trim() === '') {
downloadBtn.disabled = true;
downloadBtn.textContent = `DOWNLOAD ${format.toUpperCase()}`;
return;
}
downloadBtn.textContent = `DOWNLOAD ${format.toUpperCase()}`;
let size = parseInt(optSize.value) || 256;
if (size < 64) size = 64;
if (size > 4000) size = 4000;
try {
const instance = new QRCode(qrContainer, {
text: textData,
width: size,
height: size,
colorDark : colorDark,
colorLight : colorLight,
correctLevel : ecc
});
if (format === 'svg') {
const modules = instance._oQRCode.modules;
const nodes = qrContainer.childNodes;
for(let i=0; i<nodes.length; i++) {
if(nodes[i].style) nodes[i].style.display = 'none';
}
const svgString = generateSVGString(modules, size, colorDark, colorLight);
qrContainer.insertAdjacentHTML('beforeend', svgString);
}
else {
const img = qrContainer.querySelector('img');
if(img) img.style.display = 'block';
}
downloadBtn.disabled = false;
} catch (e) {
const blob = new Blob([textData]);
const bytes = blob.size;
const maxCapacityMap = { 'L': 2953, 'M': 2331, 'Q': 1663, 'H': 1273 };
const maxBytes = maxCapacityMap[optEcc.value] || 1273;
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 (Button Only) ---
function resetApp() {
// Clear Inputs
const inputs = document.querySelectorAll('.input-form input, .input-form textarea');
inputs.forEach(el => el.value = '');
// Reset Mode to Text
modeSelector.value = 'text';
// Update UI Classes
document.querySelectorAll('.input-form').forEach(f => f.classList.remove('active'));
document.getElementById('form-text').classList.add('active');
// Update Output
handleUpdate(true);
// Focus
const firstField = document.querySelector('#form-text input');
if (firstField) firstField.focus();
}
// --- 6. LISTENERS ---
[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));
});
// Clear Button
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 = `qr-manifesto-${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 = `qr-manifesto-${Date.now()}.svg`;
document.body.appendChild(link);
link.click();
link.remove();
}
}
});
// --- 7. STARTUP SEQUENCE (Browser Restore Friendly) ---
// Check what mode the browser remembered
const initialMode = modeSelector.value;
// Force the correct form to show based on that mode
document.querySelectorAll('.input-form').forEach(f => f.classList.remove('active'));
const initialForm = document.getElementById(`form-${initialMode}`);
if (initialForm) {
initialForm.classList.add('active');
// Auto-focus the restored form
const startField = initialForm.querySelector('input, textarea');
if (startField) startField.focus();
}
// Render whatever data the browser remembered
handleUpdate(true);
});