feat: spesa mode stats banner + scan zoom x1/x2 toggle

Spesa mode banner:
- Tracks each added product in _spesaSession[]
- Shows a rotating stat/phrase below the title: count, top category,
  duplicates, fun milestone messages (primo prodotto, ottimo ritmo, spesa epica…)
- Banner gains two-line layout (title + stat)

Scan zoom:
- Small pill button 'x1'/'x2' overlaid top-right of the camera viewport
- On hardware-zoom capable devices (Android Chrome) uses track.applyConstraints zoom
- Falls back to CSS scale(2) on video element for all other browsers
- Zoom resets to x1 on stopScanner()
This commit is contained in:
dadaloop82
2026-04-06 09:16:50 +00:00
parent 7782eb1519
commit a6bc05cd2d
3 changed files with 98 additions and 2 deletions
+34
View File
@@ -156,12 +156,23 @@ body {
font-size: 0.95rem;
box-shadow: var(--shadow);
}
.spesa-banner-left {
display: flex;
flex-direction: column;
gap: 2px;
}
.spesa-stat {
font-size: 0.78rem;
font-weight: 400;
opacity: 0.92;
}
.spesa-mode-banner .btn {
background: rgba(255,255,255,0.25);
color: #fff;
border: 1px solid rgba(255,255,255,0.5);
font-weight: 600;
padding: 6px 14px;
flex-shrink: 0;
}
@keyframes pulse-scan {
@@ -968,10 +979,33 @@ body {
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%;
object-fit: cover;
transform-origin: center center;
transition: transform 0.2s ease;
}
.scanner-overlay {
+59 -1
View File
@@ -490,6 +490,30 @@ let currentLocation = '';
let scannerStream = null;
let quaggaRunning = false;
let aiStream = null;
let _scanZoomLevel = 1; // 1 or 2
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)';
}
}
}
}
// ===== CAMERA HELPER =====
function getCameraConstraints(extraVideo = {}) {
@@ -2074,12 +2098,15 @@ function enhanceCanvasForBarcode(ctx, w, h) {
function stopScanner() {
quaggaRunning = false;
_scanZoomLevel = 1;
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';
// Also stop AI camera
if (aiStream) {
@@ -7834,6 +7861,7 @@ function generateScreensaverFact() {
// ===== SPESA MODE (long-press camera for continuous scanning) =====
let _spesaMode = false;
let _longPressTimer = null;
let _spesaSession = []; // { name, qty, unit } per ogni prodotto aggiunto
function initSpesaMode() {
const btn = document.getElementById('btn-header-scan');
@@ -7863,6 +7891,7 @@ function initSpesaMode() {
function startSpesaMode() {
_spesaMode = true;
_spesaSession = [];
showToast('🛒 Modalità Spesa attivata!', 'success');
showPage('scan');
updateSpesaBanner();
@@ -7877,16 +7906,45 @@ function endSpesaMode() {
function updateSpesaBanner() {
const banner = document.getElementById('spesa-mode-banner');
if (banner) banner.style.display = _spesaMode ? 'flex' : 'none';
if (!banner) return;
banner.style.display = _spesaMode ? 'flex' : 'none';
const statEl = banner.querySelector('.spesa-stat');
if (statEl) statEl.textContent = _spesaBannerStat();
}
// Called after successful add — returns true if spesa mode handled navigation
function spesaModeAfterAdd() {
if (!_spesaMode) return false;
// Track this product in the session
if (currentProduct) {
_spesaSession.push({ name: currentProduct.name, category: currentProduct.category || '' });
updateSpesaBanner();
}
showPage('scan');
return true;
}
function _spesaBannerStat() {
const n = _spesaSession.length;
if (n === 0) return '🛒 Nessun prodotto ancora';
const cats = {};
_spesaSession.forEach(p => { const c = p.category || 'altro'; cats[c] = (cats[c]||0)+1; });
const topCat = Object.entries(cats).sort((a,b)=>b[1]-a[1])[0];
const names = _spesaSession.map(p => p.name);
const unique = [...new Set(names)];
const dupes = names.length - unique.length;
const phrases = [
n === 1 ? `Primo prodotto: ${_spesaSession[0].name}!` : null,
n >= 2 && n < 5 ? `${n} prodotti — stai scaldando i motori 🚀` : null,
n >= 5 && n < 10 ? `${n} prodotti — ottimo ritmo! 💪` : null,
n >= 10 && n < 20 ? `${n} prodotti — quasi un recordman 🏆` : null,
n >= 20 ? `${n} prodotti — spesa epica! 🛒🔥` : null,
dupes > 0 ? `${dupes} bis ${dupes===1?'(stessa cosa due volte)':'(roba presa più volte)'}` : null,
topCat && topCat[1] > 1 ? `Categoria top: ${topCat[0]} (${topCat[1]}×)` : null,
].filter(Boolean);
return phrases[n % phrases.length] || `${n} prodott${n===1?'o':'i'} aggiunti`;
}
function _initScreensaverShortcutBtn(btnId, targetPage, longPressFn) {
const btn = document.getElementById(btnId);
if (!btn) return;