diff --git a/assets/css/style.css b/assets/css/style.css index 592510b..ad16b02 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -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; diff --git a/assets/js/app.js b/assets/js/app.js index 16dffd2..83c2947 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -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'); } diff --git a/index.html b/index.html index e6eca8a..74cb570 100644 --- a/index.html +++ b/index.html @@ -251,6 +251,11 @@
+ +