Fix the handling and placement of capacity meter

Move it near the text box.

Change handling of updates to be more robust and remove the possibility
of a particular bug that was occuring.

Set the display threshold higher, now 50%.
This commit is contained in:
Alexander Wainwright
2025-12-18 17:57:53 +10:00
parent 446b3b46fd
commit 479e22e151
2 changed files with 130 additions and 152 deletions

View File

@@ -34,6 +34,16 @@
<label for="inp-text">Text or URL</label>
<textarea id="inp-text" rows="6" placeholder="https://example.com or plain text"></textarea>
</div>
<div id="capacity-meter" style="display:none; margin-top:0.75rem; width:100%;">
<div style="display:flex; justify-content:space-between; font-size:0.7rem; font-family:var(--font-mono); margin-bottom:0.2rem;">
<span id="capacity-label">CAPACITY</span>
<span id="capacity-text">0%</span>
</div>
<div style="width:100%; height:4px; background:#eee; border:1px solid #000;">
<div id="capacity-bar" style="width:0%; height:100%; background:#000;"></div>
</div>
</div>
</div>
<div id="form-wifi" class="input-form">
@@ -128,20 +138,9 @@
<section class="pane preview-pane">
<div class="sticky-wrapper">
<div id="qr-container">
</div>
<div id="qr-container"> </div>
<button id="btn-download" disabled>DOWNLOAD PNG</button>
<div id="capacity-meter" style="display:none; margin-top:0.75rem; width:100%;">
<div style="display:flex; justify-content:space-between; font-size:0.7rem; font-family:var(--font-mono); margin-bottom:0.2rem;">
<span>CAPACITY</span>
<span id="capacity-text">0%</span>
</div>
<div style="width:100%; height:4px; background:#eee; border:1px solid #000;">
<div id="capacity-bar" style="width:0%; height:100%; background:#000;"></div>
</div>
</div>
</div>
</section>

259
script.js
View File

