0.1.0 Update with new features
This commit is contained in:
370
script.js
370
script.js
@@ -1,49 +1,46 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// --- GLOBALS ---
|
||||
// --- 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');
|
||||
|
||||
// Hex Labels (for display)
|
||||
const hexFg = document.getElementById('hex-fg');
|
||||
const hexBg = document.getElementById('hex-bg');
|
||||
|
||||
let qrcodeObj = null;
|
||||
const fmtRadios = document.getElementsByName('opt-fmt');
|
||||
|
||||
let debounceTimer;
|
||||
let renderSeq = 0;
|
||||
|
||||
// --- LOGIC ---
|
||||
// --- 2. STATE GETTERS ---
|
||||
|
||||
function createQRInstance() {
|
||||
qrContainer.innerHTML = '';
|
||||
|
||||
// Validation: Clamp size to prevent crashing browser
|
||||
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 Hex Labels
|
||||
hexFg.textContent = colorDark;
|
||||
hexBg.textContent = colorLight;
|
||||
|
||||
qrcodeObj = new QRCode(qrContainer, {
|
||||
width: size,
|
||||
height: size,
|
||||
colorDark : colorDark,
|
||||
colorLight : colorLight,
|
||||
correctLevel : ecc
|
||||
});
|
||||
|
||||
updateQR();
|
||||
function getFormat() {
|
||||
for (const r of fmtRadios) {
|
||||
if (r.checked) return r.value;
|
||||
}
|
||||
return 'png';
|
||||
}
|
||||
|
||||
function getDataString() {
|
||||
@@ -54,7 +51,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;
|
||||
if (!ssid) return '';
|
||||
if (!ssid) return '';
|
||||
const cleanSSID = ssid.replace(/([\\;,:])/g, '\\$1');
|
||||
const cleanPass = pass.replace(/([\\;,:])/g, '\\$1');
|
||||
return `WIFI:S:${cleanSSID};T:${type};P:${cleanPass};;`;
|
||||
@@ -68,63 +65,298 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return '';
|
||||
}
|
||||
|
||||
function updateQR() {
|
||||
if (!qrcodeObj) return;
|
||||
const data = getDataString();
|
||||
|
||||
if (!data || data.trim() === '') {
|
||||
qrcodeObj.clear();
|
||||
downloadBtn.disabled = true;
|
||||
// --- 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;
|
||||
|
||||
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.disabled = false;
|
||||
qrcodeObj.makeCode(data);
|
||||
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'}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- EVENT LISTENERS ---
|
||||
function handleUpdate(immediate = false) {
|
||||
const text = getDataString();
|
||||
updateCapacityUI(text);
|
||||
clearTimeout(debounceTimer);
|
||||
if (immediate) {
|
||||
renderQR();
|
||||
} else {
|
||||
debounceTimer = setTimeout(renderQR, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Config Changes (Re-create instance)
|
||||
// We listen to 'change' for colors (happens when picker closes)
|
||||
// and 'input' (happens while dragging) depending on preference.
|
||||
// 'input' is smoother but more CPU intensive. Let's use 'input'.
|
||||
// --- 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', () => {
|
||||
// Debounce the recreation slightly to prevent lag during color dragging
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(createQRInstance, 100);
|
||||
});
|
||||
el.addEventListener('input', () => handleUpdate(false));
|
||||
});
|
||||
|
||||
// 2. Mode Switching
|
||||
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');
|
||||
updateQR();
|
||||
|
||||
const newForm = document.getElementById(`form-${newMode}`);
|
||||
const firstField = newForm.querySelector('input, textarea');
|
||||
if (firstField) firstField.focus();
|
||||
|
||||
handleUpdate(true);
|
||||
});
|
||||
|
||||
// 3. Data Entry (Text, WiFi, Email)
|
||||
document.querySelectorAll('.input-form').forEach(form => {
|
||||
form.addEventListener('input', () => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(updateQR, 300);
|
||||
});
|
||||
form.addEventListener('input', () => handleUpdate(false));
|
||||
});
|
||||
|
||||
// 4. Download
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', resetApp);
|
||||
}
|
||||
|
||||
downloadBtn.addEventListener('click', () => {
|
||||
const img = qrContainer.querySelector('img');
|
||||
if (img && img.src) {
|
||||
const link = document.createElement('a');
|
||||
link.href = img.src;
|
||||
link.download = `qrcode-${Date.now()}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Init
|
||||
createQRInstance();
|
||||
// --- 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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user