Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 426cc9df7e | |||
| 6f2d6d9944 | |||
| 98426bf861 | |||
| b89df961a6 | |||
| 3b100df26c | |||
| c2004fd0f8 | |||
| 3a1f6cfd1e | |||
| 66f5a03503 | |||
| a37d97dfcd | |||
| 149621651d | |||
| ccc2f8907d | |||
| 7b60f1dbe3 | |||
| ac8b5acc0c | |||
| 87eac171bf | |||
| f77b3259ad | |||
| 84934c1908 | |||
| fa0442e2f6 | |||
| c07439fea4 | |||
| d7aadff598 | |||
| 7364e75881 | |||
| 4515ff7246 |
@@ -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: how many months to cache a price before re-fetching (default 3)
|
||||||
PRICE_UPDATE_MONTHS=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 ─────────────────────────────────────────────────────────────────
|
# ── Security ─────────────────────────────────────────────────────────────────
|
||||||
# SETTINGS_TOKEN: if set, the Settings screen requires this token to save changes.
|
# 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.
|
# Leave empty to allow anyone with access to the server to change settings.
|
||||||
|
|||||||
@@ -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.
|
- **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 3–4 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
|
## [1.7.23] - 2026-05-18
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
[](https://www.sqlite.org/)
|
[](https://www.sqlite.org/)
|
||||||
[](Dockerfile)
|
[](Dockerfile)
|
||||||
[](translations/)
|
[](translations/)
|
||||||
[](CHANGELOG.md)
|
[](CHANGELOG.md)
|
||||||
[](https://github.com/dadaloop82/EverShelf/stargazers)
|
[](https://github.com/dadaloop82/EverShelf/stargazers)
|
||||||
[](https://github.com/dadaloop82/EverShelf/commits/main)
|
[](https://github.com/dadaloop82/EverShelf/commits/main)
|
||||||
[](https://github.com/dadaloop82/EverShelf/graphs/contributors)
|
[](https://github.com/dadaloop82/EverShelf/graphs/contributors)
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
> ⚙️ **New in v1.7.23 — Global settings tab, DB auto-cleanup, vacuum-sealed expiry**
|
> ⚙️ **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.
|
> 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.
|
> 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:00–07:00) instead of the OS setting, making it server-friendly.
|
> Auto theme now follows **time of day** (dark 20:00–07:00) instead of the OS setting, making it server-friendly.
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
|
|
||||||
### 🌙 Appearance
|
### 🌙 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
|
- **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
|
### �️ 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
|
- **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
|
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
|
## 🏗️ 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
|
## 📄 License
|
||||||
|
|
||||||
This project is licensed under the **MIT License** — see the [LICENSE](LICENSE) file for details.
|
This project is licensed under the **MIT License** — see the [LICENSE](LICENSE) file for details.
|
||||||
|
|||||||
@@ -87,11 +87,45 @@ try {
|
|||||||
ob_end_clean();
|
ob_end_clean();
|
||||||
echo '[' . date('Y-m-d H:i:s') . '] DB cleanup done'
|
echo '[' . date('Y-m-d H:i:s') . '] DB cleanup done'
|
||||||
. ' (recipes >' . env('RECIPE_RETENTION_DAYS','7') . 'd'
|
. ' (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) {
|
} catch (Throwable $ce) {
|
||||||
echo '[' . date('Y-m-d H:i:s') . '] DB cleanup warning: ' . $ce->getMessage() . "\n";
|
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) {
|
} catch (Throwable $e) {
|
||||||
$msg = $e->getMessage();
|
$msg = $e->getMessage();
|
||||||
echo '[' . date('Y-m-d H:i:s') . '] ERROR: ' . $msg . "\n";
|
echo '[' . date('Y-m-d H:i:s') . '] ERROR: ' . $msg . "\n";
|
||||||
|
|||||||
@@ -48,6 +48,13 @@ function getDB(): PDO {
|
|||||||
? new LoggingPDO('sqlite:' . DB_PATH)
|
? new LoggingPDO('sqlite:' . DB_PATH)
|
||||||
: new PDO('sqlite:' . DB_PATH);
|
: new PDO('sqlite:' . DB_PATH);
|
||||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
$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->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||||
$db->exec("PRAGMA journal_mode=WAL");
|
$db->exec("PRAGMA journal_mode=WAL");
|
||||||
$db->exec("PRAGMA foreign_keys=ON");
|
$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)
|
// 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_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)");
|
$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
File diff suppressed because it is too large
Load Diff
@@ -7133,6 +7133,7 @@ body.cooking-mode-active .app-header {
|
|||||||
--bg: #0f172a;
|
--bg: #0f172a;
|
||||||
--bg-card: #1e293b;
|
--bg-card: #1e293b;
|
||||||
--bg-dark: #020617;
|
--bg-dark: #020617;
|
||||||
|
--bg-secondary: #263448;
|
||||||
--text: #e2e8f0;
|
--text: #e2e8f0;
|
||||||
--text-light: #94a3b8;
|
--text-light: #94a3b8;
|
||||||
--text-muted: #64748b;
|
--text-muted: #64748b;
|
||||||
@@ -7384,3 +7385,159 @@ body.cooking-mode-active .app-header {
|
|||||||
color: var(--primary-light);
|
color: var(--primary-light);
|
||||||
}
|
}
|
||||||
/* @media prefers-color-scheme: auto handled in JS */
|
/* @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
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
{"ts":1779204302,"filename":"evershelf_2026-05-19_1525.db","size_kb":444}
|
||||||
@@ -5,14 +5,14 @@ plugins {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "it.dadaloop.evershelf.kiosk"
|
namespace = "it.dadaloop.evershelf.kiosk"
|
||||||
compileSdk = 34
|
compileSdk = 35
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "it.dadaloop.evershelf.kiosk"
|
applicationId = "it.dadaloop.evershelf.kiosk"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 34
|
targetSdk = 35
|
||||||
versionCode = 17
|
versionCode = 18
|
||||||
versionName = "1.7.16"
|
versionName = "1.7.17"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|||||||
@@ -774,7 +774,13 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
val q = DownloadManager.Query().setFilterById(downloadId)
|
val q = DownloadManager.Query().setFilterById(downloadId)
|
||||||
val c = (getSystemService(DOWNLOAD_SERVICE) as DownloadManager).query(q)
|
val c = (getSystemService(DOWNLOAD_SERVICE) as DownloadManager).query(q)
|
||||||
var ok = false
|
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()
|
c.close()
|
||||||
if (ok) {
|
if (ok) {
|
||||||
pollHandler.removeCallbacksAndMessages(null); activeDownloadId = -1
|
pollHandler.removeCallbacksAndMessages(null); activeDownloadId = -1
|
||||||
@@ -784,7 +790,12 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
pollHandler.removeCallbacksAndMessages(null); activeDownloadId = -1
|
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)
|
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) }
|
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)
|
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||||
// Note: setAppPackageName() is intentionally omitted — it causes STATUS_FAILURE (1)
|
// Note: setAppPackageName() is intentionally omitted — it causes STATUS_FAILURE (1)
|
||||||
// on some OEM/Android versions even when the package name is correct.
|
// 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 sessionId = pi.createSession(params)
|
||||||
val session = pi.openSession(sessionId)
|
val session = pi.openSession(sessionId)
|
||||||
try {
|
try {
|
||||||
|
|||||||
+149
-3
@@ -833,7 +833,7 @@
|
|||||||
<div class="settings-tabs">
|
<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 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-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-recipe')" data-tab="tab-recipe" title="Ricette">🍳</button>
|
||||||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-mealplan')" data-tab="tab-mealplan" title="Piano Settimanale">📅</button>
|
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-mealplan')" data-tab="tab-mealplan" title="Piano Settimanale">📅</button>
|
||||||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-appliances')" data-tab="tab-appliances" title="Elettrodomestici">🔌</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-security')" data-tab="tab-security" title="Sicurezza">🔒</button>
|
||||||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-tts')" data-tab="tab-tts" title="Voce (TTS)" data-i18n-title="settings.tab_tts">🔊</button>
|
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-tts')" data-tab="tab-tts" title="Voce (TTS)" data-i18n-title="settings.tab_tts">🔊</button>
|
||||||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-scale')" data-tab="tab-scale" title="Bilancia Smart" data-i18n-title="settings.scale.tab">⚖️</button>
|
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-scale')" data-tab="tab-scale" title="Bilancia Smart" data-i18n-title="settings.scale.tab">⚖️</button>
|
||||||
|
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-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>
|
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-info'); _loadInfoTab();" data-tab="tab-info" data-i18n-title="settings.info.tab" title="Info">ℹ️</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-panels">
|
<div class="settings-panels">
|
||||||
@@ -954,9 +955,36 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Bring! Tab -->
|
<!-- Bring! Tab -->
|
||||||
<div class="settings-panel" id="tab-bring">
|
<div class="settings-panel" id="tab-bring">
|
||||||
|
<!-- Shopping enable + provider -->
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h4 data-i18n="settings.bring.title">🛒 Bring! Shopping List</h4>
|
<h4 data-i18n="settings.shopping.title">🛒 Lista della spesa</h4>
|
||||||
<p class="settings-hint" data-i18n="settings.bring.hint">Credenziali per l'integrazione con la lista della spesa Bring!</p>
|
<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">
|
<div class="form-group">
|
||||||
<label data-i18n="settings.bring.email_label">📧 Email Bring!</label>
|
<label data-i18n="settings.bring.email_label">📧 Email Bring!</label>
|
||||||
<input type="email" id="setting-bring-email" class="form-input" placeholder="email@esempio.com">
|
<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>
|
<button class="btn btn-small btn-secondary mt-2" onclick="togglePasswordVisibility('setting-bring-password')" data-i18n="btn.toggle_password">👁️ Mostra/Nascondi</button>
|
||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Price Estimation Settings -->
|
||||||
<div class="settings-card" style="margin-top:12px">
|
<div class="settings-card" style="margin-top:12px">
|
||||||
<h4 data-i18n="settings.price.title">💰 Stima Prezzi (AI)</h4>
|
<h4 data-i18n="settings.price.title">💰 Stima Prezzi (AI)</h4>
|
||||||
@@ -1342,6 +1401,93 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Language Tab -->
|
<!-- 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 -->
|
<!-- Info Tab -->
|
||||||
<div class="settings-panel" id="tab-info">
|
<div class="settings-panel" id="tab-info">
|
||||||
<!-- Gemini AI Usage card -->
|
<!-- Gemini AI Usage card -->
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "EverShelf",
|
"name": "EverShelf",
|
||||||
"short_name": "EverShelf",
|
"short_name": "EverShelf",
|
||||||
"description": "Gestione completa della dispensa di casa con scansione barcode",
|
"description": "Gestione completa della dispensa di casa con scansione barcode",
|
||||||
"version": "1.7.23",
|
"version": "1.7.24",
|
||||||
"start_url": "/evershelf/",
|
"start_url": "/evershelf/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#f0f4e8",
|
"background_color": "#f0f4e8",
|
||||||
|
|||||||
+67
-2
@@ -762,6 +762,53 @@
|
|||||||
"card_hint": "Zeige während des Kochens Tipps zur Wiederverwendung von Abfällen (Schalen, Kochwasser usw.). Standardmäßig deaktiviert.",
|
"card_hint": "Zeige während des Kochens Tipps zur Wiederverwendung von Abfällen (Schalen, Kochwasser usw.). Standardmäßig deaktiviert.",
|
||||||
"label": "Tipps beim Kochen anzeigen"
|
"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 & Dienste → APIs aktivieren → Google Drive API</em></li><li>Gehe zu <em>APIs & 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": {
|
"info": {
|
||||||
"tab": "Info",
|
"tab": "Info",
|
||||||
"ai_title": "Gemini AI — Token-Nutzung",
|
"ai_title": "Gemini AI — Token-Nutzung",
|
||||||
@@ -802,7 +849,22 @@
|
|||||||
"currency_title": "Währung",
|
"currency_title": "Währung",
|
||||||
"currency_hint": "Die Währung, die für alle Kosten und Preise in der App verwendet wird."
|
"currency_hint": "Die Währung, die für alle Kosten und Preise in der App verwendet wird."
|
||||||
},
|
},
|
||||||
"tab_general": "Allgemein"
|
"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": {
|
"expiry": {
|
||||||
"today": "HEUTE",
|
"today": "HEUTE",
|
||||||
@@ -1034,7 +1096,10 @@
|
|||||||
"retake_btn": "🔄 Erneut aufnehmen",
|
"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.",
|
"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",
|
"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": {
|
"lowstock": {
|
||||||
"title": "⚠️ Wird knapp!",
|
"title": "⚠️ Wird knapp!",
|
||||||
|
|||||||
+67
-2
@@ -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.",
|
"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"
|
"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 & Services → Enable APIs → Google Drive API</em></li><li>Go to <em>APIs & 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": {
|
"info": {
|
||||||
"tab": "Info",
|
"tab": "Info",
|
||||||
"ai_title": "Gemini AI — Token Usage",
|
"ai_title": "Gemini AI — Token Usage",
|
||||||
@@ -802,7 +849,22 @@
|
|||||||
"currency_title": "Currency",
|
"currency_title": "Currency",
|
||||||
"currency_hint": "The currency used for all costs and prices in the app."
|
"currency_hint": "The currency used for all costs and prices in the app."
|
||||||
},
|
},
|
||||||
"tab_general": "General"
|
"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": {
|
"expiry": {
|
||||||
"today": "TODAY",
|
"today": "TODAY",
|
||||||
@@ -1034,7 +1096,10 @@
|
|||||||
"retake_btn": "🔄 Retake",
|
"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.",
|
"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",
|
"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": {
|
"lowstock": {
|
||||||
"title": "⚠️ Running low!",
|
"title": "⚠️ Running low!",
|
||||||
|
|||||||
@@ -759,6 +759,68 @@
|
|||||||
"card_title": "♻️ Consejos sin desperdicios",
|
"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.",
|
"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"
|
"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": {
|
"expiry": {
|
||||||
|
|||||||
@@ -759,6 +759,68 @@
|
|||||||
"card_title": "♻️ Conseils zéro déchet",
|
"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.",
|
"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"
|
"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 d’application : <strong>Application Web</strong> ; ajoutez l’URL 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 l’accè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": {
|
"expiry": {
|
||||||
|
|||||||
+67
-2
@@ -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.",
|
"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"
|
"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": {
|
"info": {
|
||||||
"tab": "Info",
|
"tab": "Info",
|
||||||
"ai_title": "Gemini AI — Utilizzo Token",
|
"ai_title": "Gemini AI — Utilizzo Token",
|
||||||
@@ -802,7 +849,22 @@
|
|||||||
"currency_title": "Valuta",
|
"currency_title": "Valuta",
|
||||||
"currency_hint": "La valuta usata per tutti i costi e i prezzi nell'app."
|
"currency_hint": "La valuta usata per tutti i costi e i prezzi nell'app."
|
||||||
},
|
},
|
||||||
"tab_general": "Generali"
|
"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": {
|
"expiry": {
|
||||||
"today": "OGGI",
|
"today": "OGGI",
|
||||||
@@ -1034,7 +1096,10 @@
|
|||||||
"retake_btn": "🔄 Riscatta",
|
"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.",
|
"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",
|
"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": {
|
"lowstock": {
|
||||||
"title": "⚠️ Sta per finire!",
|
"title": "⚠️ Sta per finire!",
|
||||||
|
|||||||
Reference in New Issue
Block a user