ЧЗ scanner: zoom + torch, looser ROI, CSP wasm-unsafe-eval (wasm verified on device)

- The tight guide box forced the phone closer than it can focus. Drop it: decode
  the whole visible center square (WYSIWYG under object-fit:cover) at up to 768px;
  zxing-wasm locates the code anywhere in it.
- Add a zoom slider and a torch toggle driven by MediaStreamTrack capabilities
  (getCapabilities/applyConstraints), plus continuous focus and a 1920x1080 request
  — the user enlarges a small code optically instead of moving too close.
- CSP: add "script-src ... 'wasm-unsafe-eval'". Tauri's generated hash-based
  script-src dropped unsafe-eval, so WebAssembly.instantiate was blocked
  ("violates CSP") — verified fixed on-device via CDP (zxReady ok, code_33 decodes).
This commit is contained in:
4aiman 2026-08-27 11:28:00 +03:00
commit 0e44dc07e2
2 changed files with 54 additions and 7 deletions

View file

@ -1864,7 +1864,12 @@ function showChznScanModal(onSaved) {
<h3>Скан «Честный знак»</h3>
<div style="position:relative;background:#000;border-radius:8px;overflow:hidden;aspect-ratio:1/1;max-height:60vh">
<video id="cz-video" playsinline muted style="width:100%;height:100%;object-fit:cover"></video>
<div style="position:absolute;inset:14%;border:2px solid rgba(255,255,255,.7);border-radius:8px;pointer-events:none"></div>
<div style="position:absolute;inset:4%;border:1px solid rgba(255,255,255,.35);border-radius:8px;pointer-events:none"></div>
<button id="cz-torch" class="btn-secondary" style="display:none;position:absolute;top:8px;right:8px;padding:6px 10px;font-size:16px;line-height:1">&#128161;</button>
</div>
<div id="cz-zoom-wrap" style="display:none;align-items:center;gap:10px;margin-top:10px">
<span style="font-size:16px">&#128269;</span>
<input id="cz-zoom" type="range" style="flex:1" />
</div>
<div id="cz-status" style="margin:10px 0;font-size:13px;color:var(--muted);min-height:18px">Запуск камеры...</div>
<div style="display:flex;gap:8px">
@ -1920,9 +1925,13 @@ function showChznScanModal(onSaved) {
function grabROI() {
let vw = video.videoWidth, vh = video.videoHeight;
if (!vw || !vh) return null;
let side = Math.min(vw, vh) * 0.72;
// The square container shows the video center-cropped (object-fit:cover), so
// 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 sx = (vw - side) / 2, sy = (vh - side) / 2;
let out = 540;
let out = Math.min(side, 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);
@ -1940,6 +1949,41 @@ function showChznScanModal(onSaved) {
if (scanning) setTimeout(tick, 100);
}
// Wire zoom slider / torch / continuous focus from the track capabilities.
// Zoom is the real answer to "the code is too small at focus distance" — the
// user enlarges the code optically instead of moving the phone dangerously close.
let torchOn = false;
function setupCameraControls() {
let track = stream && stream.getVideoTracks && stream.getVideoTracks()[0];
if (!track || !track.getCapabilities) return;
let caps = {};
try { caps = track.getCapabilities() || {}; } catch {}
try {
if (caps.focusMode && caps.focusMode.includes("continuous")) {
track.applyConstraints({ advanced: [{ focusMode: "continuous" }] }).catch(() => {});
}
} catch {}
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.oninput = () => { track.applyConstraints({ advanced: [{ zoom: parseFloat(zi.value) }] }).catch(() => {}); };
zw.style.display = "flex";
}
if (caps.torch) {
let tb = ov.querySelector("#cz-torch");
tb.style.display = "block";
tb.onclick = () => {
torchOn = !torchOn;
track.applyConstraints({ advanced: [{ torch: torchOn }] }).catch(() => {});
tb.style.background = torchOn ? "rgba(255,214,10,.35)" : "";
};
}
}
async function startCamera() {
try {
statusEl.textContent = "Загрузка декодера...";
@ -1949,11 +1993,14 @@ function showChznScanModal(onSaved) {
return;
}
try {
stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: "environment" } } });
stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: { ideal: "environment" }, width: { ideal: 1920 }, height: { ideal: 1080 } },
});
video.srcObject = stream;
await video.play();
setupCameraControls();
scanning = true;
statusEl.textContent = "Наведите на код Честного знака...";
statusEl.textContent = "Наведите на код (используйте зум, если код мелкий)…";
tick();
} catch (e) {
statusEl.textContent = "Камера недоступна (" + (e.name || e.message) + "). Выберите фото или введите код.";

View file

@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/nicegui/tauri-v2/refs/heads/main/tooling/cli/schema.json",
"productName": "BalDav",
"version": "0.1.423",
"version": "0.1.426",
"identifier": "dev.baldav.app",
"build": {
"frontendDist": "../frontend"
@ -9,7 +9,7 @@
"app": {
"withGlobalTauri": true,
"security": {
"csp": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src 'self' https://4aiman.top https://*.tile.openstreetmap.org https://*.tile.thunderforest.com https://api.open-meteo.com https://wttr.in http://192.168.1.1:88; img-src 'self' blob: data: https://*.tile.openstreetmap.org https://*.tile.thunderforest.com"
"csp": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-eval' 'unsafe-inline'; connect-src 'self' https://4aiman.top https://*.tile.openstreetmap.org https://*.tile.thunderforest.com https://api.open-meteo.com https://wttr.in http://192.168.1.1:88; img-src 'self' blob: data: https://*.tile.openstreetmap.org https://*.tile.thunderforest.com"
},
"windows": [
{