feat: dark mode (Off/On/Auto) + export inventory CSV/PDF (#78, #64)

This commit is contained in:
dadaloop82
2026-05-17 08:59:40 +00:00
parent 3ba4f7eaad
commit 0a6e653692
12 changed files with 579 additions and 12 deletions
+7
View File
@@ -11,6 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Recipe scraps tips** — During cooking steps, detect "waste" generated (peels, cores, bones, eggshells, coffee grounds, citrus zest, etc.) and surface AI-powered tips on how to reuse them (compost, natural cleaner, broth, candied peel, etc.). Could be shown as an optional collapsible hint card below the step that generates the scrap. - **Recipe scraps tips** — During cooking steps, detect "waste" generated (peels, cores, bones, eggshells, coffee grounds, citrus zest, etc.) and surface AI-powered tips on how to reuse them (compost, natural cleaner, broth, candied peel, etc.). Could be shown as an optional collapsible hint card below the step that generates the scrap.
## [1.7.18] - 2026-05-19
### Added
- **Dark mode** — New theme selector in Settings (Appearance card): **Off (Light)**, **On (Dark)**, **Auto (follows system)**. Applied immediately on page load to prevent white flash. Resolves [#78](https://github.com/dadaloop82/EverShelf/issues/78).
- **Export inventory** — New 📤 button in inventory page header opens a modal to download the inventory as **CSV** (UTF-8 with BOM, Excel-compatible) or open a **print-ready HTML page** (auto-triggers print dialog for PDF). Export card also available in Settings tab. Resolves [#64](https://github.com/dadaloop82/EverShelf/issues/64).
- `translations/de.json`: fixed missing `log.recipe_prefix` key.
## [1.7.17] - 2026-05-19 ## [1.7.17] - 2026-05-19
### Added ### Added
+1 -1
View File
@@ -25,7 +25,7 @@
[![SQLite](https://img.shields.io/badge/SQLite-3-blue.svg)](https://www.sqlite.org/) [![SQLite](https://img.shields.io/badge/SQLite-3-blue.svg)](https://www.sqlite.org/)
[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED.svg)](Dockerfile) [![Docker](https://img.shields.io/badge/Docker-Ready-2496ED.svg)](Dockerfile)
[![i18n](https://img.shields.io/badge/i18n-IT%20%7C%20EN%20%7C%20DE%20%7C%20FR%20%7C%20ES-orange.svg)](translations/) [![i18n](https://img.shields.io/badge/i18n-IT%20%7C%20EN%20%7C%20DE%20%7C%20FR%20%7C%20ES-orange.svg)](translations/)
[![Version](https://img.shields.io/badge/version-1.7.17-brightgreen.svg)](CHANGELOG.md) [![Version](https://img.shields.io/badge/version-1.7.18-brightgreen.svg)](CHANGELOG.md)
[![GitHub stars](https://img.shields.io/github/stars/dadaloop82/EverShelf?style=social)](https://github.com/dadaloop82/EverShelf/stargazers) [![GitHub stars](https://img.shields.io/github/stars/dadaloop82/EverShelf?style=social)](https://github.com/dadaloop82/EverShelf/stargazers)
[![Last commit](https://img.shields.io/github/last-commit/dadaloop82/EverShelf/main)](https://github.com/dadaloop82/EverShelf/commits/main) [![Last commit](https://img.shields.io/github/last-commit/dadaloop82/EverShelf/main)](https://github.com/dadaloop82/EverShelf/commits/main)
[![Contributors](https://img.shields.io/github/contributors/dadaloop82/EverShelf)](https://github.com/dadaloop82/EverShelf/graphs/contributors) [![Contributors](https://img.shields.io/github/contributors/dadaloop82/EverShelf)](https://github.com/dadaloop82/EverShelf/graphs/contributors)
+105
View File
@@ -474,6 +474,10 @@ try {
guessCategoryFromAI(); guessCategoryFromAI();
break; break;
case 'export_inventory':
exportInventory($db);
break;
default: default:
http_response_code(404); http_response_code(404);
echo json_encode(['error' => 'Unknown action: ' . $action]); echo json_encode(['error' => 'Unknown action: ' . $action]);
@@ -485,6 +489,107 @@ try {
} }
endif; // end !CRON_MODE endif; // end !CRON_MODE
// ===== EXPORT INVENTORY =====
function exportInventory(PDO $db): void {
$format = strtolower($_GET['format'] ?? 'csv');
$stmt = $db->query("
SELECT p.name, p.brand, p.category, i.location, i.quantity, p.unit,
i.expiry_date, i.added_at, i.opened_at,
COALESCE(i.vacuum_sealed, 0) as vacuum_sealed,
p.barcode, p.notes
FROM inventory i
JOIN products p ON i.product_id = p.id
WHERE i.quantity > 0
ORDER BY p.name ASC
");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$date = date('Y-m-d');
if ($format === 'html') {
// Print-ready HTML for browser PDF
header('Content-Type: text/html; charset=utf-8');
$rows_html = '';
foreach ($rows as $r) {
$loc_icon = ['dispensa'=>'🗄️','frigo'=>'🧊','freezer'=>'❄️','altro'=>'📦'][$r['location']] ?? '📦';
$expiry = $r['expiry_date'] ? htmlspecialchars($r['expiry_date']) : '—';
$brand = $r['brand'] ? htmlspecialchars($r['brand']) : '';
$rows_html .= '<tr>'
. '<td>' . htmlspecialchars($r['name']) . ($brand ? '<br><small>' . $brand . '</small>' : '') . '</td>'
. '<td>' . htmlspecialchars(ucfirst($r['category'] ?? '')) . '</td>'
. '<td>' . $loc_icon . ' ' . htmlspecialchars(ucfirst($r['location'])) . '</td>'
. '<td style="text-align:right">' . htmlspecialchars($r['quantity']) . ' ' . htmlspecialchars($r['unit'] ?? 'pz') . '</td>'
. '<td>' . $expiry . '</td>'
. '<td>' . ($r['opened_at'] ? '📭 ' . htmlspecialchars($r['opened_at']) : '') . '</td>'
. '</tr>';
}
$count = count($rows);
echo <<<HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>EverShelf Inventory Export {$date}</title>
<style>
body{font-family:Arial,sans-serif;font-size:12px;margin:24px;color:#1a1a1a}
h1{font-size:18px;margin-bottom:4px}
.subtitle{color:#6b7280;font-size:11px;margin-bottom:16px}
table{width:100%;border-collapse:collapse}
th{background:#2d5016;color:#fff;padding:7px 10px;text-align:left;font-size:11px}
td{padding:6px 10px;border-bottom:1px solid #e5e7eb;vertical-align:top}
tr:nth-child(even) td{background:#f8fafc}
small{color:#6b7280}
@media print{
body{margin:12px}
button{display:none}
@page{margin:15mm}
}
</style>
</head>
<body>
<button onclick="window.print()" style="margin-bottom:16px;padding:8px 16px;background:#2d5016;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px">🖨️ Print / Save as PDF</button>
<h1>🏠 EverShelf Inventory</h1>
<div class="subtitle">Exported: {$date} &nbsp;·&nbsp; {$count} items</div>
<table>
<thead><tr>
<th>Name / Brand</th><th>Category</th><th>Location</th><th>Qty</th><th>Expiry</th><th>Opened</th>
</tr></thead>
<tbody>{$rows_html}</tbody>
</table>
<script>window.onload=function(){window.print();}</script>
</body>
</html>
HTML;
exit;
}
// Default: CSV download
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="evershelf-inventory-' . $date . '.csv"');
// UTF-8 BOM for Excel compatibility
echo "\xEF\xBB\xBF";
$out = fopen('php://output', 'w');
fputcsv($out, ['Name','Brand','Category','Location','Quantity','Unit','Expiry Date','Added','Opened At','Vacuum Sealed','Barcode','Notes']);
foreach ($rows as $r) {
fputcsv($out, [
$r['name'],
$r['brand'] ?? '',
$r['category'] ?? '',
$r['location'],
$r['quantity'],
$r['unit'] ?? 'pz',
$r['expiry_date'] ?? '',
$r['added_at'] ?? '',
$r['opened_at'] ?? '',
$r['vacuum_sealed'] ? 'Yes' : 'No',
$r['barcode'] ?? '',
$r['notes'] ?? '',
]);
}
fclose($out);
exit;
}
// ===== TTS PROXY ===== // ===== TTS PROXY =====
function ttsProxy() { function ttsProxy() {
$body = json_decode(file_get_contents('php://input'), true); $body = json_decode(file_get_contents('php://input'), true);
+274
View File
@@ -6857,3 +6857,277 @@ body.cooking-mode-active .app-header {
color: #9ca3af; color: #9ca3af;
font-size: 0.8em; font-size: 0.8em;
} }
/* ===== PAGE HEADER ACTION BUTTON (export etc.) ===== */
.page-header-action-btn {
margin-left: auto;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 8px 12px;
font-size: 1.1rem;
cursor: pointer;
transition: all 0.2s;
color: var(--primary);
}
.page-header-action-btn:active {
transform: scale(0.95);
opacity: 0.8;
}
/* ===== DARK MODE ===== */
[data-theme="dark"] {
--bg: #0f172a;
--bg-card: #1e293b;
--bg-dark: #020617;
--text: #e2e8f0;
--text-light: #94a3b8;
--text-muted: #64748b;
--text-secondary: #94a3b8;
--border: #334155;
--shadow: 0 2px 8px rgba(0,0,0,0.45);
--shadow-lg: 0 4px 16px rgba(0,0,0,0.6);
color-scheme: dark;
}
[data-theme="dark"] body {
background: var(--bg);
color: var(--text);
}
/* Bottom nav */
[data-theme="dark"] .bottom-nav {
box-shadow: 0 -2px 10px rgba(0,0,0,0.4);
}
/* Location tabs */
[data-theme="dark"] .tab {
background: var(--bg-card);
color: var(--text-light);
}
[data-theme="dark"] .tab.active {
background: var(--primary);
color: #fff;
}
/* Location selector (add/use modal) */
[data-theme="dark"] .location-option {
background: var(--bg-card);
border-color: var(--border);
color: var(--text);
}
[data-theme="dark"] .location-option.selected {
border-color: var(--primary);
background: rgba(45,80,22,0.3);
}
/* Inputs & selects */
[data-theme="dark"] .form-input,
[data-theme="dark"] .form-control,
[data-theme="dark"] input[type="text"],
[data-theme="dark"] input[type="email"],
[data-theme="dark"] input[type="password"],
[data-theme="dark"] input[type="number"],
[data-theme="dark"] textarea,
[data-theme="dark"] select {
background: var(--bg-card);
color: var(--text);
border-color: var(--border);
}
[data-theme="dark"] input::placeholder,
[data-theme="dark"] textarea::placeholder {
color: var(--text-muted);
}
/* Buttons */
[data-theme="dark"] .btn-secondary,
[data-theme="dark"] .btn-outline,
[data-theme="dark"] .back-btn,
[data-theme="dark"] .page-header-action-btn {
background: var(--bg-card);
color: var(--text);
border-color: var(--border);
}
[data-theme="dark"] .btn-outline {
color: var(--primary-light);
border-color: var(--primary-light);
}
/* Inventory items */
[data-theme="dark"] .inventory-item {
background: var(--bg-card);
}
[data-theme="dark"] .inv-location-badge {
background: rgba(45,80,22,0.35);
color: #86efac;
}
/* Shopping items */
[data-theme="dark"] .shopping-item {
background: var(--bg-card) !important;
}
[data-theme="dark"] .shopping-item-tag-menu-container {
background: var(--bg-card);
border-color: var(--border);
}
[data-theme="dark"] .shopping-item-tag-btn {
background: #1e293b;
color: var(--text-light);
border-color: var(--border);
}
[data-theme="dark"] .badge-local-tag {
background: #0c2a4e;
color: #7dd3fc;
}
[data-theme="dark"] .badge-freq-med {
background: #2e1a4a;
color: #c4b5fd;
}
[data-theme="dark"] .badge-freq-low {
background: #1e293b;
color: #94a3b8;
}
/* Settings rows */
[data-theme="dark"] .settings-row {
border-color: var(--border);
}
[data-theme="dark"] .settings-label {
color: var(--text);
}
[data-theme="dark"] .settings-hint {
color: var(--text-muted);
}
/* Toggle switch */
[data-theme="dark"] .toggle-slider {
background: #334155;
}
/* Search bar */
[data-theme="dark"] .search-bar input {
background: var(--bg-card);
color: var(--text);
border-color: var(--border);
}
/* Action modal location selector */
[data-theme="dark"] .action-location-btn {
background: var(--bg-card);
border-color: var(--border);
color: var(--text);
}
/* Scan page */
[data-theme="dark"] .scan-input-row {
background: var(--bg-card);
}
[data-theme="dark"] .scan-result-item {
background: var(--bg-card);
border-color: var(--border);
}
/* Quick access chips */
[data-theme="dark"] .quick-access-chip {
background: var(--bg-card);
border-color: var(--border);
color: var(--text);
}
/* Scan recents */
[data-theme="dark"] .scan-recent-chip {
background: var(--bg-card);
border-color: var(--border);
color: var(--text-light);
}
/* Alert banners */
[data-theme="dark"] .alert-banner {
background: #1e293b;
border-color: #334155;
}
[data-theme="dark"] .alert-banner.banner-expiring {
background: #1c1300;
border-color: #78350f;
}
[data-theme="dark"] .alert-banner.banner-expired {
background: #1f0808;
border-color: #7f1d1d;
}
[data-theme="dark"] .alert-banner.banner-finished {
background: #0f1f0f;
border-color: #166534;
}
[data-theme="dark"] .alert-banner.banner-anomaly {
background: #1a1a2e;
border-color: #4c1d95;
}
/* Recipe dialog */
[data-theme="dark"] .recipe-dialog-content {
background: var(--bg-card);
}
[data-theme="dark"] .recipe-option-btn {
background: var(--bg-card);
border-color: var(--border);
color: var(--text);
}
[data-theme="dark"] .recipe-option-btn.active {
background: rgba(45,80,22,0.4);
border-color: var(--primary-light);
color: var(--primary-light);
}
/* Log rows */
[data-theme="dark"] .log-item {
background: var(--bg-card);
border-color: var(--border);
}
/* Dashboard stat cards */
[data-theme="dark"] .stat-card {
background: var(--bg-card);
}
/* Screensaver */
[data-theme="dark"] .screensaver-overlay {
background: #020617;
}
/* Charts / nutrition */
[data-theme="dark"] .nutrition-chart-bg {
background: var(--bg-card);
}
/* AW badges */
[data-theme="dark"] .aw-badge-rate { background: #2e1a4a; color: #c4b5fd; border-color: #6d28d9; }
[data-theme="dark"] .aw-badge-money { background: #1c1300; color: #fde047; border-color: #78350f; }
[data-theme="dark"] .aw-badge-meals { background: #0f1f0f; color: #4ade80; border-color: #166534; }
[data-theme="dark"] .aw-badge-co2 { background: #0c1f3a; color: #7dd3fc; border-color: #1e3a5f; }
[data-theme="dark"] .aw-badge-wasted{ background: #1f0808; color: #fca5a5; border-color: #7f1d1d; }
[data-theme="dark"] .aw-badge-better{ background: #0f1f0f; color: #4ade80; border-color: #166534; }
/* Chat */
[data-theme="dark"] .chat-input {
background: var(--bg-card);
color: var(--text);
border-color: var(--border);
}
[data-theme="dark"] .chat-message.user {
background: var(--primary-dark);
}
[data-theme="dark"] .chat-message.bot {
background: var(--bg-card);
}
/* Smart shopping forecast */
[data-theme="dark"] .smart-item {
background: var(--bg-card);
border-color: var(--border);
}
[data-theme="dark"] .smart-filter-btn {
background: var(--bg-card);
color: var(--text-light);
border-color: var(--border);
}
[data-theme="dark"] .smart-filter-btn.active {
background: var(--primary);
color: #fff;
border-color: var(--primary);
}
/* Offline banner */
[data-theme="dark"] #offline-banner {
background: #450a0a;
border-color: #7f1d1d;
}
/* Setup wizard */
[data-theme="dark"] .setup-content {
background: var(--bg-card);
}
[data-theme="dark"] .setup-lang-btn {
background: var(--bg-card);
color: var(--text);
border-color: var(--border);
}
[data-theme="dark"] .setup-lang-btn.selected {
background: rgba(45,80,22,0.4);
border-color: var(--primary-light);
color: var(--primary-light);
}
/* @media prefers-color-scheme: auto handled in JS */
+80
View File
@@ -1047,6 +1047,16 @@ let _currentLang = localStorage.getItem('evershelf_lang') || navigator.language?
const _SUPPORTED_LANGS = { it: 'Italiano', en: 'English', de: 'Deutsch', fr: 'Français', es: 'Español' }; const _SUPPORTED_LANGS = { it: 'Italiano', en: 'English', de: 'Deutsch', fr: 'Français', es: 'Español' };
if (!_SUPPORTED_LANGS[_currentLang]) _currentLang = 'en'; if (!_SUPPORTED_LANGS[_currentLang]) _currentLang = 'en';
// Apply theme IMMEDIATELY to prevent flash of unstyled content
(function _earlyTheme() {
try {
const s = JSON.parse(localStorage.getItem('evershelf_settings') || '{}');
const mode = s.dark_mode || 'auto';
const dark = mode === 'on' || (mode === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
} catch(e) {}
})();
// Flatten nested JSON: { a: { b: "x" } } → { "a.b": "x" } // Flatten nested JSON: { a: { b: "x" } } → { "a.b": "x" }
function _flattenI18n(obj, prefix = '') { function _flattenI18n(obj, prefix = '') {
const result = {}; const result = {};
@@ -1156,6 +1166,69 @@ function changeLanguage(lang) {
location.reload(); location.reload();
} }
// ===== DARK MODE =====
function _applyTheme() {
const s = getSettings();
const mode = s.dark_mode || 'auto';
let isDark;
if (mode === 'on') {
isDark = true;
} else if (mode === 'off') {
isDark = false;
} else {
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
}
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
}
function _setThemeMode(mode) {
const s = getSettings();
s.dark_mode = mode;
saveSettingsToStorage(s);
_applyTheme();
}
// Listen to system theme changes (for 'auto' mode)
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
const s = getSettings();
if ((s.dark_mode || 'auto') === 'auto') _applyTheme();
});
// ===== EXPORT INVENTORY =====
function exportInventory(format) {
const url = `api/index.php?action=export_inventory&format=${encodeURIComponent(format)}&_t=${Date.now()}`;
if (format === 'csv') {
// Direct download via <a> trick
const a = document.createElement('a');
a.href = url;
a.download = `evershelf-inventory-${new Date().toISOString().slice(0,10)}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
} else {
// Open print-ready HTML in new tab
window.open(url, '_blank', 'noopener');
}
}
function _showExportModal() {
const html = `
<div class="modal-header">
<h3>📤 ${t('export.title')}</h3>
<button class="modal-close" onclick="closeModal()"></button>
</div>
<div style="padding:16px;display:flex;flex-direction:column;gap:12px">
<p style="color:var(--text-light);font-size:0.9rem">${t('export.hint')}</p>
<button class="btn btn-primary full-width" onclick="exportInventory('csv');closeModal()">
📊 ${t('export.btn_csv')}
</button>
<button class="btn btn-outline full-width" onclick="exportInventory('html');closeModal()">
🖨 ${t('export.btn_pdf')}
</button>
</div>`;
openModal(html);
}
const LOCATIONS = { const LOCATIONS = {
'dispensa': { icon: '🗄️', label: t('locations.dispensa') }, 'dispensa': { icon: '🗄️', label: t('locations.dispensa') },
'frigo': { icon: '🧊', label: t('locations.frigo') }, 'frigo': { icon: '🧊', label: t('locations.frigo') },
@@ -2462,6 +2535,10 @@ async function loadSettingsUI() {
if (nativePanel) nativePanel.style.display = ''; if (nativePanel) nativePanel.style.display = '';
} }
// Dark mode setting
const dmEl = document.getElementById('setting-dark-mode');
if (dmEl) dmEl.value = s.dark_mode || 'auto';
// Populate About section version // Populate About section version
_loadAboutSection(); _loadAboutSection();
} }
@@ -2784,6 +2861,9 @@ async function saveSettings() {
if (ssEl) s.screensaver_enabled = ssEl.checked; if (ssEl) s.screensaver_enabled = ssEl.checked;
const ssTimeoutEl = document.getElementById('setting-screensaver-timeout'); const ssTimeoutEl = document.getElementById('setting-screensaver-timeout');
if (ssTimeoutEl) s.screensaver_timeout = parseInt(ssTimeoutEl.value, 10) || 5; if (ssTimeoutEl) s.screensaver_timeout = parseInt(ssTimeoutEl.value, 10) || 5;
// Dark mode
const dmSaveEl = document.getElementById('setting-dark-mode');
if (dmSaveEl) { s.dark_mode = dmSaveEl.value; _applyTheme(); }
// Meal plan enabled toggle // Meal plan enabled toggle
const mpEnabledEl = document.getElementById('setting-meal-plan-enabled'); const mpEnabledEl = document.getElementById('setting-meal-plan-enabled');
if (mpEnabledEl) s.meal_plan_enabled = mpEnabledEl.checked; if (mpEnabledEl) s.meal_plan_enabled = mpEnabledEl.checked;
+27 -2
View File
@@ -11,7 +11,7 @@
<title>EverShelf</title> <title>EverShelf</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<link rel="icon" type="image/png" href="assets/img/logo/logo_icon.png"> <link rel="icon" type="image/png" href="assets/img/logo/logo_icon.png">
<link rel="stylesheet" href="assets/css/style.css?v=20260519a"> <link rel="stylesheet" href="assets/css/style.css?v=20260519b">
<!-- QuaggaJS for barcode scanning --> <!-- QuaggaJS for barcode scanning -->
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script> <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 --> <!-- @xenova/transformers: ES-module bootstrap that exposes a lazy category-classifier as window._categoryPipelinePromise -->
@@ -185,6 +185,7 @@
<div class="page-header"> <div class="page-header">
<button class="back-btn" onclick="showPage('dashboard')" data-i18n="btn.back">← Indietro</button> <button class="back-btn" onclick="showPage('dashboard')" data-i18n="btn.back">← Indietro</button>
<h2 id="inventory-title" data-i18n="inventory.title">Dispensa</h2> <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>
<div class="location-tabs" id="location-tabs"> <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 active" onclick="filterLocation('')" data-loc="" data-i18n="inventory.filter_all">Tutti</button>
@@ -1287,6 +1288,30 @@
</select> </select>
</div> </div>
</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="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> </div>
</div> </div>
@@ -1560,6 +1585,6 @@
</div> </div>
</div> </div>
<script src="assets/js/app.js?v=20260519a"></script> <script src="assets/js/app.js?v=20260519b"></script>
</body> </body>
</html> </html>
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "EverShelf", "name": "EverShelf",
"short_name": "EverShelf", "short_name": "EverShelf",
"description": "Gestione completa della dispensa di casa con scansione barcode", "description": "Gestione completa della dispensa di casa con scansione barcode",
"version": "1.7.17", "version": "1.7.18",
"start_url": "/evershelf/", "start_url": "/evershelf/",
"display": "standalone", "display": "standalone",
"background_color": "#f0f4e8", "background_color": "#f0f4e8",
+18 -2
View File
@@ -500,7 +500,8 @@
"undo_success": "↩ Vorgang rückgängig gemacht für {name}", "undo_success": "↩ Vorgang rückgängig gemacht für {name}",
"already_undone": "Vorgang bereits rückgängig gemacht", "already_undone": "Vorgang bereits rückgängig gemacht",
"too_old": "Vorgänge älter als 24 Stunden können nicht rückgängig gemacht werden", "too_old": "Vorgänge älter als 24 Stunden können nicht rückgängig gemacht werden",
"undo_error": "Fehler beim Rückgängigmachen" "undo_error": "Fehler beim Rückgängigmachen",
"recipe_prefix": "Rezept"
}, },
"chat": { "chat": {
"title": "Gemini Chef", "title": "Gemini Chef",
@@ -743,7 +744,15 @@
}, },
"saved": "✅ Konfiguration gespeichert!", "saved": "✅ Konfiguration gespeichert!",
"saved_local": "✅ Konfiguration lokal gespeichert", "saved_local": "✅ Konfiguration lokal gespeichert",
"saved_local_error": "⚠️ Lokal gespeichert, Serverfehler: {error}" "saved_local_error": "⚠️ Lokal gespeichert, Serverfehler: {error}",
"theme": {
"title": "🌙 Erscheinungsbild",
"hint": "Wähle das Interface-Design.",
"label": "🌙 Design",
"off": "☀️ Hell",
"on": "🌙 Dunkel",
"auto": "🔄 Automatisch (System)"
}
}, },
"expiry": { "expiry": {
"today": "HEUTE", "today": "HEUTE",
@@ -1178,5 +1187,12 @@
"report_bug_error": "Bericht konnte nicht gesendet werden. Verbindung prüfen.", "report_bug_error": "Bericht konnte nicht gesendet werden. Verbindung prüfen.",
"changelog": "Changelog", "changelog": "Changelog",
"github": "GitHub-Repository" "github": "GitHub-Repository"
},
"export": {
"title": "Inventar exportieren",
"hint": "Lade das aktuelle Inventar als CSV herunter oder öffne die druckfertige Version (PDF).",
"btn_csv": "CSV herunterladen",
"btn_pdf": "PDF / Drucken",
"btn_title": "Exportieren"
} }
} }
+16 -1
View File
@@ -744,7 +744,15 @@
}, },
"saved": "✅ Configuration saved!", "saved": "✅ Configuration saved!",
"saved_local": "✅ Configuration saved locally", "saved_local": "✅ Configuration saved locally",
"saved_local_error": "⚠️ Saved locally, server error: {error}" "saved_local_error": "⚠️ Saved locally, server error: {error}",
"theme": {
"title": "🌙 Appearance",
"hint": "Choose the interface theme.",
"label": "🌙 Theme",
"off": "☀️ Light",
"on": "🌙 Dark",
"auto": "🔄 Auto (system)"
}
}, },
"expiry": { "expiry": {
"today": "TODAY", "today": "TODAY",
@@ -1179,5 +1187,12 @@
"report_bug_error": "Could not send the report. Check your connection.", "report_bug_error": "Could not send the report. Check your connection.",
"changelog": "Changelog", "changelog": "Changelog",
"github": "GitHub Repository" "github": "GitHub Repository"
},
"export": {
"title": "Export inventory",
"hint": "Download the current inventory as CSV or open a print-ready version (PDF).",
"btn_csv": "Download CSV",
"btn_pdf": "PDF / Print",
"btn_title": "Export"
} }
} }
+16 -1
View File
@@ -744,7 +744,15 @@
}, },
"saved": "✅ ¡Configuración guardada!", "saved": "✅ ¡Configuración guardada!",
"saved_local": "✅ Configuración guardada localmente", "saved_local": "✅ Configuración guardada localmente",
"saved_local_error": "⚠️ Guardado localmente, error del servidor: {error}" "saved_local_error": "⚠️ Guardado localmente, error del servidor: {error}",
"theme": {
"title": "🌙 Apariencia",
"hint": "Elige el tema de la interfaz.",
"label": "🌙 Tema",
"off": "☀️ Claro",
"on": "🌙 Oscuro",
"auto": "🔄 Automático (sistema)"
}
}, },
"expiry": { "expiry": {
"today": "HOY", "today": "HOY",
@@ -1179,5 +1187,12 @@
"report_bug_error": "No se pudo enviar el informe. Comprueba tu conexión.", "report_bug_error": "No se pudo enviar el informe. Comprueba tu conexión.",
"changelog": "Registro de cambios", "changelog": "Registro de cambios",
"github": "Repositorio GitHub" "github": "Repositorio GitHub"
},
"export": {
"title": "Exportar inventario",
"hint": "Descarga el inventario actual en CSV o abre la versión imprimible (PDF).",
"btn_csv": "Descargar CSV",
"btn_pdf": "PDF / Imprimir",
"btn_title": "Exportar"
} }
} }
+16 -1
View File
@@ -744,7 +744,15 @@
}, },
"saved": "✅ Configuration enregistrée !", "saved": "✅ Configuration enregistrée !",
"saved_local": "✅ Configuration enregistrée localement", "saved_local": "✅ Configuration enregistrée localement",
"saved_local_error": "⚠️ Enregistré localement, erreur serveur : {error}" "saved_local_error": "⚠️ Enregistré localement, erreur serveur : {error}",
"theme": {
"title": "🌙 Apparence",
"hint": "Choisissez le thème de l'interface.",
"label": "🌙 Thème",
"off": "☀️ Clair",
"on": "🌙 Sombre",
"auto": "🔄 Automatique (système)"
}
}, },
"expiry": { "expiry": {
"today": "AUJOURD'HUI", "today": "AUJOURD'HUI",
@@ -1179,5 +1187,12 @@
"report_bug_error": "Impossible d'envoyer le rapport. Vérifiez votre connexion.", "report_bug_error": "Impossible d'envoyer le rapport. Vérifiez votre connexion.",
"changelog": "Journal des modifications", "changelog": "Journal des modifications",
"github": "Dépôt GitHub" "github": "Dépôt GitHub"
},
"export": {
"title": "Exporter l'inventaire",
"hint": "Téléchargez l'inventaire actuel en CSV ou ouvrez la version imprimable (PDF).",
"btn_csv": "Télécharger CSV",
"btn_pdf": "PDF / Imprimer",
"btn_title": "Exporter"
} }
} }
+16 -1
View File
@@ -744,7 +744,15 @@
}, },
"saved": "✅ Configurazione salvata!", "saved": "✅ Configurazione salvata!",
"saved_local": "✅ Configurazione salvata localmente", "saved_local": "✅ Configurazione salvata localmente",
"saved_local_error": "⚠️ Salvato localmente, errore server: {error}" "saved_local_error": "⚠️ Salvato localmente, errore server: {error}",
"theme": {
"title": "🌙 Tema / Aspetto",
"hint": "Scegli il tema dell interfaccia.",
"label": "🌙 Tema",
"off": "☀️ Chiaro",
"on": "🌙 Scuro",
"auto": "🔄 Automatico (sistema)"
}
}, },
"expiry": { "expiry": {
"today": "OGGI", "today": "OGGI",
@@ -1179,5 +1187,12 @@
"report_bug_error": "Impossibile inviare la segnalazione. Controlla la connessione.", "report_bug_error": "Impossibile inviare la segnalazione. Controlla la connessione.",
"changelog": "Changelog", "changelog": "Changelog",
"github": "Repository GitHub" "github": "Repository GitHub"
},
"export": {
"title": "Esporta inventario",
"hint": "Scarica l inventario corrente in CSV o apri la versione stampabile (PDF).",
"btn_csv": "Scarica CSV",
"btn_pdf": "PDF / Stampa",
"btn_title": "Esporta"
} }
} }