diff --git a/CHANGELOG.md b/CHANGELOG.md
index fbab979..fc1faba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
+## [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
### Added
diff --git a/README.md b/README.md
index 7fea76e..b191c39 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@
[](https://www.sqlite.org/)
[](Dockerfile)
[](translations/)
-[](CHANGELOG.md)
+[](CHANGELOG.md)
[](https://github.com/dadaloop82/EverShelf/stargazers)
[](https://github.com/dadaloop82/EverShelf/commits/main)
[](https://github.com/dadaloop82/EverShelf/graphs/contributors)
diff --git a/api/index.php b/api/index.php
index 7977f7b..479b9c0 100644
--- a/api/index.php
+++ b/api/index.php
@@ -474,6 +474,10 @@ try {
guessCategoryFromAI();
break;
+ case 'export_inventory':
+ exportInventory($db);
+ break;
+
default:
http_response_code(404);
echo json_encode(['error' => 'Unknown action: ' . $action]);
@@ -485,6 +489,107 @@ try {
}
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 .= '
'
+ . '' . htmlspecialchars($r['name']) . ($brand ? ' ' . $brand . '' : '') . ' | '
+ . '' . htmlspecialchars(ucfirst($r['category'] ?? '')) . ' | '
+ . '' . $loc_icon . ' ' . htmlspecialchars(ucfirst($r['location'])) . ' | '
+ . '' . htmlspecialchars($r['quantity']) . ' ' . htmlspecialchars($r['unit'] ?? 'pz') . ' | '
+ . '' . $expiry . ' | '
+ . '' . ($r['opened_at'] ? '📭 ' . htmlspecialchars($r['opened_at']) : '') . ' | '
+ . '
';
+ }
+ $count = count($rows);
+ echo <<
+
+
+
+EverShelf — Inventory Export {$date}
+
+
+
+
+🏠 EverShelf — Inventory
+Exported: {$date} · {$count} items
+
+
+ | Name / Brand | Category | Location | Qty | Expiry | Opened |
+
+{$rows_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 =====
function ttsProxy() {
$body = json_decode(file_get_contents('php://input'), true);
diff --git a/assets/css/style.css b/assets/css/style.css
index 9372da4..6459c7b 100644
--- a/assets/css/style.css
+++ b/assets/css/style.css
@@ -6857,3 +6857,277 @@ body.cooking-mode-active .app-header {
color: #9ca3af;
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 */
diff --git a/assets/js/app.js b/assets/js/app.js
index 564ab90..663ff3b 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -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' };
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" }
function _flattenI18n(obj, prefix = '') {
const result = {};
@@ -1156,6 +1166,69 @@ function changeLanguage(lang) {
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 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 = `
+
+
+
${t('export.hint')}
+
+
+
`;
+ openModal(html);
+}
+
const LOCATIONS = {
'dispensa': { icon: '🗄️', label: t('locations.dispensa') },
'frigo': { icon: '🧊', label: t('locations.frigo') },
@@ -2462,6 +2535,10 @@ async function loadSettingsUI() {
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
_loadAboutSection();
}
@@ -2784,6 +2861,9 @@ async function saveSettings() {
if (ssEl) s.screensaver_enabled = ssEl.checked;
const ssTimeoutEl = document.getElementById('setting-screensaver-timeout');
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
const mpEnabledEl = document.getElementById('setting-meal-plan-enabled');
if (mpEnabledEl) s.meal_plan_enabled = mpEnabledEl.checked;
diff --git a/index.html b/index.html
index c53152c..6b4792f 100644
--- a/index.html
+++ b/index.html
@@ -11,7 +11,7 @@
EverShelf
-
+
@@ -185,6 +185,7 @@
@@ -1287,6 +1288,30 @@
+
+
🌙 Tema / Aspetto
+
Scegli il tema dell'interfaccia.
+
+
+
+
+
+
+
📤 Esporta inventario
+
Scarica l'inventario corrente in CSV o apri una versione stampabile (PDF).
+
+
+
+
+
@@ -1560,6 +1585,6 @@
-
+