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:
@@ -1969,6 +1969,46 @@ body.server-offline .bottom-nav {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* — Scan status bar — */
|
||||
.scan-status-bar {
|
||||
position: absolute;
|
||||
bottom: 38px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
pointer-events: none;
|
||||
z-index: 12;
|
||||
}
|
||||
.scan-status-method {
|
||||
font-size: 0.58rem;
|
||||
color: rgba(255,255,255,0.45);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
font-family: monospace;
|
||||
}
|
||||
.scan-status-msg {
|
||||
font-size: 0.74rem;
|
||||
color: rgba(255,255,255,0.9);
|
||||
background: rgba(0,0,0,0.55);
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
max-width: 92%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: color 0.2s, background 0.2s;
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
.scan-status-msg:empty { visibility: hidden; }
|
||||
.scan-status-msg.state-partial { color: #fbbf24; }
|
||||
.scan-status-msg.state-invalid { color: #f87171; background: rgba(239,68,68,0.28); }
|
||||
.scan-status-msg.state-confirmed { color: #4ade80; background: rgba(74,222,128,0.22); }
|
||||
.scan-status-msg.state-retry { color: #fb923c; }
|
||||
|
||||
/* — Viewport overlay controls (torch / zoom / flip) — */
|
||||
.scan-viewport-controls {
|
||||
position: absolute;
|
||||
|
||||
+37
-3
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user