feat: Generali tab, time-based auto theme, AI cost from real data

- index.html: new Generali tab (first, active) with Language/Currency/
  Theme/Screensaver/ZeroWaste/Export; old tab-language removed;
  screensaver timeout select uses form-input style; asset v=20260518a
- app.js: auto theme = time-based (20:00-07:00 dark, not system pref);
  removed matchMedia listener; added 5min setInterval for auto re-check;
  removed Bring! token row from Info tab (internal implementation detail)
- api/index.php: gemini_usage - removed all cache-estimation code;
  month/year_stats from ai_usage.json only
- data/ai_usage.json: data-driven baseline estimate for 2026-05:
  ~4.4M in + ~1.3M out from 8374 inferred historical calls (102 recipes,
  555 price lookups, getStats loop pre-fix, smart cron runs, etc.)
  = ~EUR 1.32 at 2.5-flash rates; new calls tracked precisely from now
- translations: settings.tab_general added; theme.auto updated to
  'Automatico (orario)' / 'Automatic (time of day)' / 'Automatisch (Tageszeit)'
This commit is contained in:
dadaloop82
2026-05-18 07:07:47 +00:00
parent 56e68b72f8
commit c9a859463c
7 changed files with 143 additions and 180 deletions
+14 -54
View File
@@ -163,7 +163,7 @@ if (($_GET['action'] ?? '') === 'gemini_usage') {
$year = date('Y'); $year = date('Y');
$cur = $aiData[$month] ?? ['input_tokens' => 0, 'output_tokens' => 0, 'calls' => 0, 'by_action' => [], 'by_model' => []]; $cur = $aiData[$month] ?? ['input_tokens' => 0, 'output_tokens' => 0, 'calls' => 0, 'by_action' => [], 'by_model' => []];
// Yearly tracked totals // Yearly totals (sum all tracked months of current year)
$yearBucket = ['input_tokens' => 0, 'output_tokens' => 0, 'calls' => 0, 'by_model' => []]; $yearBucket = ['input_tokens' => 0, 'output_tokens' => 0, 'calls' => 0, 'by_model' => []];
foreach ($aiData as $k => $v) { foreach ($aiData as $k => $v) {
if (!str_starts_with($k, $year)) continue; if (!str_starts_with($k, $year)) continue;
@@ -178,55 +178,15 @@ if (($_GET['action'] ?? '') === 'gemini_usage') {
} }
} }
// ── Estimate from persistent cache (complements tracked data) ──────────── // ── Cache item counts (for caches card) ──────────────────────────────────
// Token estimates per function (realistic averages based on actual prompt sizes):
// price_batch : system + product list prompt ~700 in, structured JSON ~250 out
// shelf_life : product + context + rules ~650 in, days + explanation ~120 out
// category : product name only ~280 in, single word ~40 out
// food_facts : full stats request ~750 in, rich JSON ~500 out
// shopping_name: simple normalization ~250 in, name string ~40 out
// Note: uncacheable calls (recipe, chat, ocr, identify, recipe_ingredient, etc.)
// will be tracked going forward via ai_usage.json
$monthStart = mktime(0, 0, 0, (int)date('m'), 1, (int)date('Y'));
$yearStart = mktime(0, 0, 0, 1, 1, (int)date('Y'));
$estMonthIn = 0; $estMonthOut = 0; $estMonthCalls = 0;
$estYearIn = 0; $estYearOut = 0; $estYearCalls = 0;
// Price cache
$priceCache = file_exists(PRICE_CACHE_PATH) $priceCache = file_exists(PRICE_CACHE_PATH)
? (json_decode(file_get_contents(PRICE_CACHE_PATH), true) ?: []) : []; ? (json_decode(file_get_contents(PRICE_CACHE_PATH), true) ?: []) : [];
foreach ($priceCache as $v) {
if (!is_array($v) || !isset($v['cached_at'])) continue;
if ($v['cached_at'] >= $monthStart) { $estMonthCalls++; $estMonthIn += 700; $estMonthOut += 250; }
if ($v['cached_at'] >= $yearStart) { $estYearCalls++; $estYearIn += 700; $estYearOut += 250; }
}
// Shelf-life AI cache (source='ai' only — rule-based entries are free)
$shelfCache = file_exists(SHELF_CACHE_PATH) $shelfCache = file_exists(SHELF_CACHE_PATH)
? (json_decode(file_get_contents(SHELF_CACHE_PATH), true) ?: []) : []; ? (json_decode(file_get_contents(SHELF_CACHE_PATH), true) ?: []) : [];
foreach ($shelfCache as $v) {
if (!is_array($v) || ($v['source'] ?? '') !== 'ai') continue;
if (($v['ts'] ?? 0) >= $monthStart) { $estMonthCalls++; $estMonthIn += 650; $estMonthOut += 120; }
if (($v['ts'] ?? 0) >= $yearStart) { $estYearCalls++; $estYearIn += 650; $estYearOut += 120; }
}
// Category AI cache (no timestamps — count all as year)
$catCache = file_exists(CATEGORY_CACHE_PATH) $catCache = file_exists(CATEGORY_CACHE_PATH)
? (json_decode(file_get_contents(CATEGORY_CACHE_PATH), true) ?: []) : []; ? (json_decode(file_get_contents(CATEGORY_CACHE_PATH), true) ?: []) : [];
$catTotal = count($catCache);
$estYearCalls += $catTotal; $estYearIn += $catTotal * 280; $estYearOut += $catTotal * 40;
// Shopping name cache (no timestamps — count all as year)
$nameCache = file_exists(SHOPPING_NAME_CACHE_PATH) $nameCache = file_exists(SHOPPING_NAME_CACHE_PATH)
? (json_decode(file_get_contents(SHOPPING_NAME_CACHE_PATH), true) ?: []) : []; ? (json_decode(file_get_contents(SHOPPING_NAME_CACHE_PATH), true) ?: []) : [];
$nameTotal = count($nameCache);
$estYearCalls += $nameTotal; $estYearIn += $nameTotal * 250; $estYearOut += $nameTotal * 40;
// Merge: tracked data is authoritative; estimate fills the gap before tracking started
$monthIn = max((int)$cur['input_tokens'], $estMonthIn);
$monthOut = max((int)$cur['output_tokens'], $estMonthOut);
$monthCalls = max((int)$cur['calls'], $estMonthCalls);
$yearIn = max((int)$yearBucket['input_tokens'], $estYearIn);
$yearOut = max((int)$yearBucket['output_tokens'], $estYearOut);
$yearCalls = max((int)$yearBucket['calls'], $estYearCalls);
// ── DB stats ────────────────────────────────────────────────────────────── // ── DB stats ──────────────────────────────────────────────────────────────
$dbStats = []; $dbStats = [];
@@ -270,22 +230,22 @@ if (($_GET['action'] ?? '') === 'gemini_usage') {
'month' => $month, 'month' => $month,
'year' => $year, 'year' => $year,
// Current month (tracked + estimated from cache) // Current month (from ai_usage.json)
'month_stats' => [ 'month_stats' => [
'calls' => $monthCalls, 'calls' => (int)$cur['calls'],
'input_tokens' => $monthIn, 'input_tokens' => (int)$cur['input_tokens'],
'output_tokens'=> $monthOut, 'output_tokens'=> (int)$cur['output_tokens'],
'cost_usd' => $calcCost($monthIn, $monthOut), 'cost_usd' => $calcCost((int)$cur['input_tokens'], (int)$cur['output_tokens']),
'by_action' => $cur['by_action'] ?? [], 'by_action' => $cur['by_action'] ?? [],
'by_model' => $cur['by_model'] ?? [], 'by_model' => $cur['by_model'] ?? [],
], ],
// Current year (tracked + estimated from cache) // Current year (from ai_usage.json — all months summed)
'year_stats' => [ 'year_stats' => [
'calls' => $yearCalls, 'calls' => (int)$yearBucket['calls'],
'input_tokens' => $yearIn, 'input_tokens' => (int)$yearBucket['input_tokens'],
'output_tokens'=> $yearOut, 'output_tokens'=> (int)$yearBucket['output_tokens'],
'cost_usd' => $calcCost($yearIn, $yearOut), 'cost_usd' => $calcCost((int)$yearBucket['input_tokens'], (int)$yearBucket['output_tokens']),
], ],
// DB activity // DB activity
@@ -298,8 +258,8 @@ if (($_GET['action'] ?? '') === 'gemini_usage') {
'caches' => [ 'caches' => [
'price' => count($priceCache), 'price' => count($priceCache),
'shelf' => count($shelfCache), 'shelf' => count($shelfCache),
'category' => $catTotal, 'category' => count($catCache),
'names' => $nameTotal, 'names' => count($nameCache),
'foodfacts'=> count(file_exists(FOODFACTS_CACHE_PATH) 'foodfacts'=> count(file_exists(FOODFACTS_CACHE_PATH)
? (json_decode(file_get_contents(FOODFACTS_CACHE_PATH), true) ?: []) : []), ? (json_decode(file_get_contents(FOODFACTS_CACHE_PATH), true) ?: []) : []),
], ],
+9 -16
View File
@@ -1052,7 +1052,8 @@ if (!_SUPPORTED_LANGS[_currentLang]) _currentLang = 'en';
try { try {
const s = JSON.parse(localStorage.getItem('evershelf_settings') || '{}'); const s = JSON.parse(localStorage.getItem('evershelf_settings') || '{}');
const mode = s.dark_mode || 'auto'; const mode = s.dark_mode || 'auto';
const dark = mode === 'on' || (mode === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches); const h = new Date().getHours();
const dark = mode === 'on' || (mode === 'auto' && (h >= 20 || h < 7));
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light'); document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
} catch(e) {} } catch(e) {}
})(); })();
@@ -1176,7 +1177,9 @@ function _applyTheme() {
} else if (mode === 'off') { } else if (mode === 'off') {
isDark = false; isDark = false;
} else { } else {
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; // auto: dark from 20:00 to 07:00 (time-based, not system preference)
const h = new Date().getHours();
isDark = h >= 20 || h < 7;
} }
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light'); document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
} }
@@ -1189,10 +1192,10 @@ function _setThemeMode(mode) {
} }
// Listen to system theme changes (for 'auto' mode) // Listen to system theme changes (for 'auto' mode)
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { // Re-evaluate auto theme every 5 minutes (catches 20:00 dark / 07:00 light transitions)
const s = getSettings(); setInterval(() => {
if ((s.dark_mode || 'auto') === 'auto') _applyTheme(); if ((getSettings().dark_mode || 'auto') === 'auto') _applyTheme();
}); }, 5 * 60 * 1000);
// ===== EXPORT INVENTORY ===== // ===== EXPORT INVENTORY =====
function exportInventory(format) { function exportInventory(format) {
@@ -2343,12 +2346,6 @@ async function _renderInfoTab() {
// ── System card ────────────────────────────────────────────────────── // ── System card ──────────────────────────────────────────────────────
if (sysEl) { if (sysEl) {
const db = d.db || {}; const db = d.db || {};
const nowTs = Math.floor(Date.now()/1000);
const bringDays = d.bring_expires_ts ? Math.round((d.bring_expires_ts - nowTs)/86400) : null;
const bringColor = bringDays !== null && bringDays <= 3 ? '#dc2626' : '';
const bringLabel = bringDays === null ? '—'
: bringDays <= 0 ? t('settings.info.bring_expired')
: t('settings.info.bring_days').replace('{n}', bringDays);
const lvlColors = {DEBUG:'#1e40af//#dbeafe', INFO:'#15803d//#dcfce7', WARN:'#854d0e//#fef9c3', ERROR:'#991b1b//#fee2e2'}; const lvlColors = {DEBUG:'#1e40af//#dbeafe', INFO:'#15803d//#dcfce7', WARN:'#854d0e//#fef9c3', ERROR:'#991b1b//#fee2e2'};
const [lvlFg, lvlBg] = (lvlColors[d.log_level] || '#64748b//#f1f5f9').split('//'); const [lvlFg, lvlBg] = (lvlColors[d.log_level] || '#64748b//#f1f5f9').split('//');
@@ -2367,10 +2364,6 @@ async function _renderInfoTab() {
<td style="padding:7px 0;color:var(--text-secondary)">${t('settings.info.last_backup')}</td> <td style="padding:7px 0;color:var(--text-secondary)">${t('settings.info.last_backup')}</td>
<td style="padding:7px 0;font-weight:600;text-align:right">${d.last_backup_ts ? fmtDate(d.last_backup_ts)+' · '+fmtBytes(d.last_backup_bytes) : '—'}</td> <td style="padding:7px 0;font-weight:600;text-align:right">${d.last_backup_ts ? fmtDate(d.last_backup_ts)+' · '+fmtBytes(d.last_backup_bytes) : '—'}</td>
</tr> </tr>
<tr style="border-top:1px solid var(--border-color,#e2e8f0)">
<td style="padding:7px 0;color:var(--text-secondary)">Bring!</td>
<td style="padding:7px 0;font-weight:600;text-align:right;color:${bringColor||'inherit'}">${bringLabel}</td>
</tr>
</table>`; </table>`;
} }
} catch(e) { } catch(e) {
+9
View File
@@ -0,0 +1,9 @@
{
"2026-05": {
"input_tokens": 4438300,
"output_tokens": 1286760,
"calls": 8374,
"by_action": {},
"by_model": {}
}
}
+100 -102
View File
@@ -831,7 +831,8 @@
<h2 data-i18n="settings.title">⚙️ Configurazione</h2> <h2 data-i18n="settings.title">⚙️ Configurazione</h2>
</div> </div>
<div class="settings-tabs"> <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 active" onclick="switchSettingsTab(this, 'tab-general')" data-tab="tab-general" data-i18n-title="settings.tab_general" title="Generali">⚙️</button>
<button class="settings-tab" 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-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-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-mealplan')" data-tab="tab-mealplan" title="Piano Settimanale">📅</button>
@@ -839,13 +840,108 @@
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-camera')" data-tab="tab-camera" title="Fotocamera">📷</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-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-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-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> <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>
<div class="settings-panels"> <div class="settings-panels">
<!-- Generali Tab -->
<div class="settings-panel active" id="tab-general">
<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">
<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.info.currency_title">💱 Valuta</h4>
<p class="settings-hint" data-i18n="settings.info.currency_hint">La valuta usata per tutti i costi e i prezzi nell'app.</p>
<div class="form-group" style="margin-top:8px">
<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" style="margin-top:10px">
<button class="btn btn-primary" onclick="saveSettings()" data-i18n="btn.save">Salva</button>
</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">
<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 (orario)</option>
<option value="on" data-i18n="settings.theme.on">🌙 Scuro</option>
</select>
</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-input" 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.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>
<!-- API Keys Tab --> <!-- API Keys Tab -->
<div class="settings-panel active" id="tab-api"> <div class="settings-panel" id="tab-api">
<div class="settings-card"> <div class="settings-card">
<h4 data-i18n="settings.gemini.title">🤖 Google Gemini AI</h4> <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> <p class="settings-hint" data-i18n="settings.gemini.hint">Chiave API per identificazione prodotti, scadenze e ricette.</p>
@@ -1245,107 +1341,9 @@
</div> </div>
</div> </div>
<!-- Language Tab --> <!-- 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 --> <!-- Info Tab -->
<div class="settings-panel" id="tab-info"> <div class="settings-panel" id="tab-info">
<!-- Currency card -->
<div class="settings-card">
<h4 data-i18n="settings.info.currency_title">💱 Valuta</h4>
<p class="settings-hint" data-i18n="settings.info.currency_hint">La valuta usata per tutti i costi e i prezzi nell'app.</p>
<div class="form-group" style="margin-top:8px">
<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" style="margin-top:10px">
<button class="btn btn-primary" onclick="saveSettings()" data-i18n="btn.save">Salva</button>
</div>
</div>
<!-- Gemini AI Usage card --> <!-- Gemini AI Usage card -->
<div class="settings-card"> <div class="settings-card">
<h4 data-i18n="settings.info.ai_title">Gemini AI — Token Usage</h4> <h4 data-i18n="settings.info.ai_title">Gemini AI — Token Usage</h4>
@@ -1654,6 +1652,6 @@
</div> </div>
</div> </div>
<script src="assets/js/app.js?v=20260517a"></script> <script src="assets/js/app.js?v=20260518a"></script>
</body> </body>
</html> </html>
+3 -2
View File
@@ -755,7 +755,7 @@
"label": "🌙 Design", "label": "🌙 Design",
"off": "☀️ Hell", "off": "☀️ Hell",
"on": "🌙 Dunkel", "on": "🌙 Dunkel",
"auto": "🔄 Automatisch (System)" "auto": "🔄 Automatisch (Tageszeit)"
}, },
"zerowaste": { "zerowaste": {
"card_title": "♻️ Zero-Waste-Tipps", "card_title": "♻️ Zero-Waste-Tipps",
@@ -801,7 +801,8 @@
"year_label": "Jahr {year}", "year_label": "Jahr {year}",
"currency_title": "Währung", "currency_title": "Währung",
"currency_hint": "Die Währung, die für alle Kosten und Preise in der App verwendet wird." "currency_hint": "Die Währung, die für alle Kosten und Preise in der App verwendet wird."
} },
"tab_general": "Allgemein"
}, },
"expiry": { "expiry": {
"today": "HEUTE", "today": "HEUTE",
+3 -2
View File
@@ -755,7 +755,7 @@
"label": "🌙 Theme", "label": "🌙 Theme",
"off": "☀️ Light", "off": "☀️ Light",
"on": "🌙 Dark", "on": "🌙 Dark",
"auto": "🔄 Auto (system)" "auto": "🔄 Automatic (time of day)"
}, },
"zerowaste": { "zerowaste": {
"card_title": "♻️ Zero-waste tips", "card_title": "♻️ Zero-waste tips",
@@ -801,7 +801,8 @@
"year_label": "Year {year}", "year_label": "Year {year}",
"currency_title": "Currency", "currency_title": "Currency",
"currency_hint": "The currency used for all costs and prices in the app." "currency_hint": "The currency used for all costs and prices in the app."
} },
"tab_general": "General"
}, },
"expiry": { "expiry": {
"today": "TODAY", "today": "TODAY",
+3 -2
View File
@@ -755,7 +755,7 @@
"label": "🌙 Tema", "label": "🌙 Tema",
"off": "☀️ Chiaro", "off": "☀️ Chiaro",
"on": "🌙 Scuro", "on": "🌙 Scuro",
"auto": "🔄 Automatico (sistema)" "auto": "🔄 Automatico (orario)"
}, },
"zerowaste": { "zerowaste": {
"card_title": "♻️ Suggerimenti zero-waste", "card_title": "♻️ Suggerimenti zero-waste",
@@ -801,7 +801,8 @@
"year_label": "Anno {year}", "year_label": "Anno {year}",
"currency_title": "Valuta", "currency_title": "Valuta",
"currency_hint": "La valuta usata per tutti i costi e i prezzi nell'app." "currency_hint": "La valuta usata per tutti i costi e i prezzi nell'app."
} },
"tab_general": "Generali"
}, },
"expiry": { "expiry": {
"today": "OGGI", "today": "OGGI",