@@ -1,5 +1,5 @@
document.addEventListener('DOMContentLoaded', () => {
// --- GLOBALS ---
// --- 1. DOM REFERENCES ---
const qrContainer = document.getElementById('qr-container');
const downloadBtn = document.getElementById('btn-download');
const stickyWrapper = document.querySelector('.sticky-wrapper');
@@ -8,6 +8,7 @@ document.addEventListener('DOMContentLoaded', () => {
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');
@@ -18,13 +19,11 @@ document.addEventListener('DOMContentLoaded', () => {
const hexFg = document.getElementById('hex-fg');
const hexBg = document.getElementById('hex-bg');
// Format Radios
const fmtRadios = document.getElementsByName('opt-fmt');
let qrcodeObj = null;
let debounceTimer;
// --- 1. DATA GATHERING ---
// --- 2. STATE GETTERS (Pure Functions) ---
function getFormat() {
for (const r of fmtRadios) {
@@ -41,7 +40,7 @@ document.addEventListener('DOMContentLoaded', () => {
const ssid = document.getElementById('inp-wifi-ssid').value;
const pass = document.getElementById('inp-wifi-pass').value;
const type = document.getElementById('inp-wifi-type').value;
// return `WIFI:S:${ssid};T:${type};P:${pass};;`;
// Note: Even if empty, we return the structure so the logic can decide
if (!ssid) return '';
const cleanSSID = ssid.replace(/([\\;,:])/g, '\\$1');
const cleanPass = pass.replace(/([\\;,:])/g, '\\$1');
@@ -56,42 +55,9 @@ document.addEventListener('DOMContentLoaded', () => {
return '';
}
// --- 2. INSTANCE MANAGEMENT ---
// --- 3. RENDER HELPERS ---
function createQRInstance() {
qrContainer.innerHTML = '';
let size = parseInt(optSize.value);
if (isNaN(size) || size < 64) size = 64;
if (size > 4000) size = 4000;
const ecc = QRCode.CorrectLevel[optEcc.value];
const colorDark = optFg.value;
const colorLight = optBg.value;
// Update Labels (UI)
hexFg.textContent = colorDark;
hexBg.textContent = colorLight;
try {
return new QRCode(qrContainer, {
width: size,
height: size,
colorDark : colorDark,
colorLight : colorLight,
correctLevel : ecc
});
} catch (e) {
return null;
}
}
// --- 3. RENDERING HELPERS ---
function renderSVG(modules) {
const size = parseInt(optSize.value) || 256;
const fg = optFg.value;
const bg = optBg.value;
function generateSVGString(modules, size, fg, bg) {
const count = modules.length;
const modSize = size / count;
@@ -114,27 +80,50 @@ document.addEventListener('DOMContentLoaded', () => {
`;
}
// --- 4. CORE FUNCTIONS ---
function renderError() {
qrContainer.innerHTML = `
<div class="qr-error-msg">
ERROR: CAPACITY EXCEEDED<br>
<span style="font-size:0.7em; opacity:0.8; display:block; margin-top:0.5rem;">
REDUCE TEXT OR<br>LOWER ECC LEVEL
</span>
</div>
`;
downloadBtn.disabled = true;
downloadBtn.textContent = "GENERATION FAILED";
stickyWrapper.classList.add('has-error');
}
// A. The Fast One: Updates the bar immediately
function updateCapacityGauge(textData) {
if (!textData || !capMeter) {
if (capMeter) capMeter.style.display = 'none';
// --- 4. CORE LOGIC ---
// A. Capacity Gauge (Fast / Instant)
function updateCapacityUI(textData) {
if (!textData) {
capMeter.style.display = 'none';
return;
}
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;
// Limits based on Version 40 (Byte Mode)
const maxCapacityMap = { 'L': 2950, 'M': 2328, 'Q': 1660, 'H': 1270 };
const maxBytes = maxCapacityMap[optEcc.value] || 1270;
const usage = Math.min(100, Math.round((bytes / maxBytes) * 100));
const raw_usage = Math.round((bytes / maxBytes) * 100);
const usage = Math.min(100, raw_usage);
if (usage > 5) {
if (usage > 50) {
capMeter.style.display = 'block';
capBar.style.width = `${usage}%`;
capText.textContent = `${usage}% (${bytes}B)`;
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';
@@ -148,137 +137,127 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
// B. The Heavy One: Generates the image (Debounced)
function generateQR(textData) {
// Reset Error State
stickyWrapper.classList.remove('has-error');
// B. The Main Renderer (Stateless: Flush and Rebuild)
function renderQR() {
// 1. Gather State
const textData = getDataString();
const format = getFormat();
const ecc = QRCode.CorrectLevel[optEcc.value];
const colorDark = optFg.value;
const colorLight = optBg.value;
// Update Hex Labels
hexFg.textContent = colorDark;
hexBg.textContent = colorLight;
// Handle Empty
// 2. Reset DOM
qrContainer.innerHTML = '';
stickyWrapper.classList.remove('has-error');
// 3. Handle Empty State
if (!textData || textData.trim() === '') {
if (qrcodeObj) qrcodeObj.clear();
qrcodeObj = null;
qrContainer.innerHTML = '';
downloadBtn.disabled = true;
downloadBtn.textContent = `DOWNLOAD ${format.toUpperCase()}`;
return;
return; // Stop here. Container is clean.
}
// Ensure Instance
if (!qrcodeObj) qrcodeObj = createQRInstance();
if (!qrcodeObj) return;
downloadBtn.textContent = `DOWNLOAD ${format.toUpperCase()}`;
// 4. Validate Size (Prevent Crashes)
let size = parseInt(optSize.value) || 256;
if (size < 64) size = 64;
if (size > 4000) size = 4000;
// 5. Generate
try {
// Generate
// qrcodeObj.clear()
qrcodeObj.makeCode(textData);
// We create a local instance. It populates qrContainer with a Canvas/Img.
// We do not save this instance globally. It is single-use.
const instance = new QRCode(qrContainer, {
text: textData, // Passing text here triggers makeCode immediately
width: size,
height: size,
colorDark : colorDark,
colorLight : colorLight,
correctLevel : ecc
});
// check if anything royally fucked up
const imgs = qrContainer.querySelectorAll('img');
// if (imgs.length == 0) {
// console.log('==================== YIKES =====================')
// qrcodeObj = createQRInstance();
// qrcodeObj.makeCode(textData)
// }
// Render SVG vs PNG
// 6. Post-Process (Swap to SVG if needed)
if (format === 'svg') {
const imgs = qrContainer.querySelectorAll('img');
imgs.forEach(i => i.style.display = 'none');
const canvas = qrContainer.querySelectorAll('canvas');
canvas.forEach(c => c.style.display = 'none');
if (qrcodeObj._oQRCode && qrcodeObj._oQRCode.modules) {
const svgString = renderSVG(qrcodeObj._oQRCode.modules);
// Clear old SVGs/Errors
qrContainer.querySelectorAll('svg, .qr-error-msg').forEach(e => e.remove());
qrContainer.insertAdjacentHTML('beforeend', svgString);
// The library just painted a canvas. We hide it and inject SVG.
// We use the internal data of our local 'instance'
const modules = instance._oQRCode.modules;
// Hide library output
const nodes = qrContainer.childNodes;
for(let i=0; i<nodes.length; i++) {
if(nodes[i].style) nodes[i].style.display = 'none';
}
} else {
// PNG Mode
const imgs = qrContainer.querySelectorAll('img');
imgs.forEach(i => i.style.display = 'block');
qrContainer.querySelectorAll('svg, .qr-error-msg').forEach(e => e.remove());
// Inject SVG
const svgString = generateSVGString(modules, size, colorDark, colorLight);
qrContainer.insertAdjacentHTML('beforeend', svgString);
}
else {
// PNG Mode: The library already appended an <img>. We are done.
// Just ensure it's visible (library defaults to display:none sometimes for the img)
const img = qrContainer.querySelector('img');
if(img) img.style.display = 'block';
}
downloadBtn.disabled = false;
} catch (e) {
// Error Handling
// console.log(e)
qrcodeObj = null;
qrContainer.innerHTML = `
<div class="qr-error-msg">
ERROR: CAPACITY EXCEEDED<br>
<span style="font-size:0.7em; opacity:0.8; display:block; margin-top:0.5rem;">
REDUCE TEXT OR<br>LOWER ECC LEVEL
</span>
</div>
`;
downloadBtn.disabled = true;
downloadBtn.textContent = "GENERATION FAILED";
console.log(e)
// 7. Handle Overflow/Errors
// Since we flushed the DOM at the start, we can just render the error now.
renderError();
}
}
// --- 5. THE COORDINATOR ---
// --- 5. EVENT ORCHESTRATION ---
function handleInput() {
console.log("Input detected. Text length:", getDataString().length);
function handleUpdate(immediate = false) {
const text = getDataString();
console.log(text)
// 1. Instant Feedback
updateCapacityGauge(text);
// 2. Delayed Heavy Lifting
// Always update gauge immediately
updateCapacityUI(text);
// Debounce the heavy rendering
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
generateQR(text);
}, 300);
if (immediate) {
renderQR();
} else {
debounceTimer = setTimeout(renderQR, 300);
}
}
// Force Immediate Update (for Config changes)
function forceUpdate() {
const text = getDataString();
updateCapacityGauge(text);
generateQR(text);
}
// --- 6. LISTENERS ---
// --- EVENT LISTENERS ---
// Config Changes (Require Rebuild)
// Configuration Inputs (Colors, ECC, Size) -> Trigger Rebuild
[optEcc, optSize, optFg, optBg].forEach(el => {
el.addEventListener('input', () => {
// Debounce the rebuild slightly
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
qrcodeObj = null;
forceUpdate();
}, 100);
});
el.addEventListener('input', () => handleUpdate(false));
});
// Format Change (Instant)
fmtRadios.forEach(r => r.addEventListener('change', forceUpdate));
// Format Change -> Trigger Rebuild (Instant)
fmtRadios.forEach(r => r.addEventListener('change', () => handleUpdate(true)));
// Mode Switch (Instant)
// Mode Switch -> Change Form & Rebuild
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');
forceUpdate();
handleUpdate(true);
});
// DATA ENTRY (The optimized path)
// Data Entry -> Debounced Rebuild
document.querySelectorAll('.input-form').forEach(form => {
form.addEventListener('input', handleInput);
form.addEventListener('input', () => handleUpdate(false));
});
// Download
// Download Handler
downloadBtn.addEventListener('click', () => {
const format = getFormat();
if (format === 'png') {
const img = qrContainer.querySelector('img');
if (img && img.src) {
@@ -306,6 +285,6 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
// Initial Start
forceUpdate();
// Initial Render
handleUpdate(true);
});