chore: auto-merge develop → main
Triggered by: bc39361 feat: barcode scan visual feedback + EAN checksum validation
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');
|
||||
}
|
||||
|
||||
@@ -251,6 +251,11 @@
|
||||
</div>
|
||||
<!-- Live partial code preview -->
|
||||
<div class="scan-live-code" id="scan-live-code" style="display:none"></div>
|
||||
<!-- Scan status bar -->
|
||||
<div class="scan-status-bar" id="scan-status-bar">
|
||||
<span id="scan-status-method" class="scan-status-method"></span>
|
||||
<span id="scan-status-msg" class="scan-status-msg" data-i18n="scan.status_ready"></span>
|
||||
</div>
|
||||
<!-- Success flash overlay -->
|
||||
<div class="scan-confirm-overlay" id="scan-confirm-overlay" style="display:none">
|
||||
<div class="scan-confirm-check">✓</div>
|
||||
|
||||
@@ -214,7 +214,13 @@
|
||||
"scan_barcode": "🔖 Barcode scannen",
|
||||
"create_named": "{name} erstellen",
|
||||
"new_without_barcode": "Neues Produkt ohne Barcode",
|
||||
"stock_in_pantry": "Bereits im Vorrat:"
|
||||
"stock_in_pantry": "Bereits im Vorrat:",
|
||||
"status_ready": "Kamera auf Barcode richten",
|
||||
"status_scanning": "Scanne...",
|
||||
"status_partial": "Erkannt: {code} — prüfe...",
|
||||
"status_invalid": "Ungültig: {code} — versuche erneut",
|
||||
"status_confirmed": "Bestätigt!",
|
||||
"status_parallel": "Kombinierter Scan aktiv..."
|
||||
},
|
||||
"action": {
|
||||
"title": "Was möchtest du tun?",
|
||||
|
||||
@@ -214,7 +214,13 @@
|
||||
"scan_barcode": "🔖 Scan Barcode",
|
||||
"create_named": "Create {name}",
|
||||
"new_without_barcode": "New product without barcode",
|
||||
"stock_in_pantry": "Already in pantry:"
|
||||
"stock_in_pantry": "Already in pantry:",
|
||||
"status_ready": "Point camera at barcode",
|
||||
"status_scanning": "Scanning...",
|
||||
"status_partial": "Detected: {code} — verifying...",
|
||||
"status_invalid": "Invalid: {code} — retrying",
|
||||
"status_confirmed": "Confirmed!",
|
||||
"status_parallel": "Using combined scan methods..."
|
||||
},
|
||||
"action": {
|
||||
"title": "What do you want to do?",
|
||||
|
||||
@@ -211,7 +211,13 @@
|
||||
"barcode_acquired": "🔖 Código de barras escaneado: {code}",
|
||||
"scan_barcode": "🔖 Escanear código de barras",
|
||||
"create_named": "Crear {name}",
|
||||
"new_without_barcode": "Nuevo producto sin código de barras"
|
||||
"new_without_barcode": "Nuevo producto sin código de barras",
|
||||
"status_ready": "Apunta la cámara al código de barras",
|
||||
"status_scanning": "Escaneando...",
|
||||
"status_partial": "Detectado: {code} — verificando...",
|
||||
"status_invalid": "Inválido: {code} — reintentando",
|
||||
"status_confirmed": "Confirmado!",
|
||||
"status_parallel": "Escaneo combinado activo..."
|
||||
},
|
||||
"action": {
|
||||
"title": "¿Qué quieres hacer?",
|
||||
|
||||
@@ -211,7 +211,13 @@
|
||||
"barcode_acquired": "🔖 Code-barres scanné : {code}",
|
||||
"scan_barcode": "🔖 Scanner le code-barres",
|
||||
"create_named": "Créer {name}",
|
||||
"new_without_barcode": "Nouveau produit sans code-barres"
|
||||
"new_without_barcode": "Nouveau produit sans code-barres",
|
||||
"status_ready": "Pointez la caméra sur le code-barres",
|
||||
"status_scanning": "Scan en cours...",
|
||||
"status_partial": "Lu : {code} — vérification...",
|
||||
"status_invalid": "Invalide : {code} — nouvel essai",
|
||||
"status_confirmed": "Confirmé !",
|
||||
"status_parallel": "Scan combiné actif..."
|
||||
},
|
||||
"action": {
|
||||
"title": "Que voulez-vous faire ?",
|
||||
|
||||
@@ -214,7 +214,13 @@
|
||||
"scan_barcode": "🔖 Scansiona Barcode",
|
||||
"create_named": "Crea {name}",
|
||||
"new_without_barcode": "Nuovo prodotto senza barcode",
|
||||
"stock_in_pantry": "Hai gia in dispensa:"
|
||||
"stock_in_pantry": "Hai gia in dispensa:",
|
||||
"status_ready": "Inquadra il codice a barre",
|
||||
"status_scanning": "Scansione in corso...",
|
||||
"status_partial": "Letto: {code} — verifico...",
|
||||
"status_invalid": "Non valido: {code} — riprovo",
|
||||
"status_confirmed": "Confermato!",
|
||||
"status_parallel": "Doppia scansione attiva..."
|
||||
},
|
||||
"action": {
|
||||
"title": "Cosa vuoi fare?",
|
||||
|
||||
Reference in New Issue
Block a user