Compare commits

...

21 Commits

Author SHA1 Message Date
dadaloop82 426cc9df7e release: merge develop → main for v1.7.24 2026-05-21 18:45:35 +00:00
dadaloop82 6f2d6d9944 release: v1.7.24 — changelog, readme badge, manifest version 2026-05-21 18:45:17 +00:00
dadaloop82 98426bf861 fix: dark_mode persisted in server .env (not localStorage) — add to saveSettings, getServerSettings, applySyncedSettings 2026-05-21 18:41:12 +00:00
dadaloop82 b89df961a6 fix: dark mode resets to auto on reload — bootstrap dark_mode from localStorage in getSettings() 2026-05-21 18:33:33 +00:00
dadaloop82 3b100df26c fix: cooking timer sound/TTS — shared pre-unlocked AudioContext; always speak on alarm regardless of TTS toggle; kiosk bridge TTS check 2026-05-21 18:18:45 +00:00
dadaloop82 c2004fd0f8 fix: scale use — auto-switch conf→sub (g/ml) when scale is active; show scale btn for conf+weight products 2026-05-21 06:01:04 +00:00
dadaloop82 3a1f6cfd1e fix: cooking timer — AudioContext.resume() on mobile; always play beep; show done card 3s before dismiss 2026-05-20 18:30:25 +00:00
dadaloop82 66f5a03503 fix: wrap updateInventory DB writes in a transaction to prevent concurrent lock errors (#109 #110) 2026-05-20 15:38:34 +00:00
dadaloop82 a37d97dfcd fix: kiosk — permanently hide native settings btn; add web ⚙️ in overlay; remove scattered show/hide calls 2026-05-20 15:37:57 +00:00
dadaloop82 149621651d fix: depleted items urgency based on usesPerMonth only (not recency) 2026-05-20 13:46:00 +00:00
dadaloop82 ccc2f8907d fix: depleted items urgency — use buyCount/useCount from internal history to assign medium/low 2026-05-20 13:41:04 +00:00
dadaloop82 7b60f1dbe3 fix: 0.5 conf use page (default conf mode + fraction btns); depleted items always in shopping; conf decimals in history log 2026-05-20 13:35:27 +00:00
dadaloop82 ac8b5acc0c fix: restore Bring! health check; token warning only when truly invalid
- Restore the if($bringEnabled) block that was accidentally removed in fa0442e
- Check is skipped entirely when SHOPPING_MODE != bring or credentials not set
- Missing token file = first launch, auto-created on next shopping open → ok:true
- Warning shown only if token file exists but access_token field is missing (corrupt)
- Expired tokens are OK (refreshed automatically)

Fixes spurious 'Token Bring!' warning on installs without Bring! configured
2026-05-19 17:26:06 +00:00
dadaloop82 87eac171bf fix: recipe quantities for conf+weight; move modal remembers location
- PHP (all 3 recipe endpoints): conf products with weight/volume package_unit
  (e.g., 300g/conf) now keep qty_number in sub-units (grams/ml) instead of
  converting to fractional conf. Post-normalisation block converts any AI-
  returned fractional conf values to grams automatically.
- JS submitRecipeUse: vacuum state now read from actual inventory item at the
  used location (_recipeUseContext.items), not from recipe ingredient data.
- JS showRecipeMoveModal: now uses _prefMoveLocCache preference system —
  after 2 consistent choices the modal is skipped automatically. 'Stay' button
  records the choice. Added _recipeMoveCancelStay() helper.
- JS confirmRecipeMove: records move choice via _recordMoveLocChoice();
  accepts optional forcedVacuum param for preference-triggered auto-moves.

Closes #108 (duplicate of #107 — data dir permissions)
2026-05-19 16:51:37 +00:00
dadaloop82 f77b3259ad refactor: remove localStorage for settings — all settings server-centralised
- getSettings() no longer reads from localStorage; uses _settingsCache only
- saveSettingsToStorage() no longer writes to localStorage
- _applySyncedSettings() no longer writes to localStorage
- syncSettingsFromDB() meal_plan/tts_voice blocks no longer write to localStorage
- loadSettingsUI() server-merge block no longer writes to localStorage
- flipCamera() saves camera_facing directly to server via _saveSettingToServer()
- New helper _saveSettingToServer(data): calls save_settings API for partial updates
- onShoppingEnabledChange() / onShoppingModeChange() now immediately persist to
  server .env via _saveSettingToServer() — no wait for Save button
- Early-theme IIFE: reads dedicated evershelf_dark_mode key (falls back to old
  evershelf_settings for backward compat) — only dark_mode kept in localStorage
  as a technical necessity for pre-render theme application
2026-05-19 16:41:07 +00:00
dadaloop82 84934c1908 fix: sync shopping settings across clients (serverKeys was missing shopping_* keys) 2026-05-19 16:35:54 +00:00
dadaloop82 fa0442e2f6 feat: native shopping list — decouple from Bring! (#105)
- New shopping_list SQLite table (migration in migrateDB)
- shoppingGetList/Add/Remove — delegates to Bring! or internal DB
  based on SHOPPING_MODE env var (default: internal)
- isShoppingBringMode() guard: requires mode=bring + BRING credentials
- bringQuickSyncProduct updated to support both modes
- All bring_* JS calls replaced with shopping_* (bring_migrate_names kept)
- New settings tab 'Lista spesa' (tab-bring) with:
  - Enable/disable shopping list toggle
  - Provider radio: internal vs Bring!
  - Bring! sub-section (shown only when mode=bring)
  - AI smart suggestions toggle
  - Forecast toggle
  - Auto-add threshold (qty slider)
  - Price estimation section
- _applyShoppingSettingsUI, onShoppingEnabledChange, onShoppingModeChange
- SHOPPING_* env vars documented in .env.example
- cron_smart_shopping respects SHOPPING_MODE and SHOPPING_SMART_SUGGESTIONS
- Translations: 12 new keys in all 5 languages (it/en/de/fr/es)
- DB busy_timeout=5000ms + WAL pragma in getDB() (fixes #95)
2026-05-19 16:05:49 +00:00
dadaloop82 c07439fea4 docs: add Contributing + Community section to README
- New '🤝 Contributing' section with translation table, skill matrix,
  and links to good first issues and help wanted labels
- New '💬 Community' section linking to GitHub Discussions
- Visible call-to-action for translators pointing to issue #93
2026-05-18 19:10:00 +00:00
dadaloop82 d7aadff598 fix(kiosk): target SDK 35 + setInstallReason for Android 16 compatibility
- compileSdk/targetSdk 34 → 35 (Android 15 stable)
- versionCode 17 → 18, versionName 1.7.16 → 1.7.17
- PackageInstaller.SessionParams: add setInstallReason(INSTALL_REASON_USER)
  on API 26+ — required on Android 14+ to avoid STATUS_FAILURE=1 on self-update
- DownloadManager failure: report dm_status + dm_reason in error payload
  so future issues include the HTTP error code (e.g. 404 vs network error)

Fixes #91 #92
2026-05-18 19:04:32 +00:00
dadaloop82 7364e75881 feat: Google Drive OAuth via http://localhost redirect (no public domain required)
- Switch redirect URI from server IP to http://localhost (works everywhere)
- Add manual code exchange flow: user copies URL from browser, pastes in app
- New PHP action gdrive_oauth_exchange to exchange auth code for refresh token
- Fix  null bug in gdrive_oauth_exchange (was read before initialization)
- Add #gdrive-code-section UI with input + submit button in index.html
- Update _gdriveAuthorize() to show code section and store redirect_uri
- Add _gdriveSubmitCode() JS function for manual code submission
- Update setup wizard and backup tab to show http://localhost as redirect URI
- Add 5 new translation keys (gdrive_redirect_uri_hint, gdrive_code_title,
  gdrive_code_hint, gdrive_code_submit, gdrive_code_empty) in all 5 languages
- Update gdrive_oauth_steps in all translations to reflect new flow
- Document Google Drive OAuth setup in README.md
- Dark mode: comprehensive fix for 30+ components with hardcoded light colors
2026-05-18 18:41:56 +00:00
dadaloop82 4515ff7246 i18n: replace all hardcoded Italian strings with English
- api/index.php: health check hints (disk space, DB, Gemini, TTS, scale,
  internet) translated to English; Bring! error strings (credentials,
  list not found, fetch error, missing params) translated; Gemini chat
  and identify_product error strings translated; transaction note
  [Correzione manuale] -> [Manual correction]
- assets/js/app.js: expiry scanner result strings now use t() keys
  (scanner.expiry_found, scanner.expiry_read_fail, scanner.expiry_raw_label);
  removed Italian fallback from kiosk native_update_hint toast
- translations/{it,en,de}.json: added scanner.expiry_found,
  scanner.expiry_read_fail, scanner.expiry_raw_label keys
- README.md: 'Generali' tab label -> 'General' (2 occurrences)
2026-05-18 07:32:41 +00:00
18 changed files with 2262 additions and 223 deletions
+35
View File
@@ -89,6 +89,41 @@ PRICE_CURRENCY=EUR
# PRICE_UPDATE_MONTHS: how many months to cache a price before re-fetching (default 3)
PRICE_UPDATE_MONTHS=3
# ── Cleanup / retention ──────────────────────────────────────────────────────
# RECIPE_RETENTION_DAYS: delete auto-generated recipe plans older than N days
RECIPE_RETENTION_DAYS=7
# TRANSACTION_RETENTION_DAYS: keep stock transaction history for N days.
# Smart Shopping uses this history to compute purchase frequencies.
# WARNING: values below 30 will cause the shopping list to appear nearly empty.
# Minimum enforced at runtime: 30 days.
TRANSACTION_RETENTION_DAYS=90
# ── Local Backup ─────────────────────────────────────────────────────────────
# BACKUP_ENABLED: run a daily incremental backup via cron (true/false)
BACKUP_ENABLED=true
# BACKUP_RETENTION_DAYS: keep local backups for N days (minimum 1)
BACKUP_RETENTION_DAYS=3
# ── Google Drive Backup ───────────────────────────────────────────────────────
# GDRIVE_ENABLED: upload the daily backup to Google Drive (requires a service account)
GDRIVE_ENABLED=false
#
# Setup steps:
# 1. Create a Google Cloud project and enable the Drive API
# 2. Create a Service Account and download the JSON key
# 3. Create a Drive folder and share it with the service account email
# 4. Paste the JSON content below (or set GDRIVE_SERVICE_ACCOUNT_FILE to the path)
# 5. Set GDRIVE_FOLDER_ID to the Drive folder ID (from its URL)
#
# GDRIVE_SERVICE_ACCOUNT_JSON: full JSON content of the service account key
GDRIVE_SERVICE_ACCOUNT_JSON=
# GDRIVE_SERVICE_ACCOUNT_FILE: alternative — path to the service account JSON file
GDRIVE_SERVICE_ACCOUNT_FILE=
# GDRIVE_FOLDER_ID: ID of the Drive folder where backups will be stored
GDRIVE_FOLDER_ID=
# GDRIVE_RETENTION_DAYS: delete Drive backups older than N days (0 = keep all)
GDRIVE_RETENTION_DAYS=30
# ── Security ─────────────────────────────────────────────────────────────────
# SETTINGS_TOKEN: if set, the Settings screen requires this token to save changes.
# Leave empty to allow anyone with access to the server to change settings.
+21
View File
@@ -11,6 +11,27 @@ 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.24] - 2026-05-21
### Fixed
- **Dark mode resets to Auto on every reload** — `dark_mode` was never saved to `.env` (missing from `saveSettings` and `getServerSettings`). It is now fully server-side like all other settings; `localStorage` retains only a pre-render hint for the flash-prevention IIFE.
- **Cooking timer — no sound or speech on Android kiosk** — Three independent root causes fixed: (1) `AudioContext` was created fresh outside a user gesture, starting in `suspended` state and failing silently; a shared pre-unlocked context (`_sharedAudioCtx`) is now created during user gestures (`startCookingMode`, `addCookingTimer`). (2) The `_cookingTTS` gate (for step narration) was incorrectly blocking timer alarm speech — timer alerts now always speak regardless of that flag. (3) `_kioskBridge.speak()` (native Android TTS) was never considered as a fallback when `window.speechSynthesis` is absent in the WebView.
- **Scale use ignored for conf products** — `_scaleAutoFillUse()` returned early when `_activeUnit !== 'sub'`, but conf products default to `conf` mode. The function now auto-switches to sub mode before processing the weight reading. Scale button (`btnUse`) is also now visible for conf products that have a g/ml package unit.
- **Kiosk — native settings button reappearing unexpectedly** — `closeModal()` was calling `setNativeSettingsVisible(true)`, restoring the native Android settings button after every modal close. `_injectKioskOverlay()` now permanently hides the native button; scattered per-modal show/hide calls removed; a ⚙️ web button opens the in-app settings page.
- **SQLite database locked during inventory update** — `updateInventory()` made 34 separate write statements without a transaction; a concurrent cron job could acquire the write lock between them, causing a `database is locked` PDO error. All writes are now wrapped in `beginTransaction()`/`commit()`, with the Bring! HTTP sync deferred to after `commit()`. Closes [#109](https://github.com/dadaloop82/EverShelf/issues/109), [#110](https://github.com/dadaloop82/EverShelf/issues/110).
- **Depleted-item urgency incorrect** — Items with zero quantity were assigned urgency based on recency of use rather than consumption frequency. Urgency is now computed from `usesPerMonth` only, so frequently-used depleted items are correctly flagged as urgent.
- **0.5 conf use and decimal display** — Default mode on the use-quantity page is now conf for conf products; fraction buttons (½, ¼, ¾) work correctly; conf decimals are shown in the transaction history log.
- **Bring! health check token warning** — Token validity warning was shown even for valid tokens; health check is now restored with correct token-format detection.
- **Recipe quantities for conf+weight products** — Quantities are now calculated correctly when a conf product has a gram-based package unit.
- **Shopping settings not syncing across clients** — `shopping_*` keys were missing from `serverKeys` in `_applySyncedSettings`; shopping settings were client-local. All shopping keys now sync from server on load.
### Added
- **Native shopping list** — Built-in shopping list (no Bring! required) as an alternative mode (`SHOPPING_MODE=internal`). Resolves [#105](https://github.com/dadaloop82/EverShelf/issues/105).
- **Google Drive backup via localhost OAuth** — GDrive backup no longer requires a public domain; the OAuth redirect flow uses `http://localhost` via a temporary local server, compatible with self-hosted setups. Resolves [#107](https://github.com/dadaloop82/EverShelf/issues/107).
### Changed
- **All settings fully server-centralised** — Removed remaining `localStorage` usage for user preferences; all settings are now read from and written to `.env` via the API. Preferences are shared across all devices (desktop, phone, kiosk) automatically.
## [1.7.23] - 2026-05-18
### Added
+69 -3
View File
@@ -25,7 +25,7 @@
[![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)
[![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.19-brightgreen.svg)](CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-1.7.24-brightgreen.svg)](CHANGELOG.md)
[![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)
[![Contributors](https://img.shields.io/github/contributors/dadaloop82/EverShelf)](https://github.com/dadaloop82/EverShelf/graphs/contributors)
@@ -39,7 +39,7 @@
## ✨ Features
> ⚙️ **New in v1.7.23 — Global settings tab, DB auto-cleanup, vacuum-sealed expiry**
> A new **Generali** tab groups all global settings (language, currency, theme, screensaver, zero-waste, export) in one place.
> A new **General** tab groups all global settings (language, currency, theme, screensaver, zero-waste, export) in one place.
> Recipes older than `RECIPE_RETENTION_DAYS` and transactions older than `TRANSACTION_RETENTION_DAYS` are deleted automatically every cron cycle, followed by a SQLite `VACUUM` to keep the database small.
> Vacuum-sealed products get an extended grace period (`VACUUM_EXPIRY_EXTENSION_DAYS`, default 30 days) before being flagged as expired.
> Auto theme now follows **time of day** (dark 20:0007:00) instead of the OS setting, making it server-friendly.
@@ -100,7 +100,7 @@
### 🌙 Appearance
- **Dark mode** — Three modes: Light, Dark, and Auto (time-based: dark from 20:00 to 07:00, light otherwise); applies immediately without page reload; auto mode re-evaluates every 5 minutes, so night/day transitions happen automatically even on always-on kiosk displays; theme is applied before the first render to prevent a white flash
- **Global settings tab** — A dedicated **⚙️ Generali** tab groups all system-wide settings (language, currency, theme, screensaver, zero-waste tips, export) at the top of the Settings panel
- **Global settings tab** — A dedicated **⚙️ General** tab groups all system-wide settings (language, currency, theme, screensaver, zero-waste tips, export) at the top of the Settings panel
### Database Maintenance
- **Automatic cleanup** — Recipes older than `RECIPE_RETENTION_DAYS` (default 7) and transactions older than `TRANSACTION_RETENTION_DAYS` (default 7) are deleted automatically on every cron cycle; SQLite `VACUUM` runs after each cleanup to keep the file compact
@@ -296,6 +296,24 @@ The included `backup.sh` creates local daily backups of your database:
0 3 * * * /path/to/evershelf/backup.sh
```
### Google Drive Backup (Optional)
EverShelf supports automatic daily backups to Google Drive via OAuth 2.0. This works on any server, including private IP / local network setups (no public domain required).
**Setup:**
1. Go to [console.cloud.google.com](https://console.cloud.google.com) and select or create a project.
2. Enable the **Google Drive API** (`APIs & Services → Enable APIs → Google Drive API`).
3. Go to `APIs & Services → Credentials → Create Credentials → OAuth client ID`.
4. Application type: **Web application**.
5. Add **`http://localhost`** as an Authorized Redirect URI (this is the key — it works even without a real domain).
6. Copy **Client ID** and **Client Secret** into EverShelf Settings → Backup.
7. Enter your **Google Drive Folder ID** (the last part of the folder URL).
8. Click **Authorize with Google** and sign in.
9. The browser will redirect to `http://localhost` and may show a connection error — **this is expected**. Copy the full URL from the address bar (e.g. `http://localhost/?code=4%2F0A...`) and paste it into the field that appears in EverShelf, then click **Submit**.
> **Note:** While the OAuth app is in *Testing* status in Google Cloud Console, you must add your Google account as a test user under `APIs & Services → OAuth consent screen → Test users`.
---
## 🏗️ Architecture
@@ -421,6 +439,54 @@ Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed g
---
## 🤝 Contributing
EverShelf is a community project and contributions of any size are welcome!
### Easiest way to start — translate EverShelf into your language
Translations are just JSON files. No coding, no setup — fork → edit → PR.
```
translations/
├── it.json ✅ Italian (base)
├── en.json ✅ English
├── de.json ✅ German
├── fr.json ✅ French
├── es.json ✅ Spanish
├── pt.json ❌ Portuguese — wanted!
├── nl.json ❌ Dutch — wanted!
└── ... ❌ Your language here!
```
👉 See [issue #93](https://github.com/dadaloop82/EverShelf/issues/93) to claim a language.
### Other ways to contribute
| What | Skill needed |
|---|---|
| 🐛 Report a bug | None |
| 📖 Improve the wiki | Markdown |
| 🌍 Add a translation | JSON editing |
| 🎨 Fix a CSS/UI issue | CSS / HTML |
| ⚙️ Implement a feature | PHP / JS |
| ⭐ Star the repo | Clicking |
👉 Browse [`help wanted`](https://github.com/dadaloop82/EverShelf/labels/help%20wanted) issues for good starting points.
Read [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide (branch naming, code style, how to run locally).
---
## 💬 Community
Join the conversation in [GitHub Discussions](https://github.com/dadaloop82/EverShelf/discussions):
- **Vote on upcoming features** — tell us what to build next
- **Show your setup** — share your kitchen kiosk
- **Ask questions** — get help from the community
---
## 📄 License
This project is licensed under the **MIT License** — see the [LICENSE](LICENSE) file for details.
+35 -1
View File
@@ -87,11 +87,45 @@ try {
ob_end_clean();
echo '[' . date('Y-m-d H:i:s') . '] DB cleanup done'
. ' (recipes >' . env('RECIPE_RETENTION_DAYS','7') . 'd'
. ', tx >' . env('TRANSACTION_RETENTION_DAYS','7') . 'd' . ")\n";
. ', tx >' . env('TRANSACTION_RETENTION_DAYS','90') . 'd' . ")\n";
} catch (Throwable $ce) {
echo '[' . date('Y-m-d H:i:s') . '] DB cleanup warning: ' . $ce->getMessage() . "\n";
}
// ── Daily incremental backup ──────────────────────────────────────────
// Create a local backup at most once every 23 h; also push to Google Drive
// if GDRIVE_ENABLED=true. The guard prevents multiple backups per day even
// though the cron runs every 5 minutes.
if (env('BACKUP_ENABLED', 'true') === 'true') {
try {
$lastBackupTs = 0;
if (file_exists(BACKUP_LAST_TS_PATH)) {
$lastData = json_decode(file_get_contents(BACKUP_LAST_TS_PATH), true) ?: [];
$lastBackupTs = (int)($lastData['ts'] ?? 0);
}
if (time() - $lastBackupTs >= 82800) { // 23 h
$backupResult = createLocalBackup($db);
if ($backupResult['success']) {
echo '[' . date('Y-m-d H:i:s') . '] Backup local: ' . $backupResult['filename']
. ' (' . $backupResult['size_kb'] . 'KB, purged ' . $backupResult['purged'] . " old)\n";
if (env('GDRIVE_ENABLED', 'false') === 'true') {
$gResult = backupToGDrive($db);
if ($gResult['success']) {
echo '[' . date('Y-m-d H:i:s') . '] Backup GDrive: OK'
. ' (purged remote: ' . ($gResult['purged_remote'] ?? 0) . ")\n";
} else {
echo '[' . date('Y-m-d H:i:s') . '] Backup GDrive warning: ' . ($gResult['error'] ?? 'unknown') . "\n";
}
}
} else {
echo '[' . date('Y-m-d H:i:s') . '] Backup warning: ' . ($backupResult['error'] ?? 'unknown') . "\n";
}
}
} catch (Throwable $be) {
echo '[' . date('Y-m-d H:i:s') . '] Backup error: ' . $be->getMessage() . "\n";
}
}
} catch (Throwable $e) {
$msg = $e->getMessage();
echo '[' . date('Y-m-d H:i:s') . '] ERROR: ' . $msg . "\n";
+23
View File
@@ -48,6 +48,13 @@ function getDB(): PDO {
? new LoggingPDO('sqlite:' . DB_PATH)
: new PDO('sqlite:' . DB_PATH);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Set a busy timeout to prevent "database is locked" errors under high concurrency.
// This gives SQLite up to 5 seconds to acquire a lock before throwing an exception.
$db->setAttribute(PDO::ATTR_TIMEOUT, 5); // PDO::ATTR_TIMEOUT is in seconds for MySQL, but not directly for SQLite.
// For SQLite, we use PRAGMA busy_timeout.
$db->exec('PRAGMA journal_mode = WAL;');
$db->exec('PRAGMA busy_timeout = 5000;'); // 5000 milliseconds = 5 seconds
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$db->exec("PRAGMA journal_mode=WAL");
$db->exec("PRAGMA foreign_keys=ON");
@@ -244,6 +251,22 @@ function migrateDB(PDO $db): void {
// Ensure composite indexes exist (added in v1.7.5 for performance)
$db->exec("CREATE INDEX IF NOT EXISTS idx_transactions_type_date ON transactions(type, created_at)");
$db->exec("CREATE INDEX IF NOT EXISTS idx_transactions_pid_type_undone ON transactions(product_id, type, undone)");
// Internal shopping list table (v1.8.0) — used when SHOPPING_MODE=internal
$shopTables = $db->query("SELECT name FROM sqlite_master WHERE type='table' AND name='shopping_list'")->fetchAll();
if (empty($shopTables)) {
$db->exec("
CREATE TABLE shopping_list (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
raw_name TEXT NOT NULL DEFAULT '',
specification TEXT NOT NULL DEFAULT '',
added_at INTEGER DEFAULT (strftime('%s','now')),
sort_order INTEGER DEFAULT 0
)
");
$db->exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_shopping_list_name ON shopping_list(lower(name))");
}
}
/**
+841 -104
View File
File diff suppressed because it is too large Load Diff
+157
View File
@@ -7133,6 +7133,7 @@ body.cooking-mode-active .app-header {
--bg: #0f172a;
--bg-card: #1e293b;
--bg-dark: #020617;
--bg-secondary: #263448;
--text: #e2e8f0;
--text-light: #94a3b8;
--text-muted: #64748b;
@@ -7384,3 +7385,159 @@ body.cooking-mode-active .app-header {
color: var(--primary-light);
}
/* @media prefers-color-scheme: auto handled in JS */
/* ===== DARK MODE — EXTENDED COMPONENT OVERRIDES ===== */
/* ── Inventory badges ── */
[data-theme="dark"] .badge-location { background: #0c2a4e; color: #7dd3fc; }
[data-theme="dark"] .badge-category { background: #1e293b; color: #94a3b8; }
[data-theme="dark"] .badge-qty { background: #0f2a1a; color: #86efac; }
[data-theme="dark"] .badge-expiry { background: #2a1a00; color: #fcd34d; }
[data-theme="dark"] .badge-expired { background: #2a0808; color: #fca5a5; }
/* ── Urgency / priority badges ── */
[data-theme="dark"] .badge-critical { background: #2a0808; color: #fca5a5; }
[data-theme="dark"] .badge-high { background: #2a1200; color: #fdba74; }
[data-theme="dark"] .badge-medium { background: #2a1e00; color: #fde68a; }
[data-theme="dark"] .badge-low { background: #0f2a1a; color: #86efac; }
[data-theme="dark"] .badge-freq-high { background: #2e0d1a; color: #f9a8d4; }
[data-theme="dark"] .badge-tag-add { background: #1e293b; color: #94a3b8; }
/* ── Smart shopping badges ── */
[data-theme="dark"] .smart-freq-badge.freq-suggest { background: #0c2a4e; color: #7dd3fc; }
[data-theme="dark"] .smart-freq-badge.freq-suggest-approx { background: #0c1f3a; color: #93c5fd; font-style: italic; }
[data-theme="dark"] .smart-pred-badge { background: #2a1e00; color: #fde68a; }
[data-theme="dark"] .smart-pred-badge.pred-urgent { background: #2a0808; color: #fca5a5; }
[data-theme="dark"] .smart-pred-badge.pred-soon { background: #2a1200; color: #fdba74; }
[data-theme="dark"] .smart-bring-badge { background: #0c2a4e; color: #7dd3fc; }
/* ── AW trend mini-cards ── */
[data-theme="dark"] .aw-tcard-good { background: #0f2a1a; border-color: #166534; }
[data-theme="dark"] .aw-tcard-ok { background: #1c1300; border-color: #78350f; }
[data-theme="dark"] .aw-tcard-bad { background: #2a0808; border-color: #7f1d1d; }
/* ── Alert sections ── */
[data-theme="dark"] .alert-danger { background: #2a0808; border-color: var(--danger); }
[data-theme="dark"] .alert-item { background: rgba(255,255,255,0.04); }
[data-theme="dark"] .alert-item-qty { background: rgba(255,255,255,0.06); }
[data-theme="dark"] .alert-review { background: #1c1300; border-color: #78350f; }
[data-theme="dark"] .alert-review h3 { color: #fcd34d; }
[data-theme="dark"] .alert-opened { background: #0c1f3a; border-color: #1e3a8a; }
[data-theme="dark"] .alert-opened h3 { color: #7dd3fc; }
[data-theme="dark"] .alert-item-badge.opened { background: #1e40af; }
/* ── Opened expiry badges ── */
[data-theme="dark"] .opened-expiry-ok { background: #0f2a1a; color: #86efac; }
[data-theme="dark"] .opened-expiry-soon { background: #2a1e00; color: #fde68a; }
[data-theme="dark"] .opened-expiry-urgent { background: #2a0808; color: #fca5a5; }
/* ── Alert banner: gradient overrides ── */
[data-theme="dark"] .alert-banner.banner-expired { background: #2a0808; border-color: #7f1d1d; }
[data-theme="dark"] .banner-expired .alert-banner-title { color: #fca5a5; }
[data-theme="dark"] .banner-expired .alert-banner-counter { color: #f87171; }
[data-theme="dark"] .alert-banner.banner-expiring { background: #1c1300; border-color: #78350f; }
[data-theme="dark"] .banner-expiring .alert-banner-title { color: #fdba74; }
[data-theme="dark"] .banner-expiring .alert-banner-counter { color: #fb923c; }
[data-theme="dark"] .alert-banner.banner-expired-ok { background: #0f2a1a; border-color: #166534; }
[data-theme="dark"] .banner-expired-ok .alert-banner-title { color: #86efac; }
[data-theme="dark"] .banner-expired-ok .alert-banner-counter { color: #4ade80; }
[data-theme="dark"] .alert-banner.banner-expired-warning { background: #1c1300; border-color: #78350f; }
[data-theme="dark"] .banner-expired-warning .alert-banner-title { color: #fde68a; }
[data-theme="dark"] .banner-expired-warning .alert-banner-counter { color: #fcd34d; }
[data-theme="dark"] .alert-banner.banner-expired-danger { background: #2a0808; border-color: #7f1d1d; border-width: 2px; }
[data-theme="dark"] .banner-expired-danger .alert-banner-title { color: #fca5a5; }
[data-theme="dark"] .alert-banner.banner-prediction { background: #1a1040; border-color: #6d28d9; }
[data-theme="dark"] .banner-prediction .alert-banner-title { color: #c4b5fd; }
[data-theme="dark"] .banner-prediction .alert-banner-counter { color: #a78bfa; }
[data-theme="dark"] .alert-banner.banner-anomaly { background: #1a1200; border-color: #c2410c; }
[data-theme="dark"] .banner-anomaly .alert-banner-title { color: #fdba74; }
[data-theme="dark"] .alert-banner.banner-no-expiry { background: #0f2a1a; border-color: #166534; }
[data-theme="dark"] .banner-no-expiry .alert-banner-title { color: #86efac; }
/* ── Alert banner: default text & close ── */
[data-theme="dark"] .alert-banner-title { color: #e2e8f0; }
[data-theme="dark"] .alert-banner-detail { color: #94a3b8; }
[data-theme="dark"] .alert-banner-close { color: #94a3b8; background: rgba(255,255,255,0.06); }
[data-theme="dark"] .banner-safety-warning { color: #fdba74; }
[data-theme="dark"] .banner-safety-ok { color: #86efac; }
[data-theme="dark"] .banner-safety-danger { color: #fca5a5; }
/* ── Banner action buttons ── */
[data-theme="dark"] .btn-banner-ok { background: #0f2a1a; color: #86efac; }
[data-theme="dark"] .btn-banner-edit { background: #1a1040; color: #c4b5fd; }
[data-theme="dark"] .btn-banner-ai { background: #2e1a4a; color: #c4b5fd; }
[data-theme="dark"] .btn-banner-weigh { background: #2e1a4a; color: #c4b5fd; }
[data-theme="dark"] .btn-banner-confirm { background: #0f2a1a; color: #86efac; }
[data-theme="dark"] .btn-banner-use { background: #0c2a4e; color: #7dd3fc; }
[data-theme="dark"] .btn-banner-throw { background: #2a0808; color: #fca5a5; }
[data-theme="dark"] .btn-banner-throw-primary { background: #dc2626; color: #fff; }
[data-theme="dark"] .btn-banner-use-danger { background: #1e293b; color: #64748b; }
[data-theme="dark"] .btn-banner-vacuum { background: #2e1a4a; color: #c4b5fd; }
[data-theme="dark"] .btn-banner-edit2 { background: #0c2a4e; color: #7dd3fc; }
/* ── Review items ── */
[data-theme="dark"] .review-item { background: rgba(255,255,255,0.04); }
[data-theme="dark"] .review-item-meta { color: #94a3b8; }
[data-theme="dark"] .review-warn { color: #fca5a5; }
[data-theme="dark"] .review-qty-value { background: #2a0808; color: #fca5a5; }
[data-theme="dark"] .btn-review-ok { background: #0f2a1a; color: #86efac; }
[data-theme="dark"] .btn-review-ok:active { background: #0d2416; }
[data-theme="dark"] .btn-review-edit { background: #1a1040; color: #c4b5fd; }
[data-theme="dark"] .btn-review-edit:active { background: #140d36; }
/* ── Chat UI ── */
[data-theme="dark"] .chat-header-bar { background: var(--bg-card); border-color: var(--border); }
[data-theme="dark"] .chat-title { color: #818cf8; }
[data-theme="dark"] .chat-suggestion { background: #1a1040; border-color: #3730a3; color: #a5b4fc; }
[data-theme="dark"] .chat-suggestion:active { background: #2e1a4a; }
[data-theme="dark"] .chat-gemini { background: var(--bg-card); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,0.3); }
[data-theme="dark"] .chat-gemini strong { color: #818cf8; }
[data-theme="dark"] .chat-input-bar { background: var(--bg-card); border-color: var(--border); }
/* ── Settings status ── */
[data-theme="dark"] .settings-status.success { background: #0f2a1a; color: #86efac; }
[data-theme="dark"] .settings-status.error { background: #2a0808; color: #fca5a5; }
/* ── Inventory status bar ── */
[data-theme="dark"] .inventory-status-bar {
background: linear-gradient(135deg, #0c2a4e 0%, #1a1040 100%);
border-color: #1e3a8a;
}
[data-theme="dark"] .inventory-status-bar .inv-status-title { color: #7dd3fc; }
[data-theme="dark"] .inventory-status-bar .inv-status-total { color: #e2e8f0; background: rgba(0,0,0,0.3); }
[data-theme="dark"] .inventory-status-bar .inv-status-item { color: #93c5fd; background: rgba(0,0,0,0.2); }
/* ── Use inventory info ── */
[data-theme="dark"] .use-inventory-info { background: #0c2a4e; color: #7dd3fc; }
[data-theme="dark"] #use-expiry-hint { background: #2a1e00; border-color: #78350f; color: #fde68a; }
/* ── Recipe components ── */
[data-theme="dark"] .recipe-expiry-note { background: #2a1e00; color: #fde68a; }
[data-theme="dark"] .recipe-tools-banner { background: #1a1040; border-color: #3730a3; color: #c4b5fd; }
[data-theme="dark"] .recipe-tool-chip { background: #2e1a4a; color: #c4b5fd; }
[data-theme="dark"] .recipe-subtype-chip { background: #1c1300; border-color: #78350f; color: var(--text); }
[data-theme="dark"] .recipe-subtype-chip:has(input:checked) { background: #2a1e00; border-color: #d97706; }
/* ── Bug report pills ── */
[data-theme="dark"] .bug-type-pill { background: var(--bg-card); border-color: var(--border); color: var(--text-light); }
/* ── Shopping tag menu ── */
[data-theme="dark"] .shopping-tag-menu-container { background: #1a2336; }
/* ── Edit unknown card ── */
[data-theme="dark"] .edit-unknown-card.highlight { background: #1c1300; border-color: var(--warning); }
/* ── AI match image ── */
[data-theme="dark"] .ai-match-img { background: var(--bg-card); }
/* ── Inline edit button ── */
[data-theme="dark"] .btn-edit-inline { background: rgba(30,41,59,0.92); border-color: var(--border); color: var(--text); }
/* ── Setup wizard ── */
[data-theme="dark"] .setup-body p { color: var(--text-muted); }
[data-theme="dark"] .setup-footer { border-color: var(--border); }
[data-theme="dark"] .setup-skip-link { color: var(--text-muted); }
[data-theme="dark"] .setup-skip-link:hover { color: var(--text-light); }
/* ── Appliance remove active ── */
[data-theme="dark"] .appliance-item .appliance-remove:active { background: #2a0808; }
+583 -99
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
{"ts":1779204302,"filename":"evershelf_2026-05-19_1525.db","size_kb":444}
+4 -4
View File
@@ -5,14 +5,14 @@ plugins {
android {
namespace = "it.dadaloop.evershelf.kiosk"
compileSdk = 34
compileSdk = 35
defaultConfig {
applicationId = "it.dadaloop.evershelf.kiosk"
minSdk = 24
targetSdk = 34
versionCode = 17
versionName = "1.7.16"
targetSdk = 35
versionCode = 18
versionName = "1.7.17"
}
signingConfigs {
@@ -774,7 +774,13 @@ class KioskActivity : AppCompatActivity() {
val q = DownloadManager.Query().setFilterById(downloadId)
val c = (getSystemService(DOWNLOAD_SERVICE) as DownloadManager).query(q)
var ok = false
if (c.moveToFirst()) ok = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)) == DownloadManager.STATUS_SUCCESSFUL
var dmStatus = -1
var dmReason = -1
if (c.moveToFirst()) {
dmStatus = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
dmReason = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON))
ok = dmStatus == DownloadManager.STATUS_SUCCESSFUL
}
c.close()
if (ok) {
pollHandler.removeCallbacksAndMessages(null); activeDownloadId = -1
@@ -784,7 +790,12 @@ class KioskActivity : AppCompatActivity() {
pollHandler.removeCallbacksAndMessages(null); activeDownloadId = -1
setInstallUI("\u274C", getString(R.string.install_error_download), getString(R.string.install_error_download_detail), 0xFFf87171.toInt(), btnEnabled = true, progress = -2)
runOnUiThread { activeInstallBtn?.text = getString(R.string.install_btn_retry) }
ErrorReporter.reportMessage("install_download_failed", "DownloadManager returned failure for URL: $apkUrl")
ErrorReporter.reportMessage(
"install_download_failed",
"DownloadManager returned failure for URL: $apkUrl",
mapOf("dm_status" to dmStatus, "dm_reason" to dmReason,
"device" to buildDeviceLabel())
)
}
}
}
@@ -868,6 +879,11 @@ class KioskActivity : AppCompatActivity() {
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
// Note: setAppPackageName() is intentionally omitted — it causes STATUS_FAILURE (1)
// on some OEM/Android versions even when the package name is correct.
// setInstallReason is required on Android 14+ (API 34+) for PackageInstaller
// to accept self-updates; without it Android 16 returns STATUS_FAILURE=1.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
params.setInstallReason(android.content.pm.PackageManager.INSTALL_REASON_USER)
}
val sessionId = pi.createSession(params)
val session = pi.openSession(sessionId)
try {
+149 -3
View File
@@ -833,7 +833,7 @@
<div class="settings-tabs">
<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" data-i18n-title="settings.shopping.tab" title="Lista spesa">🛒</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-appliances')" data-tab="tab-appliances" title="Elettrodomestici">🔌</button>
@@ -841,6 +841,7 @@
<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-scale')" data-tab="tab-scale" title="Bilancia Smart" data-i18n-title="settings.scale.tab">⚖️</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-backup'); _loadBackupTab();" data-tab="tab-backup" data-i18n-title="settings.backup.tab" title="Backup">💾</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 class="settings-panels">
@@ -954,9 +955,36 @@
</div>
<!-- Bring! Tab -->
<div class="settings-panel" id="tab-bring">
<!-- Shopping enable + provider -->
<div class="settings-card">
<h4 data-i18n="settings.bring.title">🛒 Bring! Shopping List</h4>
<p class="settings-hint" data-i18n="settings.bring.hint">Credenziali per l'integrazione con la lista della spesa Bring!</p>
<h4 data-i18n="settings.shopping.title">🛒 Lista della spesa</h4>
<p class="settings-hint" data-i18n="settings.shopping.hint">Configura la lista della spesa integrata o collega Bring!.</p>
<div class="form-group">
<label class="toggle-row">
<span data-i18n="settings.shopping.enable_label">Abilita lista della spesa</span>
<span class="toggle-switch">
<input type="checkbox" id="setting-shopping-enabled" onchange="onShoppingEnabledChange()">
<span class="toggle-slider"></span>
</span>
</label>
</div>
<div class="form-group" id="shopping-mode-group">
<label data-i18n="settings.shopping.mode_label">Provider</label>
<div class="radio-group" style="margin-top:6px">
<label class="radio-option">
<input type="radio" name="shopping-mode" value="internal" onchange="onShoppingModeChange(this.value)">
<span data-i18n="settings.shopping.mode_internal">Interno (senza Bring!)</span>
</label>
<label class="radio-option" style="margin-left:16px">
<input type="radio" name="shopping-mode" value="bring" onchange="onShoppingModeChange(this.value)">
<span data-i18n="settings.shopping.mode_bring">Bring! (app esterna)</span>
</label>
</div>
</div>
</div>
<!-- Bring! sub-section (shown only when mode = bring) -->
<div class="settings-card" id="bring-subsection" style="display:none;margin-top:12px">
<h4 data-i18n="settings.shopping.bring_section_title">Configurazione Bring!</h4>
<div class="form-group">
<label data-i18n="settings.bring.email_label">📧 Email Bring!</label>
<input type="email" id="setting-bring-email" class="form-input" placeholder="email@esempio.com">
@@ -967,6 +995,37 @@
<button class="btn btn-small btn-secondary mt-2" onclick="togglePasswordVisibility('setting-bring-password')" data-i18n="btn.toggle_password">👁️ Mostra/Nascondi</button>
</div>
</div>
<!-- Smart suggestions + forecast -->
<div class="settings-card" style="margin-top:12px">
<h4 data-i18n="settings.shopping.ai_section_title">Assistenza AI</h4>
<div class="form-group">
<label class="toggle-row">
<span data-i18n="settings.shopping.smart_suggestions_label">Suggerimenti AI</span>
<span class="toggle-switch">
<input type="checkbox" id="setting-shopping-smart-suggestions">
<span class="toggle-slider"></span>
</span>
</label>
</div>
<div class="form-group">
<label class="toggle-row">
<span data-i18n="settings.shopping.forecast_label">Previsione prodotti in esaurimento</span>
<span class="toggle-switch">
<input type="checkbox" id="setting-shopping-forecast">
<span class="toggle-slider"></span>
</span>
</label>
</div>
<div class="form-group" style="margin-top:8px">
<label data-i18n="settings.shopping.auto_add_label">Aggiungi automaticamente quando</label>
<div class="qty-control" style="margin-top:6px">
<button type="button" class="qty-btn" onclick="adjustQty('setting-shopping-auto-add', -1, 0, 20)"></button>
<input type="number" id="setting-shopping-auto-add" value="0" min="0" max="20" class="qty-input">
<button type="button" class="qty-btn" onclick="adjustQty('setting-shopping-auto-add', 1, 0, 20)">+</button>
</div>
<p class="settings-hint" data-i18n="settings.shopping.auto_add_suffix">rimasto in magazzino (0 = solo quando esaurito)</p>
</div>
</div>
<!-- Price Estimation Settings -->
<div class="settings-card" style="margin-top:12px">
<h4 data-i18n="settings.price.title">💰 Stima Prezzi (AI)</h4>
@@ -1342,6 +1401,93 @@
</div>
<!-- Language Tab -->
<!-- Backup Tab -->
<div class="settings-panel" id="tab-backup">
<!-- Local Backup -->
<div class="settings-card">
<h4 data-i18n="settings.backup.local_title">💾 Backup Locale</h4>
<p class="settings-hint" data-i18n="settings.backup.local_hint">Snapshot giornaliero automatico del database. Massimo 3 giorni di storico (configurabile).</p>
<div id="backup-last-info" style="margin-bottom:12px;padding:10px 12px;background:var(--bg-secondary,#f8fafc);border-radius:8px;font-size:0.83rem;color:var(--text-secondary)">
<span data-i18n="settings.info.loading">Caricamento…</span>
</div>
<div class="form-group" style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:14px">
<label data-i18n="settings.backup.retention_days" style="flex-shrink:0">Retention (giorni):</label>
<input type="number" id="setting-backup-retention-days" class="form-input" style="width:80px" min="1" max="90" value="3">
</div>
<button class="btn btn-large btn-accent full-width" onclick="_backupNow()" id="btn-backup-now" data-i18n="settings.backup.backup_now">💾 Backup Ora</button>
<div id="backup-status" style="display:none;margin-top:8px" class="settings-status"></div>
<!-- List of backups -->
<div id="backup-list-container" style="margin-top:14px">
<p class="settings-hint" data-i18n="settings.info.loading">Caricamento…</p>
</div>
</div>
<!-- Google Drive -->
<div class="settings-card">
<h4 data-i18n="settings.backup.gdrive_title">☁️ Google Drive</h4>
<p class="settings-hint" data-i18n="settings.backup.gdrive_hint">Carica automaticamente il backup su Google Drive usando un Service Account.</p>
<div class="form-group" style="margin-bottom:10px">
<label class="toggle-row">
<span data-i18n="settings.backup.gdrive_enabled">Abilita backup Google Drive</span>
<span class="toggle-switch">
<input type="checkbox" id="setting-gdrive-enabled" onchange="saveSettings()">
<span class="toggle-slider"></span>
</span>
</label>
</div>
<div id="gdrive-config-section">
<!-- Folder ID (shared between both methods) -->
<div class="form-group">
<label data-i18n="settings.backup.gdrive_folder_id">ID Cartella Drive</label>
<input type="text" id="setting-gdrive-folder-id" class="form-input" placeholder="1ABCdef_xyz…">
<p class="settings-hint" data-i18n="settings.backup.gdrive_folder_id_hint">Copia l'ID dalla URL della cartella Drive: …/folders/<strong>ID</strong></p>
</div>
<!-- OAuth 2.0 section -->
<div id="gdrive-oauth-section">
<details style="margin-bottom:14px;background:var(--bg-secondary,#f8fafc);border-radius:8px;padding:10px 14px">
<summary style="cursor:pointer;font-weight:600;font-size:0.83rem" data-i18n="settings.backup.gdrive_oauth_how_to">📋 Come configurare OAuth 2.0 (passo dopo passo)</summary>
<ol style="margin:10px 0 0 16px;font-size:0.8rem;color:var(--text-secondary);line-height:1.8" data-i18n-html="settings.backup.gdrive_oauth_steps"></ol>
</details>
<div class="form-group">
<label data-i18n="settings.backup.gdrive_client_id">Client ID</label>
<input type="text" id="setting-gdrive-client-id" class="form-input" placeholder="1234567890-abc….apps.googleusercontent.com">
</div>
<div class="form-group">
<label data-i18n="settings.backup.gdrive_client_secret">Client Secret</label>
<input type="password" id="setting-gdrive-client-secret" class="form-input" placeholder="GOCSPX-…">
</div>
<div class="form-group" style="background:var(--bg-secondary,#f8fafc);border-radius:8px;padding:10px 14px;font-size:0.82rem">
<span data-i18n="settings.backup.gdrive_redirect_uri_label">Redirect URI (aggiungi in Google Cloud Console):</span>
<code id="gdrive-redirect-uri-display" style="display:block;margin-top:4px;word-break:break-all;color:var(--text-primary);font-size:0.78rem">http://localhost</code>
<p class="settings-hint" style="margin-top:6px;margin-bottom:0" data-i18n="settings.backup.gdrive_redirect_uri_hint">Registra questo URI in Google Cloud Console come "URI di reindirizzamento autorizzato". Per le installazioni senza dominio pubblico usa <strong>http://localhost</strong>.</p>
</div>
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:8px">
<button class="btn btn-secondary" onclick="_gdriveAuthorize()" id="btn-gdrive-authorize" data-i18n="settings.backup.gdrive_oauth_authorize">🔑 Autorizza con Google</button>
<span id="gdrive-oauth-token-status" style="font-size:0.83rem"></span>
</div>
<!-- Manual code entry (appears after clicking Authorize) -->
<div id="gdrive-code-section" style="display:none;margin-top:12px;padding:12px 14px;background:var(--bg-secondary,#f8fafc);border-radius:8px;border:1px solid var(--border)">
<p style="font-size:0.82rem;margin-bottom:8px;font-weight:600" data-i18n="settings.backup.gdrive_code_title">Incolla l'URL o il codice di autorizzazione</p>
<p class="settings-hint" style="margin-bottom:8px" data-i18n="settings.backup.gdrive_code_hint">Dopo aver autorizzato su Google, il browser proverà ad aprire <code>http://localhost</code> e mostrerà un errore. Copia l'intero URL dalla barra degli indirizzi e incollalo qui sotto.</p>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<input type="text" id="gdrive-code-input" class="form-input" style="flex:1;min-width:0" placeholder="http://localhost/?code=4%2F0A… oppure solo il codice">
<button class="btn btn-primary" onclick="_gdriveSubmitCode()" id="btn-gdrive-submit-code" data-i18n="settings.backup.gdrive_code_submit">Conferma</button>
</div>
</div>
</div>
<!-- Retention + action buttons (shared) -->
<div class="form-group" style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:10px">
<label data-i18n="settings.backup.gdrive_retention_days" style="flex-shrink:0">Retention Drive (giorni, 0=tutto):</label>
<input type="number" id="setting-gdrive-retention-days" class="form-input" style="width:80px" min="0" max="365" value="30">
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:12px">
<button class="btn btn-secondary" onclick="_gdriveTest()" id="btn-gdrive-test" data-i18n="settings.backup.gdrive_test">🔗 Testa Connessione</button>
<button class="btn btn-accent" onclick="_gdrivePushNow()" id="btn-gdrive-push" data-i18n="settings.backup.gdrive_push_now">☁️ Carica Ora su Drive</button>
</div>
<div id="gdrive-test-status" style="display:none;margin-top:8px" class="settings-status"></div>
</div>
</div>
</div>
<!-- Info Tab -->
<div class="settings-panel" id="tab-info">
<!-- Gemini AI Usage card -->
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "EverShelf",
"short_name": "EverShelf",
"description": "Gestione completa della dispensa di casa con scansione barcode",
"version": "1.7.23",
"version": "1.7.24",
"start_url": "/evershelf/",
"display": "standalone",
"background_color": "#f0f4e8",
+67 -2
View File
@@ -762,6 +762,53 @@
"card_hint": "Zeige während des Kochens Tipps zur Wiederverwendung von Abfällen (Schalen, Kochwasser usw.). Standardmäßig deaktiviert.",
"label": "Tipps beim Kochen anzeigen"
},
"backup": {
"tab": "Backup",
"local_title": "Lokales Backup",
"local_hint": "Täglicher Datenbank-Snapshot. Konfiguriere, wie viele Tage Backups aufbewahrt werden.",
"enabled": "Tägliches automatisches Backup aktivieren",
"retention_days": "Aufbewahrung (Tage)",
"retention_info": "Backups werden aufbewahrt für",
"backup_now": "Jetzt sichern",
"backing_up": "Sicherung läuft…",
"backed_up": "Sicherung abgeschlossen",
"backup_error": "Sicherungsfehler",
"last_backup": "Letztes Backup",
"no_backup_yet": "Noch kein Backup erstellt",
"list_empty": "Keine Backups verfügbar",
"restore_btn": "Wiederherstellen",
"restore_confirm": "Backup wiederherstellen",
"delete_btn": "Löschen",
"delete_confirm": "Backup löschen",
"gdrive_title": "Google Drive",
"gdrive_hint": "Backups automatisch via OAuth 2.0 auf Google Drive hochladen. Keine externen Bibliotheken erforderlich.",
"gdrive_enabled": "Google Drive Backup aktivieren",
"gdrive_folder_id": "Drive-Ordner-ID",
"gdrive_folder_id_hint": "Kopiere die ID aus der Drive-Ordner-URL: …/folders/<strong>ID</strong>",
"gdrive_retention_days": "Drive-Aufbewahrung (Tage, 0=alles behalten)",
"gdrive_test": "Verbindung testen",
"gdrive_ok": "Verbindung erfolgreich!",
"gdrive_error": "Verbindung fehlgeschlagen",
"gdrive_push_now": "Jetzt auf Drive hochladen",
"gdrive_pushing": "Wird hochgeladen…",
"gdrive_pushed": "Auf Drive hochgeladen",
"gdrive_wizard_hint": "Optional: täglich automatisch via OAuth 2.0 auf Google Drive sichern.",
"gdrive_skip": "Überspringen — später in Einstellungen konfigurieren",
"gdrive_client_id": "Client-ID",
"gdrive_client_secret": "Client-Secret",
"gdrive_redirect_uri_hint": "Füge <strong>http://localhost</strong> als autorisierten Weiterleitungs-URI in der Google Cloud Console hinzu. Funktioniert auf jedem Server, auch ohne öffentliche Domain.",
"gdrive_code_title": "Autorisierungs-URL oder Code einfügen",
"gdrive_code_hint": "Nach der Autorisierung öffnet der Browser http://localhost und zeigt möglicherweise einen Verbindungsfehler — das ist normal. Kopiere die URL aus der Adressleiste (z.B. <code>http://localhost/?code=4%2F0A...</code>) und füge sie hier ein.",
"gdrive_code_submit": "Bestätigen",
"gdrive_code_empty": "Bitte zuerst die URL oder den Autorisierungscode einfügen",
"gdrive_redirect_uri_label": "Redirect-URI (in Google Cloud Console eintragen):",
"gdrive_oauth_authorize": "Mit Google autorisieren",
"gdrive_oauth_authorized": "Autorisiert",
"gdrive_oauth_not_authorized": "Noch nicht autorisiert",
"gdrive_oauth_window_opened": "Browserfenster geöffnet — autorisieren und zurückkehren",
"gdrive_oauth_how_to": "OAuth 2.0 einrichten (Schritt für Schritt)",
"gdrive_oauth_steps": "<li>Gehe zu <a href='https://console.cloud.google.com/' target='_blank' rel='noopener'>console.cloud.google.com</a> und wähle dein Projekt</li><li>Aktiviere die <strong>Google Drive API</strong>: <em>APIs &amp; Dienste → APIs aktivieren → Google Drive API</em></li><li>Gehe zu <em>APIs &amp; Dienste → Anmeldedaten → Anmeldedaten erstellen → OAuth-Client-ID</em></li><li>Anwendungstyp: <strong>Webanwendung</strong>; füge die unten angezeigte URL als <em>Autorisierter Weiterleitungs-URI</em> hinzu</li><li>Kopiere <strong>Client-ID</strong> und <strong>Client-Secret</strong> in die Felder oben und speichere</li><li>Klicke auf <strong>Mit Google autorisieren</strong>: melde dich an und erteile den Zugriff</li><li>Das Fenster schließt sich automatisch und Backups sind bereit</li>"
},
"info": {
"tab": "Info",
"ai_title": "Gemini AI — Token-Nutzung",
@@ -802,7 +849,22 @@
"currency_title": "Währung",
"currency_hint": "Die Währung, die für alle Kosten und Preise in der App verwendet wird."
},
"tab_general": "Allgemein"
"tab_general": "Allgemein",
"shopping": {
"tab": "Einkaufsliste",
"title": "Einkaufsliste",
"hint": "Konfiguriere die integrierte Einkaufsliste oder verbinde Bring!.",
"enable_label": "Einkaufsliste aktivieren",
"mode_label": "Anbieter",
"mode_internal": "Intern (ohne Bring!)",
"mode_bring": "Bring! (externe App)",
"bring_section_title": "Bring!-Konfiguration",
"ai_section_title": "KI-Unterstützung",
"smart_suggestions_label": "KI-Vorschläge",
"forecast_label": "Prognose für bald leere Produkte",
"auto_add_label": "Automatisch hinzufügen wenn",
"auto_add_suffix": "im Lager verbleibend (0 = nur wenn leer)"
}
},
"expiry": {
"today": "HEUTE",
@@ -1034,7 +1096,10 @@
"retake_btn": "🔄 Erneut aufnehmen",
"camera_error_hint": "Stelle sicher, dass du HTTPS verwendest und Kameraberechtigungen erteilt hast.<br>Du kannst den Barcode manuell eingeben oder die KI-Identifikation verwenden.",
"no_barcode": "Kein Barcode",
"save_new_btn": "🆕 Keines davon — als neu speichern"
"save_new_btn": "🆕 Keines davon — als neu speichern",
"expiry_found": "Datum gefunden",
"expiry_read_fail": "Datum konnte nicht gelesen werden.",
"expiry_raw_label": "Erkannt"
},
"lowstock": {
"title": "⚠️ Wird knapp!",
+67 -2
View File
@@ -762,6 +762,53 @@
"card_hint": "During cooking, show tips on how to reuse scraps generated in each step (peels, cooking water, etc.). Disabled by default.",
"label": "Show tips during cooking"
},
"backup": {
"tab": "Backup",
"local_title": "Local Backup",
"local_hint": "Daily database snapshot. Configure how many days of backups to keep.",
"enabled": "Enable daily automatic backup",
"retention_days": "Retention (days)",
"retention_info": "Backups are kept for",
"backup_now": "Backup Now",
"backing_up": "Backing up…",
"backed_up": "Backup complete",
"backup_error": "Backup error",
"last_backup": "Last backup",
"no_backup_yet": "No backup has been created yet",
"list_empty": "No backups available",
"restore_btn": "Restore",
"restore_confirm": "Restore backup",
"delete_btn": "Delete",
"delete_confirm": "Delete backup",
"gdrive_title": "Google Drive",
"gdrive_hint": "Automatically back up to Google Drive via OAuth 2.0. No external libraries required.",
"gdrive_enabled": "Enable Google Drive backup",
"gdrive_folder_id": "Drive Folder ID",
"gdrive_folder_id_hint": "Copy the ID from the Drive folder URL: …/folders/<strong>ID</strong>",
"gdrive_retention_days": "Drive retention (days, 0=keep all)",
"gdrive_test": "Test Connection",
"gdrive_ok": "Connection successful!",
"gdrive_error": "Connection failed",
"gdrive_push_now": "Upload to Drive Now",
"gdrive_pushing": "Uploading…",
"gdrive_pushed": "Uploaded to Drive",
"gdrive_wizard_hint": "Optional: automatically back up to Google Drive daily via OAuth 2.0.",
"gdrive_skip": "Skip — configure later in Settings",
"gdrive_client_id": "Client ID",
"gdrive_client_secret": "Client Secret",
"gdrive_redirect_uri_hint": "Add <strong>http://localhost</strong> as an authorized redirect URI in Google Cloud Console. This works on any server, even without a public domain.",
"gdrive_code_title": "Paste the authorization URL or code",
"gdrive_code_hint": "After authorizing, the browser will open http://localhost and may show a connection error — that is expected. Copy the URL from the address bar (e.g. <code>http://localhost/?code=4%2F0A...</code>) and paste it here.",
"gdrive_code_submit": "Submit",
"gdrive_code_empty": "Paste the URL or authorization code first",
"gdrive_redirect_uri_label": "Redirect URI (add this in Google Cloud Console):",
"gdrive_oauth_authorize": "Authorize with Google",
"gdrive_oauth_authorized": "Authorized",
"gdrive_oauth_not_authorized": "Not authorized yet",
"gdrive_oauth_window_opened": "Browser window opened — authorize and come back",
"gdrive_oauth_how_to": "How to set up OAuth 2.0 (step by step)",
"gdrive_oauth_steps": "<li>Go to <a href='https://console.cloud.google.com/' target='_blank' rel='noopener'>console.cloud.google.com</a> and select your project</li><li>Enable the <strong>Google Drive API</strong>: <em>APIs &amp; Services → Enable APIs → Google Drive API</em></li><li>Go to <em>APIs &amp; Services → Credentials → Create Credentials → OAuth client ID</em></li><li>Application type: <strong>Web application</strong>; add <strong>http://localhost</strong> as an <em>Authorized redirect URI</em></li><li>Copy the <strong>Client ID</strong> and <strong>Client Secret</strong> into the fields above and save</li><li>Click <strong>Authorize with Google</strong>, sign in and grant access</li><li>The browser will open <code>http://localhost</code> (a connection error is expected): copy the URL from the address bar and paste it in the field that appears below</li>"
},
"info": {
"tab": "Info",
"ai_title": "Gemini AI — Token Usage",
@@ -802,7 +849,22 @@
"currency_title": "Currency",
"currency_hint": "The currency used for all costs and prices in the app."
},
"tab_general": "General"
"tab_general": "General",
"shopping": {
"tab": "Shopping list",
"title": "Shopping list",
"hint": "Configure the built-in shopping list or connect Bring!.",
"enable_label": "Enable shopping list",
"mode_label": "Provider",
"mode_internal": "Built-in (no Bring!)",
"mode_bring": "Bring! (external app)",
"bring_section_title": "Bring! configuration",
"ai_section_title": "AI assistance",
"smart_suggestions_label": "AI suggestions",
"forecast_label": "Forecast low-stock products",
"auto_add_label": "Auto-add to list when",
"auto_add_suffix": "remaining in stock (0 = only when empty)"
}
},
"expiry": {
"today": "TODAY",
@@ -1034,7 +1096,10 @@
"retake_btn": "🔄 Retake",
"camera_error_hint": "Ensure you use HTTPS and have granted camera permissions.<br>You can enter the barcode manually or use AI identification.",
"no_barcode": "No barcode",
"save_new_btn": "🆕 None of these — save as new"
"save_new_btn": "🆕 None of these — save as new",
"expiry_found": "Date found",
"expiry_read_fail": "Cannot read the date.",
"expiry_raw_label": "Read"
},
"lowstock": {
"title": "⚠️ Running low!",
+62
View File
@@ -759,6 +759,68 @@
"card_title": "♻️ Consejos sin desperdicios",
"card_hint": "Durante la cocción, muestra consejos sobre cómo reutilizar los restos generados en cada paso (peladuras, agua de cocción, etc.). Desactivado por defecto.",
"label": "Mostrar consejos durante la cocción"
},
"backup": {
"tab": "Copia de seguridad",
"local_title": "Copia local",
"local_hint": "Instantánea diaria de la base de datos. Configura cuántos días de copias de seguridad conservar.",
"enabled": "Activar copia de seguridad diaria automática",
"retention_days": "Retención (días)",
"retention_info": "Las copias se conservan durante",
"backup_now": "Hacer copia ahora",
"backing_up": "Haciendo copia…",
"backed_up": "Copia completada",
"backup_error": "Error en la copia",
"last_backup": "Última copia",
"no_backup_yet": "Aún no se ha creado ninguna copia",
"list_empty": "No hay copias disponibles",
"restore_btn": "Restaurar",
"restore_confirm": "Restaurar la copia",
"delete_btn": "Eliminar",
"delete_confirm": "Eliminar la copia",
"gdrive_title": "Google Drive",
"gdrive_hint": "Copias de seguridad automáticas en Google Drive via OAuth 2.0. No se requieren bibliotecas externas.",
"gdrive_enabled": "Activar copia en Google Drive",
"gdrive_folder_id": "ID de carpeta de Drive",
"gdrive_folder_id_hint": "Copia el ID desde la URL de la carpeta de Drive: …/folders/<strong>ID</strong>",
"gdrive_retention_days": "Retención en Drive (días, 0=mantener todo)",
"gdrive_test": "Probar conexión",
"gdrive_ok": "Conexión exitosa!",
"gdrive_error": "Conexión fallida",
"gdrive_push_now": "Subir a Drive ahora",
"gdrive_pushing": "Subiendo…",
"gdrive_pushed": "Subido a Drive",
"gdrive_wizard_hint": "Opcional: copia de seguridad diaria automática en Google Drive via OAuth 2.0.",
"gdrive_skip": "Omitir — configurar después en Ajustes",
"gdrive_client_id": "Client ID",
"gdrive_client_secret": "Client Secret",
"gdrive_redirect_uri_hint": "Agrega <strong>http://localhost</strong> como URI de redireccionamiento autorizado en Google Cloud Console. Funciona en cualquier servidor, incluso sin dominio público.",
"gdrive_code_title": "Pegar la URL o el código de autorización",
"gdrive_code_hint": "Tras autorizar, el navegador abrirá http://localhost y puede mostrar un error de conexión — es normal. Copia la URL de la barra de direcciones (ej. <code>http://localhost/?code=4%2F0A...</code>) y pégala aquí.",
"gdrive_code_submit": "Confirmar",
"gdrive_code_empty": "Pega primero la URL o el código de autorización",
"gdrive_redirect_uri_label": "URI de redirección (agregar en Google Cloud Console):",
"gdrive_oauth_authorize": "Autorizar con Google",
"gdrive_oauth_authorized": "Autorizado",
"gdrive_oauth_not_authorized": "Aún no autorizado",
"gdrive_oauth_window_opened": "Ventana abierta — autoriza y regresa aquí",
"gdrive_oauth_how_to": "Cómo configurar OAuth 2.0 (paso a paso)",
"gdrive_oauth_steps": "<li>Ve a <a href='https://console.cloud.google.com/' target='_blank' rel='noopener'>console.cloud.google.com</a> y selecciona tu proyecto</li><li>Habilita la <strong>API de Google Drive</strong>: <em>API y servicios → Habilitar API → Google Drive API</em></li><li>Ve a <em>API y servicios → Credenciales → Crear credenciales → ID de cliente OAuth</em></li><li>Tipo de aplicación: <strong>Aplicación web</strong>; agrega la URL mostrada abajo como <em>URI de redirección autorizado</em></li><li>Copia el <strong>Client ID</strong> y el <strong>Client Secret</strong> en los campos de arriba y guarda</li><li>Haz clic en <strong>Autorizar con Google</strong>: inicia sesión en tu cuenta de Google y concede acceso</li><li>La ventana se cierra automáticamente al finalizar y las copias de seguridad están listas</li>"
},
"shopping": {
"tab": "Lista de la compra",
"title": "Lista de la compra",
"hint": "Configura la lista de la compra integrada o conecta Bring!.",
"enable_label": "Activar lista de la compra",
"mode_label": "Proveedor",
"mode_internal": "Integrado (sin Bring!)",
"mode_bring": "Bring! (app externa)",
"bring_section_title": "Configuración de Bring!",
"ai_section_title": "Asistencia IA",
"smart_suggestions_label": "Sugerencias IA",
"forecast_label": "Previsión de productos por agotar",
"auto_add_label": "Añadir automáticamente cuando",
"auto_add_suffix": "restante en stock (0 = solo cuando se agota)"
}
},
"expiry": {
+62
View File
@@ -759,6 +759,68 @@
"card_title": "♻️ Conseils zéro déchet",
"card_hint": "Pendant la cuisson, affichez des conseils pour réutiliser les déchets produits à chaque étape (épluchures, eau de cuisson, etc.). Désactivé par défaut.",
"label": "Afficher les conseils pendant la cuisson"
},
"backup": {
"tab": "Sauvegarde",
"local_title": "Sauvegarde locale",
"local_hint": "Instantané quotidien de la base de données. Configurez le nombre de jours de rétention.",
"enabled": "Activer la sauvegarde automatique quotidienne",
"retention_days": "Rétention (jours)",
"retention_info": "Les sauvegardes sont conservées pendant",
"backup_now": "Sauvegarder maintenant",
"backing_up": "Sauvegarde en cours…",
"backed_up": "Sauvegarde terminée",
"backup_error": "Erreur de sauvegarde",
"last_backup": "Dernière sauvegarde",
"no_backup_yet": "Aucune sauvegarde créée",
"list_empty": "Aucune sauvegarde disponible",
"restore_btn": "Restaurer",
"restore_confirm": "Restaurer la sauvegarde",
"delete_btn": "Supprimer",
"delete_confirm": "Supprimer la sauvegarde",
"gdrive_title": "Google Drive",
"gdrive_hint": "Sauvegardez automatiquement sur Google Drive via OAuth 2.0. Aucune bibliothèque externe requise.",
"gdrive_enabled": "Activer la sauvegarde Google Drive",
"gdrive_folder_id": "ID du dossier Drive",
"gdrive_folder_id_hint": "Copiez l'ID depuis l'URL du dossier Drive : …/folders/<strong>ID</strong>",
"gdrive_retention_days": "Rétention Drive (jours, 0=tout garder)",
"gdrive_test": "Tester la connexion",
"gdrive_ok": "Connexion réussie !",
"gdrive_error": "Échec de la connexion",
"gdrive_push_now": "Téléverser sur Drive maintenant",
"gdrive_pushing": "Téléversement en cours…",
"gdrive_pushed": "Téléversé sur Drive",
"gdrive_wizard_hint": "Optionnel : sauvegarde quotidienne automatique sur Google Drive via OAuth 2.0.",
"gdrive_skip": "Passer — configurer plus tard dans Paramètres",
"gdrive_client_id": "Client ID",
"gdrive_client_secret": "Client Secret",
"gdrive_redirect_uri_hint": "Ajoute <strong>http://localhost</strong> comme URI de redirection autorisé dans la Google Cloud Console. Fonctionne sur n'importe quel serveur, même sans domaine public.",
"gdrive_code_title": "Coller l'URL ou le code d'autorisation",
"gdrive_code_hint": "Après autorisation, le navigateur ouvre http://localhost et peut afficher une erreur de connexion — c'est normal. Copie l'URL dans la barre d'adresse (ex. <code>http://localhost/?code=4%2F0A...</code>) et colle-la ici.",
"gdrive_code_submit": "Confirmer",
"gdrive_code_empty": "Coller d'abord l'URL ou le code d'autorisation",
"gdrive_redirect_uri_label": "URI de redirection (ajouter dans Google Cloud Console) :",
"gdrive_oauth_authorize": "Autoriser avec Google",
"gdrive_oauth_authorized": "Autorisé",
"gdrive_oauth_not_authorized": "Pas encore autorisé",
"gdrive_oauth_window_opened": "Fenêtre ouverte — autorisez et revenez ici",
"gdrive_oauth_how_to": "Configurer OAuth 2.0 (étape par étape)",
"gdrive_oauth_steps": "<li>Allez sur <a href='https://console.cloud.google.com/' target='_blank' rel='noopener'>console.cloud.google.com</a> et sélectionnez votre projet</li><li>Activez l<strong>API Google Drive</strong> : <em>API et services → Activer les API → Google Drive API</em></li><li>Allez dans <em>API et services → Identifiants → Créer des identifiants → ID client OAuth</em></li><li>Type dapplication : <strong>Application Web</strong> ; ajoutez lURL affichée ci-dessous comme <em>URI de redirection autorisé</em></li><li>Copiez le <strong>Client ID</strong> et le <strong>Client Secret</strong> dans les champs ci-dessus et enregistrez</li><li>Cliquez sur <strong>Autoriser avec Google</strong> : connectez-vous et accordez laccès</li><li>La fenêtre se ferme automatiquement une fois terminé et les sauvegardes sont prêtes</li>"
},
"shopping": {
"tab": "Liste de courses",
"title": "Liste de courses",
"hint": "Configurez la liste de courses intégrée ou connectez Bring!.",
"enable_label": "Activer la liste de courses",
"mode_label": "Fournisseur",
"mode_internal": "Intégré (sans Bring!)",
"mode_bring": "Bring! (application externe)",
"bring_section_title": "Configuration Bring!",
"ai_section_title": "Assistance IA",
"smart_suggestions_label": "Suggestions IA",
"forecast_label": "Prévision des produits bientôt épuisés",
"auto_add_label": "Ajouter automatiquement quand",
"auto_add_suffix": "restant en stock (0 = seulement quand épuisé)"
}
},
"expiry": {
+67 -2
View File
@@ -762,6 +762,53 @@
"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.",
"label": "Mostra suggerimenti durante la cottura"
},
"backup": {
"tab": "Backup",
"local_title": "Backup Locale",
"local_hint": "Snapshot giornaliero del database. Configura quanti giorni di backup conservare.",
"enabled": "Backup automatico quotidiano",
"retention_days": "Giorni di retention",
"retention_info": "I backup vengono conservati per",
"backup_now": "Backup Ora",
"backing_up": "Backup in corso…",
"backed_up": "Backup completato",
"backup_error": "Errore backup",
"last_backup": "Ultimo backup",
"no_backup_yet": "Nessun backup ancora eseguito",
"list_empty": "Nessun backup disponibile",
"restore_btn": "Ripristina",
"restore_confirm": "Ripristinare il backup",
"delete_btn": "Elimina",
"delete_confirm": "Eliminare il backup",
"gdrive_title": "Google Drive",
"gdrive_hint": "Backup automatici su Google Drive via OAuth 2.0. Nessuna libreria esterna richiesta.",
"gdrive_enabled": "Abilita backup Google Drive",
"gdrive_folder_id": "ID Cartella Drive",
"gdrive_folder_id_hint": "Copia l'ID dalla URL della cartella Drive: …/folders/<strong>ID</strong>",
"gdrive_retention_days": "Retention Drive (giorni, 0=tutto)",
"gdrive_test": "Testa Connessione",
"gdrive_ok": "Connessione riuscita!",
"gdrive_error": "Connessione fallita",
"gdrive_push_now": "Carica Ora su Drive",
"gdrive_pushing": "Upload in corso…",
"gdrive_pushed": "Caricato su Drive",
"gdrive_wizard_hint": "Opzionale: backup giornaliero automatico su Google Drive via OAuth 2.0.",
"gdrive_skip": "Salta — configura dopo in Impostazioni",
"gdrive_client_id": "Client ID",
"gdrive_client_secret": "Client Secret",
"gdrive_redirect_uri_label": "Redirect URI (da aggiungere in Google Cloud Console):",
"gdrive_redirect_uri_hint": "Aggiungi <strong>http://localhost</strong> come URI di reindirizzamento autorizzato in Google Cloud Console. Funziona su qualsiasi server, anche senza dominio pubblico.",
"gdrive_oauth_authorize": "Autorizza con Google",
"gdrive_oauth_authorized": "Autorizzato",
"gdrive_oauth_not_authorized": "Non ancora autorizzato",
"gdrive_oauth_window_opened": "Finestra aperta — autorizza e torna qui",
"gdrive_oauth_how_to": "Come configurare OAuth 2.0 (passo dopo passo)",
"gdrive_oauth_steps": "<li>Vai su <a href='https://console.cloud.google.com/' target='_blank' rel='noopener'>console.cloud.google.com</a> e seleziona il progetto</li><li>Abilita la <strong>Google Drive API</strong>: <em>API e servizi → Abilita API → Google Drive API</em></li><li>Vai su <em>API e servizi → Credenziali → Crea credenziali → ID client OAuth 2.0</em></li><li>Tipo applicazione: <strong>Applicazione web</strong>; aggiungi <strong>http://localhost</strong> come <em>URI di reindirizzamento autorizzato</em></li><li>Copia <strong>Client ID</strong> e <strong>Client Secret</strong> nei campi qui sopra e salva</li><li>Clicca <strong>Autorizza con Google</strong>, accedi e concedi l'accesso</li><li>Il browser aprirà <code>http://localhost</code> (possibile errore di connessione è normale): copia l'URL dalla barra degli indirizzi e incollalo nel campo che appare qui sotto</li>",
"gdrive_code_title": "Incolla l'URL o il codice di autorizzazione",
"gdrive_code_hint": "Dopo aver autorizzato, il browser aprirà http://localhost e potrebbe mostrare un errore. Copia l'URL dalla barra degli indirizzi (es. <code>http://localhost/?code=4%2F0A...</code>) e incollalo qui.",
"gdrive_code_submit": "Conferma",
"gdrive_code_empty": "Incolla prima l'URL o il codice di autorizzazione"
},
"info": {
"tab": "Info",
"ai_title": "Gemini AI — Utilizzo Token",
@@ -802,7 +849,22 @@
"currency_title": "Valuta",
"currency_hint": "La valuta usata per tutti i costi e i prezzi nell'app."
},
"tab_general": "Generali"
"tab_general": "Generali",
"shopping": {
"tab": "Lista spesa",
"title": "Lista della spesa",
"hint": "Configura la lista della spesa integrata o collega Bring!.",
"enable_label": "Abilita lista della spesa",
"mode_label": "Provider",
"mode_internal": "Interno (senza Bring!)",
"mode_bring": "Bring! (app esterna)",
"bring_section_title": "Configurazione Bring!",
"ai_section_title": "Assistenza AI",
"smart_suggestions_label": "Suggerimenti AI",
"forecast_label": "Previsione prodotti in esaurimento",
"auto_add_label": "Aggiungi automaticamente quando",
"auto_add_suffix": "rimasto in magazzino (0 = solo quando esaurito)"
}
},
"expiry": {
"today": "OGGI",
@@ -1034,7 +1096,10 @@
"retake_btn": "🔄 Riscatta",
"camera_error_hint": "Assicurati di usare HTTPS e di aver concesso i permessi della fotocamera.<br>Puoi inserire il barcode manualmente o usare l'identificazione AI.",
"no_barcode": "Senza barcode",
"save_new_btn": "🆕 Non è nessuno di questi — salva come nuovo"
"save_new_btn": "🆕 Non è nessuno di questi — salva come nuovo",
"expiry_found": "Data trovata",
"expiry_read_fail": "Non riesco a leggere la data.",
"expiry_raw_label": "Letto"
},
"lowstock": {
"title": "⚠️ Sta per finire!",