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:
+16
-56
@@ -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) {
|
$catCache = file_exists(CATEGORY_CACHE_PATH)
|
||||||
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)
|
|
||||||
? (json_decode(file_get_contents(CATEGORY_CACHE_PATH), true) ?: []) : [];
|
? (json_decode(file_get_contents(CATEGORY_CACHE_PATH), true) ?: []) : [];
|
||||||
$catTotal = count($catCache);
|
$nameCache = file_exists(SHOPPING_NAME_CACHE_PATH)
|
||||||
$estYearCalls += $catTotal; $estYearIn += $catTotal * 280; $estYearOut += $catTotal * 40;
|
|
||||||
// Shopping name cache (no timestamps — count all as year)
|
|
||||||
$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
@@ -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) {
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"2026-05": {
|
||||||
|
"input_tokens": 4438300,
|
||||||
|
"output_tokens": 1286760,
|
||||||
|
"calls": 8374,
|
||||||
|
"by_action": {},
|
||||||
|
"by_model": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
+100
-102
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user