Chestny Znak scanner: bundle offline ZXing decoder, ЧЗ badge, session auto-push
- Native BarcodeDetector needs Google Play Services (fails "Barcode detection service unavailable" on de-Googled ROMs). Replace it with a bundled, fully offline ZXing decoder (frontend/lib/zxing.js, @zxing/library UMD): scanner uses BrowserDatamatrixCodeReader (TRY_HARDER) for live camera and decodeFromImageUrl for the photo fallback. No Google, codes never leave the device. - Source badge "ЧЗ" (src-chzn) for products added via the scanner. - tools/chzn_export.sh: adb push the exported session to the device Downloads so BalDav's file picker can import it directly.
This commit is contained in:
parent
c3ec590da5
commit
3ca2bc00c0
4 changed files with 46 additions and 33 deletions
|
|
@ -1649,6 +1649,7 @@ select {
|
|||
.src-kdv { background: rgba(255, 167, 38, 0.25); color: #ffb74d; }
|
||||
.src-ion { background: rgba(158, 158, 158, 0.2); color: #9e9e9e; }
|
||||
.src-manual { background: rgba(255, 235, 59, 0.2); color: #fff176; }
|
||||
.src-chzn { background: rgba(66, 165, 245, 0.22); color: #64b5f6; }
|
||||
|
||||
.food-nutrients {
|
||||
font-size: 11px;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
<script src="lib/chart.min.js"></script>
|
||||
<script src="lib/chartjs-plugin-zoom.min.js"></script>
|
||||
<script src="lib/jszip.min.js"></script>
|
||||
<script src="lib/zxing.js"></script>
|
||||
<script src="js/db.js"></script>
|
||||
<script src="js/bmr.js"></script>
|
||||
<script src="js/tilecache.js"></script>
|
||||
|
|
|
|||
|
|
@ -1844,15 +1844,16 @@ function showAddProductModal(onSaved, prefill) {
|
|||
});
|
||||
}
|
||||
|
||||
// Честный знак scanner: camera (BarcodeDetector) with photo/manual fallbacks.
|
||||
// On a decoded DataMatrix -> ChZ.resolve -> prefill the new-product modal.
|
||||
// Честный знак scanner: camera + photo/manual, decoded by bundled ZXing
|
||||
// (frontend/lib/zxing.js) — fully offline, no Google Play Services (the native
|
||||
// BarcodeDetector needs GMS and is absent on de-Googled ROMs). On a decoded
|
||||
// DataMatrix -> ChZ.resolve -> prefill the new-product modal.
|
||||
function showChznScanModal(onSaved) {
|
||||
if (!ChZ.hasSession()) {
|
||||
appAlert("Сессия Честного знака не импортирована.\nНастройки → Честный знак → Импорт сессии (файл chzn_session.json из tools/chzn_export.sh).");
|
||||
return;
|
||||
}
|
||||
let hasBD = ("BarcodeDetector" in window);
|
||||
let detector = hasBD ? new BarcodeDetector({ formats: ["data_matrix", "qr_code"] }) : null;
|
||||
let hasZX = (typeof ZXing !== "undefined" && ZXing.BrowserDatamatrixCodeReader);
|
||||
|
||||
let ov = document.createElement("div");
|
||||
ov.className = "modal-overlay";
|
||||
|
|
@ -1874,16 +1875,28 @@ function showChznScanModal(onSaved) {
|
|||
</div>`;
|
||||
document.body.appendChild(ov);
|
||||
|
||||
let stream = null, scanning = false, busy = false;
|
||||
let controls = null, busy = false;
|
||||
let base = winStack.length;
|
||||
let statusEl = ov.querySelector("#cz-status");
|
||||
let video = ov.querySelector("#cz-video");
|
||||
|
||||
function cleanup() {
|
||||
scanning = false;
|
||||
if (stream) { stream.getTracks().forEach(t => t.stop()); stream = null; }
|
||||
let reader = null;
|
||||
if (hasZX) {
|
||||
let hints = new Map();
|
||||
hints.set(ZXing.DecodeHintType.TRY_HARDER, true); // better on curved/low-contrast codes
|
||||
reader = new ZXing.BrowserDatamatrixCodeReader(hints, { delayBetweenScanAttempts: 120 });
|
||||
}
|
||||
winPush("chzn-scan", () => { cleanup(); ov.remove(); });
|
||||
|
||||
function stopCamera() {
|
||||
try { if (controls) controls.stop(); } catch {}
|
||||
controls = null;
|
||||
try {
|
||||
let s = video && video.srcObject;
|
||||
if (s && s.getTracks) s.getTracks().forEach(t => t.stop());
|
||||
if (video) video.srcObject = null;
|
||||
} catch {}
|
||||
}
|
||||
winPush("chzn-scan", () => { stopCamera(); ov.remove(); });
|
||||
function close() {
|
||||
let count = winStack.length - base;
|
||||
for (let i = 0; i < count; i++) winStack.pop().close();
|
||||
|
|
@ -1893,38 +1906,31 @@ function showChznScanModal(onSaved) {
|
|||
|
||||
async function onDecoded(rawVal) {
|
||||
if (busy) return;
|
||||
busy = true; scanning = false;
|
||||
busy = true;
|
||||
try { if (controls) controls.stop(); } catch {}
|
||||
statusEl.textContent = "Код распознан, запрашиваю КБЖУ...";
|
||||
try {
|
||||
let prefill = await ChZ.resolve(rawVal);
|
||||
cleanup();
|
||||
stopCamera();
|
||||
close();
|
||||
showAddProductModal(onSaved, prefill);
|
||||
} catch (e) {
|
||||
busy = false;
|
||||
statusEl.textContent = (e.notFound ? "Код не найден в Честном знаке. " : "Ошибка: " + e.message + ". ") + "Наведите ещё раз.";
|
||||
if (detector && stream) { scanning = true; requestAnimationFrame(loop); }
|
||||
if (reader) startCamera(); // resume continuous scan
|
||||
}
|
||||
}
|
||||
|
||||
async function loop() {
|
||||
if (busy || !scanning) return;
|
||||
try {
|
||||
let codes = await detector.detect(video);
|
||||
if (codes && codes.length) { scanning = false; onDecoded(codes[0].rawValue); return; }
|
||||
} catch (e) { /* transient decode error, keep scanning */ }
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
async function startCamera() {
|
||||
if (!detector) { statusEl.textContent = "Сканер недоступен в этом WebView. Выберите фото кода или введите вручную."; return; }
|
||||
if (!reader) { statusEl.textContent = "Декодер ZXing не загрузился — пересобери приложение."; return; }
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: "environment" } } });
|
||||
video.srcObject = stream;
|
||||
await video.play();
|
||||
scanning = true;
|
||||
statusEl.textContent = "Наведите на код Честного знака...";
|
||||
loop();
|
||||
controls = await reader.decodeFromConstraints(
|
||||
{ video: { facingMode: { ideal: "environment" } } },
|
||||
video,
|
||||
(result) => { if (result && !busy) onDecoded(result.getText()); }
|
||||
// per-frame NotFoundException is normal — ignored
|
||||
);
|
||||
} catch (e) {
|
||||
statusEl.textContent = "Камера недоступна (" + (e.name || e.message) + "). Выберите фото или введите код.";
|
||||
}
|
||||
|
|
@ -1932,13 +1938,16 @@ function showChznScanModal(onSaved) {
|
|||
|
||||
ov.querySelector("#cz-photo").onclick = () => {
|
||||
pickFile("image/*", async (buf) => {
|
||||
if (!detector) { statusEl.textContent = "Сканер недоступен — введите код вручную."; return; }
|
||||
if (!reader) { statusEl.textContent = "Декодер ZXing не загрузился."; return; }
|
||||
let url = URL.createObjectURL(new Blob([buf]));
|
||||
try {
|
||||
let bmp = await createImageBitmap(new Blob([buf]));
|
||||
let codes = await detector.detect(bmp);
|
||||
if (codes && codes.length) onDecoded(codes[0].rawValue);
|
||||
else statusEl.textContent = "Код на фото не распознан. Попробуйте более резкое фото.";
|
||||
} catch (e) { statusEl.textContent = "Ошибка распознавания: " + e.message; }
|
||||
let result = await reader.decodeFromImageUrl(url);
|
||||
onDecoded(result.getText());
|
||||
} catch (e) {
|
||||
statusEl.textContent = (e && e.name === "NotFoundException")
|
||||
? "Код на фото не распознан. Попробуйте более резкое фото."
|
||||
: "Ошибка распознавания: " + (e.message || e);
|
||||
} finally { URL.revokeObjectURL(url); }
|
||||
}, true);
|
||||
};
|
||||
ov.querySelector("#cz-manual").onclick = async () => {
|
||||
|
|
@ -7434,6 +7443,7 @@ function sourceBadge(source) {
|
|||
"5ka": ["5ка", "src-5ka"],
|
||||
kdv: ["KDV", "src-kdv"],
|
||||
ion: ["ИОН", "src-ion"],
|
||||
chzn: ["ЧЗ", "src-chzn"],
|
||||
manual: ["Свой", "src-manual"],
|
||||
recipe_import: ["Импорт", "src-manual"],
|
||||
};
|
||||
|
|
|
|||
1
frontend/lib/zxing.js
Normal file
1
frontend/lib/zxing.js
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue