ЧЗ scanner: digital-zoom fallback + fix back-stack desync on scan->add

- Zoom slider: many Android WebViews don't report a `zoom` track capability
  (torch works, zoom doesn't), so the slider never showed. Add a digital-zoom
  fallback (always-visible slider 1-5x): CSS-scale the preview + crop the decode
  ROI tighter to center. Hardware zoom still used when available.
- Back gesture did nothing after scanning: onDecoded closed the scan modal with
  the async history.go(-1) and then synchronously opened the add-product modal,
  so history.go unwound the NEW modal's entry and desynced the winStack/history.
  Add closeThen(): await the close's popstate(s) before opening the next modal.
This commit is contained in:
4aiman 2026-08-27 18:08:25 +03:00
commit bda0cdcf21

View file

@ -1880,7 +1880,7 @@ function showChznScanModal(onSaved) {
</div>`;
document.body.appendChild(ov);
let stream = null, scanning = false, busy = false;
let stream = null, scanning = false, busy = false, zoomFactor = 1;
let base = winStack.length;
let statusEl = ov.querySelector("#cz-status");
let video = ov.querySelector("#cz-video");
@ -1902,6 +1902,22 @@ function showChznScanModal(onSaved) {
_skipPops = count;
if (count > 0) history.go(-count);
}
// Close this modal AND wait for its history.go popstate(s) to flush before the
// caller opens the next modal. Opening it synchronously after close() races the
// async history.go (it would unwind the NEW modal's entry) and desyncs the back
// stack — that was the "swipe-back does nothing" bug on scan -> add-product.
function closeThen() {
let count = winStack.length - base;
for (let i = 0; i < count; i++) winStack.pop().close();
if (count <= 0) return Promise.resolve();
_skipPops = count;
return new Promise(res => {
let remaining = count;
let h = () => { if (--remaining <= 0) { window.removeEventListener("popstate", h); res(); } };
window.addEventListener("popstate", h);
history.go(-count);
});
}
async function onDecoded(rawVal) {
if (busy) return;
@ -1910,7 +1926,7 @@ function showChznScanModal(onSaved) {
try {
let prefill = await ChZ.resolve(rawVal);
stopCamera();
close();
await closeThen();
showAddProductModal(onSaved, prefill);
} catch (e) {
busy = false;
@ -1929,9 +1945,9 @@ function showChznScanModal(onSaved) {
// sample exactly that center square (WYSIWYG). zxing-wasm locates the code
// anywhere within it — no need for the user to fill a tiny box; use zoom to
// enlarge the code at a focusable distance instead.
let side = Math.min(vw, vh);
let side = Math.min(vw, vh) / zoomFactor; // digital zoom crops tighter to center
let sx = (vw - side) / 2, sy = (vh - side) / 2;
let out = Math.min(side, 768); // enough detail, still fast
let out = Math.min(Math.min(vw, vh), 768); // enough detail, still fast
canvas.width = out; canvas.height = out;
cctx.drawImage(video, sx, sy, side, side, 0, 0, out, out);
return cctx.getImageData(0, 0, out, out);
@ -1963,16 +1979,25 @@ function showChznScanModal(onSaved) {
track.applyConstraints({ advanced: [{ focusMode: "continuous" }] }).catch(() => {});
}
} catch {}
// Zoom slider: prefer hardware zoom; otherwise fall back to digital zoom
// (CSS-scale the preview + crop the decode ROI). Always shown so the user can
// enlarge a small code instead of moving the phone inside its focus range.
let zw = ov.querySelector("#cz-zoom-wrap");
let zi = ov.querySelector("#cz-zoom");
if (caps.zoom && caps.zoom.max > caps.zoom.min) {
let zw = ov.querySelector("#cz-zoom-wrap");
let zi = ov.querySelector("#cz-zoom");
zi.min = caps.zoom.min; zi.max = caps.zoom.max;
zi.step = caps.zoom.step || (caps.zoom.max - caps.zoom.min) / 100 || 0.1;
let cur = ((track.getSettings && track.getSettings()) || {}).zoom;
zi.value = cur || caps.zoom.min;
zi.value = ((track.getSettings && track.getSettings()) || {}).zoom || caps.zoom.min;
zi.oninput = () => { track.applyConstraints({ advanced: [{ zoom: parseFloat(zi.value) }] }).catch(() => {}); };
zw.style.display = "flex";
} else {
zi.min = 1; zi.max = 5; zi.step = 0.1; zi.value = 1;
zi.oninput = () => {
zoomFactor = parseFloat(zi.value) || 1;
video.style.transformOrigin = "center center";
video.style.transform = "scale(" + zoomFactor + ")";
};
}
zw.style.display = "flex";
if (caps.torch) {
let tb = ov.querySelector("#cz-torch");
tb.style.display = "block";