cc0d9763ed
api/index.php: - gemini_usage: retroactive AI call estimate from cache files (price/shelf/category) with per-entry token estimates (price ~475tok, shelf ~580tok, category ~230tok) - yearly totals: sum tracked months + retro estimate for full 2026 view - DB activity stats: products, inventory, transactions, expired, expiring_soon - cache stats: price (255), shelf (30), category (7), foodfacts (10) - system info: last backup timestamp+size, Bring! token expiry - new constants: SHELF_CACHE_PATH, FOODFACTS_CACHE_PATH, BRING_TOKEN_PATH assets/js/app.js: - _renderInfoTab(): full rewrite — 4 cards (AI, Inventory, Activity, System) - month displayed as localized name via Intl.DateTimeFormat (es. 'maggio 2026') - tracked section shown when calls > 0; retro estimate always shown if gap exists - year section: tracked + retro combined total - pill() helper for consistent stat display index.html: 4 cards with ids info-ai-content, info-inv-content, info-act-content, info-system-content translations: updated settings.info.* keys in it/en/de (overview subtitle, retro labels, inv/act/system keys)
1653 lines
112 KiB
HTML
1653 lines
112 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="it">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||
<meta name="theme-color" content="#2d5016">
|
||
<meta name="author" content="Stimpfl Daniel">
|
||
<meta name="description" content="Self-hosted pantry manager with barcode scanning, AI identification, and shopping list integration.">
|
||
<title>EverShelf</title>
|
||
<link rel="manifest" href="manifest.json">
|
||
<link rel="icon" type="image/png" href="assets/img/logo/logo_icon.png">
|
||
<link rel="stylesheet" href="assets/css/style.css?v=20260517a">
|
||
<!-- QuaggaJS for barcode scanning -->
|
||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
|
||
<!-- @xenova/transformers: ES-module bootstrap that exposes a lazy category-classifier as window._categoryPipelinePromise -->
|
||
<script type="module">
|
||
// Lazy-load the embedding pipeline only when first needed.
|
||
// Using a dynamic import so the ~2 MB WASM is not fetched on page load.
|
||
window._categoryPipelineReady = false;
|
||
window._categoryPipelinePromise = null;
|
||
|
||
window._getCategoryPipeline = async function() {
|
||
if (window._categoryPipelinePromise) return window._categoryPipelinePromise;
|
||
window._categoryPipelinePromise = (async () => {
|
||
try {
|
||
const { pipeline, env } = await import(
|
||
'https://cdn.jsdelivr.net/npm/@xenova/transformers@2/src/transformers.min.js'
|
||
);
|
||
// Keep WASM/model files in the browser cache; disable remote model check
|
||
// to avoid CORS issues with the self-hosted instance.
|
||
env.allowRemoteModels = true;
|
||
env.useBrowserCache = true;
|
||
const pipe = await pipeline(
|
||
'feature-extraction',
|
||
'Xenova/all-MiniLM-L6-v2',
|
||
{ quantized: true }
|
||
);
|
||
window._categoryPipelineReady = true;
|
||
return pipe;
|
||
} catch (e) {
|
||
console.warn('[EverShelf] Embedding model unavailable, regex fallback only:', e);
|
||
return null;
|
||
}
|
||
})();
|
||
return window._categoryPipelinePromise;
|
||
};
|
||
</script>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ===== APP PRELOADER (hidden by JS once _initApp completes) ===== -->
|
||
<div id="app-preloader" aria-hidden="true">
|
||
<div class="app-preloader-inner">
|
||
<img src="assets/img/logo/logo.png" alt="EverShelf" class="app-preloader-logo" />
|
||
<div class="app-preloader-spinner" id="preloader-spinner"></div>
|
||
<div id="preloader-progress-wrap" class="preloader-progress-wrap" style="display:none">
|
||
<div class="preloader-bar-track">
|
||
<div id="preloader-bar" class="preloader-bar"></div>
|
||
</div>
|
||
<div id="check-ticker" class="check-ticker"></div>
|
||
</div>
|
||
<div id="preloader-warnings" class="preloader-warnings" style="display:none"></div>
|
||
<div id="preloader-error-msg" class="preloader-error-msg" style="display:none"></div>
|
||
<button id="preloader-retry-btn" class="preloader-retry-btn" style="display:none" onclick="_startupRetry()">🔄 <span data-i18n="startup.retry">Riprova</span></button>
|
||
<span class="app-preloader-version" id="preloader-version">v1.7.22</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Top Header -->
|
||
<header class="app-header">
|
||
<div class="header-content">
|
||
<!-- Kiosk buttons (exit / refresh) — injected here by _injectKioskOverlay() -->
|
||
<div class="header-left" id="header-left"></div>
|
||
|
||
<!-- Title — left-aligned; grows to fill space -->
|
||
<div class="header-title-wrap">
|
||
<h1 class="header-title" onclick="showPage('dashboard')">
|
||
<img src="assets/img/logo/logo_icon.png" alt="" class="header-logo-icon" aria-hidden="true" /><span data-i18n="nav.title">EverShelf</span><span class="header-version">v1.7.22</span>
|
||
</h1>
|
||
<!-- Update badge — shown alongside title, never replaces it -->
|
||
<span class="header-update-badge" id="header-update-badge" style="display:none"></span>
|
||
</div>
|
||
|
||
<!-- Actions — fixed set of icon buttons, all same size -->
|
||
<div class="header-actions">
|
||
<button id="scale-status-indicator" class="header-btn scale-status-indicator scale-status-disconnected"
|
||
style="display:none" data-i18n-title="scale.status_disconnected" title="⚖️ Bilancia"
|
||
onclick="_scaleShowInfo()">
|
||
<span class="scale-icon-wrapper">
|
||
<span class="scale-icon-emoji">⚖️</span>
|
||
<span class="scale-status-dot"></span>
|
||
</span>
|
||
</button>
|
||
<button class="header-btn header-gemini-btn" onclick="showPage('chat')" title="Chat con Gemini" data-i18n-title="chat.title">
|
||
<svg class="gemini-icon" viewBox="0 0 24 24" width="24" height="24" fill="white"><path d="M12 0C12 6.627 6.627 12 0 12c6.627 0 12 5.373 12 12 0-6.627 5.373-12 12-12-6.627 0-12-5.373-12-12z"/></svg>
|
||
</button>
|
||
<button class="header-btn header-scan-btn" id="btn-header-scan"
|
||
title="Scansiona prodotto (tieni premuto per modalità spesa)" data-i18n-title="scan.hint">
|
||
📷
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Offline / server-unreachable banner -->
|
||
<div id="offline-banner" class="offline-banner" style="display:none" role="alert" aria-live="assertive">
|
||
<span class="offline-banner-icon" aria-hidden="true">🔌</span>
|
||
<span class="offline-banner-text" data-i18n="error.server_offline">Connessione al server persa</span>
|
||
<button class="offline-banner-retry" onclick="_heartbeatRetry()" data-i18n="error.server_retry">Riprova</button>
|
||
</div>
|
||
|
||
<!-- Main Content Area -->
|
||
<main class="app-content" id="app-content">
|
||
|
||
<!-- ===== DASHBOARD ===== -->
|
||
<section class="page active" id="page-dashboard">
|
||
<div class="dashboard-stats" id="dashboard-stats">
|
||
<div class="stat-card" onclick="showPage('inventory', 'dispensa')">
|
||
<span class="stat-icon">🗄️</span>
|
||
<span class="stat-value" id="stat-dispensa">-</span>
|
||
<span class="stat-label" data-i18n="locations.dispensa">Dispensa</span>
|
||
</div>
|
||
<div class="stat-card" onclick="showPage('inventory', 'frigo')">
|
||
<span class="stat-icon">🧊</span>
|
||
<span class="stat-value" id="stat-frigo">-</span>
|
||
<span class="stat-label" data-i18n="locations.frigo">Frigo</span>
|
||
</div>
|
||
<div class="stat-card" onclick="showPage('inventory', 'freezer')">
|
||
<span class="stat-icon">❄️</span>
|
||
<span class="stat-value" id="stat-freezer">-</span>
|
||
<span class="stat-label" data-i18n="locations.freezer">Freezer</span>
|
||
</div>
|
||
<div class="stat-card" onclick="showPage('shopping')">
|
||
<span class="stat-icon">🛒</span>
|
||
<span class="stat-value" id="stat-spesa">-</span>
|
||
<span class="stat-label" data-i18n="nav.shopping">Spesa</span>
|
||
<span class="stat-price-total" id="stat-price-total" style="display:none"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Top notification banner (anomalous qty + consumption predictions) -->
|
||
<div id="alert-banner" class="alert-banner" style="display:none">
|
||
<div class="alert-banner-inner">
|
||
<div class="alert-banner-icon" id="alert-banner-icon">⚠️</div>
|
||
<div class="alert-banner-body">
|
||
<div class="alert-banner-title" id="alert-banner-title"></div>
|
||
<div class="alert-banner-detail" id="alert-banner-detail"></div>
|
||
</div>
|
||
<button class="alert-banner-close" id="alert-banner-close" onclick="dismissBannerItem()">✕</button>
|
||
</div>
|
||
<div class="alert-banner-actions" id="alert-banner-actions"></div>
|
||
<div class="alert-banner-counter" id="alert-banner-counter"></div>
|
||
</div>
|
||
|
||
<!-- Quick recipe suggestion -->
|
||
<div class="quick-recipe-bar" id="quick-recipe-bar" style="display:none">
|
||
<button class="btn-quick-recipe" onclick="quickRecipeSuggestion()">
|
||
<span>🍳</span>
|
||
<span class="quick-recipe-text" data-i18n="dashboard.quick_recipe">🍳 Ricetta veloce con prodotti in scadenza</span>
|
||
<span>→</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Alert for expired items (on top) -->
|
||
<div class="alert-section alert-danger" id="alert-expired" style="display:none">
|
||
<h3 data-i18n="dashboard.expired_title">🚫 Scaduti</h3>
|
||
<div id="expired-list"></div>
|
||
</div>
|
||
|
||
<!-- Anti-Waste Report Card + Nutrition Analysis (alternating, content rendered by JS) -->
|
||
<div id="dashboard-insight-wrap" style="position:relative">
|
||
<div id="waste-chart-section" style="display:none"></div>
|
||
<div id="nutrition-section" style="display:none"></div>
|
||
</div>
|
||
|
||
<!-- Alert for soonest expiring items -->
|
||
<div class="alert-section" id="alert-expiring" style="display:none">
|
||
<h3 data-i18n="dashboard.expiring_title">⏰ Prossime Scadenze</h3>
|
||
<div id="expiring-list"></div>
|
||
</div>
|
||
|
||
<!-- Opened (partially used) products -->
|
||
<div class="alert-section alert-opened" id="alert-opened" style="display:none">
|
||
<h3 data-i18n="dashboard.opened_title">📦 Prodotti Aperti</h3>
|
||
<div id="opened-list"></div>
|
||
</div>
|
||
|
||
</section>
|
||
|
||
<!-- ===== INVENTORY LIST ===== -->
|
||
<section class="page" id="page-inventory">
|
||
<div class="page-header">
|
||
<button class="back-btn" onclick="showPage('dashboard')" data-i18n="btn.back">← Indietro</button>
|
||
<h2 id="inventory-title" data-i18n="inventory.title">Dispensa</h2>
|
||
<button class="page-header-action-btn" onclick="_showExportModal()" title="Export" data-i18n-title="export.btn_title">📤</button>
|
||
</div>
|
||
<div class="location-tabs" id="location-tabs">
|
||
<button class="tab active" onclick="filterLocation('')" data-loc="" data-i18n="inventory.filter_all">Tutti</button>
|
||
<button class="tab" onclick="filterLocation('dispensa')" data-loc="dispensa">🗄️ <span data-i18n="locations.dispensa">Dispensa</span></button>
|
||
<button class="tab" onclick="filterLocation('frigo')" data-loc="frigo">🧊 <span data-i18n="locations.frigo">Frigo</span></button>
|
||
<button class="tab" onclick="filterLocation('freezer')" data-loc="freezer">❄️ <span data-i18n="locations.freezer">Freezer</span></button>
|
||
<button class="tab" onclick="filterLocation('altro')" data-loc="altro">📦 <span data-i18n="locations.altro">Altro</span></button>
|
||
</div>
|
||
<div class="search-bar">
|
||
<input type="text" id="inventory-search" placeholder="🔍 Cerca prodotto..." oninput="filterInventory()" data-i18n-placeholder="inventory.search_placeholder">
|
||
</div>
|
||
<!-- Quick access: recent & popular products -->
|
||
<div id="quick-access-section" style="display:none">
|
||
<div class="quick-access-group" id="quick-recent-group" style="display:none">
|
||
<h4 class="quick-access-label" data-i18n="inventory.recent_title">🕐 Ultimi usati</h4>
|
||
<div class="quick-access-grid" id="quick-recent-grid"></div>
|
||
</div>
|
||
<div class="quick-access-group" id="quick-popular-group" style="display:none">
|
||
<h4 class="quick-access-label" data-i18n="inventory.popular_title">⭐ Più usati</h4>
|
||
<div class="quick-access-grid quick-access-grid-8" id="quick-popular-grid"></div>
|
||
</div>
|
||
</div>
|
||
<div class="inventory-list" id="inventory-list"></div>
|
||
</section>
|
||
|
||
<!-- ===== SCAN PAGE ===== -->
|
||
<section class="page" id="page-scan">
|
||
<div class="page-header">
|
||
<button class="back-btn" onclick="stopScanner(); showPage('dashboard')" data-i18n="btn.back">← Indietro</button>
|
||
<h2 data-i18n="scan.title">Scansiona</h2>
|
||
<button class="scan-spesa-chip" id="scan-spesa-btn" onclick="startSpesaMode()" data-i18n="scan.spesa_btn">🛒 Spesa</button>
|
||
</div>
|
||
<div class="spesa-mode-banner" id="spesa-mode-banner" style="display:none">
|
||
<div class="spesa-banner-left">
|
||
<span data-i18n="scan.mode_shopping">🛒 Modalità Spesa</span>
|
||
<span class="spesa-stat"></span>
|
||
</div>
|
||
<button class="btn btn-small" onclick="endSpesaMode()" data-i18n="scan.mode_shopping_end">✅ Fine spesa</button>
|
||
</div>
|
||
<div class="scan-container">
|
||
<!-- Camera viewport -->
|
||
<div class="scanner-viewport" id="scanner-viewport">
|
||
<!-- Guide corners -->
|
||
<div class="scan-guide-frame">
|
||
<div class="sgf-corner sgf-tl"></div>
|
||
<div class="sgf-corner sgf-tr"></div>
|
||
<div class="sgf-corner sgf-bl"></div>
|
||
<div class="sgf-corner sgf-br"></div>
|
||
</div>
|
||
<div class="scanner-overlay">
|
||
<div class="scanner-line"></div>
|
||
</div>
|
||
<!-- Live partial code preview -->
|
||
<div class="scan-live-code" id="scan-live-code" style="display:none"></div>
|
||
<!-- Success flash overlay -->
|
||
<div class="scan-confirm-overlay" id="scan-confirm-overlay" style="display:none">
|
||
<div class="scan-confirm-check">✓</div>
|
||
<div class="scan-confirm-name" id="scan-confirm-name"></div>
|
||
</div>
|
||
<!-- Viewport inline controls (torch / zoom badge / flip) -->
|
||
<div class="scan-viewport-controls">
|
||
<button class="scan-ctrl-btn" id="scan-torch-btn" onclick="toggleTorch()" data-i18n-title="scan.torch_hint" title="Torcia">🔦</button>
|
||
<span class="scan-zoom-badge">2×</span>
|
||
<button class="scan-ctrl-btn" id="scan-flip-btn" onclick="flipCamera()" data-i18n-title="scan.flip_hint" title="Cambia fotocamera">🔄</button>
|
||
</div>
|
||
<video id="scanner-video" autoplay playsinline></video>
|
||
<canvas id="scanner-canvas" style="display:none"></canvas>
|
||
</div>
|
||
|
||
<!-- Scan errors -->
|
||
<div class="scan-result" id="scan-result" style="display:none"></div>
|
||
|
||
<!-- Recent scans -->
|
||
<div class="scan-recents" id="scan-recents" style="display:none">
|
||
<span class="scan-recents-label" data-i18n="scan.recents_label">Recenti</span>
|
||
<div class="scan-recents-chips" id="scan-recents-chips"></div>
|
||
</div>
|
||
|
||
<!-- Input panel with tabs -->
|
||
<div class="scan-input-panel">
|
||
<div class="scan-input-tabs">
|
||
<button class="scan-input-tab active" id="scan-tab-barcode" onclick="switchScanTab('barcode')">
|
||
<span>📊</span><span data-i18n="scan.tab_barcode">Barcode</span>
|
||
</button>
|
||
<button class="scan-input-tab" id="scan-tab-name" onclick="switchScanTab('name')">
|
||
<span>✏️</span><span data-i18n="scan.tab_name">Nome</span>
|
||
</button>
|
||
<button class="scan-input-tab" id="scan-tab-ai" onclick="switchScanTab('ai')">
|
||
<span>🤖</span><span data-i18n="scan.tab_ai">AI</span>
|
||
</button>
|
||
</div>
|
||
<!-- Tab: Barcode manuale -->
|
||
<div class="scan-tab-content" id="scan-tabcontent-barcode">
|
||
<div class="barcode-input-row">
|
||
<input type="text" id="manual-barcode-input" class="form-input"
|
||
inputmode="numeric" pattern="[0-9]*" maxlength="14"
|
||
oninput="autoSubmitEAN(this)"
|
||
onkeydown="if(event.key==='Enter')submitManualBarcode()"
|
||
data-i18n-placeholder="scan.barcode_placeholder"
|
||
placeholder="Inserisci codice a barre...">
|
||
<button class="btn btn-primary" onclick="submitManualBarcode()">🔍</button>
|
||
</div>
|
||
</div>
|
||
<!-- Tab: Cerca per nome -->
|
||
<div class="scan-tab-content quick-name-entry" id="scan-tabcontent-name" style="display:none">
|
||
<div class="barcode-input-row">
|
||
<input type="text" id="quick-product-name" class="form-input"
|
||
autocomplete="off"
|
||
onkeydown="if(event.key==='Enter')submitQuickName()"
|
||
data-i18n-placeholder="scan.quick_name_placeholder"
|
||
placeholder="Es: Mele, Zucchine, Pane...">
|
||
<button class="btn btn-accent" onclick="submitQuickName()">✅</button>
|
||
</div>
|
||
</div>
|
||
<!-- Tab: AI e Manuale -->
|
||
<div class="scan-tab-content" id="scan-tabcontent-ai" style="display:none">
|
||
<div class="scan-ai-tab-btns">
|
||
<button class="btn btn-accent" onclick="captureForAI()" data-i18n="scan.ai_identify">🤖 Identifica con AI</button>
|
||
<button class="btn btn-secondary" onclick="startManualEntry()" data-i18n="scan.manual_entry">✏️ Inserimento Manuale</button>
|
||
</div>
|
||
<button class="btn scan-num-ocr-btn" id="scan-num-ocr-btn" style="display:none" onclick="_tryGeminiNumberOCR()" data-i18n="scan.num_ocr_btn">🔢 Leggi numeri con AI</button>
|
||
</div>
|
||
</div>
|
||
<!-- Hidden debug log (accessible via Settings) -->
|
||
<div id="scan-debug-log" style="display:none;margin-top:12px;padding:10px;background:#1a1a2e;color:#0f0;font-family:monospace;font-size:0.7rem;max-height:200px;overflow-y:auto;border-radius:8px;white-space:pre-wrap"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===== PRODUCT ACTION (IN/OUT after scan) ===== -->
|
||
<section class="page" id="page-action">
|
||
<div class="page-header">
|
||
<button class="back-btn" id="action-back-btn" onclick="showPage('scan')" data-i18n="btn.back">← Indietro</button>
|
||
<h2 data-i18n="action.title">Cosa vuoi fare?</h2>
|
||
</div>
|
||
<!-- Banner: shopping list scan context -->
|
||
<div id="shopping-scan-target-banner" class="shopping-scan-target-banner" style="display:none"></div>
|
||
<div class="product-preview product-preview-large" id="action-product-preview"></div>
|
||
<div class="inventory-status-bar" id="action-inventory-status" style="display:none"></div>
|
||
<div class="action-buttons" id="action-buttons-container">
|
||
<button class="btn btn-huge btn-success" onclick="showAddForm()">
|
||
<span class="btn-icon">📥</span>
|
||
<span class="btn-text"><span data-i18n="action.add_btn">AGGIUNGI</span><br><small data-i18n="action.add_sub">in dispensa/frigo</small></span>
|
||
</button>
|
||
<button class="btn btn-huge btn-danger" onclick="showUseForm()">
|
||
<span class="btn-icon">📤</span>
|
||
<span class="btn-text"><span data-i18n="action.use_btn">USA / CONSUMA</span><br><small data-i18n="action.use_sub">dalla dispensa/frigo</small></span>
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===== ADD TO INVENTORY FORM ===== -->
|
||
<section class="page" id="page-add">
|
||
<div class="page-header">
|
||
<button class="back-btn" onclick="showPage('action')" data-i18n="btn.back">← Indietro</button>
|
||
<h2 data-i18n="add.title">Aggiungi alla Dispensa</h2>
|
||
</div>
|
||
<div class="product-preview-small" id="add-product-preview"></div>
|
||
<form class="form" onsubmit="submitAdd(event)">
|
||
<div class="form-group">
|
||
<label data-i18n="add.location_label">📍 Dove lo metti?</label>
|
||
<div class="location-selector">
|
||
<button type="button" class="loc-btn active" onclick="selectLocation(this, 'dispensa')">🗄️ <span data-i18n="locations.dispensa">Dispensa</span></button>
|
||
<button type="button" class="loc-btn" onclick="selectLocation(this, 'frigo')">🧊 <span data-i18n="locations.frigo">Frigo</span></button>
|
||
<button type="button" class="loc-btn" onclick="selectLocation(this, 'freezer')">❄️ <span data-i18n="locations.freezer">Freezer</span></button>
|
||
<button type="button" class="loc-btn" onclick="selectLocation(this, 'altro')">📦 <span data-i18n="locations.altro">Altro</span></button>
|
||
</div>
|
||
<input type="hidden" id="add-location" value="dispensa">
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="add.quantity_label">📦 Quantità</label>
|
||
<div class="qty-unit-row">
|
||
<div class="qty-control flex-1">
|
||
<button type="button" class="qty-btn" onclick="adjustAddQty(-1)">−</button>
|
||
<input type="number" id="add-quantity" value="1" min="0.1" step="any" class="qty-input">
|
||
<button type="button" class="qty-btn" onclick="adjustAddQty(1)">+</button>
|
||
</div>
|
||
<select id="add-unit" class="form-input unit-select" onchange="onAddUnitChange()">
|
||
<option value="pz">pz</option>
|
||
<option value="conf">conf</option>
|
||
<option value="g">g</option>
|
||
<option value="ml">ml</option>
|
||
</select>
|
||
</div>
|
||
<button type="button" id="btn-scale-add" class="btn btn-secondary scale-read-btn" style="display:none" onclick="readScaleWeight('add-quantity', function(){ return document.getElementById('add-unit').value; })" data-i18n="scale.read_btn">⚖️ Leggi dalla bilancia</button>
|
||
<div id="add-conf-size-row" class="conf-size-row" style="display:none">
|
||
<label class="conf-size-label" data-i18n="add.conf_size_label">📦 Ogni confezione contiene:</label>
|
||
<div class="conf-size-inputs">
|
||
<input type="number" id="add-conf-size" class="form-input conf-size-input" min="1" step="any" placeholder="es. 300">
|
||
<select id="add-conf-unit" class="form-input conf-size-unit">
|
||
<option value="g">g</option>
|
||
<option value="ml">ml</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div id="add-weight-info" class="form-hint" style="display:none"></div>
|
||
</div>
|
||
<div class="form-group" id="add-vacuum-group">
|
||
<label class="toggle-row" onclick="toggleVacuumSealed()">
|
||
<span data-i18n="add.vacuum_label">🫙 Sotto vuoto</span>
|
||
<span class="toggle-switch" id="add-vacuum-toggle">
|
||
<input type="checkbox" id="add-vacuum-sealed" onchange="onVacuumSealedChange()">
|
||
<span class="toggle-slider"></span>
|
||
</span>
|
||
</label>
|
||
<p class="form-hint" id="add-vacuum-hint" style="display:none" data-i18n="add.vacuum_hint">La scadenza verrà estesa automaticamente</p>
|
||
</div>
|
||
<div class="form-group" id="add-expiry-section">
|
||
<!-- Populated dynamically by showAddForm() -->
|
||
</div>
|
||
<button type="submit" class="btn btn-large btn-success full-width" data-i18n="add.submit">✅ Aggiungi</button>
|
||
</form>
|
||
</section>
|
||
|
||
<!-- ===== USE FROM INVENTORY FORM ===== -->
|
||
<section class="page" id="page-use">
|
||
<div class="page-header">
|
||
<button class="back-btn" onclick="showPage('action')" data-i18n="btn.back">← Indietro</button>
|
||
<h2 data-i18n="use.title">Usa / Consuma</h2>
|
||
</div>
|
||
<div class="product-preview-small" id="use-product-preview"></div>
|
||
<div class="use-inventory-info" id="use-inventory-info"></div>
|
||
<div id="use-expiry-hint" style="display:none"></div>
|
||
<form class="form" onsubmit="submitUse(event)">
|
||
<div class="form-group" id="use-location-group">
|
||
<label data-i18n="use.location_label">📍 Da dove?</label>
|
||
<div class="location-selector" id="use-location-selector">
|
||
<button type="button" class="loc-btn active" onclick="selectUseLocation(this, 'dispensa')">🗄️ <span data-i18n="locations.dispensa">Dispensa</span></button>
|
||
<button type="button" class="loc-btn" onclick="selectUseLocation(this, 'frigo')">🧊 <span data-i18n="locations.frigo">Frigo</span></button>
|
||
<button type="button" class="loc-btn" onclick="selectUseLocation(this, 'freezer')">❄️ <span data-i18n="locations.freezer">Freezer</span></button>
|
||
<button type="button" class="loc-btn" onclick="selectUseLocation(this, 'altro')">📦 <span data-i18n="locations.altro">Altro</span></button>
|
||
</div>
|
||
<input type="hidden" id="use-location" value="dispensa">
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="use.quantity_label">Quanto hai usato?</label>
|
||
<button type="button" id="btn-scale-use" class="btn btn-secondary scale-read-btn" style="display:none" onclick="readScaleWeight('use-quantity', function(){ return _useNormalUnit || 'g'; })" data-i18n="scale.read_btn">⚖️ Leggi dalla bilancia</button>
|
||
<!-- Live scale weight box (visible when scale connected and unit is g/ml) -->
|
||
<div id="scale-live-box" class="scale-live-box" style="display:none">
|
||
<span class="scale-live-icon">⚖️</span>
|
||
<div class="scale-live-content">
|
||
<div id="scale-live-val" class="scale-live-val">— —</div>
|
||
<div class="scale-live-progress"><div id="scale-live-progress-bar" class="scale-live-progress-bar"></div></div>
|
||
<div id="scale-live-label" class="scale-live-label"></div>
|
||
</div>
|
||
</div>
|
||
<div class="use-unit-switch" id="use-unit-switch" style="display:none">
|
||
<button type="button" class="use-unit-btn active" id="use-unit-sub" onclick="switchUseUnit('sub')"></button>
|
||
<button type="button" class="use-unit-btn" id="use-unit-conf" onclick="switchUseUnit('conf')" data-i18n="units.boxes">Confezioni</button>
|
||
</div>
|
||
<div class="use-options">
|
||
<button type="button" class="btn btn-large btn-danger full-width use-all-btn" onclick="submitUseAll()" data-i18n="use.use_all">
|
||
🗑️ Usato TUTTO / Finito
|
||
</button>
|
||
<div class="use-partial">
|
||
<p id="use-partial-hint" data-i18n="use.partial_hint">Oppure specifica la quantità usata:</p>
|
||
<div class="qty-control">
|
||
<button type="button" class="qty-btn" id="use-qty-minus" onclick="adjustUseQty(-1)">−</button>
|
||
<input type="number" id="use-quantity" value="1" min="0.1" step="any" class="qty-input"
|
||
oninput="_scaleUserDismissed=true; _cancelScaleTimersOnly();">
|
||
<button type="button" class="qty-btn" id="use-qty-plus" onclick="adjustUseQty(1)">+</button>
|
||
</div>
|
||
<button type="submit" id="btn-use-submit" class="btn btn-large btn-warning full-width mt-2 move-countdown-btn" data-i18n="use.submit">📤 Usa questa quantità</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</form>
|
||
</section>
|
||
|
||
<!-- ===== MANUAL / EDIT PRODUCT FORM ===== -->
|
||
<section class="page" id="page-product-form">
|
||
<div class="page-header">
|
||
<button class="back-btn" onclick="showPage('scan')" data-i18n="btn.back">← Indietro</button>
|
||
<h2 id="product-form-title">Nuovo Prodotto</h2>
|
||
</div>
|
||
<form class="form" onsubmit="submitProduct(event)">
|
||
<input type="hidden" id="pf-id">
|
||
<div id="pf-ai-fill-row" class="form-group">
|
||
<button type="button" class="btn btn-accent full-width" onclick="captureForAIFormFill()" data-i18n="product.ai_fill">
|
||
📷 Scatta foto e identifica con AI
|
||
</button>
|
||
<p class="form-hint" style="text-align:center;margin-top:4px" data-i18n="product.ai_fill_hint">L'AI compilerà automaticamente i campi del prodotto</p>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="product.name_label">🏷️ Nome Prodotto *</label>
|
||
<input type="text" id="pf-name" class="form-input" required placeholder="Es: Latte intero, Pasta penne rigate..."
|
||
list="common-products" autocomplete="off">
|
||
<datalist id="common-products">
|
||
<option value="Latte intero">
|
||
<option value="Latte parzialmente scremato">
|
||
<option value="Latte scremato">
|
||
<option value="Yogurt bianco">
|
||
<option value="Yogurt greco">
|
||
<option value="Mozzarella">
|
||
<option value="Burrata">
|
||
<option value="Parmigiano Reggiano">
|
||
<option value="Grana Padano">
|
||
<option value="Ricotta">
|
||
<option value="Mascarpone">
|
||
<option value="Burro">
|
||
<option value="Panna fresca">
|
||
<option value="Uova">
|
||
<option value="Prosciutto cotto">
|
||
<option value="Prosciutto crudo">
|
||
<option value="Bresaola">
|
||
<option value="Salame">
|
||
<option value="Mortadella">
|
||
<option value="Petto di pollo">
|
||
<option value="Macinato di manzo">
|
||
<option value="Salmone fresco">
|
||
<option value="Tonno in scatola">
|
||
<option value="Sgombro in scatola">
|
||
<option value="Pasta spaghetti">
|
||
<option value="Pasta penne rigate">
|
||
<option value="Pasta fusilli">
|
||
<option value="Riso">
|
||
<option value="Riso basmati">
|
||
<option value="Farina 00">
|
||
<option value="Pane fresco">
|
||
<option value="Pan carrè">
|
||
<option value="Fette biscottate">
|
||
<option value="Passata di pomodoro">
|
||
<option value="Pomodori pelati">
|
||
<option value="Olio extravergine d'oliva">
|
||
<option value="Aceto balsamico">
|
||
<option value="Sale fino">
|
||
<option value="Zucchero">
|
||
<option value="Caffè macinato">
|
||
<option value="Biscotti">
|
||
<option value="Nutella">
|
||
<option value="Marmellata">
|
||
<option value="Miele">
|
||
<option value="Cereali">
|
||
<option value="Lenticchie">
|
||
<option value="Ceci">
|
||
<option value="Fagioli">
|
||
<option value="Insalata mista">
|
||
<option value="Pomodori">
|
||
<option value="Zucchine">
|
||
<option value="Patate">
|
||
<option value="Cipolle">
|
||
<option value="Mele">
|
||
<option value="Banane">
|
||
<option value="Arance">
|
||
<option value="Acqua naturale">
|
||
<option value="Succo d'arancia">
|
||
<option value="Birra">
|
||
<option value="Vino rosso">
|
||
</datalist>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="product.brand_label">🏢 Marca</label>
|
||
<input type="text" id="pf-brand" class="form-input" placeholder="Es: Barilla, Granarolo, Mutti..."
|
||
list="common-brands" autocomplete="off">
|
||
<datalist id="common-brands">
|
||
<option value="Barilla">
|
||
<option value="De Cecco">
|
||
<option value="Rummo">
|
||
<option value="Voiello">
|
||
<option value="Divella">
|
||
<option value="Granarolo">
|
||
<option value="Parmalat">
|
||
<option value="Müller">
|
||
<option value="Danone">
|
||
<option value="Galbani">
|
||
<option value="Ferrero">
|
||
<option value="Lavazza">
|
||
<option value="Illy">
|
||
<option value="Mulino Bianco">
|
||
<option value="Pan di Stelle">
|
||
<option value="Mutti">
|
||
<option value="Cirio">
|
||
<option value="De Rica">
|
||
<option value="Star">
|
||
<option value="Knorr">
|
||
<option value="Findus">
|
||
<option value="4 Salti in Padella">
|
||
<option value="Rio Mare">
|
||
<option value="Valfrutta">
|
||
<option value="Auricchio">
|
||
<option value="Zanetti">
|
||
<option value="Beretta">
|
||
<option value="Rovagnati">
|
||
<option value="Amadori">
|
||
<option value="AIA">
|
||
<option value="Esselunga">
|
||
<option value="Conad">
|
||
<option value="Coop">
|
||
</datalist>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="product.category_label">📂 Categoria</label>
|
||
<select id="pf-category" class="form-input" onchange="onCategoryChange(false)">
|
||
<option value="" data-i18n="form.select_placeholder">-- Seleziona --</option>
|
||
<option value="latticini">🥛 Latticini</option>
|
||
<option value="carne">🥩 Carne</option>
|
||
<option value="pesce">🐟 Pesce</option>
|
||
<option value="frutta">🍎 Frutta</option>
|
||
<option value="verdura">🥬 Verdura</option>
|
||
<option value="pasta">🍝 Pasta & Riso</option>
|
||
<option value="pane">🍞 Pane & Forno</option>
|
||
<option value="surgelati">🧊 Surgelati</option>
|
||
<option value="bevande">🥤 Bevande</option>
|
||
<option value="condimenti">🧂 Condimenti</option>
|
||
<option value="snack">🍪 Snack & Dolci</option>
|
||
<option value="conserve">🥫 Conserve</option>
|
||
<option value="cereali">🌾 Cereali & Legumi</option>
|
||
<option value="igiene">🧴 Igiene</option>
|
||
<option value="pulizia">🧹 Pulizia Casa</option>
|
||
<option value="altro">📦 Altro</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group flex-1">
|
||
<label data-i18n="product.unit_label">📏 Unità di misura</label>
|
||
<select id="pf-unit" class="form-input" onchange="onPfUnitChange()">
|
||
<option value="pz" data-i18n="units.pieces">Pezzi</option>
|
||
<option value="g" data-i18n="units.grams">Grammi</option>
|
||
<option value="ml">ml</option>
|
||
<option value="conf" data-i18n="units.box">Confezione</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group flex-1">
|
||
<label data-i18n="product.default_qty_label">🔢 Quantità default</label>
|
||
<input type="number" id="pf-defqty" class="form-input" value="1" min="0.1" step="any">
|
||
</div>
|
||
</div>
|
||
<div id="pf-conf-size-row" class="conf-size-row" style="display:none">
|
||
<label class="conf-size-label" data-i18n="product.conf_size_label">📦 Ogni confezione contiene:</label>
|
||
<div class="conf-size-inputs">
|
||
<input type="number" id="pf-conf-size" class="form-input conf-size-input" min="1" step="any" placeholder="es. 300">
|
||
<select id="pf-conf-unit" class="form-input conf-size-unit">
|
||
<option value="g">g</option>
|
||
<option value="ml">ml</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="product.notes_label">📝 Note</label>
|
||
<textarea id="pf-notes" class="form-input" rows="2" placeholder="Es: senza lattosio, bio, conservare in frigo dopo apertura..."></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="product.barcode_label">🔖 Barcode</label>
|
||
<div class="expiry-input-row">
|
||
<input type="text" id="pf-barcode" class="form-input" placeholder="Codice a barre (se disponibile)" inputmode="numeric" data-i18n-placeholder="product.barcode_placeholder">
|
||
<button type="button" class="btn btn-accent btn-scan-expiry" id="pf-barcode-scan-btn" onclick="scanBarcodeForForm()" title="Scansiona barcode">📷</button>
|
||
</div>
|
||
<p class="form-hint" id="pf-barcode-hint" style="display:none" data-i18n="product.barcode_hint">⚠️ Aggiungi il barcode così al prossimo acquisto basta scansionarlo!</p>
|
||
</div>
|
||
<input type="hidden" id="pf-image">
|
||
<div class="product-image-preview" id="pf-image-preview" style="display:none">
|
||
<img id="pf-image-img" src="" alt="Product">
|
||
</div>
|
||
<button type="submit" class="btn btn-large btn-primary full-width" data-i18n="btn.save_product">💾 Salva Prodotto</button>
|
||
</form>
|
||
</section>
|
||
|
||
<!-- ===== ALL PRODUCTS PAGE ===== -->
|
||
<section class="page" id="page-products">
|
||
<div class="page-header">
|
||
<button class="back-btn" onclick="showPage('dashboard')" data-i18n="btn.back">← Indietro</button>
|
||
<h2 data-i18n="products.title">📦 Tutti i Prodotti</h2>
|
||
</div>
|
||
<div class="search-bar">
|
||
<input type="text" id="products-search" placeholder="🔍 Cerca prodotto..." oninput="searchAllProducts()">
|
||
</div>
|
||
<div class="products-list" id="products-list"></div>
|
||
</section>
|
||
|
||
<!-- ===== RECIPE PAGE ===== -->
|
||
<section class="page" id="page-recipe">
|
||
<div class="page-header">
|
||
<button class="back-btn" onclick="showPage('dashboard')" data-i18n="btn.back">← Indietro</button>
|
||
<h2 data-i18n="recipes.title">🍳 Ricette</h2>
|
||
</div>
|
||
<div class="recipe-page-container">
|
||
<button class="btn btn-large btn-success full-width" onclick="openRecipeDialog()" data-i18n="recipes.generate">
|
||
✨ Genera nuova ricetta
|
||
</button>
|
||
<div id="recipe-archive" class="recipe-archive"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===== SHOPPING LIST (BRING!) PAGE ===== -->
|
||
<section class="page" id="page-shopping">
|
||
<div class="page-header">
|
||
<button class="back-btn" onclick="showPage('dashboard')" data-i18n="btn.back">← Indietro</button>
|
||
<h2 data-i18n="shopping.title">🛒 Lista della Spesa</h2>
|
||
</div>
|
||
<div class="shopping-container">
|
||
<div class="bring-status" id="bring-status">
|
||
<div class="bring-loading" data-i18n="shopping.bring_loading">Connessione a Bring!...</div>
|
||
</div>
|
||
|
||
<!-- Tab navigation -->
|
||
<div class="shopping-tabs" id="shopping-tabs" style="display:none">
|
||
<button class="shopping-tab active" id="tab-acquisto" onclick="switchShoppingTab('acquisto')">
|
||
<span data-i18n="shopping.tab_to_buy">🛍️ Da comprare</span> <span class="shopping-tab-count" id="tab-count-acquisto">0</span>
|
||
</button>
|
||
<button class="shopping-tab" id="tab-previsione" onclick="switchShoppingTab('previsione')">
|
||
<span data-i18n="shopping.tab_forecast">🧠 In previsione</span> <span class="shopping-tab-count" id="tab-count-previsione">0</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Tab panel: Da comprare -->
|
||
<div id="tab-panel-acquisto" class="tab-panel-shopping active">
|
||
<!-- Price summary bar (shown when prices are enabled) -->
|
||
<div id="shopping-price-bar" style="display:none">
|
||
<div class="shopping-price-total-row">
|
||
<span class="price-total-label" data-i18n="shopping.price_total_label">💰 Spesa stimata:</span>
|
||
<span class="price-total-value" id="price-total-value">–</span>
|
||
<button class="btn-price-refresh" id="btn-price-refresh" onclick="fetchAllPrices(false)" title="Aggiorna prezzi">🔄</button>
|
||
</div>
|
||
<div id="price-loading-bar" style="display:none" class="price-loading-bar">
|
||
<div class="price-loading-inner"></div>
|
||
</div>
|
||
</div>
|
||
<div class="shopping-current" id="shopping-current" style="display:none">
|
||
<div class="shopping-section-header">
|
||
<h3 data-i18n="shopping.section_to_buy">🛍️ Da comprare</h3>
|
||
<span class="shopping-count" id="shopping-count">0</span>
|
||
</div>
|
||
<div class="shopping-items" id="shopping-items"></div>
|
||
</div>
|
||
<div class="shopping-suggestions" id="shopping-suggestions" style="display:none">
|
||
<div class="shopping-section-header">
|
||
<h3 data-i18n="shopping.suggestions_title">💡 Suggerimenti AI</h3>
|
||
</div>
|
||
<div class="seasonal-tip" id="seasonal-tip" style="display:none"></div>
|
||
<div class="suggestion-items" id="suggestion-items"></div>
|
||
<div class="suggestion-actions" id="suggestion-actions" style="display:none">
|
||
<button class="btn btn-success" onclick="addSelectedSuggestions()" data-i18n="shopping.bring_add_selected">
|
||
✅ Aggiungi selezionati a Bring!
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="shopping-actions">
|
||
<button class="btn btn-large btn-accent" onclick="generateSuggestions()" id="btn-suggest" data-i18n="shopping.suggest_btn">
|
||
Suggerisci cosa comprare
|
||
</button>
|
||
<button class="btn btn-secondary" onclick="forceSyncBring()" style="margin-top:4px" data-i18n="shopping.force_sync">
|
||
🔄 Forza sincronizzazione Bring!
|
||
</button>
|
||
<button class="btn btn-secondary" id="btn-fetch-prices" onclick="fetchAllPrices(false)" style="margin-top:4px;display:none" data-i18n="shopping.btn_fetch_prices">
|
||
Cerca i prezzi
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab panel: In previsione -->
|
||
<div id="tab-panel-previsione" class="tab-panel-shopping">
|
||
<!-- Smart shopping predictions -->
|
||
<div class="smart-shopping" id="smart-shopping">
|
||
<div class="smart-shopping-empty" id="smart-shopping-empty" style="display:none">
|
||
<div class="empty-state" style="padding:30px">
|
||
<div class="empty-state-icon">🧠</div>
|
||
<p data-i18n-html="shopping.smart_empty">Nessuna previsione disponibile.<br>Aggiungi prodotti alla dispensa per ricevere previsioni intelligenti.</p>
|
||
</div>
|
||
</div>
|
||
<div id="smart-shopping-content">
|
||
<div class="shopping-section-header" style="margin-bottom:4px">
|
||
<h3 data-i18n="shopping.smart_title">🧠 Previsioni intelligenti</h3>
|
||
<span class="shopping-count" id="smart-count">0</span>
|
||
</div>
|
||
<div class="smart-last-update-row">
|
||
<span id="smart-last-update" class="smart-last-update"></span>
|
||
</div>
|
||
<div class="smart-filter-row" id="smart-filter-row">
|
||
<button class="smart-filter active" data-filter="all" onclick="filterSmart('all')" data-i18n="shopping.smart_filter_all">Tutti</button>
|
||
<button class="smart-filter" data-filter="critical" onclick="filterSmart('critical')" data-i18n="shopping.smart_filter_critical">🔴 Urgenti</button>
|
||
<button class="smart-filter" data-filter="high" onclick="filterSmart('high')" data-i18n="shopping.smart_filter_high">🟠 Presto</button>
|
||
<button class="smart-filter" data-filter="medium" onclick="filterSmart('medium')" data-i18n="shopping.smart_filter_medium">🟡 Pianifica</button>
|
||
<button class="smart-filter" data-filter="low" onclick="filterSmart('low')" data-i18n="shopping.smart_filter_low">🟢 Previsione</button>
|
||
</div>
|
||
<div class="smart-items" id="smart-items"></div>
|
||
<div class="smart-actions" id="smart-actions" style="display:none">
|
||
<button class="btn btn-success full-width" onclick="addSmartToBring()" data-i18n="shopping.smart_add">
|
||
🛒 Aggiungi selezionati a Bring!
|
||
</button>
|
||
</div>
|
||
<div style="text-align:center;margin-top:8px">
|
||
<button class="btn btn-secondary btn-sm" onclick="forceSyncBring()" id="btn-force-sync" data-i18n="shopping.force_sync">
|
||
🔄 Forza sincronizzazione Bring!
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===== AI IDENTIFICATION PAGE ===== -->
|
||
<section class="page" id="page-ai">
|
||
<div class="page-header">
|
||
<button class="back-btn" onclick="stopScanner(); showPage('scan')" data-i18n="btn.back">← Indietro</button>
|
||
<h2 data-i18n="ai.title">🤖 Identificazione AI</h2>
|
||
</div>
|
||
<div class="ai-container">
|
||
<div class="ai-capture" id="ai-capture">
|
||
<video id="ai-video" autoplay playsinline></video>
|
||
<canvas id="ai-canvas" style="display:none"></canvas>
|
||
</div>
|
||
<div class="ai-preview" id="ai-preview" style="display:none">
|
||
<img id="ai-image" src="" alt="Captured">
|
||
</div>
|
||
<div class="ai-actions">
|
||
<button class="btn btn-large btn-accent" onclick="takePhotoForAI()" id="ai-capture-btn" data-i18n="ai.capture">
|
||
📸 Scatta Foto
|
||
</button>
|
||
<button class="btn btn-large btn-secondary" onclick="retakePhotoAI()" id="ai-retake-btn" style="display:none" data-i18n="ai.retake">
|
||
🔄 Riscatta
|
||
</button>
|
||
</div>
|
||
<div class="ai-result" id="ai-result" style="display:none"></div>
|
||
<p class="scan-hint" data-i18n="ai.hint">Scatta una foto del prodotto e l'AI cercherà di identificarlo</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Storico Page -->
|
||
<section id="page-log" class="page">
|
||
<div class="page-header">
|
||
<h2 data-i18n="log.title">📋 Storico</h2>
|
||
</div>
|
||
<div id="log-list" class="log-list"></div>
|
||
<button class="btn btn-secondary full-width mt-2" id="log-load-more" style="display:none" onclick="loadLog(true)" data-i18n="btn.load_more">
|
||
Carica altri...
|
||
</button>
|
||
</section>
|
||
|
||
<!-- ===== SETTINGS PAGE ===== -->
|
||
<section class="page" id="page-settings">
|
||
<div class="page-header">
|
||
<button class="back-btn" onclick="showPage('dashboard')" data-i18n="btn.back">← Indietro</button>
|
||
<h2 data-i18n="settings.title">⚙️ Configurazione</h2>
|
||
</div>
|
||
<div class="settings-tabs">
|
||
<button class="settings-tab active" onclick="switchSettingsTab(this, 'tab-api')" data-tab="tab-api" title="API Keys">🔑</button>
|
||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-bring')" data-tab="tab-bring" title="Bring!">🛒</button>
|
||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-recipe')" data-tab="tab-recipe" title="Ricette">🍳</button>
|
||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-mealplan')" data-tab="tab-mealplan" title="Piano Settimanale">📅</button>
|
||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-appliances')" data-tab="tab-appliances" title="Elettrodomestici">🔌</button>
|
||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-camera')" data-tab="tab-camera" title="Fotocamera">📷</button>
|
||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-security')" data-tab="tab-security" title="Sicurezza">🔒</button>
|
||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-tts')" data-tab="tab-tts" title="Voce (TTS)" data-i18n-title="settings.tab_tts">🔊</button>
|
||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-language')" data-tab="tab-language" title="Lingua" data-i18n-title="settings.tab_language">🌐</button>
|
||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-scale')" data-tab="tab-scale" title="Bilancia Smart" data-i18n-title="settings.scale.tab">⚖️</button>
|
||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-info'); _loadInfoTab();" data-tab="tab-info" data-i18n-title="settings.info.tab" title="Info">ℹ️</button>
|
||
</div>
|
||
<div class="settings-panels">
|
||
<!-- API Keys Tab -->
|
||
<div class="settings-panel active" id="tab-api">
|
||
<div class="settings-card">
|
||
<h4 data-i18n="settings.gemini.title">🤖 Google Gemini AI</h4>
|
||
<p class="settings-hint" data-i18n="settings.gemini.hint">Chiave API per identificazione prodotti, scadenze e ricette.</p>
|
||
<div class="form-group">
|
||
<label data-i18n="settings.gemini.key_label">API Key Gemini</label>
|
||
<input type="password" id="setting-gemini-key" class="form-input" placeholder="AIza...">
|
||
<button class="btn btn-small btn-secondary mt-2" onclick="togglePasswordVisibility('setting-gemini-key')" data-i18n="btn.toggle_password">👁️ Mostra/Nascondi</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Bring! Tab -->
|
||
<div class="settings-panel" id="tab-bring">
|
||
<div class="settings-card">
|
||
<h4 data-i18n="settings.bring.title">🛒 Bring! Shopping List</h4>
|
||
<p class="settings-hint" data-i18n="settings.bring.hint">Credenziali per l'integrazione con la lista della spesa Bring!</p>
|
||
<div class="form-group">
|
||
<label data-i18n="settings.bring.email_label">📧 Email Bring!</label>
|
||
<input type="email" id="setting-bring-email" class="form-input" placeholder="email@esempio.com">
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="settings.bring.password_label">🔒 Password Bring!</label>
|
||
<input type="password" id="setting-bring-password" class="form-input" placeholder="Password">
|
||
<button class="btn btn-small btn-secondary mt-2" onclick="togglePasswordVisibility('setting-bring-password')" data-i18n="btn.toggle_password">👁️ Mostra/Nascondi</button>
|
||
</div>
|
||
</div>
|
||
<!-- Price Estimation Settings -->
|
||
<div class="settings-card" style="margin-top:12px">
|
||
<h4 data-i18n="settings.price.title">💰 Stima Prezzi (AI)</h4>
|
||
<p class="settings-hint" data-i18n="settings.price.hint">Mostra il costo stimato di ogni prodotto nella lista della spesa usando l'AI.</p>
|
||
<div class="form-group">
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.price.enabled_label">Attiva stima prezzi</span>
|
||
<span class="toggle-switch">
|
||
<input type="checkbox" id="setting-price-enabled">
|
||
<span class="toggle-slider"></span>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
<div id="price-settings-sub" style="display:none">
|
||
<div class="form-group">
|
||
<label data-i18n="settings.price.country_label">🌍 Paese di riferimento</label>
|
||
<select id="setting-price-country" class="form-input" onchange="onPriceCountryChange()">
|
||
<option value="Italia">🇮🇹 Italia</option>
|
||
<option value="USA">🇺🇸 USA</option>
|
||
<option value="Germany">🇩🇪 Germania</option>
|
||
<option value="France">🇫🇷 Francia</option>
|
||
<option value="Spain">🇪🇸 Spagna</option>
|
||
<option value="UK">🇬🇧 Regno Unito</option>
|
||
<option value="Switzerland">🇨🇭 Svizzera</option>
|
||
<option value="Austria">🇦🇹 Austria</option>
|
||
<option value="Netherlands">🇳🇱 Olanda</option>
|
||
<option value="Belgium">🇧🇪 Belgio</option>
|
||
<option value="Canada">🇨🇦 Canada</option>
|
||
<option value="Australia">🇦🇺 Australia</option>
|
||
<option value="Brazil">🇧🇷 Brasile</option>
|
||
<option value="Japan">🇯🇵 Giappone</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="settings.price.currency_label">💱 Valuta</label>
|
||
<select id="setting-price-currency" class="form-input">
|
||
<option value="EUR">€ Euro (EUR)</option>
|
||
<option value="USD">$ Dollaro USA (USD)</option>
|
||
<option value="GBP">£ Sterlina (GBP)</option>
|
||
<option value="CHF">CHF Franco Svizzero</option>
|
||
<option value="CAD">CA$ Dollaro Canadese</option>
|
||
<option value="AUD">A$ Dollaro Australiano</option>
|
||
<option value="BRL">R$ Real Brasiliano</option>
|
||
<option value="JPY">¥ Yen Giapponese</option>
|
||
<option value="SEK">kr Corona Svedese</option>
|
||
<option value="NOK">kr Corona Norvegese</option>
|
||
<option value="DKK">kr Corona Danese</option>
|
||
<option value="PLN">zł Zloty Polacco</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="settings.price.update_label">🔄 Aggiorna prezzi ogni</label>
|
||
<div class="qty-control">
|
||
<button type="button" class="qty-btn" onclick="adjustQty('setting-price-update-months', -1, 1, 24)">−</button>
|
||
<input type="number" id="setting-price-update-months" value="3" min="1" max="24" class="qty-input">
|
||
<button type="button" class="qty-btn" onclick="adjustQty('setting-price-update-months', 1, 1, 24)">+</button>
|
||
</div>
|
||
<span class="settings-hint" style="display:inline;margin-left:8px" data-i18n="settings.price.update_suffix">mesi</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Recipe Tab -->
|
||
<div class="settings-panel" id="tab-recipe">
|
||
<div class="settings-card">
|
||
<h4 data-i18n="settings.recipe.title">🍳 Preferenze Ricette</h4>
|
||
<p class="settings-hint" data-i18n="settings.recipe.hint">Configura le opzioni predefinite per la generazione delle ricette.</p>
|
||
<div class="form-group">
|
||
<label data-i18n="settings.recipe.persons_label">👥 Persone predefinite</label>
|
||
<div class="qty-control">
|
||
<button type="button" class="qty-btn" onclick="adjustQty('setting-default-persons', -1)">−</button>
|
||
<input type="number" id="setting-default-persons" value="1" min="1" max="20" class="qty-input">
|
||
<button type="button" class="qty-btn" onclick="adjustQty('setting-default-persons', 1)">+</button>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="settings.recipe.options_label">🎯 Opzioni ricetta predefinite</label>
|
||
<div class="recipe-pref-checks">
|
||
<label class="checkbox-label"><input type="checkbox" id="setting-pref-veloce"> <span data-i18n="settings.recipe.fast">⚡ Pasto Veloce</span></label>
|
||
<label class="checkbox-label"><input type="checkbox" id="setting-pref-pocafame"> <span data-i18n="settings.recipe.light">🥗 Poca Fame</span></label>
|
||
<label class="checkbox-label"><input type="checkbox" id="setting-pref-scadenze"> <span data-i18n="settings.recipe.expiry">⏰ Priorità Scadenze</span></label>
|
||
<label class="checkbox-label"><input type="checkbox" id="setting-pref-healthy"> <span data-i18n="settings.recipe.healthy">💚 Extra Salutare</span></label>
|
||
<label class="checkbox-label"><input type="checkbox" id="setting-pref-opened"> <span data-i18n="settings.recipe.opened">📦 Priorità Cose Aperte</span></label>
|
||
<label class="checkbox-label"><input type="checkbox" id="setting-pref-zerowaste"> <span data-i18n="settings.recipe.zerowaste">♻️ Zero Sprechi</span></label>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="settings.recipe.dietary_label">🚫 Intolleranze / Restrizioni</label>
|
||
<textarea id="setting-dietary" class="form-input" rows="2" placeholder="Es: senza glutine, senza lattosio, vegetariano..."></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Weekly Meal Plan Tab -->
|
||
<div class="settings-panel" id="tab-mealplan">
|
||
<div class="settings-card">
|
||
<h4 data-i18n="settings.mealplan.title">📅 Piano Pasti Settimanale</h4>
|
||
<p class="settings-hint" data-i18n="settings.mealplan.hint">Imposta la tipologia di pasto per ogni giorno. Sarà usata come guida nella generazione delle ricette.</p>
|
||
<div class="form-group" style="margin-bottom:10px">
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.mealplan.enabled">✅ Attiva piano pasti settimanale</span>
|
||
<span class="toggle-switch">
|
||
<input type="checkbox" id="setting-meal-plan-enabled" onchange="onMealPlanEnabledChange(this)">
|
||
<span class="toggle-slider"></span>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
<div id="meal-plan-config-section">
|
||
<div id="meal-plan-grid" class="mplan-grid"></div>
|
||
<div id="meal-plan-picker" class="mplan-picker" style="display:none"></div>
|
||
<div style="margin-top:12px; display:flex; gap:8px; flex-wrap:wrap">
|
||
<button class="btn btn-small btn-secondary" onclick="resetMealPlan()" data-i18n="settings.mealplan.reset_btn">↺ Ripristina default</button>
|
||
</div>
|
||
<div class="settings-hint" style="margin-top:10px" data-i18n-html="settings.mealplan.legend">
|
||
🌤️ = Pranzo · 🌙 = Cena · Tocca un badge per cambiarlo.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="settings-card" id="meal-plan-legend-card">
|
||
<h4 data-i18n="settings.mealplan.types_title">📋 Tipologie disponibili</h4>
|
||
<div class="mplan-legend"></div>
|
||
</div>
|
||
</div>
|
||
<!-- Appliances Tab -->
|
||
<div class="settings-panel" id="tab-appliances">
|
||
<div class="settings-card">
|
||
<h4 data-i18n="settings.appliances.title">🔌 Elettrodomestici Disponibili</h4>
|
||
<p class="settings-hint" data-i18n="settings.appliances.hint">Indica gli elettrodomestici che hai a disposizione. Saranno considerati nella generazione delle ricette.</p>
|
||
<div class="appliances-list" id="appliances-list"></div>
|
||
<div class="form-group mt-2">
|
||
<div class="barcode-input-row">
|
||
<input type="text" id="new-appliance-input" class="form-input" placeholder="Es: Macchina del pane, Bimby, Friggitrice ad aria..." onkeydown="if(event.key==='Enter'){event.preventDefault();addAppliance()}">
|
||
<button class="btn btn-accent" onclick="addAppliance()">➕</button>
|
||
</div>
|
||
</div>
|
||
<div class="common-appliances mt-2">
|
||
<p class="settings-hint" data-i18n="settings.appliances.quick_title">Aggiungi velocemente:</p>
|
||
<div class="appliance-quick-tags">
|
||
<button class="btn btn-small btn-secondary" onclick="addApplianceQuick('Forno')" data-i18n="settings.appliances.oven">🔥 Forno</button>
|
||
<button class="btn btn-small btn-secondary" onclick="addApplianceQuick('Microonde')" data-i18n="settings.appliances.microwave">📡 Microonde</button>
|
||
<button class="btn btn-small btn-secondary" onclick="addApplianceQuick('Friggitrice ad aria')" data-i18n="settings.appliances.air_fryer">🍟 Friggitrice ad aria</button>
|
||
<button class="btn btn-small btn-secondary" onclick="addApplianceQuick('Macchina del pane')" data-i18n="settings.appliances.bread_maker">🍞 Macchina pane</button>
|
||
<button class="btn btn-small btn-secondary" onclick="addApplianceQuick('Bimby/Moulinex Cookeo')" data-i18n="settings.appliances.bimby">🤖 Bimby/Cookeo</button>
|
||
<button class="btn btn-small btn-secondary" onclick="addApplianceQuick('Planetaria')" data-i18n="settings.appliances.mixer">🌀 Planetaria</button>
|
||
<button class="btn btn-small btn-secondary" onclick="addApplianceQuick('Vaporiera')" data-i18n="settings.appliances.steamer">♨️ Vaporiera</button>
|
||
<button class="btn btn-small btn-secondary" onclick="addApplianceQuick('Pentola a pressione')" data-i18n="settings.appliances.pressure_cooker">🫕 Pentola pressione</button>
|
||
<button class="btn btn-small btn-secondary" onclick="addApplianceQuick('Tostapane')" data-i18n="settings.appliances.toaster">🍞 Tostapane</button>
|
||
<button class="btn btn-small btn-secondary" onclick="addApplianceQuick('Frullatore/Mixer')" data-i18n="settings.appliances.blender">🍹 Frullatore</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Camera Tab -->
|
||
<div class="settings-panel" id="tab-camera">
|
||
<div class="settings-card">
|
||
<h4 data-i18n="settings.camera.title">📷 Fotocamera</h4>
|
||
<p class="settings-hint" data-i18n="settings.camera.hint">Scegli quale fotocamera utilizzare per la scansione barcode e l'identificazione AI.</p>
|
||
<div class="form-group">
|
||
<label data-i18n="settings.camera.device_label">📸 Fotocamera predefinita</label>
|
||
<select id="setting-camera-facing" class="form-input">
|
||
<option value="environment" data-i18n="settings.camera.back">📱 Posteriore (default)</option>
|
||
<option value="user" data-i18n="settings.camera.front">🤳 Anteriore</option>
|
||
</select>
|
||
<p class="settings-hint mt-2" data-i18n="settings.camera.devices_hint">Se hai più fotocamere, puoi selezionarne una specifica dall'elenco sopra dopo aver concesso i permessi.</p>
|
||
<button class="btn btn-small btn-secondary mt-2" onclick="loadCameraDevices()" data-i18n="settings.camera.detect_btn">🔄 Rileva fotocamere</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Security Tab -->
|
||
<div class="settings-panel" id="tab-security">
|
||
<div class="settings-card">
|
||
<h4 data-i18n="settings.security.token_title">🔑 Token Impostazioni</h4>
|
||
<p class="settings-hint" data-i18n="settings.security.token_hint">Se <code>SETTINGS_TOKEN</code> è configurato nel <code>.env</code> server, inserisci qui il token prima di salvare le impostazioni. Lascia vuoto se non configurato.</p>
|
||
<div class="form-group">
|
||
<label data-i18n="settings.security.token_label">Token di accesso</label>
|
||
<input type="password" id="setting-settings-token" class="form-input" placeholder="(vuoto = nessuna protezione)" data-i18n-placeholder="settings.security.token_placeholder">
|
||
<button class="btn btn-small btn-secondary mt-2" onclick="togglePasswordVisibility('setting-settings-token')" data-i18n="btn.toggle_password">👁️ Mostra/Nascondi</button>
|
||
</div>
|
||
<p class="settings-hint" id="settings-token-status-hint" style="display:none;color:var(--accent)" data-i18n="settings.security.token_required_hint">🔒 Questo server richiede un token per salvare le impostazioni.</p>
|
||
</div>
|
||
<div class="settings-card">
|
||
<h4 data-i18n="settings.security.title">🔒 Certificato HTTPS</h4>
|
||
<p class="settings-hint" data-i18n="settings.security.hint">Se il browser mostra l'errore "La connessione non è privata" (ERR_CERT_AUTHORITY_INVALID), devi installare il certificato CA nel dispositivo.</p>
|
||
<div class="form-group">
|
||
<a href="ca.crt" download="EverShelf_CA.crt" class="btn btn-large btn-accent full-width" style="text-align:center;text-decoration:none;display:block" data-i18n="settings.security.download_btn">📥 Scarica Certificato CA</a>
|
||
</div>
|
||
<div class="settings-hint" style="margin-top:12px;line-height:1.6" data-i18n-html="settings.security.cert_instructions">
|
||
<strong>Istruzioni per Chrome (Android):</strong><br>
|
||
1. Scarica il certificato qui sopra<br>
|
||
2. Vai in <em>Impostazioni → Sicurezza e privacy → Altre impostazioni di sicurezza → Installa da archivio dispositivo</em><br>
|
||
3. Seleziona il file <em>EverShelf_CA.crt</em> scaricato<br>
|
||
4. Scegli "CA" e conferma<br>
|
||
5. Riavvia Chrome<br><br>
|
||
<strong>Istruzioni per Chrome (PC):</strong><br>
|
||
1. Scarica il certificato qui sopra<br>
|
||
2. Vai in <em>chrome://settings/certificates</em> (o Impostazioni → Privacy e sicurezza → Sicurezza → Gestisci certificati)<br>
|
||
3. Tab "Autorità" → Importa → seleziona il file<br>
|
||
4. Spunta "Considera attendibile per identificare siti web"<br>
|
||
5. Riavvia Chrome
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- TTS Tab -->
|
||
<div class="settings-panel" id="tab-tts">
|
||
<div class="settings-card">
|
||
<h4 data-i18n="settings.tts.title">🔊 Voce & TTS</h4>
|
||
<p class="settings-hint" data-i18n="settings.tts.hint">Configura la sintesi vocale. Puoi usare la voce offline del browser oppure un endpoint REST esterno (Home Assistant, ecc.).</p>
|
||
<div class="form-group" style="margin-bottom:10px">
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.tts.enabled">✅ Attiva TTS</span>
|
||
<span class="toggle-switch">
|
||
<input type="checkbox" id="setting-tts-enabled">
|
||
<span class="toggle-slider"></span>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="settings.tts.engine_label">⚙️ Motore TTS</label>
|
||
<select id="setting-tts-engine" class="form-input" onchange="onTtsEngineChange(this.value)">
|
||
<option value="browser" data-i18n="settings.tts.engine_browser">🔇 Browser (offline, nessuna configurazione)</option>
|
||
<option value="server" data-i18n="settings.tts.engine_server">🌐 Server esterno (Home Assistant, API REST...)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Browser TTS section -->
|
||
<div id="tts-browser-section">
|
||
<div class="form-group">
|
||
<label data-i18n="settings.tts.voice_label">🗣️ Voce</label>
|
||
<div style="display:flex;gap:8px;align-items:center">
|
||
<select id="setting-tts-voice" class="form-input" style="flex:1">
|
||
<option value="" data-i18n="settings.tts.voices_loading">— Caricamento voci… —</option>
|
||
</select>
|
||
<button type="button" class="btn btn-secondary" style="padding:8px 12px;white-space:nowrap;flex-shrink:0" onclick="_initBrowserTtsVoices(document.getElementById('setting-tts-voice').value)">↺</button>
|
||
</div>
|
||
<p class="settings-hint" data-i18n="settings.tts.voices_hint">Le voci disponibili dipendono dal sistema operativo e dal browser. Su macOS/iOS è disponibile la voce <strong>Paola</strong> (italiano). Premi ↺ se la lista non si carica.</p>
|
||
</div>
|
||
<div class="form-group">
|
||
<label><span data-i18n="settings.tts.rate_label">⚡ Velocità</span>: <span id="tts-rate-label">1.0</span>×</label>
|
||
<input type="range" id="setting-tts-rate" class="form-input" min="0.5" max="2" step="0.1" value="1" oninput="document.getElementById('tts-rate-label').textContent=parseFloat(this.value).toFixed(1)">
|
||
</div>
|
||
<div class="form-group">
|
||
<label><span data-i18n="settings.tts.pitch_label">🎵 Tono</span>: <span id="tts-pitch-label">1.0</span></label>
|
||
<input type="range" id="setting-tts-pitch" class="form-input" min="0" max="2" step="0.1" value="1" oninput="document.getElementById('tts-pitch-label').textContent=parseFloat(this.value).toFixed(1)">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Server TTS section -->
|
||
<div id="tts-server-section" style="display:none">
|
||
<div class="form-group">
|
||
<label data-i18n="settings.tts.url_label">🌐 URL Endpoint</label>
|
||
<input type="url" id="setting-tts-url" class="form-input" placeholder="https://...">
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="settings.tts.method_label">📡 Metodo HTTP</label>
|
||
<select id="setting-tts-method" class="form-input">
|
||
<option value="POST">POST</option>
|
||
<option value="PUT">PUT</option>
|
||
<option value="PATCH">PATCH</option>
|
||
<option value="GET">GET</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="settings.tts.auth_label">🔐 Autenticazione</label>
|
||
<select id="setting-tts-auth-type" class="form-input" onchange="onTtsAuthTypeChange(this.value)">
|
||
<option value="bearer" data-i18n="settings.tts.auth_bearer">Bearer Token</option>
|
||
<option value="header" data-i18n="settings.tts.auth_custom">Header personalizzato</option>
|
||
<option value="none" data-i18n="settings.tts.auth_none">Nessuna</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group" id="tts-token-group">
|
||
<label data-i18n="settings.tts.token_label">🔑 Bearer Token</label>
|
||
<input type="password" id="setting-tts-token" class="form-input" placeholder="eyJhbGci...">
|
||
<button class="btn btn-small btn-secondary mt-2" onclick="togglePasswordVisibility('setting-tts-token')" data-i18n="btn.toggle_password">👁️ Mostra/Nascondi</button>
|
||
</div>
|
||
<div id="tts-custom-header-group" style="display:none">
|
||
<div class="form-group">
|
||
<label data-i18n="settings.tts.custom_header_name">📋 Nome header</label>
|
||
<input type="text" id="setting-tts-auth-header-name" class="form-input" placeholder="X-API-Key">
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="settings.tts.custom_header_value">📋 Valore header</label>
|
||
<input type="text" id="setting-tts-auth-header-value" class="form-input" placeholder="...">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="settings.tts.content_type_label">📄 Content-Type</label>
|
||
<select id="setting-tts-content-type" class="form-input">
|
||
<option value="application/json">application/json</option>
|
||
<option value="application/x-www-form-urlencoded">application/x-www-form-urlencoded</option>
|
||
<option value="text/plain">text/plain</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="settings.tts.payload_key_label">🗝️ Campo testo nel payload</label>
|
||
<input type="text" id="setting-tts-payload-key" class="form-input" placeholder="message">
|
||
<p class="settings-hint">Nome del campo JSON che conterrà il testo da leggere (es: <code>message</code>, <code>text</code>).</p>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="settings.tts.extra_fields_label">➕ Campi extra (JSON)</label>
|
||
<textarea id="setting-tts-extra-fields" class="form-input" rows="3" placeholder='{"entity_id": "media_player.living_room"}'></textarea>
|
||
<p class="settings-hint" data-i18n="settings.tts.extra_fields_hint">Campi aggiuntivi da includere nel payload, in formato JSON. Lascia vuoto se non necessario.</p>
|
||
</div>
|
||
</div><!-- /tts-server-section -->
|
||
|
||
<button class="btn btn-large btn-accent full-width mt-2" onclick="testTTS()" data-i18n="settings.tts.test_btn">🔊 Invia Test Vocale</button>
|
||
<div id="tts-test-status" style="display:none;margin-top:8px"></div>
|
||
</div>
|
||
</div>
|
||
<!-- Scale Tab -->
|
||
<div class="settings-panel" id="tab-scale">
|
||
<div class="settings-card">
|
||
<h4 data-i18n="settings.scale.title">⚖️ Bilancia Smart</h4>
|
||
<p class="settings-hint" data-i18n="settings.scale.hint">Collega una bilancia Bluetooth tramite il gateway Android per leggere il peso automaticamente.</p>
|
||
|
||
<!-- Kiosk-mode panel: replace WebSocket config with native reconfigure button -->
|
||
<div id="scale-kiosk-panel" style="display:none;background:rgba(16,185,129,0.07);border:1px solid rgba(16,185,129,0.25);border-radius:10px;padding:14px;margin-bottom:16px">
|
||
<p style="margin:0 0 6px;font-weight:600" data-i18n="settings.scale.kiosk_title">📡 Bilancia BLE integrata nel Kiosk</p>
|
||
<p class="settings-hint" style="margin-bottom:12px" data-i18n="settings.scale.kiosk_hint">La bilancia è gestita direttamente dal Gateway BLE interno al kiosk. Per abbinare un nuovo dispositivo usa il wizard di configurazione.</p>
|
||
<button class="btn btn-secondary full-width" onclick="_kioskReconfigureScale()" data-i18n="settings.scale.kiosk_reconfigure">🔄 Riconfigura bilancia BLE</button>
|
||
<!-- shown when kiosk APK is too old to have reconfigureScale() -->
|
||
<div id="kiosk-needs-update-notice" style="display:none;margin-top:10px;padding:8px 12px;background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.35);border-radius:8px;font-size:0.83rem">
|
||
<span data-i18n="settings.kiosk.needs_update">⚠️ Il kiosk installato non supporta questa funzione.
|
||
Aggiorna l'app kiosk per abilitarla.</span>
|
||
<a href="https://github.com/dadaloop82/EverShelf/releases/download/kiosk-latest/evershelf-kiosk.apk" target="_blank" rel="noopener noreferrer" style="display:block;margin-top:6px;color:#d97706;font-weight:600;text-decoration:none" data-i18n="settings.kiosk.download_btn">📥 Scarica aggiornamento kiosk</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Download gateway app -->
|
||
<div id="scale-gateway-download-section" style="background:rgba(124,58,237,0.07);border:1px solid rgba(124,58,237,0.2);border-radius:10px;padding:14px;margin-bottom:16px">
|
||
<p style="margin:0 0 4px;font-weight:600">📱 EverShelf Scale Gateway</p>
|
||
<p class="settings-hint" style="margin-bottom:10px" data-i18n="settings.scale.download_hint">App Android che fa da ponte tra la bilancia BLE e questo sito.</p>
|
||
<a href="https://github.com/dadaloop82/EverShelf/releases/latest/download/evershelf-scale-gateway.apk" target="_blank" rel="noopener noreferrer" class="btn btn-large btn-accent full-width" style="text-decoration:none;display:block;text-align:center" data-i18n="settings.scale.download_btn">📥 Scarica Gateway Android (APK)</a>
|
||
<p class="settings-hint" style="margin-top:8px" data-i18n="settings.scale.download_sub">Sorgente: <code>evershelf-scale-gateway/</code> nella root del progetto</p>
|
||
</div>
|
||
|
||
<!-- Enable toggle -->
|
||
<div class="form-group" style="margin-bottom:10px">
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.scale.enabled">✅ Abilita bilancia smart</span>
|
||
<span class="toggle-switch">
|
||
<input type="checkbox" id="setting-scale-enabled" onchange="onScaleEnabledChange()">
|
||
<span class="toggle-slider"></span>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
|
||
<!-- Gateway URL -->
|
||
<div id="scale-websocket-section" class="form-group">
|
||
<label data-i18n="settings.scale.url_label">🌐 URL Gateway WebSocket</label>
|
||
<div style="display:flex;gap:8px;align-items:center">
|
||
<input type="url" id="setting-scale-url" class="form-input" style="flex:1" placeholder="ws://192.168.1.x:8765" data-i18n-placeholder="settings.scale.url_placeholder">
|
||
<button class="btn btn-secondary" style="white-space:nowrap;flex-shrink:0" onclick="discoverScaleGateway()" id="btn-scale-discover" title="Scan local network">🔍 Auto</button>
|
||
</div>
|
||
<div id="scale-discover-status" style="display:none;margin-top:6px;font-size:0.82rem;color:var(--text-secondary)"></div>
|
||
<p class="settings-hint" data-i18n="settings.scale.url_hint">Copia l'URL mostrato dall'app Android (stessa rete Wi-Fi). Es: <code>ws://192.168.1.100:8765</code></p>
|
||
</div>
|
||
|
||
<!-- Test button -->
|
||
<div id="scale-test-section">
|
||
<button class="btn btn-secondary full-width mt-2" onclick="testScaleConnection()" data-i18n="settings.scale.test_btn">🔗 Testa connessione</button>
|
||
<div id="scale-test-status" style="display:none;margin-top:8px" class="settings-status"></div>
|
||
</div>
|
||
|
||
<!-- Live diagnostic: device info + real-time weight (visible when connected) -->
|
||
<div id="scale-live-diag" style="display:none;margin-top:14px;padding:12px 14px;background:var(--bg-secondary,#f8fafc);border-radius:10px;border:1px solid var(--border,#e2e8f0)">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px">
|
||
<span style="font-size:0.8rem;color:var(--text-secondary)">🔗 <span id="scale-diag-device">—</span></span>
|
||
<span id="scale-diag-battery" style="font-size:0.8rem;color:var(--text-secondary)"></span>
|
||
</div>
|
||
<div style="text-align:center">
|
||
<div id="scale-diag-weight" style="font-size:2rem;font-weight:700;line-height:1;letter-spacing:1px">— g</div>
|
||
<div style="font-size:0.72rem;color:var(--text-secondary);margin-top:3px" data-i18n="settings.scale.live_weight">peso in tempo reale</div>
|
||
</div>
|
||
<div style="margin-top:10px;display:flex;gap:8px;font-size:0.78rem;color:var(--text-secondary)">
|
||
<span data-i18n="settings.scale.auto_reconnect">🔁 Riconnessione: automatica</span>
|
||
<span style="margin-left:auto" id="scale-diag-proto">—</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Protocol info -->
|
||
<div class="settings-hint" style="margin-top:16px;padding:10px;background:var(--bg-secondary,#f8fafc);border-radius:8px" data-i18n-html="settings.scale.ble_protocols">
|
||
<p style="margin:0 0 6px;font-weight:600">🔌 Protocolli BLE supportati:</p>
|
||
<ul style="margin:0 0 0 16px;padding:0;font-size:0.8rem">
|
||
<li>Bluetooth SIG Weight Scale (0x181D)</li>
|
||
<li>Bluetooth SIG Body Composition (0x181B) — peso, grasso, BMI</li>
|
||
<li>Xiaomi Mi Body Composition Scale 2</li>
|
||
<li>Generico — heuristica automatica su 100+ modelli</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Language Tab -->
|
||
<div class="settings-panel" id="tab-language">
|
||
<div class="settings-card">
|
||
<h4 data-i18n="settings.language.title">🌐 Lingua / Language</h4>
|
||
<p class="settings-hint" data-i18n="settings.language.hint">Seleziona la lingua dell'interfaccia. Select the interface language.</p>
|
||
<div class="form-group">
|
||
<label data-i18n="settings.language.label">🌐 Lingua</label>
|
||
<select id="setting-language" class="form-input" onchange="changeLanguage(this.value)">
|
||
</select>
|
||
<p class="settings-hint mt-2" data-i18n="settings.language.restart_notice">La pagina verrà ricaricata per applicare la nuova lingua.</p>
|
||
</div>
|
||
</div>
|
||
<div class="settings-card">
|
||
<h4 data-i18n="settings.screensaver.card_title">🌙 Salvaschermo</h4>
|
||
<p class="settings-hint" data-i18n="settings.screensaver.card_hint">Mostra un orologio con fatti utili dopo un periodo di inattività. Di default è disattivato.</p>
|
||
<div class="form-group">
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.screensaver.label">Attiva salvaschermo</span>
|
||
<span class="toggle-switch">
|
||
<input type="checkbox" id="setting-screensaver-enabled">
|
||
<span class="toggle-slider"></span>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
<div class="form-group" id="screensaver-timeout-row" style="margin-top:10px">
|
||
<label for="setting-screensaver-timeout" style="font-size:0.85rem;font-weight:600;color:var(--text-secondary)" data-i18n="settings.screensaver.start_after">⏱️ Avvia dopo</label>
|
||
<select id="setting-screensaver-timeout" class="form-control" style="margin-top:6px;max-width:200px">
|
||
<option value="1" data-i18n="settings.screensaver.timeout_1">1 minuto</option>
|
||
<option value="2" data-i18n="settings.screensaver.timeout_2">2 minuti</option>
|
||
<option value="5" selected data-i18n="settings.screensaver.timeout_5">5 minuti</option>
|
||
<option value="10" data-i18n="settings.screensaver.timeout_10">10 minuti</option>
|
||
<option value="15" data-i18n="settings.screensaver.timeout_15">15 minuti</option>
|
||
<option value="30" data-i18n="settings.screensaver.timeout_30">30 minuti</option>
|
||
<option value="60" data-i18n="settings.screensaver.timeout_60">1 ora</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="settings-card">
|
||
<h4 data-i18n="settings.theme.title">🌙 Tema / Aspetto</h4>
|
||
<p class="settings-hint" data-i18n="settings.theme.hint">Scegli il tema dell'interfaccia.</p>
|
||
<div class="form-group">
|
||
<label data-i18n="settings.theme.label">🌙 Tema</label>
|
||
<select id="setting-dark-mode" class="form-input" onchange="_setThemeMode(this.value)">
|
||
<option value="off" data-i18n="settings.theme.off">☀️ Chiaro</option>
|
||
<option value="auto" selected data-i18n="settings.theme.auto">🔄 Automatico (sistema)</option>
|
||
<option value="on" data-i18n="settings.theme.on">🌙 Scuro</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="settings-card">
|
||
<h4 data-i18n="settings.zerowaste.card_title">♻️ Suggerimenti zero-waste</h4>
|
||
<p class="settings-hint" data-i18n="settings.zerowaste.card_hint">Durante la cottura, mostra consigli su come riutilizzare gli scarti prodotti in ogni passo (bucce, acqua di cottura, ecc.). Disattivo per impostazione predefinita.</p>
|
||
<div class="form-group">
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.zerowaste.label">Mostra suggerimenti durante la cottura</span>
|
||
<span class="toggle-switch">
|
||
<input type="checkbox" id="setting-zerowaste-tips">
|
||
<span class="toggle-slider"></span>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="settings-card">
|
||
<h4 data-i18n="export.title">📤 Esporta inventario</h4>
|
||
<p class="settings-hint" data-i18n="export.hint">Scarica l'inventario corrente in CSV o apri una versione stampabile (PDF).</p>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||
<button class="btn btn-outline" onclick="exportInventory('csv')" style="flex:1;min-width:120px">
|
||
📊 <span data-i18n="export.btn_csv">CSV</span>
|
||
</button>
|
||
<button class="btn btn-outline" onclick="exportInventory('html')" style="flex:1;min-width:120px">
|
||
🖨️ <span data-i18n="export.btn_pdf">PDF / Stampa</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Info Tab -->
|
||
<div class="settings-panel" id="tab-info">
|
||
<!-- Gemini AI Usage card -->
|
||
<div class="settings-card">
|
||
<h4 data-i18n="settings.info.ai_title">Gemini AI — Token Usage</h4>
|
||
<p class="settings-hint info-ai-subtitle" data-i18n="settings.info.ai_overview">Utilizzo AI, inventario e sistema</p>
|
||
<div id="info-ai-content" style="margin-top:10px">
|
||
<p class="settings-hint" data-i18n="settings.info.loading">Caricamento…</p>
|
||
</div>
|
||
</div>
|
||
<!-- Inventory card -->
|
||
<div class="settings-card">
|
||
<h4 data-i18n="settings.info.inv_title">Inventario</h4>
|
||
<div id="info-inv-content" style="margin-top:10px">
|
||
<p class="settings-hint" data-i18n="settings.info.loading">Caricamento…</p>
|
||
</div>
|
||
</div>
|
||
<!-- Activity card -->
|
||
<div class="settings-card">
|
||
<h4 data-i18n="settings.info.act_title">Attività del mese</h4>
|
||
<div id="info-act-content" style="margin-top:10px">
|
||
<p class="settings-hint" data-i18n="settings.info.loading">Caricamento…</p>
|
||
</div>
|
||
</div>
|
||
<!-- System Info card -->
|
||
<div class="settings-card">
|
||
<h4 data-i18n="settings.info.system_title">Sistema</h4>
|
||
<div id="info-system-content" style="margin-top:10px">
|
||
<p class="settings-hint" data-i18n="settings.info.loading">Caricamento…</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Kiosk app download banner (hidden inside kiosk WebView) -->
|
||
<div id="kiosk-download-banner" style="background:linear-gradient(135deg,rgba(16,185,129,0.08),rgba(5,150,105,0.12));border:1.5px solid rgba(16,185,129,0.25);border-radius:12px;padding:16px;margin-top:16px">
|
||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px">
|
||
<span style="font-size:1.6rem">📺</span>
|
||
<div>
|
||
<p style="margin:0;font-weight:700;font-size:0.95rem;color:#065f46">EverShelf Kiosk</p>
|
||
<p class="settings-hint" style="margin:2px 0 0" data-i18n="settings.kiosk.hint">Trasforma un tablet Android in un pannello EverShelf sempre acceso, con bilancia BLE integrata.</p>
|
||
</div>
|
||
</div>
|
||
<a href="https://github.com/dadaloop82/EverShelf/releases/download/kiosk-latest/evershelf-kiosk.apk" target="_blank" rel="noopener noreferrer" class="btn btn-large btn-accent full-width" style="text-decoration:none;display:block;text-align:center;background:linear-gradient(135deg,#059669,#10b981);color:#fff" data-i18n="settings.kiosk.download_btn">📥 Scarica EverShelf Kiosk (APK)</a>
|
||
<p class="settings-hint" style="margin-top:8px" data-i18n="settings.kiosk.download_sub">Modalità kiosk full-screen + gateway bilancia integrato. Sorgente: <code>evershelf-kiosk/</code></p>
|
||
</div>
|
||
|
||
<!-- Kiosk native settings panel (visible only inside kiosk WebView) -->
|
||
<div id="kiosk-native-settings-panel" style="display:none;background:rgba(99,102,241,0.06);border:1.5px solid rgba(99,102,241,0.2);border-radius:12px;padding:16px;margin-top:16px">
|
||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">
|
||
<span style="font-size:1.4rem">🖥️</span>
|
||
<div>
|
||
<p style="margin:0;font-weight:700;font-size:0.9rem" data-i18n="settings.kiosk.native_title">Configurazione Kiosk</p>
|
||
<p class="settings-hint" style="margin:2px 0 0" data-i18n="settings.kiosk.native_hint">URL server, bilancia BLE, salvaschermo e setup wizard.</p>
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-secondary full-width" onclick="_openKioskNativeSettings()">⚙️ <span data-i18n="settings.kiosk.native_btn">Apri configurazione kiosk</span></button>
|
||
</div>
|
||
|
||
<!-- Kiosk self-update panel (visible only inside kiosk WebView) -->
|
||
<div id="kiosk-update-panel" style="display:none;background:rgba(16,185,129,0.06);border:1.5px solid rgba(16,185,129,0.2);border-radius:12px;padding:16px;margin-top:16px">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px">
|
||
<div style="display:flex;align-items:center;gap:8px">
|
||
<span style="font-size:1.4rem">📦</span>
|
||
<div>
|
||
<p style="margin:0;font-weight:700;font-size:0.9rem" data-i18n="settings.kiosk.update_title">Aggiornamento Kiosk</p>
|
||
<p class="settings-hint" style="margin:2px 0 0" id="kiosk-update-version-label">—</p>
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-secondary" style="white-space:nowrap;min-width:120px" id="btn-kiosk-check-update" onclick="_kioskCheckForUpdates()" data-i18n="settings.kiosk.check_updates_btn">🔍 Cerca aggiornamenti</button>
|
||
</div>
|
||
<div id="kiosk-update-status" style="display:none;padding:10px 12px;border-radius:8px;font-size:0.85rem;line-height:1.4"></div>
|
||
<button id="btn-kiosk-install-update" style="display:none;width:100%;margin-top:10px" class="btn btn-accent btn-large" onclick="_kioskInstallUpdate()">⬇️ Installa aggiornamento</button>
|
||
</div>
|
||
|
||
<button class="btn btn-large btn-success full-width mt-2" onclick="saveSettings()" data-i18n="btn.save_config">💾 Salva Configurazione</button>
|
||
<div id="settings-status" class="settings-status" style="display:none"></div>
|
||
|
||
<!-- About & Support -->
|
||
<div class="settings-section" style="margin-top:24px">
|
||
<h3 class="settings-section-title" data-i18n="about.title">About</h3>
|
||
<div class="settings-row" style="justify-content:space-between;align-items:center">
|
||
<span class="settings-label" data-i18n="about.version">Version</span>
|
||
<span id="about-version-label" class="settings-hint" style="font-family:monospace">—</span>
|
||
</div>
|
||
<div style="margin-top:10px;display:flex;flex-direction:column;gap:8px">
|
||
<button class="btn btn-outline full-width" onclick="reportBugManual()" id="btn-report-bug">
|
||
🐛 <span data-i18n="about.report_bug">Segnala un problema</span>
|
||
</button>
|
||
<p class="settings-hint" style="text-align:center;margin:0" data-i18n="about.report_bug_hint">Qualcosa non funziona? Inviaci una segnalazione direttamente dall'app.</p>
|
||
<div style="display:flex;gap:8px">
|
||
<a class="btn btn-outline full-width" style="text-decoration:none;text-align:center"
|
||
href="https://github.com/dadaloop82/EverShelf/blob/main/CHANGELOG.md"
|
||
target="_blank" rel="noopener" data-i18n="about.changelog">Changelog</a>
|
||
<a class="btn btn-outline full-width" style="text-decoration:none;text-align:center"
|
||
href="https://github.com/dadaloop82/EverShelf"
|
||
target="_blank" rel="noopener" data-i18n="about.github">GitHub</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===== GEMINI CHAT ===== -->
|
||
<section class="page" id="page-chat">
|
||
<div class="chat-container">
|
||
<div class="chat-header-bar">
|
||
<div class="chat-header-info">
|
||
<svg class="gemini-icon-sm" viewBox="0 0 24 24" width="22" height="22" fill="#6366f1"><path d="M12 0C12 6.627 6.627 12 0 12c6.627 0 12 5.373 12 12 0-6.627 5.373-12 12-12-6.627 0-12-5.373-12-12z"/></svg>
|
||
<span class="chat-title" data-i18n="chat.title">Gemini Chef</span>
|
||
</div>
|
||
<button class="btn-chat-clear" onclick="clearChat()" title="Nuova conversazione" data-i18n-title="chat.clear">🗑️</button>
|
||
</div>
|
||
<div class="chat-messages" id="chat-messages">
|
||
<div class="chat-welcome">
|
||
<svg class="gemini-icon-lg" viewBox="0 0 24 24" width="48" height="48" fill="#6366f1"><path d="M12 0C12 6.627 6.627 12 0 12c6.627 0 12 5.373 12 12 0-6.627 5.373-12 12-12-6.627 0-12-5.373-12-12z"/></svg>
|
||
<h3 data-i18n="chat.welcome">Ciao! Sono il tuo assistente cucina</h3>
|
||
<p data-i18n="chat.welcome_desc">Chiedimi di prepararti un succo, uno spuntino, un piatto veloce... Conosco la tua dispensa, i tuoi elettrodomestici e le tue preferenze!</p>
|
||
<div class="chat-suggestions">
|
||
<button class="chat-suggestion" onclick="sendChatSuggestion('Cosa posso preparare per uno spuntino veloce?')" data-i18n="chat.suggestion_snack">🍿 Spuntino veloce</button>
|
||
<button class="chat-suggestion" onclick="sendChatSuggestion('Fammi un succo o frullato con quello che ho')" data-i18n="chat.suggestion_juice">🥤 Succo/Frullato</button>
|
||
<button class="chat-suggestion" onclick="sendChatSuggestion('Ho fame ma voglio qualcosa di leggero')" data-i18n="chat.suggestion_light">🥗 Qualcosa di leggero</button>
|
||
<button class="chat-suggestion" onclick="sendChatSuggestion('Cosa sta per scadere e come posso usarlo?')" data-i18n="chat.suggestion_expiry">⏰ Usa le scadenze</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="chat-input-bar">
|
||
<input type="text" id="chat-input" class="chat-input" placeholder="Chiedi qualcosa..." onkeydown="if(event.key==='Enter')sendChatMessage()" data-i18n-placeholder="chat.placeholder">
|
||
<button class="btn-chat-send" id="btn-chat-send" onclick="sendChatMessage()">
|
||
<svg viewBox="0 0 24 24" width="22" height="22" fill="white"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
</main>
|
||
|
||
<!-- Bottom Navigation -->
|
||
<nav class="bottom-nav">
|
||
<button class="nav-btn" onclick="showPage('dashboard')" data-page="dashboard">
|
||
<span class="nav-icon"><img src="assets/img/logo/logo_icon.png" alt="" class="nav-logo-icon" /></span>
|
||
<span class="nav-label" data-i18n="nav.home">Home</span>
|
||
</button>
|
||
<button class="nav-btn" onclick="showPage('inventory', '')" data-page="inventory">
|
||
<span class="nav-icon">📋</span>
|
||
<span class="nav-label" data-i18n="nav.inventory">Dispensa</span>
|
||
</button>
|
||
<button class="nav-btn" onclick="showPage('recipe')" data-page="recipe">
|
||
<span class="nav-icon">🍳</span>
|
||
<span class="nav-label" data-i18n="nav.recipes">Ricette</span>
|
||
</button>
|
||
<button class="nav-btn" onclick="showPage('shopping')" data-page="shopping">
|
||
<span class="nav-icon">🛒</span>
|
||
<span class="nav-label" data-i18n="nav.shopping">Spesa</span>
|
||
</button>
|
||
<button class="nav-btn" onclick="showPage('log')" data-page="log">
|
||
<span class="nav-icon">📋</span>
|
||
<span class="nav-label" data-i18n="nav.log">Storico</span>
|
||
</button>
|
||
<button class="nav-btn" onclick="showPage('settings')" data-page="settings">
|
||
<span class="nav-icon">⚙️</span>
|
||
<span class="nav-label" data-i18n="nav.settings">Config</span>
|
||
</button>
|
||
</nav>
|
||
|
||
<!-- Recipe Dialog -->
|
||
<div class="modal-overlay" id="recipe-overlay" style="display:none" onclick="closeRecipeDialog()">
|
||
<div class="modal-content recipe-dialog" onclick="event.stopPropagation()">
|
||
<div id="recipe-mealplan-banner" class="recipe-mealplan-banner" style="display:none"></div>
|
||
<div id="recipe-ask" class="recipe-ask">
|
||
<h3 id="recipe-meal-title" data-i18n="recipes.dialog_title">🍳 Ricetta</h3>
|
||
<p class="recipe-desc" data-i18n="recipes.dialog_desc">Genero una ricetta sana con gli ingredienti in dispensa, dando priorità a quelli in scadenza.</p>
|
||
<div class="form-group" style="text-align:left">
|
||
<label data-i18n="recipes.meal_label">🕐 Per quale pasto?</label>
|
||
<div class="recipe-meal-grid" id="recipe-meal-grid" onchange="updateRecipeMealTitle()"></div>
|
||
<div class="recipe-meal-grid recipe-subtype-grid" id="recipe-subtype-group" style="display:none"></div>
|
||
</div>
|
||
<div id="recipe-mealplan-hint" class="recipe-mealplan-hint" style="display:none"></div>
|
||
<div class="form-group">
|
||
<label data-i18n="recipes.persons_label">👥 Quante persone?</label>
|
||
<div class="qty-control">
|
||
<button type="button" class="qty-btn" onclick="adjustRecipePersons(-1)">−</button>
|
||
<input type="number" id="recipe-persons" value="1" min="1" max="20" class="qty-input">
|
||
<button type="button" class="qty-btn" onclick="adjustRecipePersons(1)">+</button>
|
||
</div>
|
||
</div>
|
||
<div class="form-group" style="text-align:left">
|
||
<label data-i18n="recipes.meal_type_label">🎯 Tipo di pasto</label>
|
||
<div class="recipe-options-grid">
|
||
<label class="recipe-option-chip recipe-opt-mealplan-chip" id="recipe-opt-mealplan-wrap" style="display:none"><input type="checkbox" id="recipe-opt-mealplan" checked onchange="onMealPlanChipChange(this)"> <span id="recipe-opt-mealplan-label"></span></label>
|
||
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-veloce"> <span data-i18n="recipes.opt_fast">⚡ Pasto Veloce</span></label>
|
||
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-pocafame"> <span data-i18n="recipes.opt_light">🥗 Poca Fame</span></label>
|
||
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-scadenze"> <span data-i18n="recipes.opt_expiry">⏰ Priorità Scadenze</span></label>
|
||
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-healthy"> <span data-i18n="recipes.opt_healthy">💚 Extra Salutare</span></label>
|
||
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-opened"> <span data-i18n="recipes.opt_opened">📦 Priorità Cose Aperte</span></label>
|
||
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-zerowaste"> <span data-i18n="recipes.opt_zero_waste">♻️ Zero Sprechi</span></label>
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-large btn-success full-width" onclick="generateRecipe()" data-i18n="recipes.generate_btn">
|
||
✨ Genera Ricetta
|
||
</button>
|
||
<button class="btn btn-large btn-secondary full-width mt-2" onclick="closeRecipeDialog()" data-i18n="btn.cancel">
|
||
Annulla
|
||
</button>
|
||
</div>
|
||
<div id="recipe-loading" style="display:none" class="recipe-loading">
|
||
<div class="loading-spinner"></div>
|
||
<p id="recipe-loading-msg" data-i18n="recipes.loading_msg">Sto preparando la ricetta...</p>
|
||
</div>
|
||
<div id="recipe-result" style="display:none" class="recipe-result">
|
||
<div id="recipe-content"></div>
|
||
<button class="btn btn-large btn-secondary full-width mt-2" onclick="regenerateRecipe()" data-i18n="recipes.regenerate">
|
||
🔄 Generane un'altra
|
||
</button>
|
||
<button class="btn btn-large btn-primary full-width mt-2" onclick="closeRecipeDialog()" data-i18n="recipes.close_btn">
|
||
✅ Chiudi
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Setup Wizard (first-run) -->
|
||
<div class="modal-overlay" id="setup-wizard" style="display:none">
|
||
<div class="modal-content setup-wizard-content" onclick="event.stopPropagation()">
|
||
<div class="setup-header">
|
||
<h2><img src="assets/img/logo/logo_icon.png" alt="" class="header-logo-icon" style="height:28px;vertical-align:middle;margin-right:6px" /> EverShelf</h2>
|
||
<div class="setup-progress" id="setup-progress"></div>
|
||
</div>
|
||
<div class="setup-body" id="setup-body"></div>
|
||
<div class="setup-footer">
|
||
<button class="btn btn-secondary" id="setup-prev" onclick="setupWizardNav(-1)" style="display:none" data-i18n="btn.back">← Indietro</button>
|
||
<button class="btn btn-accent" id="setup-next" onclick="setupWizardNav(1)" data-i18n="btn.next">Avanti →</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toast notification -->
|
||
<div class="toast" id="toast"></div>
|
||
|
||
<!-- Loading Overlay -->
|
||
<div class="loading-overlay" id="loading" style="display:none">
|
||
<div class="loading-spinner"></div>
|
||
<p data-i18n="app.loading">Caricamento...</p>
|
||
</div>
|
||
|
||
<!-- Modal for product details from inventory -->
|
||
<div class="modal-overlay" id="modal-overlay" style="display:none" onclick="closeModal()">
|
||
<div class="modal-content" id="modal-content" onclick="event.stopPropagation()"></div>
|
||
</div>
|
||
|
||
<!-- Screensaver Overlay -->
|
||
<div id="screensaver" class="screensaver-overlay" style="display:none">
|
||
<div class="screensaver-content">
|
||
<div class="screensaver-clock" id="screensaver-clock"></div>
|
||
<div id="screensaver-shopping" class="screensaver-shopping" style="display:none"></div>
|
||
<div id="screensaver-mealplan" class="screensaver-mealplan" style="display:none"></div>
|
||
<!-- Nutrition pie charts panel (shown alternately with fact) -->
|
||
<div id="screensaver-nutrition" class="screensaver-nutrition" style="display:none"></div>
|
||
<div class="screensaver-fact" id="screensaver-fact"></div>
|
||
</div>
|
||
<div class="screensaver-shortcuts">
|
||
<button class="screensaver-shortcut-btn" id="screensaver-recipe-btn" title="Ricette">
|
||
🍳
|
||
</button>
|
||
<button class="screensaver-shortcut-btn" id="screensaver-scan-btn" title="Scansiona prodotto (tieni premuto per modalità spesa)">
|
||
📷
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== COOKING MODE OVERLAY ===== -->
|
||
<div id="cooking-overlay" class="cooking-overlay" style="display:none" aria-live="polite">
|
||
<div id="cooking-flash-overlay" class="cooking-flash-overlay"></div>
|
||
<div class="cooking-header">
|
||
<button class="cooking-close-btn" onclick="closeCookingMode()" title="Chiudi">✕</button>
|
||
<span class="cooking-title" id="cooking-title"></span>
|
||
<button class="cooking-tts-btn" id="cooking-tts-btn" onclick="toggleCookingTTS()" title="Leggi ad alta voce">🔊</button>
|
||
</div>
|
||
<div id="cooking-timers-bar" class="cooking-timers-bar" style="display:none"></div>
|
||
<div id="cooking-tools-bar" class="cooking-tools-bar" style="display:none"></div>
|
||
<div class="cooking-body">
|
||
<div class="cooking-step-header">
|
||
<div class="cooking-step-num" id="cooking-step-num">1 / 1</div>
|
||
<button class="cooking-restart-btn" onclick="restartCookingMode()" title="Ricomincia dall'inizio">↺ Ricomincia</button>
|
||
</div>
|
||
<div class="cooking-progress-dots" id="cooking-progress-dots"></div>
|
||
<div class="cooking-wheel" id="cooking-wheel" tabindex="0" aria-label="Navigazione passi ricetta">
|
||
<div class="cooking-step-ghost cooking-step-prev" id="cooking-step-prev"></div>
|
||
<div class="cooking-step-text" id="cooking-step-text"></div>
|
||
<div class="cooking-step-ghost cooking-step-next" id="cooking-step-next"></div>
|
||
</div>
|
||
<button class="cooking-replay-btn" id="cooking-replay" onclick="replayCookingTTS()" title="Rileggi questo passo">🔊 Rileggi</button>
|
||
<div class="cooking-timer-suggest" id="cooking-timer-suggest" style="display:none">
|
||
<button class="cooking-timer-add-btn" onclick="addSuggestedCookingTimer()">
|
||
<span id="cooking-timer-suggest-text">⏱️ 00:00 · Timer</span>
|
||
</button>
|
||
</div>
|
||
<div class="cooking-step-ings" id="cooking-step-ings" style="display:none"></div>
|
||
<div id="cooking-zerowaste-tip" class="cooking-zerowaste-tip" style="display:none">
|
||
<span class="cooking-zerowaste-label" data-i18n="cooking.zerowaste_label">♻️ Scarto</span>
|
||
<span id="cooking-zerowaste-scrap" class="cooking-zerowaste-scrap"></span>
|
||
<p id="cooking-zerowaste-text" class="cooking-zerowaste-text"></p>
|
||
<button class="cooking-zerowaste-close" onclick="_dismissZeroWasteTip()" aria-label="Chiudi">✕</button>
|
||
</div>
|
||
</div>
|
||
<div class="cooking-nav">
|
||
<button class="cooking-nav-btn cooking-prev-btn" id="cooking-prev" onclick="navigateCookingStep(-1)">◀ Precedente</button>
|
||
<button class="cooking-nav-btn cooking-next-btn" id="cooking-next" onclick="navigateCookingStep(1)">Successivo ▶</button>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="assets/js/app.js?v=20260517a"></script>
|
||
</body>
|
||
</html>
|