@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+105
@@ -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 .= '<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} · {$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 =====
|
||||
function ttsProxy() {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 <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 = {
|
||||
'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;
|
||||
|
||||
+27
-2
@@ -11,7 +11,7 @@
|
||||
<title>EverShelf</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="icon" type="image/png" href="assets/img/logo/logo_icon.png">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=20260519a">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=20260519b">
|
||||
<!-- QuaggaJS for barcode scanning -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
|
||||
<!-- @xenova/transformers: ES-module bootstrap that exposes a lazy category-classifier as window._categoryPipelinePromise -->
|
||||
@@ -185,6 +185,7 @@
|
||||
<div class="page-header">
|
||||
<button class="back-btn" onclick="showPage('dashboard')" data-i18n="btn.back">← Indietro</button>
|
||||
<h2 id="inventory-title" data-i18n="inventory.title">Dispensa</h2>
|
||||
<button class="page-header-action-btn" onclick="_showExportModal()" title="Export" data-i18n-title="export.btn_title">📤</button>
|
||||
</div>
|
||||
<div class="location-tabs" id="location-tabs">
|
||||
<button class="tab active" onclick="filterLocation('')" data-loc="" data-i18n="inventory.filter_all">Tutti</button>
|
||||
@@ -1287,6 +1288,30 @@
|
||||
</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="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>
|
||||
|
||||
@@ -1560,6 +1585,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/app.js?v=20260519a"></script>
|
||||
<script src="assets/js/app.js?v=20260519b"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "EverShelf",
|
||||
"short_name": "EverShelf",
|
||||
"description": "Gestione completa della dispensa di casa con scansione barcode",
|
||||
"version": "1.7.17",
|
||||
"version": "1.7.18",
|
||||
"start_url": "/evershelf/",
|
||||
"display": "standalone",
|
||||
"background_color": "#f0f4e8",
|
||||
|
||||
+18
-2
@@ -500,7 +500,8 @@
|
||||
"undo_success": "↩ Vorgang rückgängig gemacht für {name}",
|
||||
"already_undone": "Vorgang bereits rückgängig gemacht",
|
||||
"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": {
|
||||
"title": "Gemini Chef",
|
||||
@@ -743,7 +744,15 @@
|
||||
},
|
||||
"saved": "✅ Konfiguration 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": {
|
||||
"today": "HEUTE",
|
||||
@@ -1178,5 +1187,12 @@
|
||||
"report_bug_error": "Bericht konnte nicht gesendet werden. Verbindung prüfen.",
|
||||
"changelog": "Changelog",
|
||||
"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
@@ -744,7 +744,15 @@
|
||||
},
|
||||
"saved": "✅ Configuration saved!",
|
||||
"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": {
|
||||
"today": "TODAY",
|
||||
@@ -1179,5 +1187,12 @@
|
||||
"report_bug_error": "Could not send the report. Check your connection.",
|
||||
"changelog": "Changelog",
|
||||
"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
@@ -744,7 +744,15 @@
|
||||
},
|
||||
"saved": "✅ ¡Configuración guardada!",
|
||||
"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": {
|
||||
"today": "HOY",
|
||||
@@ -1179,5 +1187,12 @@
|
||||
"report_bug_error": "No se pudo enviar el informe. Comprueba tu conexión.",
|
||||
"changelog": "Registro de cambios",
|
||||
"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
@@ -744,7 +744,15 @@
|
||||
},
|
||||
"saved": "✅ Configuration enregistrée !",
|
||||
"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": {
|
||||
"today": "AUJOURD'HUI",
|
||||
@@ -1179,5 +1187,12 @@
|
||||
"report_bug_error": "Impossible d'envoyer le rapport. Vérifiez votre connexion.",
|
||||
"changelog": "Journal des modifications",
|
||||
"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
@@ -744,7 +744,15 @@
|
||||
},
|
||||
"saved": "✅ Configurazione salvata!",
|
||||
"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": {
|
||||
"today": "OGGI",
|
||||
@@ -1179,5 +1187,12 @@
|
||||
"report_bug_error": "Impossibile inviare la segnalazione. Controlla la connessione.",
|
||||
"changelog": "Changelog",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user