feat: add cm/px unit toggle with DPI selector for dimensions
This commit is contained in:
155
cmd/web/main.go
155
cmd/web/main.go
@@ -223,6 +223,7 @@ const indexHTML = `<!DOCTYPE html>
|
||||
.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 = `<!DOCTYPE html>
|
||||
}
|
||||
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 = `<!DOCTYPE html>
|
||||
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 = `<!DOCTYPE html>
|
||||
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 = `<!DOCTYPE html>
|
||||
<div class="preview-info" id="previewInfo"></div>
|
||||
</div>
|
||||
|
||||
<!-- Unit toggle + DPI -->
|
||||
<div class="unit-row">
|
||||
<span>Dimensions in</span>
|
||||
<div class="toggle-group">
|
||||
<button id="btnPx" class="active" onclick="setUnit('px')">px</button>
|
||||
<button id="btnCm" onclick="setUnit('cm')">cm</button>
|
||||
</div>
|
||||
<span style="margin-left:auto">Printer DPI</span>
|
||||
<select id="dpi" style="width:110px">
|
||||
<option value="203" selected>203 dpi</option>
|
||||
<option value="300">300 dpi</option>
|
||||
<option value="600">600 dpi</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div>
|
||||
<label for="width">Width (px) <span style="font-weight:400;color:#718096">0 = auto</span></label>
|
||||
<input type="number" id="width" min="0" value="0" placeholder="0">
|
||||
<label id="labelW">Width (px)</label>
|
||||
<input type="number" id="width" min="0" step="1" value="0" placeholder="0">
|
||||
<div class="sub-label" id="subW"></div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="height">Height (px) <span style="font-weight:400;color:#718096">0 = auto</span></label>
|
||||
<input type="number" id="height" min="0" value="0" placeholder="0">
|
||||
<label id="labelH">Height (px)</label>
|
||||
<input type="number" id="height" min="0" step="1" value="0" placeholder="0">
|
||||
<div class="sub-label" id="subH"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user