diff --git a/api/index.php b/api/index.php index 707f9f1..2ce3e45 100644 --- a/api/index.php +++ b/api/index.php @@ -118,7 +118,7 @@ function checkRateLimit(string $action): void { } // Determine limit based on action - $aiActions = ['gemini_readExpiry', 'gemini_chat', 'gemini_identify', 'gemini_suggest_shopping', 'chat_to_recipe', 'recipe_from_ingredient']; + $aiActions = ['gemini_readExpiry', 'gemini_chat', 'gemini_identify', 'gemini_suggest_shopping', 'chat_to_recipe', 'recipe_from_ingredient', 'gemini_number_ocr']; $loginActions = []; $recipeActions = ['generate_recipe', 'generate_recipe_stream']; $errorActions = ['report_error', 'check_update']; @@ -454,6 +454,10 @@ try { geminiAnomalyExplain(); break; + case 'gemini_number_ocr': + geminiNumberOCR(); + break; + case 'get_shopping_price': getShoppingPrice($db); break; @@ -7287,6 +7291,44 @@ function geminiShoppingEnrich(PDO $db): void { echo json_encode(['success' => true, 'items' => $enriched, 'source' => 'gemini']); } +// ============================================================================= +// ===== GEMINI AI: NUMBER OCR (read barcode digits from image) ================ +// ============================================================================= +/** + * POST /api/?action=gemini_number_ocr + * Body: { image: base64-jpeg } + * Returns: { success, barcode } or { success: false, error } + * Uses Gemini vision to read the barcode number printed on a product label. + */ +function geminiNumberOCR(): void { + $apiKey = env('GEMINI_API_KEY'); + if (empty($apiKey)) { echo json_encode(['success' => false, 'error' => 'no_api_key']); return; } + + $input = json_decode(file_get_contents('php://input'), true); + $imageBase64 = $input['image'] ?? ''; + if (!$imageBase64) { echo json_encode(['success' => false, 'error' => 'no_image']); return; } + + $payload = [ + 'contents' => [[ + 'parts' => [ + ['text' => 'Look at this product image. Find the barcode number (EAN-13 or EAN-8) printed on the label — it is usually a sequence of 8 or 13 digits printed below or near the barcode stripes. Return ONLY the digit sequence, nothing else. If you cannot find a valid barcode number, return exactly: none'], + ['inline_data' => ['mime_type' => 'image/jpeg', 'data' => $imageBase64]] + ] + ]], + 'generationConfig' => ['temperature' => 0, 'maxOutputTokens' => 20, 'thinkingConfig' => ['thinkingBudget' => 0]] + ]; + + $result = callGeminiWithFallback($apiKey, $payload, 10); + $text = trim($result['text'] ?? ''); + $digits = preg_replace('/\D/', '', $text); + + if (strlen($digits) === 13 || strlen($digits) === 8) { + echo json_encode(['success' => true, 'barcode' => $digits]); + } else { + echo json_encode(['success' => false, 'error' => 'not_found']); + } +} + // ============================================================================= // ===== GEMINI AI: ANOMALY EXPLANATION ======================================= // ============================================================================= diff --git a/assets/css/style.css b/assets/css/style.css index efe870c..de9918a 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -1596,39 +1596,35 @@ body.server-offline .bottom-nav { .scan-container { display: flex; flex-direction: column; - gap: 16px; + gap: 12px; } +/* — Spesa chip nel page-header — */ +.scan-spesa-chip { + margin-left: auto; + padding: 5px 12px; + border-radius: 20px; + border: 1.5px solid var(--accent); + background: rgba(124,58,237,0.08); + color: var(--accent); + font-size: 0.82rem; + font-weight: 700; + cursor: pointer; + white-space: nowrap; + transition: background 0.15s; +} +.scan-spesa-chip:active { background: rgba(124,58,237,0.18); } + +/* — Viewport 16/9 — */ .scanner-viewport { position: relative; width: 100%; - aspect-ratio: 4/3; + aspect-ratio: 16/9; background: #000; border-radius: var(--radius); overflow: hidden; } -.scan-zoom-btn { - position: absolute; - top: 10px; - right: 10px; - z-index: 20; - background: rgba(0,0,0,0.55); - color: #fff; - border: 1.5px solid rgba(255,255,255,0.5); - border-radius: 20px; - padding: 5px 13px; - font-size: 0.85rem; - font-weight: 700; - cursor: pointer; - backdrop-filter: blur(4px); - -webkit-backdrop-filter: blur(4px); - transition: background 0.15s; -} -.scan-zoom-btn:active { - background: rgba(255,255,255,0.25); -} - .scanner-viewport video { width: 100%; height: 100%; @@ -1637,6 +1633,26 @@ body.server-offline .bottom-nav { transition: transform 0.2s ease; } +/* — Guide frame corners — */ +.scan-guide-frame { + position: absolute; + inset: 0; + z-index: 11; + pointer-events: none; +} +.sgf-corner { + position: absolute; + width: 22px; + height: 22px; + border-color: rgba(255,255,255,0.85); + border-style: solid; +} +.sgf-tl { top: 12px; left: 12px; border-width: 3px 0 0 3px; border-radius: 3px 0 0 0; } +.sgf-tr { top: 12px; right: 12px; border-width: 3px 3px 0 0; border-radius: 0 3px 0 0; } +.sgf-bl { bottom: 36px; left: 12px; border-width: 0 0 3px 3px; border-radius: 0 0 0 3px; } +.sgf-br { bottom: 36px; right: 12px; border-width: 0 3px 3px 0; border-radius: 0 0 3px 0; } + +/* — Scan line — */ .scanner-overlay { position: absolute; top: 50%; @@ -1647,7 +1663,6 @@ body.server-offline .bottom-nav { z-index: 10; pointer-events: none; } - .scanner-line { width: 100%; height: 3px; @@ -1656,37 +1671,127 @@ body.server-offline .bottom-nav { animation: scanLine 2s ease-in-out infinite; transition: background 0.2s, box-shadow 0.2s, height 0.2s; } - -/* While Quagga is actively scanning frames */ .scanner-line.scanning { background: #00c853; box-shadow: 0 0 12px #00c853; animation: scanLineActive 0.8s ease-in-out infinite; } - -/* Barcode partially detected — strong pulse */ .scanner-line.detecting { background: #ffd600; box-shadow: 0 0 20px #ffd600, 0 0 40px rgba(255,214,0,0.4); height: 5px; animation: scanLineDetect 0.3s ease-in-out infinite; } - @keyframes scanLine { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } } - @keyframes scanLineActive { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } - @keyframes scanLineDetect { 0%, 100% { opacity: 1; transform: scaleY(1); } 50% { opacity: 0.7; transform: scaleY(2); } } +/* — Live partial code — */ +.scan-live-code { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, calc(-50% - 14px)); + z-index: 12; + background: rgba(0,0,0,0.65); + color: #ffd600; + font-family: monospace; + font-size: 1.1rem; + font-weight: 700; + letter-spacing: 2px; + padding: 4px 12px; + border-radius: 8px; + pointer-events: none; + white-space: nowrap; +} + +/* — Success confirm overlay — */ +.scan-confirm-overlay { + position: absolute; + inset: 0; + z-index: 20; + background: rgba(0,180,80,0.82); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6px; + border-radius: var(--radius); + animation: scanConfirmFade 0.12s ease-in; +} +@keyframes scanConfirmFade { + from { opacity: 0; transform: scale(0.94); } + to { opacity: 1; transform: scale(1); } +} +.scan-confirm-check { + font-size: 2.8rem; + color: #fff; + line-height: 1; +} +.scan-confirm-name { + font-size: 0.92rem; + font-weight: 700; + color: #fff; + text-align: center; + padding: 0 16px; + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* — Viewport overlay controls (torch / zoom / flip) — */ +.scan-viewport-controls { + position: absolute; + bottom: 0; + left: 0; + right: 0; + z-index: 15; + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 10px; + background: linear-gradient(transparent, rgba(0,0,0,0.55)); +} +.scan-ctrl-btn { + background: rgba(255,255,255,0.15); + border: 1.5px solid rgba(255,255,255,0.45); + border-radius: 50%; + width: 38px; + height: 38px; + font-size: 1.1rem; + color: #fff; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.15s; + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); +} +.scan-ctrl-btn:active { background: rgba(255,255,255,0.35); } +.scan-ctrl-btn.torch-on { background: rgba(255,220,0,0.35); border-color: #ffd600; } +.scan-zoom-badge { + background: rgba(0,0,0,0.55); + color: #fff; + font-size: 0.85rem; + font-weight: 800; + padding: 4px 10px; + border-radius: 14px; + border: 1.5px solid rgba(255,255,255,0.4); + letter-spacing: 0.5px; +} + +/* — Scan result errors — */ .scan-result { background: var(--bg-card); border-radius: var(--radius); @@ -1694,64 +1799,126 @@ body.server-offline .bottom-nav { box-shadow: var(--shadow); } -.scan-actions { +/* — Recent scans — */ +.scan-recents { display: flex; - flex-direction: column; - gap: 10px; -} - -.barcode-manual-entry { - margin-bottom: 12px; + align-items: center; + gap: 8px; + overflow: hidden; +} +.scan-recents-label { + font-size: 0.75rem; + font-weight: 700; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; + white-space: nowrap; + flex-shrink: 0; +} +.scan-recents-chips { + display: flex; + gap: 6px; + overflow-x: auto; + scrollbar-width: none; + -webkit-overflow-scrolling: touch; + flex: 1; +} +.scan-recents-chips::-webkit-scrollbar { display: none; } +.scan-recent-chip { + display: flex; + align-items: center; + gap: 5px; + padding: 5px 10px; + background: var(--bg-card); + border-radius: 20px; + border: 1px solid var(--border); + font-size: 0.78rem; + font-weight: 600; + white-space: nowrap; + cursor: pointer; + flex-shrink: 0; + box-shadow: var(--shadow); + transition: background 0.12s, transform 0.1s; +} +.scan-recent-chip:active { background: var(--bg-main); transform: scale(0.96); } +.scan-recent-chip-icon { font-size: 1rem; } + +/* — Input panel (bottom card) — */ +.scan-input-panel { + background: var(--bg-card); + border-radius: var(--radius); + box-shadow: var(--shadow); + overflow: hidden; +} +.scan-input-tabs { + display: flex; + border-bottom: 1px solid var(--border); +} +.scan-input-tab { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 5px; + padding: 10px 6px; + border: none; + background: transparent; + font-size: 0.82rem; + font-weight: 600; + color: var(--text-muted); + cursor: pointer; + transition: color 0.15s, background 0.15s; + border-bottom: 2px solid transparent; + margin-bottom: -1px; +} +.scan-input-tab.active { + color: var(--accent); + border-bottom-color: var(--accent); + background: rgba(124,58,237,0.05); +} +.scan-tab-content { + padding: 12px; } +/* — Barcode input row — */ .barcode-input-row { display: flex; gap: 8px; align-items: center; } - .barcode-input-row .form-input { flex: 1; margin: 0; font-size: 1.1rem; letter-spacing: 1px; } - .barcode-input-row .btn { white-space: nowrap; flex-shrink: 0; } -.quick-name-entry { - margin-bottom: 12px; +/* — AI tab buttons — */ +.scan-ai-tab-btns { + display: flex; + flex-direction: column; + gap: 8px; } +.scan-ai-tab-btns .btn { width: 100%; } -.quick-name-divider { - text-align: center; - margin: 10px 0 8px; - position: relative; -} - -.quick-name-divider::before { - content: ''; - position: absolute; - top: 50%; - left: 0; - right: 0; - height: 1px; - background: var(--border); -} - -.quick-name-divider span { - background: var(--bg-main); - padding: 0 12px; - position: relative; - font-size: 0.85rem; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.5px; +/* — AI number OCR fallback button — */ +.scan-num-ocr-btn { + width: 100%; + margin-top: 8px; + background: rgba(124,58,237,0.08); + border: 1.5px dashed var(--accent); + color: var(--accent); + font-size: 0.88rem; + padding: 9px; + border-radius: var(--radius); } +.scan-num-ocr-btn:active { background: rgba(124,58,237,0.18); } +/* — Quick name results (dropdown inside name tab) — */ .quick-name-results { margin-top: 8px; display: flex; @@ -1760,56 +1927,31 @@ body.server-offline .bottom-nav { max-height: 200px; overflow-y: auto; } - .quick-name-result-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; - background: var(--bg-card); - border-radius: var(--radius); - box-shadow: var(--shadow); + background: var(--bg-main); + border-radius: calc(var(--radius) - 2px); + border: 1px solid var(--border); cursor: pointer; transition: transform 0.1s; } - -.quick-name-result-item:active { - transform: scale(0.98); -} - -.quick-name-result-item .qnr-icon { - font-size: 1.5rem; - flex-shrink: 0; -} - -.quick-name-result-item .qnr-info { - flex: 1; - min-width: 0; -} - +.quick-name-result-item:active { transform: scale(0.98); } +.quick-name-result-item .qnr-icon { font-size: 1.4rem; flex-shrink: 0; } +.quick-name-result-item .qnr-info { flex: 1; min-width: 0; } .quick-name-result-item .qnr-name { font-weight: 600; - font-size: 0.95rem; + font-size: 0.92rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - -.quick-name-result-item .qnr-detail { - font-size: 0.8rem; - color: var(--text-muted); -} - +.quick-name-result-item .qnr-detail { font-size: 0.78rem; color: var(--text-muted); } .quick-name-result-item.qnr-new { border: 1px dashed var(--accent); - background: rgba(124, 58, 237, 0.06); -} - -.scan-hint { - text-align: center; - font-size: 0.85rem; - color: var(--text-muted); - margin-top: 4px; + background: rgba(124,58,237,0.05); } /* ===== SHOPPING LIST (BRING!) ===== */ diff --git a/assets/js/app.js b/assets/js/app.js index c08face..166280e 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1774,28 +1774,176 @@ let currentLocation = ''; let scannerStream = null; let quaggaRunning = false; let aiStream = null; -let _scanZoomLevel = 1; // 1 or 2 +let _scanZoomLevel = 2; // always 2x +let _torchActive = false; -async function toggleScanZoom() { - _scanZoomLevel = _scanZoomLevel === 1 ? 2 : 1; - const btn = document.getElementById('scan-zoom-btn'); - if (btn) btn.textContent = `x${_scanZoomLevel}`; - if (scannerStream) { - const track = scannerStream.getVideoTracks()[0]; - if (track) { - const caps = track.getCapabilities ? track.getCapabilities() : {}; - if (caps.zoom) { - // Hardware zoom (Android Chrome) - const z = _scanZoomLevel === 2 - ? Math.min(caps.zoom.max, caps.zoom.min * 2 || 2) - : caps.zoom.min; - try { await track.applyConstraints({ advanced: [{ zoom: z }] }); } catch(e) {} - } else { - // Software zoom via CSS scale on the video element - const video = document.getElementById('scanner-video'); - if (video) video.style.transform = _scanZoomLevel === 2 ? 'scale(2)' : 'scale(1)'; +// Apply fixed 2x zoom (hardware if available, CSS fallback) +async function _applyFixedZoom() { + if (!scannerStream) return; + const track = scannerStream.getVideoTracks()[0]; + if (!track) return; + const caps = track.getCapabilities ? track.getCapabilities() : {}; + if (caps.zoom && caps.zoom.max >= 2) { + const z = Math.min(caps.zoom.max, caps.zoom.min * 2); + try { await track.applyConstraints({ advanced: [{ zoom: z }] }); scanLog(`HW zoom: ${z}`); } catch(e) {} + } else { + const video = document.getElementById('scanner-video'); + if (video) video.style.transform = 'scale(2)'; + scanLog('SW zoom: scale(2)'); + } +} + +async function toggleTorch() { + if (!scannerStream) return; + const track = scannerStream.getVideoTracks()[0]; + if (!track) return; + const caps = track.getCapabilities ? track.getCapabilities() : {}; + if (!caps.torch) { showToast(t('scan.torch_unavailable'), 'info'); return; } + _torchActive = !_torchActive; + try { + await track.applyConstraints({ advanced: [{ torch: _torchActive }] }); + const btn = document.getElementById('scan-torch-btn'); + if (btn) btn.classList.toggle('torch-on', _torchActive); + showToast(_torchActive ? t('scan.torch_on') : t('scan.torch_off'), 'info'); + } catch(e) { showToast(t('scan.torch_unavailable'), 'info'); _torchActive = false; } +} + +async function flipCamera() { + const s = getSettings(); + const current = s.camera_facing || 'environment'; + const next = current === 'environment' ? 'user' : 'environment'; + s.camera_facing = next; + try { localStorage.setItem('evershelf_settings', JSON.stringify(s)); } catch(_) {} + showToast(next === 'user' ? t('scan.flip_front') : t('scan.flip_back'), 'info'); + stopScanner(); + setTimeout(() => initScanner(), 150); +} + +// ===== SCAN TAB SWITCHING ===== +function switchScanTab(tab) { + ['barcode','name','ai'].forEach(id => { + const btn = document.getElementById(`scan-tab-${id}`); + const content = document.getElementById(`scan-tabcontent-${id}`); + const active = id === tab; + if (btn) btn.classList.toggle('active', active); + if (content) content.style.display = active ? '' : 'none'; + }); + // Focus input on tab switch + if (tab === 'barcode') { + const el = document.getElementById('manual-barcode-input'); + if (el) setTimeout(() => el.focus(), 80); + } else if (tab === 'name') { + const el = document.getElementById('quick-product-name'); + if (el) setTimeout(() => el.focus(), 80); + } +} + +// ===== SCAN RECENTS (localStorage) ===== +const _SCAN_RECENTS_KEY = 'evershelf_scan_recents'; +const _SCAN_RECENTS_MAX = 6; + +function _getScanRecents() { + try { return JSON.parse(localStorage.getItem(_SCAN_RECENTS_KEY) || '[]'); } catch(_) { return []; } +} + +function addToScanRecents(product) { + if (!product || !product.id) return; + let list = _getScanRecents().filter(r => r.id !== product.id); + list.unshift({ id: product.id, name: product.name, brand: product.brand || '', category: product.category || '' }); + if (list.length > _SCAN_RECENTS_MAX) list = list.slice(0, _SCAN_RECENTS_MAX); + try { localStorage.setItem(_SCAN_RECENTS_KEY, JSON.stringify(list)); } catch(_) {} +} + +function updateScanRecents() { + const list = _getScanRecents(); + const wrap = document.getElementById('scan-recents'); + const chips = document.getElementById('scan-recents-chips'); + if (!wrap || !chips) return; + if (list.length === 0) { wrap.style.display = 'none'; return; } + wrap.style.display = 'flex'; + chips.innerHTML = list.map(r => { + const icon = CATEGORY_ICONS[mapToLocalCategory(r.category, r.name)] || '📦'; + const label = escapeHtml(r.name) + (r.brand ? ` ${escapeHtml(r.brand)}` : ''); + return ``; + }).join(''); +} + +async function _selectRecentProduct(productId) { + showLoading(true); + try { + const data = await api('product_get', { id: productId }); + if (data.product) { + currentProduct = data.product; + if (!currentProduct.weight_info && currentProduct.notes) { + const m = currentProduct.notes.match(/Peso:\s*([^·]+)/); + if (m) currentProduct.weight_info = m[1].trim(); } + showLoading(false); + stopScanner(); + showProductAction(); + } else { + showLoading(false); + showToast(t('error.not_found'), 'error'); } + } catch(e) { + showLoading(false); + showToast(t('error.connection'), 'error'); + } +} + +// ===== SCAN LIVE CODE / CONFIRM OVERLAY ===== +let _liveCodeTimer = null; +function _showScanLiveCode(code) { + const el = document.getElementById('scan-live-code'); + if (!el) return; + el.textContent = code; + el.style.display = 'block'; + clearTimeout(_liveCodeTimer); + _liveCodeTimer = setTimeout(() => { if (el) el.style.display = 'none'; }, 1500); +} +function _hideScanLiveCode() { + const el = document.getElementById('scan-live-code'); + if (el) { el.style.display = 'none'; clearTimeout(_liveCodeTimer); } +} + +function _showScanConfirm(name) { + const overlay = document.getElementById('scan-confirm-overlay'); + const nameEl = document.getElementById('scan-confirm-name'); + if (!overlay) return; + if (nameEl) nameEl.textContent = name || ''; + overlay.style.display = 'flex'; + setTimeout(() => { if (overlay) overlay.style.display = 'none'; }, 900); +} + +// ===== AI NUMBER OCR (Gemini reads printed barcode digits) ===== +let _numOcrRunning = false; +async function _tryGeminiNumberOCR() { + if (_numOcrRunning || !_requireGemini()) return; + const video = document.getElementById('scanner-video'); + if (!video || !video.videoWidth) { showToast(t('error.camera'), 'error'); return; } + _numOcrRunning = true; + const btn = document.getElementById('scan-num-ocr-btn'); + if (btn) { btn.disabled = true; btn.textContent = t('scan.num_ocr_searching'); } + try { + const canvas = document.createElement('canvas'); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + canvas.getContext('2d').drawImage(video, 0, 0); + const imageBase64 = canvas.toDataURL('image/jpeg', 0.88).split(',')[1]; + const result = await api('gemini_number_ocr', {}, 'POST', { image: imageBase64 }); + if (result.barcode) { + showToast(t('scan.num_ocr_found').replace('{code}', result.barcode), 'success'); + onBarcodeDetected(result.barcode); + } else { + showToast(t('scan.num_ocr_not_found'), 'warning'); + } + } catch(e) { + showToast(t('error.connection'), 'error'); + } finally { + _numOcrRunning = false; + if (btn) { btn.disabled = false; btn.textContent = t('scan.num_ocr_btn'); } } } @@ -2594,7 +2742,7 @@ function showPage(pageId, param = null) { } loadInventory(); break; - case 'scan': initScanner(); clearQuickNameResults(); updateSpesaBanner(); + case 'scan': initScanner(); clearQuickNameResults(); updateSpesaBanner(); updateScanRecents(); switchScanTab('barcode'); // Pre-warm the embedding model the first time user visits scan page if (typeof window._getCategoryPipeline === 'function' && !window._categoryPipelineReady) { window._getCategoryPipeline(); // fire-and-forget @@ -4942,12 +5090,25 @@ async function initScanner() { video.srcObject = stream; await video.play(); scanLog(`Video playing — videoWidth: ${video.videoWidth}, videoHeight: ${video.videoHeight}`); - + + // Apply fixed 2x zoom + await _applyFixedZoom(); + if (_useBarcodeDetector) { startNativeScanner(video); } else { startQuaggaScanner(video); } + + // After 4s without a scan, reveal the AI number OCR fallback button + if (_geminiAvailable) { + setTimeout(() => { + if (scannerStream) { // still scanning + const btn = document.getElementById('scan-num-ocr-btn'); + if (btn) btn.style.display = ''; + } + }, 4000); + } } catch (err) { scanLog(`CAMERA ERROR: ${err.name}: ${err.message}`); @@ -5024,6 +5185,7 @@ async function startNativeScanner(videoEl) { partialCount++; scanLog(`Native detect #${partialCount} [f${frameCount}]: ${code} (${format})`); updateFeedback('detecting'); + _showScanLiveCode(code); if (!detectionHistory[code]) detectionHistory[code] = { count: 0 }; detectionHistory[code].count++; @@ -5191,9 +5353,11 @@ function startQuaggaScanner(videoEl) { quaggaRunning = false; updateScannerFeedback(null); scanLog(`CONFIRMED: ${code} [${passName2}] f${frameCount} consec:${detectCount} total:${dominated.count}`); + _hideScanLiveCode(); onBarcodeDetected(code); return; } + _showScanLiveCode(code); } else { updateScannerFeedback('scanning'); } @@ -5235,16 +5399,19 @@ function enhanceCanvasForBarcode(ctx, w, h) { function stopScanner() { quaggaRunning = false; - _scanZoomLevel = 1; + _scanZoomLevel = 2; // always 2x on next start + _torchActive = false; if (scannerStream) { scannerStream.getTracks().forEach(t => t.stop()); scannerStream = null; } const video = document.getElementById('scanner-video'); - if (video) video.srcObject = null; - const zoomBtn = document.getElementById('scan-zoom-btn'); - if (zoomBtn) zoomBtn.textContent = 'x1'; - + if (video) { video.srcObject = null; video.style.transform = ''; } + // Reset torch button + const tb = document.getElementById('scan-torch-btn'); + if (tb) tb.classList.remove('torch-on'); + // Hide live code + _hideScanLiveCode(); // Also stop AI camera if (aiStream) { aiStream.getTracks().forEach(t => t.stop()); @@ -5304,8 +5471,10 @@ async function onBarcodeDetected(barcode) { if (detected.confCount) currentProduct._confCount = detected.confCount; } showLoading(false); + addToScanRecents(currentProduct); + _showScanConfirm(currentProduct.name); stopScanner(); - showProductAction(); + setTimeout(() => showProductAction(), 300); return; } @@ -5362,8 +5531,10 @@ async function onBarcodeDetected(barcode) { stores: p.stores || '', }; showLoading(false); + addToScanRecents(currentProduct); + _showScanConfirm(currentProduct.name); stopScanner(); - showProductAction(); + setTimeout(() => showProductAction(), 300); return; } } diff --git a/index.html b/index.html index 71e37a1..3bd5a79 100644 --- a/index.html +++ b/index.html @@ -11,7 +11,7 @@