diff --git a/cmd/web/main.go b/cmd/web/main.go index 1da1295..d0b38de 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -223,6 +223,7 @@ const indexHTML = ` .preview-info { margin-top: 0.75rem; font-size: 0.8rem; color: #a0aec0; min-height: 1.2em; } .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem; } + .grid3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; margin-bottom: 1rem; } label { display: block; font-size: 0.8rem; font-weight: 600; color: #a0aec0; margin-bottom: 0.35rem; } input[type=number], select { width: 100%; @@ -237,6 +238,22 @@ const indexHTML = ` } input[type=number]:focus, select:focus { border-color: #6366f1; } + /* unit toggle */ + .unit-row { + display: flex; align-items: center; gap: 0.5rem; + margin-bottom: 0.75rem; + } + .unit-row span { font-size: 0.8rem; color: #a0aec0; font-weight: 600; } + .toggle-group { display: flex; border: 1px solid #3a4060; border-radius: 6px; overflow: hidden; } + .toggle-group button { + flex: 1; background: #252a3d; color: #a0aec0; + border: none; border-radius: 0; padding: 0.3rem 0.8rem; + font-size: 0.8rem; font-weight: 600; cursor: pointer; width: auto; + transition: background .15s, color .15s; + } + .toggle-group button.active { background: #6366f1; color: #fff; } + .sub-label { font-size: 0.7rem; color: #718096; font-weight: 400; margin-top: 0.15rem; } + .checks { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1.5rem; } .checks label { display: flex; align-items: center; gap: 0.4rem; @@ -264,10 +281,7 @@ const indexHTML = ` button:hover { background: #4f52d0; } button:disabled { opacity: 0.5; cursor: not-allowed; } - .output { - margin-top: 1.5rem; - display: none; - } + .output { margin-top: 1.5rem; display: none; } .output-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; @@ -285,10 +299,7 @@ const indexHTML = ` color: #7dd3fc; font-family: monospace; font-size: 0.75rem; padding: 0.75rem; resize: vertical; outline: none; } - .dl-btn { - margin-top: 0.75rem; - background: #2d6a4f; - } + .dl-btn { margin-top: 0.75rem; background: #2d6a4f; } .dl-btn:hover { background: #215040; } .error { color: #fc8181; font-size: 0.85rem; margin-top: 0.75rem; display: none; } .spinner { @@ -313,14 +324,31 @@ const indexHTML = `
+ +
+ Dimensions in +
+ + +
+ Printer DPI + +
+
- - + + +
- - + + +
@@ -366,7 +394,69 @@ const spinner = document.getElementById('spinner'); const output = document.getElementById('output'); const zplOutput = document.getElementById('zplOutput'); const errorEl = document.getElementById('error'); + let selectedFile = null; +let currentUnit = 'px'; // 'px' or 'cm' +let origPxW = 0, origPxH = 0; // image native pixel dimensions + +function getDPI() { return parseInt(document.getElementById('dpi').value, 10); } + +// cm ↔ px helpers +function pxToCm(px) { return +(px * 2.54 / getDPI()).toFixed(3); } +function cmToPx(cm) { return Math.round(cm * getDPI() / 2.54); } + +function setUnit(unit) { + const wEl = document.getElementById('width'); + const hEl = document.getElementById('height'); + const curW = parseFloat(wEl.value) || 0; + const curH = parseFloat(hEl.value) || 0; + + if (unit === 'cm' && currentUnit === 'px') { + wEl.step = '0.01'; + hEl.step = '0.01'; + wEl.value = curW > 0 ? pxToCm(curW) : 0; + hEl.value = curH > 0 ? pxToCm(curH) : 0; + } else if (unit === 'px' && currentUnit === 'cm') { + wEl.step = '1'; + hEl.step = '1'; + wEl.value = curW > 0 ? cmToPx(curW) : 0; + hEl.value = curH > 0 ? cmToPx(curH) : 0; + } + currentUnit = unit; + document.getElementById('btnPx').classList.toggle('active', unit === 'px'); + document.getElementById('btnCm').classList.toggle('active', unit === 'cm'); + updateLabels(); +} + +function updateLabels() { + const unit = currentUnit; + document.getElementById('labelW').textContent = 'Width (' + unit + ')' + (unit === 'px' ? ' — 0 = auto' : ''); + document.getElementById('labelH').textContent = 'Height (' + unit + ')' + (unit === 'px' ? ' — 0 = auto' : ''); + updateSubLabels(); +} + +function updateSubLabels() { + if (!origPxW) return; + if (currentUnit === 'px') { + document.getElementById('subW').textContent = pxToCm(origPxW) + ' cm at ' + getDPI() + ' dpi'; + document.getElementById('subH').textContent = pxToCm(origPxH) + ' cm at ' + getDPI() + ' dpi'; + } else { + document.getElementById('subW').textContent = origPxW + ' px native'; + document.getElementById('subH').textContent = origPxH + ' px native'; + } +} + +// Recalc sub-labels when DPI changes +document.getElementById('dpi').addEventListener('change', () => { + if (currentUnit === 'cm') { + // re-fill cm fields based on original px + if (origPxW) { + document.getElementById('width').value = pxToCm(origPxW); + document.getElementById('height').value = pxToCm(origPxH); + } + } + updateSubLabels(); +}); dropZone.addEventListener('click', () => fileInput.click()); dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('over'); }); @@ -388,10 +478,25 @@ function setFile(file) { fetch('/preview', { method: 'POST', body: fd }) .then(r => r.ok ? r.json() : Promise.reject()) .then(info => { - previewInfo.textContent = file.name + ' — ' + info.width + ' × ' + info.height + ' px'; - // Pre-fill dimensions - document.getElementById('width').value = info.width; - document.getElementById('height').value = info.height; + origPxW = info.width; + origPxH = info.height; + + const wEl = document.getElementById('width'); + const hEl = document.getElementById('height'); + if (currentUnit === 'px') { + wEl.value = origPxW; + hEl.value = origPxH; + } else { + wEl.value = pxToCm(origPxW); + hEl.value = pxToCm(origPxH); + } + + const cmW = pxToCm(origPxW), cmH = pxToCm(origPxH); + previewInfo.textContent = + file.name + ' — ' + origPxW + ' × ' + origPxH + ' px' + + ' (' + cmW + ' × ' + cmH + ' cm at ' + getDPI() + ' dpi)'; + + updateSubLabels(); convertBtn.disabled = false; }) .catch(() => { @@ -400,6 +505,16 @@ function setFile(file) { }); } +// Returns pixel values regardless of current unit +function getPixelDimensions() { + const w = parseFloat(document.getElementById('width').value) || 0; + const h = parseFloat(document.getElementById('height').value) || 0; + if (currentUnit === 'cm') { + return { w: cmToPx(w), h: cmToPx(h) }; + } + return { w: Math.round(w), h: Math.round(h) }; +} + convertBtn.addEventListener('click', async () => { if (!selectedFile) return; errorEl.style.display = 'none'; @@ -407,11 +522,12 @@ convertBtn.addEventListener('click', async () => { spinner.style.display = 'block'; output.style.display = 'none'; + const { w, h } = getPixelDimensions(); const edits = [...document.querySelectorAll('input[name=edit]:checked')].map(c => c.value); const fd = new FormData(); fd.append('file', selectedFile); - fd.append('width', document.getElementById('width').value); - fd.append('height', document.getElementById('height').value); + fd.append('width', w); + fd.append('height', h); fd.append('type', document.getElementById('enctype').value); fd.append('edit', edits.join(',')); @@ -446,6 +562,9 @@ document.getElementById('dlBtn').addEventListener('click', () => { a.download = (selectedFile ? selectedFile.name.replace(/\.[^.]+$/, '') : 'label') + '.zpl'; a.click(); }); + +// init labels +updateLabels();