feat: barcode scan visual feedback + EAN checksum validation

- Add scan-status-bar overlay inside scanner viewport showing:
  - Active scan method (Native API / Quagga / Native + Quagga)
  - Scanning state: ready, scanning, partial read, invalid, confirmed
- Validate EAN-8/EAN-13/UPC checksums in Quagga path before confirming
  (native BarcodeDetector validates internally; Quagga can return false positives)
- Show 'invalid barcode, retrying' message with invalid code highlighted
- Reset invalid barcode confidence counter on invalid read so scanner retries
- Spawn parallel Quagga scan with 'combined scan active' status message
- Add 6 translation keys (scan.status_*) in all 5 language files
This commit is contained in:
dadaloop82
2026-05-27 05:26:47 +00:00
parent b83db76a8d
commit bc39361246
8 changed files with 117 additions and 8 deletions
+37 -3
View File
@@ -6420,6 +6420,9 @@ async function initScanner() {
// Apply fixed 2x zoom
await _applyFixedZoom();
_invalidBarcodeCount = 0;
_setScanStatus(t('scan.status_ready'), '', '');
if (_useBarcodeDetector) {
startNativeScanner(video);
} else {
@@ -6460,6 +6463,19 @@ function validateEANChecksum(code) {
return check === last;
}
// ===== SCAN STATUS BAR =====
let _invalidBarcodeCount = 0;
function _setScanStatus(msg, state, method) {
const msgEl = document.getElementById('scan-status-msg');
const methodEl = document.getElementById('scan-status-method');
if (msgEl) {
msgEl.textContent = msg || '';
msgEl.className = 'scan-status-msg' + (state ? ' state-' + state : '');
}
if (methodEl && method !== undefined) methodEl.textContent = method || '';
}
// ===== NATIVE BarcodeDetector SCANNER =====
async function startNativeScanner(videoEl) {
if (quaggaRunning) return;
@@ -6491,14 +6507,18 @@ async function startNativeScanner(videoEl) {
if (!scanning || !scannerStream) return;
frameCount++;
if (frameCount === 1) updateFeedback('scanning');
if (frameCount === 1) {
updateFeedback('scanning');
_setScanStatus(t('scan.status_scanning'), '', 'Native API');
}
// After 2s without detection, also start Quagga in parallel as backup
if (!quaggaParallelStarted && (Date.now() - startTime) > 2000) {
quaggaParallelStarted = true;
scanLog('Native: 2s elapsed, spawning Quagga in parallel');
_setScanStatus(t('scan.status_parallel'), 'retry', 'Native + Quagga');
quaggaRunning = false; // temporarily release so Quagga can start
startQuaggaScanner(videoEl);
startQuaggaScanner(videoEl, false);
quaggaRunning = true; // re-take ownership (Quagga will share)
}
@@ -6512,6 +6532,7 @@ async function startNativeScanner(videoEl) {
scanLog(`Native detect #${partialCount} [f${frameCount}]: ${code} (${format})`);
updateFeedback('detecting');
_showScanLiveCode(code);
_setScanStatus(t('scan.status_partial').replace('{code}', code), 'partial');
if (!detectionHistory[code]) detectionHistory[code] = { count: 0 };
detectionHistory[code].count++;
@@ -6531,6 +6552,7 @@ async function startNativeScanner(videoEl) {
quaggaRunning = false;
updateFeedback(null);
scanLog(`CONFIRMED: ${code} after ${frameCount} frames (${format})`);
_setScanStatus(t('scan.status_confirmed'), 'confirmed');
onBarcodeDetected(code);
return;
}
@@ -6553,7 +6575,7 @@ async function startNativeScanner(videoEl) {
}
// ===== QUAGGA FALLBACK SCANNER =====
function startQuaggaScanner(videoEl) {
function startQuaggaScanner(videoEl, isPrimary = true) {
if (quaggaRunning) return;
const canvas = document.getElementById('scanner-canvas');
@@ -6619,6 +6641,7 @@ function startQuaggaScanner(videoEl) {
if (frameCount === 1) {
scanLog(`Frame #1 — video: ${videoEl.videoWidth}x${videoEl.videoHeight}`);
updateScannerFeedback('scanning');
if (isPrimary) _setScanStatus(t('scan.status_scanning'), '', 'Quagga');
}
let callbackCalled = false;
@@ -6675,15 +6698,26 @@ function startQuaggaScanner(videoEl) {
// EAN/UPC: confirm on first hit (checksum validated)
const highConf = ['ean_reader','ean_8_reader','upc_reader','upc_e_reader'].includes(format);
if (highConf || detectCount >= 2 || dominated.count >= 2) {
// Validate EAN/UPC checksum — Quagga can occasionally return false positives
if (highConf && !validateEANChecksum(code)) {
_invalidBarcodeCount++;
scanLog(`Invalid EAN checksum: ${code} (retry #${_invalidBarcodeCount})`);
_setScanStatus(t('scan.status_invalid').replace('{code}', code), 'invalid', 'Quagga');
lastDetected = ''; detectCount = 0; // reset confidence
if (scanning) setTimeout(scanFrame, 60);
return;
}
scanning = false;
quaggaRunning = false;
updateScannerFeedback(null);
scanLog(`CONFIRMED: ${code} [${passName2}] f${frameCount} consec:${detectCount} total:${dominated.count}`);
_hideScanLiveCode();
_setScanStatus(t('scan.status_confirmed'), 'confirmed');
onBarcodeDetected(code);
return;
}
_showScanLiveCode(code);
_setScanStatus(t('scan.status_partial').replace('{code}', code), 'partial');
} else {
updateScannerFeedback('scanning');
}