Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f0acd0dfa | |||
| ba0c4c3d88 | |||
| a58ef241e9 | |||
| bd5d4bcac6 | |||
| c9a859463c | |||
| b3454062bf | |||
| 56e68b72f8 | |||
| b91203f151 | |||
| cc0d9763ed | |||
| d8c7d1545a | |||
| 9f554c6e22 | |||
| 4f715730ec | |||
| dc3cefefd0 | |||
| a2eaf695bb | |||
| db2e32322b | |||
| 36821bde7a | |||
| 9d49609e4b | |||
| de897cc0f9 | |||
| 30f4bf4a1b | |||
| 1379cfc388 | |||
| 2806cb0903 | |||
| 56b6eb5f0d | |||
| 83d1868309 | |||
| 788d4fe848 | |||
| 91616b3a6d | |||
| 844fe3ba1e | |||
| da4aa5a1ae | |||
| 9541e3a385 | |||
| 47ce849311 | |||
| ea2dae2be9 | |||
| 8360f5a0a0 | |||
| f5b1913ffa | |||
| d26dce283d | |||
| e67e490162 | |||
| 92048c9eba | |||
| ce504d5d41 | |||
| a690d2e7cf | |||
| e858b3cc85 | |||
| 78f499205c | |||
| b3a0e83dde | |||
| d3b119c7fe | |||
| 9b8164b141 | |||
| 8750e44687 | |||
| 57f66c17df | |||
| 2630905146 | |||
| a602726531 | |||
| 3f55f07220 | |||
| 06f6d58fb5 | |||
| c1ef4c5e13 | |||
| 0a6e653692 | |||
| a99b35225a | |||
| 3ba4f7eaad | |||
| fdfd5cd0ec | |||
| b973284aeb | |||
| 0a5629e881 | |||
| d901939da1 | |||
| 245e14cc3b | |||
| aaf9323ba5 | |||
| 78c3306d9e | |||
| 0f567c4ba0 | |||
| 169e32bff3 | |||
| d28055a512 | |||
| 68f7756e2c | |||
| b82b4d9d94 | |||
| 91b4ecd670 | |||
| 380fa8ee99 | |||
| 89b8686f4f | |||
| b6aa07a1fd | |||
| 47c26ffdc8 | |||
| 12357db933 | |||
| 6def94948b |
+94
-17
@@ -1,25 +1,102 @@
|
||||
# EverShelf - Configuration
|
||||
# Copy this file to .env and fill in your values
|
||||
# cp .env.example .env
|
||||
# EverShelf — Configuration
|
||||
# Copy this file to .env and fill in your values:
|
||||
# cp .env.example .env
|
||||
#
|
||||
# All settings here can also be changed from the in-app Settings screen and
|
||||
# will be written back to this file automatically.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# Google Gemini AI API Key (required for AI features)
|
||||
# Get one at: https://aistudio.google.com/app/apikey
|
||||
# ── AI ────────────────────────────────────────────────────────────────────────
|
||||
# Google Gemini API key (required for AI features: expiry reading, recipe gen, …)
|
||||
# Get one free at: https://aistudio.google.com/app/apikey
|
||||
GEMINI_API_KEY=
|
||||
|
||||
# Bring! Shopping List credentials (optional)
|
||||
# Sign up at: https://www.getbring.com/
|
||||
# ── Shopping list (Bring!) ────────────────────────────────────────────────────
|
||||
# Credentials for the Bring! app (optional — app works without it)
|
||||
BRING_EMAIL=
|
||||
BRING_PASSWORD=
|
||||
|
||||
# TTS (Text-to-Speech) for cooking mode voice guidance (optional)
|
||||
# Works with Home Assistant, or any HTTP endpoint that accepts text
|
||||
TTS_URL=
|
||||
TTS_TOKEN=
|
||||
TTS_METHOD=POST
|
||||
TTS_AUTH_TYPE=bearer
|
||||
TTS_CONTENT_TYPE=application/json
|
||||
TTS_PAYLOAD_KEY=message
|
||||
# ── Text-to-Speech (TTS) ─────────────────────────────────────────────────────
|
||||
# Works with Home Assistant, a local TTS server, or any HTTP endpoint.
|
||||
# TTS_ENABLED: master switch (true/false)
|
||||
TTS_ENABLED=false
|
||||
# TTS_URL: endpoint that receives the text payload
|
||||
TTS_URL=
|
||||
# TTS_TOKEN: Authorization token sent as Bearer header (or empty)
|
||||
TTS_TOKEN=
|
||||
# TTS_METHOD: HTTP method (POST or GET)
|
||||
TTS_METHOD=POST
|
||||
# TTS_AUTH_TYPE: how the token is sent (bearer | basic | none)
|
||||
TTS_AUTH_TYPE=bearer
|
||||
# TTS_CONTENT_TYPE: request Content-Type header
|
||||
TTS_CONTENT_TYPE=application/json
|
||||
# TTS_PAYLOAD_KEY: JSON key that carries the text (e.g. "message", "text")
|
||||
TTS_PAYLOAD_KEY=message
|
||||
# TTS_ENGINE: preferred browser TTS engine ('browser', 'server', 'custom') — optional
|
||||
TTS_ENGINE=
|
||||
# TTS_RATE / TTS_PITCH: speech rate and pitch multipliers (1 = normal)
|
||||
TTS_RATE=1
|
||||
TTS_PITCH=1
|
||||
# TTS_AUTH_HEADER_NAME / VALUE: custom HTTP header for authentication (optional)
|
||||
TTS_AUTH_HEADER_NAME=
|
||||
TTS_AUTH_HEADER_VALUE=
|
||||
# TTS_EXTRA_FIELDS: additional JSON fields as key=value pairs, comma-separated (optional)
|
||||
TTS_EXTRA_FIELDS=
|
||||
|
||||
# GitHub Error Reporting: token is hardcoded in api/index.php (same for all clients).
|
||||
# No .env entry needed — update GH_ISSUE_TOKEN constant in api/index.php to rotate.
|
||||
# ── User preferences ─────────────────────────────────────────────────────────
|
||||
# These mirror the toggle switches in the Settings screen.
|
||||
DEFAULT_PERSONS=1
|
||||
PREF_VELOCE=false
|
||||
PREF_POCAFAME=false
|
||||
PREF_SCADENZE=true
|
||||
PREF_HEALTHY=false
|
||||
PREF_OPENED=true
|
||||
PREF_ZEROWASTE=false
|
||||
# Dietary restrictions shown to the AI (e.g. "vegetariano,senza glutine")
|
||||
DIETARY=
|
||||
|
||||
# ── Appliances ────────────────────────────────────────────────────────────────
|
||||
# Comma-separated list of appliances available in your kitchen.
|
||||
# Used by the AI when generating recipes.
|
||||
APPLIANCES=Forno,Microonde,Friggitrice ad aria,Pentola a pressione
|
||||
|
||||
# ── Camera ───────────────────────────────────────────────────────────────────
|
||||
# Default camera for barcode scanning ('environment' = rear, 'user' = front)
|
||||
CAMERA_FACING=environment
|
||||
|
||||
# ── Smart Kitchen Scale ───────────────────────────────────────────────────────
|
||||
# SCALE_ENABLED: enables the scale integration
|
||||
SCALE_ENABLED=false
|
||||
# SCALE_GATEWAY_URL: address of the EverShelf Scale Gateway (Android app)
|
||||
SCALE_GATEWAY_URL=
|
||||
|
||||
# ── Meal Plan ────────────────────────────────────────────────────────────────
|
||||
# MEAL_PLAN_ENABLED: show the weekly meal planner tab in Settings
|
||||
MEAL_PLAN_ENABLED=false
|
||||
|
||||
# ── Screensaver (kiosk / tablet mode) ────────────────────────────────────────
|
||||
SCREENSAVER_ENABLED=false
|
||||
# SCREENSAVER_TIMEOUT: inactivity seconds before screensaver activates (default 5 min)
|
||||
SCREENSAVER_TIMEOUT=300
|
||||
|
||||
# ── Price estimates ───────────────────────────────────────────────────────────
|
||||
# PRICE_ENABLED: show AI-estimated price column on the shopping list
|
||||
PRICE_ENABLED=false
|
||||
# PRICE_COUNTRY: country used for price context (e.g. "Italia", "Germany")
|
||||
PRICE_COUNTRY=Italia
|
||||
# PRICE_CURRENCY: ISO 4217 currency code (e.g. EUR, USD, GBP)
|
||||
PRICE_CURRENCY=EUR
|
||||
# PRICE_UPDATE_MONTHS: how many months to cache a price before re-fetching (default 3)
|
||||
PRICE_UPDATE_MONTHS=3
|
||||
|
||||
# ── 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.
|
||||
SETTINGS_TOKEN=
|
||||
|
||||
# ── Developer / demo ─────────────────────────────────────────────────────────
|
||||
# DEMO_MODE: when true, all write operations are blocked (for public demos)
|
||||
DEMO_MODE=false
|
||||
|
||||
# NOTE: GitHub error reporting uses a token hardcoded in api/index.php.
|
||||
# To rotate it, update the GH_ISSUE_TOKEN constant there.
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
name: PHP Syntax Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
name: JavaScript Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Check JS syntax
|
||||
run: |
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
name: Docker Build Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Build Docker image
|
||||
run: docker build -t evershelf-test .
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
name: Validate Translation Files
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Validate JSON syntax
|
||||
run: |
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout (full history)
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Build Docker image
|
||||
run: docker build -t evershelf:scan .
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Run Trivy filesystem scanner
|
||||
uses: aquasecurity/trivy-action@v0.36.0
|
||||
|
||||
@@ -50,3 +50,5 @@ data/error_reports.log
|
||||
data/latest_release_cache.json
|
||||
data/food_facts_cache.json
|
||||
data/category_ai_cache.json
|
||||
assets/img/logo/*_backup.*
|
||||
logs/*.log
|
||||
|
||||
@@ -11,6 +11,78 @@ 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.23] - 2026-05-18
|
||||
|
||||
### Added
|
||||
- **⚙️ Generali tab** — new first tab in Settings groups all global settings: language, currency, theme, screensaver, zero-waste tips, inventory export. Old Language tab removed.
|
||||
- **DB auto-cleanup** — `RECIPE_RETENTION_DAYS` (default 7) and `TRANSACTION_RETENTION_DAYS` (default 7) added to `.env`; old rows are deleted automatically every cron cycle, followed by `VACUUM` to compact the database. Manual trigger: `GET /api/?action=db_cleanup`.
|
||||
- **Vacuum-sealed expiry grace period** — `VACUUM_EXPIRY_EXTENSION_DAYS` (default 30): vacuum-sealed products are only flagged as expired N days *after* the printed date, preventing false alarms on long-lasting items like cured meats.
|
||||
- **Gemini AI usage tracking** — monthly and yearly token/cost stats now shown in Settings → ℹ️ Info tab, using tracked data from `data/ai_usage.json`. Cost rates configurable via `GEMINI_COST_25F_IN/OUT` and `GEMINI_COST_20F_IN/OUT` in `.env`.
|
||||
|
||||
### Changed
|
||||
- **Auto theme is now time-based** — "Automatico" mode switches to dark at 20:00 and back to light at 07:00, based on server/device clock (not OS preference). Re-evaluates every 5 minutes; ideal for always-on kiosk displays.
|
||||
- **`dispensa.db` auto-deleted** — if the legacy empty `dispensa.db` file appears alongside `evershelf.db`, it is now removed automatically by the health check.
|
||||
- **ZeroWaste tips and screensaver timeout** — these settings were not being persisted to `.env` on save (missing from POST payload); fixed.
|
||||
|
||||
## [1.7.22] - 2026-05-17
|
||||
|
||||
### Fixed
|
||||
- **DB name corrected** — `health_check` now looks for `evershelf.db` (was wrongly looking for `dispensa.db`). Auto-migration included: if `evershelf.db` is missing but `dispensa.db` exists, it is renamed automatically on startup.
|
||||
- **Removed legacy `data/dispensa.db`** — the old database file has been deleted; only `evershelf.db` is used.
|
||||
- **Conditional checks** — Bring!, TTS, Scale and Internet checks only run when the respective feature is enabled in `.env` (no more false ❌/⚠️ for unconfigured features).
|
||||
- **Backups check** — no longer checks if `data/backups/` is writable by www-data (cron writes as root). Now checks that backup files actually exist and the most recent one is recent.
|
||||
- **Bring! token check** — reads `data/bring_token.json` file instead of looking for a non-existent `BRING_ACCESS_TOKEN` env var.
|
||||
|
||||
### Changed
|
||||
- **Warning popup with 5s countdown** — when non-critical checks fail at startup, a styled popup appears showing each warning with its label and a plain-language hint explaining the problem. A countdown bar auto-closes the popup after 5 seconds, then the app starts normally.
|
||||
- **Error blocking popup** — when critical checks fail, a clear blocking panel shows with title "Errore critico", each failed check listed with its explanation hint, and a Retry button. The app does not start.
|
||||
- **`db_legacy` check added** — warns (optional) if the old `dispensa.db` file is still present alongside `evershelf.db`.
|
||||
- **32 total checks** — added `db_legacy`, `tts_url`, `scale_gateway` to the check set (conditional).
|
||||
- **Hint messages** — every check now has an Italian-language `hint` field explaining what is wrong and how to fix it.
|
||||
|
||||
## [1.7.21] - 2026-05-20
|
||||
|
||||
### Changed
|
||||
- **Startup health check** — Complete redesign from a banner checklist to a **real-time progress bar**. The bar fills smoothly as each of 29 diagnostic checks runs, with the current check name shown below in real time. Warnings (⚠️) are displayed as amber badges that remain visible for 2 seconds before the app proceeds. Critical failures turn the bar red and show a detailed error block with a Retry button.
|
||||
- **29 comprehensive checks**: PHP version, 8 PHP extensions (pdo_sqlite, curl, json, mbstring, openssl, fileinfo, zip, intl), PHP memory/timeout/upload config, data directory, rate_limits dir, backups dir, disk write test, free disk space, SQLite connection, required tables, integrity (PRAGMA quick_check), WAL mode, DB size, inventory row count, .env file, Gemini AI key, Bring! credentials, Bring! token, cURL SSL, internet reachability.
|
||||
- Warnings now clearly visible: each non-critical failure shows as a named amber badge (e.g. "⚠️ Bring! token") that cannot be missed.
|
||||
|
||||
## [1.7.20] - 2026-05-20
|
||||
|
||||
### Added
|
||||
- **Startup health check** — During the splash screen, the app now runs a comprehensive server-side diagnostic before loading: PHP version, required extensions (pdo_sqlite, curl, mbstring, json), `data/` directory writability, SQLite database connection and table integrity, `.env` file presence, Gemini AI key and Bring! token. Results are displayed as an animated checklist (✅ / ⚠️ / ❌). Critical failures (DB, extensions, data dir) block the app with a clear error message and a "Retry" button — the app never starts silently broken. Non-critical warnings (missing Gemini key, Bring! token) are shown as amber items but do not block startup.
|
||||
- New `?action=health_check` PHP endpoint (early-exit, no rate-limit, no auth).
|
||||
- New translation keys `startup.*` in all 5 languages (IT, EN, DE, FR, ES).
|
||||
|
||||
## [1.7.19] - 2026-05-19
|
||||
|
||||
### Added
|
||||
- **Zero-waste tips during cooking** — When cooking mode is active, a ♻️ card appears below each step that generates reusable scraps (peels, cooking water, egg whites, cheese rinds, bread crusts, vegetable tops, etc.). Gemini generates the tips as part of the recipe JSON at no extra API cost. Tips are dismissible per-step and reset on recipe restart. Opt-in toggle in Settings → Zero-waste tips (default OFF). Resolves [#76](https://github.com/dadaloop82/EverShelf/issues/76).
|
||||
- New translation keys `cooking.zerowaste_*` and `settings.zerowaste.*` in all 5 languages (IT, EN, DE, FR, ES).
|
||||
|
||||
## [1.7.18] - 2026-05-19
|
||||
|
||||
### Added
|
||||
- **Dark mode** — New theme selector in Settings (Appearance card): **Off (Light)**, **On (Dark)**, **Auto (follows system)**. Applied immediately on page load to prevent white flash. Resolves [#78](https://github.com/dadaloop82/EverShelf/issues/78).
|
||||
- **Export inventory** — New 📤 button in inventory page header opens a modal to download the inventory as **CSV** (UTF-8 with BOM, Excel-compatible) or open a **print-ready HTML page** (auto-triggers print dialog for PDF). Export card also available in Settings tab. Resolves [#64](https://github.com/dadaloop82/EverShelf/issues/64).
|
||||
- `translations/de.json`: fixed missing `log.recipe_prefix` key.
|
||||
|
||||
## [1.7.17] - 2026-05-19
|
||||
|
||||
### Added
|
||||
- **French translation (🇫🇷 Français)** — Complete `translations/fr.json` with all 1049 translation keys. Resolves [#77](https://github.com/dadaloop82/EverShelf/issues/77).
|
||||
- **Spanish translation (🇪🇸 Español)** — Complete `translations/es.json` with all 1049 translation keys. Resolves [#77](https://github.com/dadaloop82/EverShelf/issues/77).
|
||||
- Language selector in Settings now shows all 5 languages: 🇮🇹 Italiano, 🇬🇧 English, 🇩🇪 Deutsch, 🇫🇷 Français, 🇪🇸 Español.
|
||||
- Default fallback language changed from Italian to English (for users with unsupported browser locale).
|
||||
- Setup wizard "Done" screen and navigation buttons localised for French and Spanish.
|
||||
|
||||
## [1.7.16] - 2026-05-17
|
||||
|
||||
### Added
|
||||
- **Barcode scan history** — Last 20 scanned products are stored server-side (SQLite `app_settings`) and shown as chips in the scan page (`#scan-recents-chips`). Tapping a chip selects the product directly — no need to scan again. Resolves [#68](https://github.com/dadaloop82/EverShelf/issues/68).
|
||||
- **Full server-side user-data centralisation** — All user preferences previously siloed in `localStorage` per-device are now synced to the server via `app_settings_save` and loaded back at startup via `app_settings_get`. Affected data: shopping tags, pinned Bring! items, location preferences (use/move), auto-added Bring! entries, Bring! purchased blocklist, no-expiry dismissed products. Data is now shared across all devices (desktop, phone, kiosk, Android app).
|
||||
- **One-time localStorage migration** — On first load, any data found in the old localStorage keys (`shopping_tags`, `_userPinnedBring`, `_prefUseLoc`, `_prefMoveLoc`, `_autoAddedBring`, `_bringPurchasedBlocklist`, `_noExpiryDismissed`, `evershelf_scan_recents`) is automatically migrated to the server and the local keys are removed.
|
||||
|
||||
## [1.7.15] - 2026-05-16
|
||||
|
||||
### Added
|
||||
@@ -20,11 +92,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **`pz`/`conf` unit labels translated** — "pz" now shows as "pcs" in English and "Stk" in German; "conf" shows as "pkg" / "Pkg". All `unitLabels` objects in JS now use `t('units.pz')` / `t('units.conf')`.
|
||||
|
||||
### Fixed
|
||||
- **Camera button (📷) opened kiosk SettingsActivity on Android** — The native `btnSettings` ImageButton in the kiosk layout was positioned `top|end` with `alpha=0.12` (nearly invisible), sitting directly on top of the HTML scan button in the webapp header. Every tap on the 📷 button was intercepted by the native View and opened `SettingsActivity`. Fixed: moved `btnSettings` to `bottom|end` (above the bottom nav bar, `marginBottom=80dp`) and increased `alpha` to `0.28` so it is clearly separate from the header. Kiosk versionCode bumped to 16.
|
||||
- **Camera button (📷) opened settings on Android Chrome/Brave** — `pointerleave` fired before `pointerup` when finger drifted slightly, cancelling the long-press timer and leaving the browser to dispatch a synthetic `click` that bubbled to an unintended handler. Fixed: added `setPointerCapture` (prevents `pointerleave` during touch) and `preventDefault` (blocks synthetic click); replaced `pointerleave` with `pointercancel` handler. Added `touch-action: manipulation` to `.header-scan-btn` CSS.
|
||||
- **Logo white background on splash screen** — Re-processed both `logo.png` and `logo_icon.png` with fuzz 35% alpha extraction, removing the white background that was visible against the dark splash background (`#0f172a`).
|
||||
- **Recipe button label** — Shortened to "Ricetta" / "Recipe" / "Rezept" for compact display in the inventory quick-action modal.
|
||||
- **Quantity decimal precision** — `qtyNum` in recipe/cooking ingredient buttons and `conf` fallback display in inventory cards now limited to 1 decimal place (was showing 7+ decimal places from raw AI output, e.g. `0.25353223 conf`).
|
||||
- **"Errore" / "Error" fallback strings** — All remaining Italian hardcoded `'Errore'` fallbacks in `showToast()` calls replaced with `t('error.generic')`. Italian fallback strings removed from buttons that already used `t()`.
|
||||
- **README Italian phrases** — "La quantità è giusta (2 pz)", "🤖 Spiega", "Latte / Affettato / Panna da cucina", "Buon appetito!", "L'ho buttato" replaced with English equivalents in the README.
|
||||
- **Appliance chips translated** — `renderAppliances()` now shows translated names (e.g. "Air fryer" in EN, "Heißluftfritteuse" in DE) for all known canonical Italian appliance names via `_applianceDisplayName()` lookup. `addApplianceQuick` toast no longer hardcoded Italian. Remove-button title translated.
|
||||
- **Gemini API key not preserved on settings save** — `saveSettings()` was overwriting `s.gemini_key = ""` when the Gemini input field was empty (it is intentionally not pre-populated for security). Key is now preserved if the input is blank. `_geminiAvailable` is re-fetched from the server after every settings save so the recipe buttons reflect the real state immediately.
|
||||
|
||||
## [1.7.14] - 2026-05-16
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
[](https://www.php.net/)
|
||||
[](https://www.sqlite.org/)
|
||||
[](Dockerfile)
|
||||
[](translations/)
|
||||
[](CHANGELOG.md)
|
||||
[](translations/)
|
||||
[](CHANGELOG.md)
|
||||
[](https://github.com/dadaloop82/EverShelf/stargazers)
|
||||
[](https://github.com/dadaloop82/EverShelf/commits/main)
|
||||
[](https://github.com/dadaloop82/EverShelf/graphs/contributors)
|
||||
@@ -38,13 +38,20 @@
|
||||
|
||||
## ✨ 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.
|
||||
> 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:00–07:00) instead of the OS setting, making it server-friendly.
|
||||
|
||||
### 📦 Inventory Management
|
||||
- **Barcode scanning** — Scan products with your phone camera using QuaggaJS
|
||||
- **Export inventory** — Download the full inventory as a UTF-8 CSV (Excel-compatible) or open a print-ready page to save as PDF; export button always visible in the inventory page header
|
||||
- **Barcode scanning** — Scan products with your phone camera using QuaggaJS; last 20 scanned products saved as tappable chips so you can re-select them without rescanning
|
||||
- **AI identification** — Take a photo and let Google Gemini identify the product, with suggestions from your existing inventory; gracefully shows a friendly message when AI quota is exhausted instead of a raw API error
|
||||
- **Smart locations** — Track items across Pantry, Fridge, Freezer, and custom locations
|
||||
- **Expiry tracking** — Automatic shelf-life estimation based on product type and storage
|
||||
- **Opened product tracking** — Reduced shelf-life calculation when packages are opened; opened-product expiry is now also checked when building banner alerts (not just the dashboard section)
|
||||
- **Vacuum-sealed support** — Extended expiry dates for vacuum-sealed items
|
||||
- **Vacuum-sealed support** — Extended expiry dates for vacuum-sealed items; products sealed under vacuum are only flagged as expired after a configurable grace period past the printed date (`VACUUM_EXPIRY_EXTENSION_DAYS`, default 30 days, configurable in `.env`)
|
||||
- **Anomaly detection** — Banner alerts for suspicious quantities and consumption predictions with inline correction; dismiss button now shows the current inventory quantity so the action is unambiguous ("Quantity is correct (2 pcs)")
|
||||
|
||||
### 🤖 AI-Powered (Google Gemini)
|
||||
@@ -68,6 +75,7 @@
|
||||
- **Catalog coverage** — All product types resolve to a German Bring! catalog key for icon and category display in the Bring! app
|
||||
|
||||
### 🍳 Cooking Mode
|
||||
- **♻️ Zero-waste tips** — For each cooking step that generates reusable scraps (peels, cooking water, egg whites, cheese rinds, bread crusts, vegetable tops, etc.), a dismissible ♻️ tip card appears with a practical reuse idea; tips are generated by Gemini as part of the recipe at no extra API cost; opt-in toggle in Settings (default OFF)
|
||||
- **Step-by-step guidance** — Follow recipes with a hands-free cooking interface
|
||||
- **Text-to-Speech** — Voice readout of recipe steps; supports browser Web Speech API, native Android TTS (kiosk), or a custom REST endpoint (Home Assistant, etc.); retries voice loading for up to 10 seconds with a fallback refresh button; TTS activates automatically without requiring the global TTS setting to be enabled
|
||||
- **Auto-read on navigate** — Each step is read aloud automatically when you tap Next or Previous; the first step is read when entering cooking mode
|
||||
@@ -90,10 +98,19 @@
|
||||
- **Swipe navigation** — Touch swipe or tap arrows/dots to browse banner notifications
|
||||
- **Quick-access buttons** — Recently used and most popular products shown on the inventory page for fast access
|
||||
|
||||
### 📱 Progressive Web App
|
||||
### 🌙 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
|
||||
|
||||
### �️ 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
|
||||
- **Manual cleanup** — Trigger immediately via `GET /api/?action=db_cleanup`
|
||||
- **Compact by default** — Fresh installs stay small; large accumulated databases shrink back to a few hundred KB within one cron cycle
|
||||
|
||||
### �📱 Progressive Web App
|
||||
- **Mobile-first design** — Optimized for phones, works on tablets and desktop
|
||||
- **Installable** — Add to home screen for a native app experience
|
||||
- **Multi-device** — Settings and data sync across devices on the same server
|
||||
- **Multi-device** — All user data (shopping tags, pinned items, location preferences, scan history) is stored server-side in SQLite and shared across every device on the same instance; no data is siloed in a single browser's localStorage
|
||||
|
||||
### ⚖️ Smart Scale Integration (Add-on)
|
||||
- **Bluetooth gateway** — Connects a BLE smart scale to EverShelf via local WebSocket
|
||||
@@ -182,12 +199,32 @@ TTS_URL=http://your-home-assistant:8123/api/events/tts_speak
|
||||
TTS_TOKEN=your_long_lived_token
|
||||
TTS_ENABLED=true
|
||||
|
||||
# Optional: DB retention and cleanup (applied automatically each cron cycle)
|
||||
RECIPE_RETENTION_DAYS=7 # delete recipe plans older than N days
|
||||
TRANSACTION_RETENTION_DAYS=7 # delete stock transactions older than N days
|
||||
|
||||
# Optional: Vacuum-sealed expiry grace period
|
||||
VACUUM_EXPIRY_EXTENSION_DAYS=30 # extra days before vacuum-sealed items are flagged expired
|
||||
|
||||
# Optional: Gemini cost rates (USD per million tokens, for the Info tab cost estimate)
|
||||
GEMINI_COST_25F_IN=0.15
|
||||
GEMINI_COST_25F_OUT=0.60
|
||||
GEMINI_COST_20F_IN=0.10
|
||||
GEMINI_COST_20F_OUT=0.40
|
||||
|
||||
# Optional: Security — protect the save_settings endpoint
|
||||
# Set a strong random string; the Settings UI will ask for it before saving
|
||||
SETTINGS_TOKEN=
|
||||
|
||||
# Optional: Demo mode — block all write operations at the router level
|
||||
DEMO_MODE=false
|
||||
|
||||
# Optional: Logging
|
||||
# LOG_LEVEL sets the minimum severity written to disk (DEBUG / INFO / WARN / ERROR)
|
||||
# DEBUG also logs every SQL query executed against the database
|
||||
LOG_LEVEL=INFO
|
||||
LOG_ROTATE_HOURS=24 # hours before opening a new log file (default: 24)
|
||||
LOG_MAX_FILES=14 # maximum number of rotated files to keep (default: 14)
|
||||
```
|
||||
|
||||
### Web Server Configuration
|
||||
@@ -352,35 +389,7 @@ The application uses no build tools — edit files directly and refresh.
|
||||
|
||||
## 📋 Roadmap
|
||||
|
||||
### High Priority
|
||||
- [ ] **Cooking mode — 3D wheel JS** — swipe navigation, gyroscope tilt, haptic feedback
|
||||
- [ ] **Cooking mode — step timers** — auto-detect "X minutes" in recipe steps, countdown + alert
|
||||
- [ ] **Push notifications** — daily expiry alerts via PWA Service Worker + VAPID
|
||||
- [ ] **Quick search / quick-add bar** — always-visible search above the nav, PWA shortcuts
|
||||
|
||||
### Medium Priority
|
||||
- [ ] **Receipt OCR → bulk add** — photo of receipt → Gemini Vision → auto-fill inventory
|
||||
- [ ] **CSV/JSON export & import** — download/upload inventory from Settings
|
||||
- [ ] **Custom storage locations** — user-defined locations beyond Fridge/Freezer/Pantry
|
||||
- [ ] **Multi-user support** — PIN-based user distinction, action log with user label
|
||||
- [ ] **AI optimal purchase prediction** — suggest "buy X units of Y within Z days"
|
||||
- [ ] **Price history sparklines** — per-product price chart from the AI cache data
|
||||
|
||||
### Low Priority / Nice to Have
|
||||
- [ ] **Dark mode** — CSS custom properties are already structured to support it
|
||||
- [ ] **Full offline mode** — Service Worker cache to show inventory read-only when server is down
|
||||
- [ ] **French & Spanish translations** (`fr.json`, `es.json`)
|
||||
- [ ] **Swipe actions on inventory rows** — swipe left to use/discard, right to edit
|
||||
- [ ] **PHP unit tests** — PHPUnit coverage for shelf-life, price calc, and key helpers
|
||||
|
||||
### Completed ✅
|
||||
- ✅ AI price estimation in shopping list
|
||||
- ✅ Server heartbeat + offline banner
|
||||
- ✅ In-app bug reporter → automatic GitHub issue creation
|
||||
- ✅ Cooking mode (start, steps, 3D wheel CSS)
|
||||
- ✅ Kiosk ⚙️ Settings overlay button (replaces Android native button)
|
||||
- ✅ Adaptive consumption anomaly detection
|
||||
- ✅ CI/CD pipeline (PHP lint, JS lint, Docker build, Trivy security scan)
|
||||
Feature requests, bug reports and planned work are tracked in the [**EverShelf Roadmap**](https://github.com/users/dadaloop82/projects/2) GitHub Project.
|
||||
|
||||
---
|
||||
|
||||
@@ -393,6 +402,8 @@ The app supports multiple languages via JSON translation files in the `translati
|
||||
| 🇮🇹 Italian (it) | ✅ Complete (base) |
|
||||
| 🇬🇧 English (en) | ✅ Complete |
|
||||
| 🇩🇪 German (de) | ✅ Complete |
|
||||
| 🇫🇷 French (fr) | ✅ Complete |
|
||||
| 🇪🇸 Spanish (es) | ✅ Complete |
|
||||
|
||||
**Want to add your language?** See the [Translation Guide](CONTRIBUTING.md#-adding-translations) — just copy `translations/it.json`, translate the values, and submit a PR!
|
||||
|
||||
|
||||
@@ -79,6 +79,19 @@ try {
|
||||
echo '[' . date('Y-m-d H:i:s') . '] Shelf life pre-warm warning: ' . $pe->getMessage() . "\n";
|
||||
}
|
||||
|
||||
// ── DB cleanup (retention policy) ────────────────────────────────────
|
||||
// Delete old recipes and transactions based on .env retention settings.
|
||||
try {
|
||||
ob_start();
|
||||
dbCleanup($db);
|
||||
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";
|
||||
} catch (Throwable $ce) {
|
||||
echo '[' . date('Y-m-d H:i:s') . '] DB cleanup warning: ' . $ce->getMessage() . "\n";
|
||||
}
|
||||
|
||||
} catch (Throwable $e) {
|
||||
$msg = $e->getMessage();
|
||||
echo '[' . date('Y-m-d H:i:s') . '] ERROR: ' . $msg . "\n";
|
||||
|
||||
+10
-3
@@ -40,8 +40,13 @@ function _ensureDataDir(): void {
|
||||
|
||||
function getDB(): PDO {
|
||||
_ensureDataDir();
|
||||
// logger.php is required by index.php before getDB() is called.
|
||||
// In cron context it may not be loaded yet — guard with class_exists.
|
||||
$useLogging = class_exists('LoggingPDO', false);
|
||||
$isNew = !file_exists(DB_PATH);
|
||||
$db = new PDO('sqlite:' . DB_PATH);
|
||||
$db = $useLogging
|
||||
? new LoggingPDO('sqlite:' . DB_PATH)
|
||||
: new PDO('sqlite:' . DB_PATH);
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
$db->exec("PRAGMA journal_mode=WAL");
|
||||
@@ -379,8 +384,10 @@ function estimateOpenedExpiryDaysPHP(string $name, string $category, string $loc
|
||||
if (preg_match('/\b(yogurt|yaourt|yoghurt)\b/', $n)) return 5;
|
||||
if (preg_match('/mozzarella|burrata|stracciatella/', $n)) return 3;
|
||||
if (preg_match('/philadelphia|spalmabile/', $n)) return 7;
|
||||
// Specific hard cheeses that contain 'fresco' in their commercial name (e.g. Asiago fresco)
|
||||
// must be matched BEFORE the generic 'formaggio fresco' catch-all
|
||||
if (preg_match('/parmigiano|grana|pecorino|provolone|asiago|fontina|emmental|gruyere|scamorza|groviera/', $n)) return 28;
|
||||
if (preg_match('/formaggio.*(fresco|ricotta|mascarpone|stracchino|crescenza)/', $n)) return 5;
|
||||
if (preg_match('/parmigiano|grana|pecorino|provolone/', $n)) return 21;
|
||||
if (preg_match('/formaggio/', $n)) return 10;
|
||||
if (preg_match('/\bburro\b/', $n)) return 30;
|
||||
if (preg_match('/\bpanna\b/', $n)) return 4;
|
||||
@@ -449,7 +456,7 @@ function estimateSealedExpiryDaysPHP(string $name, string $category, string $loc
|
||||
elseif (preg_match('/yogurt/', $n)) $days = 21;
|
||||
elseif (preg_match('/mozzarella|burrata|stracciatella/', $n)) $days = 5;
|
||||
elseif (preg_match('/formaggio\s+(fresco|ricotta|mascarpone|stracchino|crescenza)/', $n)) $days = 10;
|
||||
elseif (preg_match('/parmigiano|grana|pecorino|provolone/', $n)) $days = 60;
|
||||
elseif (preg_match('/parmigiano|grana|pecorino|provolone|asiago|fontina|emmental|gruyere|scamorza|groviera/', $n)) $days = 60;
|
||||
elseif (preg_match('/burro/', $n)) $days = 60;
|
||||
elseif (preg_match('/panna/', $n)) $days = 14;
|
||||
elseif (preg_match('/prosciutto\s+cotto|mortadella|wurstel/', $n)) $days = 7;
|
||||
|
||||
+926
-73
File diff suppressed because it is too large
Load Diff
+375
@@ -0,0 +1,375 @@
|
||||
<?php
|
||||
/**
|
||||
* EverShelf Logger — rotating file logger with 4 configurable levels.
|
||||
*
|
||||
* Levels (in order of verbosity):
|
||||
* DEBUG(0) — ogni minima operazione: query, cache, AI payload, function entry/exit
|
||||
* INFO (1) — azioni completate, AI result summary, sync status [default]
|
||||
* WARN (2) — rate limit, cache miss, AI fallback, token renewal, slow op
|
||||
* ERROR(3) — DB failure, AI API error, file write error, exception
|
||||
*
|
||||
* Config via .env (all optional):
|
||||
* LOG_LEVEL = INFO (DEBUG|INFO|WARN|ERROR)
|
||||
* LOG_ROTATE_HOURS = 24 (new file every N hours; 1–168; default 24)
|
||||
* LOG_MAX_FILES = 14 (max rotated files to keep; default 14)
|
||||
*
|
||||
* Log files: logs/evershelf_YYYY-MM-DD_HH.log
|
||||
* Each line: [2026-05-18 14:23:11] [INFO ] [rid=a1b2c3d4] [action] Message {ctx}
|
||||
*/
|
||||
class EverLog {
|
||||
|
||||
// ── Level constants ────────────────────────────────────────────────────
|
||||
const DEBUG = 0;
|
||||
const INFO = 1;
|
||||
const WARN = 2;
|
||||
const ERROR = 3;
|
||||
|
||||
private static bool $initialized = false;
|
||||
private static int $level = self::INFO;
|
||||
private static string $logFile = '';
|
||||
private static string $logDir = '';
|
||||
private static int $rotateHours = 24;
|
||||
private static int $maxFiles = 14;
|
||||
private static string $requestId = '';
|
||||
private static string $currentAction = '-';
|
||||
|
||||
// ── Init (called lazily on first write) ────────────────────────────────
|
||||
private static function init(): void {
|
||||
if (self::$initialized) return;
|
||||
self::$initialized = true;
|
||||
|
||||
// Read .env values via getenv() (populated by Apache SetEnv or putenv() in index.php)
|
||||
$envLevel = strtoupper((string)(getenv('LOG_LEVEL') ?: 'INFO'));
|
||||
$rotateHours = max(1, min(168, (int)(getenv('LOG_ROTATE_HOURS') ?: 24)));
|
||||
$maxFiles = max(1, min(365, (int)(getenv('LOG_MAX_FILES') ?: 14)));
|
||||
|
||||
self::$level = match($envLevel) {
|
||||
'DEBUG' => self::DEBUG,
|
||||
'WARN' => self::WARN,
|
||||
'ERROR' => self::ERROR,
|
||||
default => self::INFO,
|
||||
};
|
||||
self::$rotateHours = $rotateHours;
|
||||
self::$maxFiles = $maxFiles;
|
||||
self::$requestId = substr(bin2hex(random_bytes(4)), 0, 8);
|
||||
|
||||
// Ensure log directory exists
|
||||
$base = dirname(__DIR__) . '/logs';
|
||||
self::$logDir = $base;
|
||||
if (!is_dir($base)) {
|
||||
@mkdir($base, 0755, true);
|
||||
}
|
||||
|
||||
// Compute current log file path (slot by rotate-hours bucket)
|
||||
$slotTs = (int)(floor(time() / ($rotateHours * 3600)) * ($rotateHours * 3600));
|
||||
$slotLabel = gmdate('Y-m-d_H', $slotTs);
|
||||
self::$logFile = "$base/evershelf_{$slotLabel}.log";
|
||||
|
||||
// Rotate (delete oldest files beyond max)
|
||||
self::rotate();
|
||||
}
|
||||
|
||||
// ── Rotate old log files ───────────────────────────────────────────────
|
||||
private static function rotate(): void {
|
||||
$files = glob(self::$logDir . '/evershelf_*.log') ?: [];
|
||||
if (count($files) <= self::$maxFiles) return;
|
||||
sort($files); // oldest first (filenames are lexicographically sortable by date)
|
||||
$toDelete = array_slice($files, 0, count($files) - self::$maxFiles);
|
||||
foreach ($toDelete as $f) {
|
||||
@unlink($f);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Core write ────────────────────────────────────────────────────────
|
||||
private static function write(int $lvl, string $msg, array $ctx, string $action): void {
|
||||
self::init();
|
||||
if ($lvl < self::$level) return;
|
||||
|
||||
$labels = ['DEBUG', 'INFO ', 'WARN ', 'ERROR'];
|
||||
$ts = gmdate('Y-m-d H:i:s');
|
||||
$act = $action !== '-' ? $action : self::$currentAction;
|
||||
$ctxStr = empty($ctx) ? '' : ' ' . json_encode($ctx, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$line = "[{$ts}] [{$labels[$lvl]}] [rid=" . self::$requestId . "] [{$act}] {$msg}{$ctxStr}\n";
|
||||
|
||||
@file_put_contents(self::$logFile, $line, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────
|
||||
|
||||
/** Set the current action name (shown in every subsequent log line for this request). */
|
||||
public static function setAction(string $action): void {
|
||||
self::$currentAction = $action;
|
||||
}
|
||||
|
||||
/** Log at DEBUG level — every minor operation, query, cache hit/miss, AI payload. */
|
||||
public static function debug(string $msg, array $ctx = [], string $action = '-'): void {
|
||||
self::write(self::DEBUG, $msg, $ctx, $action);
|
||||
}
|
||||
|
||||
/** Log at INFO level — action completed, recipe generated, sync done. */
|
||||
public static function info(string $msg, array $ctx = [], string $action = '-'): void {
|
||||
self::write(self::INFO, $msg, $ctx, $action);
|
||||
}
|
||||
|
||||
/** Log at WARN level — rate limit, AI fallback, slow op, token renewal. */
|
||||
public static function warn(string $msg, array $ctx = [], string $action = '-'): void {
|
||||
self::write(self::WARN, $msg, $ctx, $action);
|
||||
}
|
||||
|
||||
/** Log at ERROR level — DB failure, AI API error, file write error, exception. */
|
||||
public static function error(string $msg, array $ctx = [], string $action = '-'): void {
|
||||
self::write(self::ERROR, $msg, $ctx, $action);
|
||||
}
|
||||
|
||||
/** Convenience: log a Throwable at ERROR level with class + location. */
|
||||
public static function exception(\Throwable $e, string $action = '-', array $extra = []): void {
|
||||
self::write(self::ERROR, $e->getMessage(), array_merge([
|
||||
'class' => get_class($e),
|
||||
'at' => basename($e->getFile()) . ':' . $e->getLine(),
|
||||
'trace' => substr($e->getTraceAsString(), 0, 800),
|
||||
], $extra), $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the start of an action request (INFO).
|
||||
* Automatically sets the current action name so subsequent lines inherit it.
|
||||
*/
|
||||
public static function request(string $action, string $method, array $params = []): void {
|
||||
self::setAction($action);
|
||||
// At DEBUG: include all params; at INFO just the action+method
|
||||
if (self::$level <= self::DEBUG) {
|
||||
self::write(self::DEBUG, "→ {$method} /{$action}", $params, $action);
|
||||
} else {
|
||||
self::write(self::INFO, "→ {$method} /{$action}", [], $action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a DB query at DEBUG level.
|
||||
* @param string $sql Truncated SQL or a descriptive label
|
||||
* @param mixed $result Number of rows affected/returned (optional)
|
||||
* @param float $elapsed Execution time in seconds (optional)
|
||||
*/
|
||||
public static function query(string $sql, $result = null, float $elapsed = 0.0): void {
|
||||
if (self::$level > self::DEBUG) return; // skip entirely unless DEBUG
|
||||
$ctx = [];
|
||||
if ($result !== null) $ctx['rows'] = $result;
|
||||
if ($elapsed > 0) $ctx['ms'] = round($elapsed * 1000, 1);
|
||||
if ($elapsed > 1.0) $ctx['SLOW'] = true; // highlight slow queries even in context
|
||||
self::write(self::DEBUG, 'DB: ' . substr($sql, 0, 200), $ctx, self::$currentAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a slow operation as WARN regardless of configured level.
|
||||
* Call this after any operation that took more than $thresholdSec.
|
||||
*/
|
||||
public static function slowOp(string $label, float $elapsed, float $thresholdSec = 2.0): void {
|
||||
if ($elapsed < $thresholdSec) return;
|
||||
self::write(self::WARN, "SLOW_OP: {$label}", ['elapsed_s' => round($elapsed, 2)], self::$currentAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an AI call at INFO level (or DEBUG for full payload).
|
||||
* @param string $model Model name (e.g. 'gemini-2.5-flash')
|
||||
* @param int $promptLen Character length of the prompt
|
||||
* @param bool $isFallback Whether this is the fallback model
|
||||
*/
|
||||
public static function aiCall(string $model, int $promptLen, bool $isFallback = false): void {
|
||||
$ctx = ['model' => $model, 'prompt_chars' => $promptLen];
|
||||
if ($isFallback) $ctx['fallback'] = true;
|
||||
$level = $isFallback ? self::WARN : self::INFO;
|
||||
self::write($level, 'AI call', $ctx, self::$currentAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an AI response at INFO level.
|
||||
* @param string $model Model that responded
|
||||
* @param int $outputLen Character length of output
|
||||
* @param float $elapsed Call duration in seconds
|
||||
* @param bool $ok Whether the call succeeded
|
||||
* @param string $errorMsg Error message if not ok
|
||||
*/
|
||||
public static function aiResponse(string $model, int $outputLen, float $elapsed, bool $ok = true, string $errorMsg = ''): void {
|
||||
$ctx = ['model' => $model, 'output_chars' => $outputLen, 'elapsed_s' => round($elapsed, 2)];
|
||||
if (!$ok) {
|
||||
$ctx['error'] = substr($errorMsg, 0, 200);
|
||||
self::write(self::ERROR, 'AI error', $ctx, self::$currentAction);
|
||||
} else {
|
||||
self::write(self::INFO, 'AI ok', $ctx, self::$currentAction);
|
||||
}
|
||||
// Warn if over 10s
|
||||
if ($ok && $elapsed > 10.0) {
|
||||
self::write(self::WARN, 'AI response slow', ['elapsed_s' => round($elapsed, 2)], self::$currentAction);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a cache event at DEBUG level.
|
||||
* @param string $cacheKey The cache key (or a label)
|
||||
* @param bool $hit true = cache hit, false = cache miss
|
||||
* @param string $cacheType 'file', 'session', 'memory'
|
||||
*/
|
||||
public static function cache(string $cacheKey, bool $hit, string $cacheType = 'file'): void {
|
||||
if (self::$level > self::DEBUG) return;
|
||||
self::write(self::DEBUG,
|
||||
($hit ? 'CACHE HIT' : 'CACHE MISS') . " [{$cacheType}]",
|
||||
['key' => substr($cacheKey, 0, 64)],
|
||||
self::$currentAction
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last $lines log lines from all available log files, newest last.
|
||||
* Used by the get_logs API endpoint.
|
||||
*/
|
||||
public static function tail(int $lines = 500): array {
|
||||
self::init();
|
||||
$files = glob(self::$logDir . '/evershelf_*.log') ?: [];
|
||||
if (empty($files)) return [];
|
||||
rsort($files); // newest file first
|
||||
|
||||
$collected = [];
|
||||
foreach ($files as $f) {
|
||||
if (count($collected) >= $lines) break;
|
||||
$content = @file_get_contents($f);
|
||||
if ($content === false) continue;
|
||||
$fLines = array_filter(explode("\n", $content));
|
||||
// Prepend so we read newest-first → older lines at front
|
||||
$collected = array_merge(array_values($fLines), $collected);
|
||||
}
|
||||
// Return last $lines, newest at end (chronological order)
|
||||
return array_values(array_slice($collected, -$lines));
|
||||
}
|
||||
|
||||
/** List available log files with their sizes and date ranges. */
|
||||
public static function listFiles(): array {
|
||||
self::init();
|
||||
$files = glob(self::$logDir . '/evershelf_*.log') ?: [];
|
||||
rsort($files);
|
||||
return array_map(fn($f) => [
|
||||
'file' => basename($f),
|
||||
'size_kb' => round(filesize($f) / 1024, 1),
|
||||
'mtime' => date('Y-m-d H:i:s', filemtime($f)),
|
||||
], $files);
|
||||
}
|
||||
|
||||
/** Current effective level name. */
|
||||
public static function levelName(): string {
|
||||
self::init();
|
||||
return ['DEBUG', 'INFO', 'WARN', 'ERROR'][self::$level];
|
||||
}
|
||||
|
||||
/** Current log file path. */
|
||||
public static function currentFile(): string {
|
||||
self::init();
|
||||
return self::$logFile;
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LoggingPDOStatement — wraps PDOStatement to time and log every execute()
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
class LoggingPDOStatement {
|
||||
private \PDOStatement $stmt;
|
||||
private string $sql;
|
||||
|
||||
public function __construct(\PDOStatement $stmt, string $sql) {
|
||||
$this->stmt = $stmt;
|
||||
$this->sql = $sql;
|
||||
}
|
||||
|
||||
public function execute(?array $params = null): bool {
|
||||
$t0 = microtime(true);
|
||||
$ok = $this->stmt->execute($params);
|
||||
$ms = round((microtime(true) - $t0) * 1000, 2);
|
||||
$ctx = ['ms' => $ms, 'rows' => $this->stmt->rowCount()];
|
||||
if ($ms > 500) $ctx['SLOW'] = true;
|
||||
EverLog::query($this->sql, $this->stmt->rowCount(), (microtime(true) - $t0));
|
||||
return $ok;
|
||||
}
|
||||
|
||||
public function fetch(int $mode = \PDO::FETCH_DEFAULT, ...$args): mixed {
|
||||
return $this->stmt->fetch($mode, ...$args);
|
||||
}
|
||||
|
||||
public function fetchAll(int $mode = \PDO::FETCH_DEFAULT, ...$args): array {
|
||||
return $this->stmt->fetchAll($mode ?: \PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
public function fetchColumn(int $col = 0): mixed {
|
||||
return $this->stmt->fetchColumn($col);
|
||||
}
|
||||
|
||||
public function rowCount(): int {
|
||||
return $this->stmt->rowCount();
|
||||
}
|
||||
|
||||
public function bindValue(int|string $param, mixed $value, int $type = \PDO::PARAM_STR): bool {
|
||||
return $this->stmt->bindValue($param, $value, $type);
|
||||
}
|
||||
|
||||
public function bindParam(int|string $param, mixed &$var, int $type = \PDO::PARAM_STR, int $maxLength = 0): bool {
|
||||
return $this->stmt->bindParam($param, $var, $type, $maxLength);
|
||||
}
|
||||
|
||||
public function closeCursor(): bool {
|
||||
return $this->stmt->closeCursor();
|
||||
}
|
||||
|
||||
public function setFetchMode(int $mode, mixed ...$args): bool {
|
||||
return $this->stmt->setFetchMode($mode, ...$args);
|
||||
}
|
||||
|
||||
public function __get(string $name): mixed {
|
||||
return $this->stmt->$name;
|
||||
}
|
||||
|
||||
public function __call(string $name, array $args): mixed {
|
||||
return $this->stmt->$name(...$args);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LoggingPDO — wraps PDO to auto-log all prepare(), query(), exec()
|
||||
// Drop-in replacement: return LoggingPDO from getDB() instead of PDO.
|
||||
// Type hint: use PDO in all functions (LoggingPDO extends PDO).
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
class LoggingPDO extends \PDO {
|
||||
public function prepare(string $query, array $options = []): LoggingPDOStatement|false {
|
||||
$stmt = parent::prepare($query, $options);
|
||||
if ($stmt === false) {
|
||||
EverLog::error('PDO::prepare failed', ['sql' => substr($query, 0, 200)]);
|
||||
return false;
|
||||
}
|
||||
return new LoggingPDOStatement($stmt, $query);
|
||||
}
|
||||
|
||||
public function query(string $query, ?int $fetchMode = null, mixed ...$fetchModeArgs): \PDOStatement|false {
|
||||
$t0 = microtime(true);
|
||||
$stmt = $fetchMode !== null
|
||||
? parent::query($query, $fetchMode, ...$fetchModeArgs)
|
||||
: parent::query($query);
|
||||
$elapsed = microtime(true) - $t0;
|
||||
if ($stmt !== false) {
|
||||
EverLog::query($query, $stmt->rowCount(), $elapsed);
|
||||
} else {
|
||||
EverLog::error('PDO::query failed', ['sql' => substr($query, 0, 200)]);
|
||||
}
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
public function exec(string $statement): int|false {
|
||||
// Skip WAL/PRAGMA logging below DEBUG (too noisy at startup)
|
||||
$isPragma = stripos(ltrim($statement), 'PRAGMA') === 0;
|
||||
$t0 = microtime(true);
|
||||
$result = parent::exec($statement);
|
||||
$elapsed = microtime(true) - $t0;
|
||||
if (!$isPragma) {
|
||||
EverLog::query($statement, $result === false ? 0 : $result, $elapsed);
|
||||
} elseif (EverLog::DEBUG >= 0) {
|
||||
// Log PRAGMAs only at DEBUG level
|
||||
EverLog::query($statement, is_int($result) ? $result : 0, $elapsed);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.7 MiB |
+543
-16
@@ -72,12 +72,12 @@ body {
|
||||
#app-preloader {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: var(--bg-dark, #0f172a);
|
||||
background: #0c1222;
|
||||
z-index: 200000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 0.35s ease;
|
||||
transition: opacity 0.45s ease;
|
||||
}
|
||||
#app-preloader.fade-out {
|
||||
opacity: 0;
|
||||
@@ -87,35 +87,213 @@ body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
gap: 28px;
|
||||
}
|
||||
.app-preloader-spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid rgba(255,255,255,0.15);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(255,255,255,0.1);
|
||||
border-top-color: #4ade80;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
animation: spin 0.9s linear infinite;
|
||||
}
|
||||
.app-preloader-label {
|
||||
color: rgba(255,255,255,0.75);
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.app-preloader-logo {
|
||||
height: 160px;
|
||||
height: 150px;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 4px 16px rgba(74,222,128,0.2));
|
||||
animation: logoPulse 3.2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes logoPulse {
|
||||
0%, 100% { filter: drop-shadow(0 4px 20px rgba(74,222,128,0.18)); }
|
||||
50% { filter: drop-shadow(0 4px 36px rgba(74,222,128,0.48)); }
|
||||
}
|
||||
.app-preloader-version {
|
||||
color: rgba(255,255,255,0.35);
|
||||
font-size: 0.72rem;
|
||||
color: rgba(255,255,255,0.22);
|
||||
font-size: 0.68rem;
|
||||
font-family: monospace;
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: -8px;
|
||||
letter-spacing: 0.6px;
|
||||
margin-top: -16px;
|
||||
}
|
||||
/* ── Startup progress section ────────────────────────────────────────── */
|
||||
.preloader-progress-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
width: min(92vw, 520px);
|
||||
animation: zwFadeIn 0.25s ease;
|
||||
}
|
||||
.preloader-bar-track {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: rgba(255,255,255,0.08);
|
||||
border-radius: 99px;
|
||||
overflow: visible;
|
||||
}
|
||||
.preloader-bar {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: linear-gradient(90deg, #4ade80, #22c55e);
|
||||
border-radius: 99px;
|
||||
transition: width 0.22s cubic-bezier(0.4,0,0.2,1), background 0.3s ease;
|
||||
box-shadow: 0 0 8px rgba(74,222,128,0.55);
|
||||
}
|
||||
.preloader-bar.bar-error { background: linear-gradient(90deg,#f87171,#ef4444); box-shadow: 0 0 8px rgba(239,68,68,0.5); }
|
||||
.preloader-bar.bar-warn { background: linear-gradient(90deg,#fbbf24,#f59e0b); box-shadow: 0 0 8px rgba(251,191,36,0.5); }
|
||||
.preloader-check-label { display: none; }
|
||||
|
||||
/* ── Status line: single element, opacity crossfade via JS ─────────── */
|
||||
.check-ticker {
|
||||
width: 100%;
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.preloader-status-text {
|
||||
font-size: clamp(0.78rem, 2vw, 0.9rem);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.03em;
|
||||
color: rgba(255,255,255,0.45);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.preloader-status-text.state-ok { color: #86efac; }
|
||||
.preloader-status-text.state-warn { color: #fde68a; }
|
||||
.preloader-status-text.state-error { color: #fca5a5; }
|
||||
.preloader-warnings {
|
||||
max-width: min(92vw, 600px);
|
||||
width: 100%;
|
||||
animation: zwFadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
/* ── Warning popup (auto-close 5s) ─────────────────────────── */
|
||||
.startup-popup {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
.startup-popup-warn {
|
||||
background: rgba(30,20,0,0.85);
|
||||
border: 1px solid rgba(251,191,36,0.45);
|
||||
}
|
||||
.startup-popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 9px 14px 7px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
color: #fcd34d;
|
||||
gap: 8px;
|
||||
}
|
||||
.startup-popup-countdown {
|
||||
background: rgba(251,191,36,0.2);
|
||||
color: #fcd34d;
|
||||
border-radius: 50%;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.startup-popup-body {
|
||||
padding: 2px 14px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.startup-warn-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
.startup-warn-icon {
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.startup-warn-body {
|
||||
font-size: 0.78rem;
|
||||
color: #d4c08a;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.startup-warn-body strong {
|
||||
display: block;
|
||||
color: #fcd34d;
|
||||
font-size: 0.82rem;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.startup-warn-body p {
|
||||
margin: 0;
|
||||
color: #c8a954;
|
||||
}
|
||||
.startup-popup-bar-wrap {
|
||||
height: 3px;
|
||||
background: rgba(251,191,36,0.15);
|
||||
}
|
||||
.startup-popup-bar {
|
||||
height: 3px;
|
||||
background: #fbbf24;
|
||||
width: 100%;
|
||||
will-change: width;
|
||||
}
|
||||
|
||||
/* ── Error popup (blocking) ─────────────────────────────────── */
|
||||
/* Keep .preloader-warn-badge for backward compat */
|
||||
.preloader-warn-badge {
|
||||
background: rgba(251,191,36,0.15);
|
||||
color: #fcd34d;
|
||||
border: 1px solid rgba(251,191,36,0.35);
|
||||
border-radius: 99px;
|
||||
padding: 3px 10px;
|
||||
font-size: 0.71rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.preloader-error-msg {
|
||||
color: #fca5a5;
|
||||
background: rgba(239,68,68,0.18);
|
||||
border: 1px solid rgba(239,68,68,0.4);
|
||||
border-radius: 12px;
|
||||
padding: 14px 18px;
|
||||
font-size: 0.80rem;
|
||||
text-align: left;
|
||||
max-width: 300px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-line;
|
||||
animation: zwFadeIn 0.3s ease;
|
||||
}
|
||||
.preloader-error-msg strong {
|
||||
display: block;
|
||||
font-size: 0.9rem;
|
||||
color: #f87171;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.preloader-retry-btn {
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 9px 22px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
animation: zwFadeIn 0.3s ease;
|
||||
}
|
||||
.preloader-retry-btn:active { opacity: 0.8; }
|
||||
.header-logo-icon {
|
||||
height: 28px;
|
||||
width: auto;
|
||||
@@ -279,6 +457,7 @@ body {
|
||||
height: 48px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.18);
|
||||
animation: pulse-scan 2s ease-in-out infinite;
|
||||
touch-action: manipulation; /* prevent 300ms delay and double-tap zoom on mobile */
|
||||
}
|
||||
.header-scan-btn:active {
|
||||
background: rgba(255,255,255,0.45);
|
||||
@@ -4012,6 +4191,15 @@ body.server-offline .bottom-nav {
|
||||
min-width: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.recipe-ing-name {
|
||||
cursor: pointer;
|
||||
border-bottom: 1px dashed rgba(74,222,128,0.5);
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
}
|
||||
.recipe-ing-name:hover {
|
||||
color: #4ade80;
|
||||
border-bottom-color: #4ade80;
|
||||
}
|
||||
|
||||
.btn-use-ingredient {
|
||||
flex-shrink: 0;
|
||||
@@ -5750,7 +5938,6 @@ body.cooking-mode-active .app-header {
|
||||
opacity: 0.75;
|
||||
}
|
||||
.alert-item-spoiled .alert-item-name {
|
||||
text-decoration: line-through;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
@@ -6857,3 +7044,343 @@ body.cooking-mode-active .app-header {
|
||||
color: #9ca3af;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.btn-banner-vacuum {
|
||||
background: #ede9fe;
|
||||
color: #6d28d9;
|
||||
}
|
||||
.btn-banner-edit2 {
|
||||
background: #e0f2fe;
|
||||
color: #0369a1;
|
||||
}
|
||||
|
||||
/* ===== PAGE HEADER ACTION BUTTON (export etc.) ===== */
|
||||
.page-header-action-btn {
|
||||
margin-left: auto;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 8px 12px;
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
color: var(--primary);
|
||||
}
|
||||
.page-header-action-btn:active {
|
||||
transform: scale(0.95);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* ===== ZERO-WASTE TIP CARD (cooking mode) ===== */
|
||||
.cooking-zerowaste-tip {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
background: rgba(16, 185, 129, 0.10);
|
||||
border: 1.5px solid rgba(16, 185, 129, 0.35);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
margin: 10px 16px 0;
|
||||
position: relative;
|
||||
animation: zwFadeIn 0.3s ease;
|
||||
flex-direction: column;
|
||||
}
|
||||
@keyframes zwFadeIn {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.cooking-zerowaste-label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #059669;
|
||||
}
|
||||
.cooking-zerowaste-scrap {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: #065f46;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.cooking-zerowaste-text {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text);
|
||||
margin: 4px 0 0;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.cooking-zerowaste-close {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 10px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
color: #6b7280;
|
||||
padding: 2px 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
.cooking-zerowaste-close:hover { color: #374151; }
|
||||
[data-theme="dark"] .cooking-zerowaste-tip {
|
||||
background: rgba(16, 185, 129, 0.08);
|
||||
border-color: rgba(16, 185, 129, 0.25);
|
||||
}
|
||||
[data-theme="dark"] .cooking-zerowaste-scrap { color: #6ee7b7; }
|
||||
[data-theme="dark"] .cooking-zerowaste-label { color: #34d399; }
|
||||
[data-theme="dark"] .cooking-zerowaste-close { color: #9ca3af; }
|
||||
|
||||
/* ===== DARK MODE ===== */
|
||||
[data-theme="dark"] {
|
||||
--bg: #0f172a;
|
||||
--bg-card: #1e293b;
|
||||
--bg-dark: #020617;
|
||||
--text: #e2e8f0;
|
||||
--text-light: #94a3b8;
|
||||
--text-muted: #64748b;
|
||||
--text-secondary: #94a3b8;
|
||||
--border: #334155;
|
||||
--shadow: 0 2px 8px rgba(0,0,0,0.45);
|
||||
--shadow-lg: 0 4px 16px rgba(0,0,0,0.6);
|
||||
color-scheme: dark;
|
||||
}
|
||||
[data-theme="dark"] body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
/* Bottom nav */
|
||||
[data-theme="dark"] .bottom-nav {
|
||||
box-shadow: 0 -2px 10px rgba(0,0,0,0.4);
|
||||
}
|
||||
/* Location tabs */
|
||||
[data-theme="dark"] .tab {
|
||||
background: var(--bg-card);
|
||||
color: var(--text-light);
|
||||
}
|
||||
[data-theme="dark"] .tab.active {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
/* Location selector (add/use modal) */
|
||||
[data-theme="dark"] .location-option {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
[data-theme="dark"] .location-option.selected {
|
||||
border-color: var(--primary);
|
||||
background: rgba(45,80,22,0.3);
|
||||
}
|
||||
/* Inputs & selects */
|
||||
[data-theme="dark"] .form-input,
|
||||
[data-theme="dark"] .form-control,
|
||||
[data-theme="dark"] input[type="text"],
|
||||
[data-theme="dark"] input[type="email"],
|
||||
[data-theme="dark"] input[type="password"],
|
||||
[data-theme="dark"] input[type="number"],
|
||||
[data-theme="dark"] textarea,
|
||||
[data-theme="dark"] select {
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
border-color: var(--border);
|
||||
}
|
||||
[data-theme="dark"] input::placeholder,
|
||||
[data-theme="dark"] textarea::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
/* Buttons */
|
||||
[data-theme="dark"] .btn-secondary,
|
||||
[data-theme="dark"] .btn-outline,
|
||||
[data-theme="dark"] .back-btn,
|
||||
[data-theme="dark"] .page-header-action-btn {
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
border-color: var(--border);
|
||||
}
|
||||
[data-theme="dark"] .btn-outline {
|
||||
color: var(--primary-light);
|
||||
border-color: var(--primary-light);
|
||||
}
|
||||
/* Inventory items */
|
||||
[data-theme="dark"] .inventory-item {
|
||||
background: var(--bg-card);
|
||||
}
|
||||
[data-theme="dark"] .inv-location-badge {
|
||||
background: rgba(45,80,22,0.35);
|
||||
color: #86efac;
|
||||
}
|
||||
/* Shopping items */
|
||||
[data-theme="dark"] .shopping-item {
|
||||
background: var(--bg-card) !important;
|
||||
}
|
||||
[data-theme="dark"] .shopping-item-tag-menu-container {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--border);
|
||||
}
|
||||
[data-theme="dark"] .shopping-item-tag-btn {
|
||||
background: #1e293b;
|
||||
color: var(--text-light);
|
||||
border-color: var(--border);
|
||||
}
|
||||
[data-theme="dark"] .badge-local-tag {
|
||||
background: #0c2a4e;
|
||||
color: #7dd3fc;
|
||||
}
|
||||
[data-theme="dark"] .badge-freq-med {
|
||||
background: #2e1a4a;
|
||||
color: #c4b5fd;
|
||||
}
|
||||
[data-theme="dark"] .badge-freq-low {
|
||||
background: #1e293b;
|
||||
color: #94a3b8;
|
||||
}
|
||||
/* Settings rows */
|
||||
[data-theme="dark"] .settings-row {
|
||||
border-color: var(--border);
|
||||
}
|
||||
[data-theme="dark"] .settings-label {
|
||||
color: var(--text);
|
||||
}
|
||||
[data-theme="dark"] .settings-hint {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
/* Toggle switch */
|
||||
[data-theme="dark"] .toggle-slider {
|
||||
background: #334155;
|
||||
}
|
||||
/* Search bar */
|
||||
[data-theme="dark"] .search-bar input {
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
border-color: var(--border);
|
||||
}
|
||||
/* Action modal location selector */
|
||||
[data-theme="dark"] .action-location-btn {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
/* Scan page */
|
||||
[data-theme="dark"] .scan-input-row {
|
||||
background: var(--bg-card);
|
||||
}
|
||||
[data-theme="dark"] .scan-result-item {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--border);
|
||||
}
|
||||
/* Quick access chips */
|
||||
[data-theme="dark"] .quick-access-chip {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
/* Scan recents */
|
||||
[data-theme="dark"] .scan-recent-chip {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--border);
|
||||
color: var(--text-light);
|
||||
}
|
||||
/* Alert banners */
|
||||
[data-theme="dark"] .alert-banner {
|
||||
background: #1e293b;
|
||||
border-color: #334155;
|
||||
}
|
||||
[data-theme="dark"] .alert-banner.banner-expiring {
|
||||
background: #1c1300;
|
||||
border-color: #78350f;
|
||||
}
|
||||
[data-theme="dark"] .alert-banner.banner-expired {
|
||||
background: #1f0808;
|
||||
border-color: #7f1d1d;
|
||||
}
|
||||
[data-theme="dark"] .alert-banner.banner-finished {
|
||||
background: #0f1f0f;
|
||||
border-color: #166534;
|
||||
}
|
||||
[data-theme="dark"] .alert-banner.banner-anomaly {
|
||||
background: #1a1a2e;
|
||||
border-color: #4c1d95;
|
||||
}
|
||||
/* Recipe dialog */
|
||||
[data-theme="dark"] .recipe-dialog-content {
|
||||
background: var(--bg-card);
|
||||
}
|
||||
[data-theme="dark"] .recipe-option-btn {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
[data-theme="dark"] .recipe-option-btn.active {
|
||||
background: rgba(45,80,22,0.4);
|
||||
border-color: var(--primary-light);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
/* Log rows */
|
||||
[data-theme="dark"] .log-item {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--border);
|
||||
}
|
||||
/* Dashboard stat cards */
|
||||
[data-theme="dark"] .stat-card {
|
||||
background: var(--bg-card);
|
||||
}
|
||||
/* Screensaver */
|
||||
[data-theme="dark"] .screensaver-overlay {
|
||||
background: #020617;
|
||||
}
|
||||
/* Charts / nutrition */
|
||||
[data-theme="dark"] .nutrition-chart-bg {
|
||||
background: var(--bg-card);
|
||||
}
|
||||
/* AW badges */
|
||||
[data-theme="dark"] .aw-badge-rate { background: #2e1a4a; color: #c4b5fd; border-color: #6d28d9; }
|
||||
[data-theme="dark"] .aw-badge-money { background: #1c1300; color: #fde047; border-color: #78350f; }
|
||||
[data-theme="dark"] .aw-badge-meals { background: #0f1f0f; color: #4ade80; border-color: #166534; }
|
||||
[data-theme="dark"] .aw-badge-co2 { background: #0c1f3a; color: #7dd3fc; border-color: #1e3a5f; }
|
||||
[data-theme="dark"] .aw-badge-wasted{ background: #1f0808; color: #fca5a5; border-color: #7f1d1d; }
|
||||
[data-theme="dark"] .aw-badge-better{ background: #0f1f0f; color: #4ade80; border-color: #166534; }
|
||||
/* Chat */
|
||||
[data-theme="dark"] .chat-input {
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
border-color: var(--border);
|
||||
}
|
||||
[data-theme="dark"] .chat-message.user {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
[data-theme="dark"] .chat-message.bot {
|
||||
background: var(--bg-card);
|
||||
}
|
||||
/* Smart shopping forecast */
|
||||
[data-theme="dark"] .smart-item {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--border);
|
||||
}
|
||||
[data-theme="dark"] .smart-filter-btn {
|
||||
background: var(--bg-card);
|
||||
color: var(--text-light);
|
||||
border-color: var(--border);
|
||||
}
|
||||
[data-theme="dark"] .smart-filter-btn.active {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
/* Offline banner */
|
||||
[data-theme="dark"] #offline-banner {
|
||||
background: #450a0a;
|
||||
border-color: #7f1d1d;
|
||||
}
|
||||
/* Setup wizard */
|
||||
[data-theme="dark"] .setup-content {
|
||||
background: var(--bg-card);
|
||||
}
|
||||
[data-theme="dark"] .setup-lang-btn {
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
border-color: var(--border);
|
||||
}
|
||||
[data-theme="dark"] .setup-lang-btn.selected {
|
||||
background: rgba(45,80,22,0.4);
|
||||
border-color: var(--primary-light);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
/* @media prefers-color-scheme: auto handled in JS */
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
+891
-117
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"2026-05": {
|
||||
"input_tokens": 4438300,
|
||||
"output_tokens": 1286760,
|
||||
"calls": 8374,
|
||||
"by_action": {},
|
||||
"by_model": {}
|
||||
}
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
{
|
||||
"226887def70e33ef73290ebfe75ed4d0": {
|
||||
"days": 7,
|
||||
"source": "ai",
|
||||
"name": "Polpa di pomodoro finissima",
|
||||
"location": "frigo",
|
||||
"ts": 1777444819
|
||||
},
|
||||
"0ed51c9496aa9edfe38caf41772f54ed": {
|
||||
"days": 7,
|
||||
"source": "rule",
|
||||
"name": "Latte di Montagna",
|
||||
"location": "frigo",
|
||||
"ts": 1777444820
|
||||
},
|
||||
"2d63d0216a75d46b465150e925d2e7ad": {
|
||||
"days": 30,
|
||||
"source": "rule",
|
||||
"name": "Burro",
|
||||
"location": "frigo",
|
||||
"ts": 1777444821
|
||||
},
|
||||
"9afdf35c4a256867ef47c32495349eb6": {
|
||||
"days": 5,
|
||||
"source": "rule",
|
||||
"name": "Yaourt Vanille",
|
||||
"location": "frigo",
|
||||
"ts": 1777480477
|
||||
},
|
||||
"584f57418733a1f2acd29fe2e8816129": {
|
||||
"days": 5,
|
||||
"source": "rule",
|
||||
"name": "Passata di pomodoro",
|
||||
"location": "frigo",
|
||||
"ts": 1778133522
|
||||
},
|
||||
"baeb7f2021b4bb91c368c9131a61f07c": {
|
||||
"days": 10,
|
||||
"source": "rule",
|
||||
"name": "Formaggio Monte Maria",
|
||||
"location": "frigo",
|
||||
"ts": 1778133523
|
||||
},
|
||||
"063f2d534407214786d039bb2bffbb93": {
|
||||
"days": 5,
|
||||
"source": "rule",
|
||||
"name": "Carote",
|
||||
"location": "frigo",
|
||||
"ts": 1778133524
|
||||
},
|
||||
"10a3d07c19bb1f889ebc9293862b4b36": {
|
||||
"days": 60,
|
||||
"source": "rule",
|
||||
"name": "Ovomaltine",
|
||||
"location": "dispensa",
|
||||
"ts": 1778419084
|
||||
},
|
||||
"0fbad7ccd8b6155c06aaa6b3c17a67d3": {
|
||||
"days": 365,
|
||||
"source": "rule",
|
||||
"name": "Linguine pasta di Gragnano Igp",
|
||||
"location": "dispensa",
|
||||
"ts": 1778419084
|
||||
},
|
||||
"b4a03e7356e7a0983b9c8af5f3cd8c57": {
|
||||
"days": 60,
|
||||
"source": "rule",
|
||||
"name": "Polpa di pomodoro finissima",
|
||||
"location": "dispensa",
|
||||
"ts": 1778419085
|
||||
},
|
||||
"b8334ff0febd5c0440c9b24c9f3132ed": {
|
||||
"days": 180,
|
||||
"source": "rule",
|
||||
"name": "Basilico tritato surgelato",
|
||||
"location": "freezer",
|
||||
"ts": 1778419086
|
||||
},
|
||||
"0cb14384d0ba763ccf12e079d6aa8d34": {
|
||||
"days": 60,
|
||||
"source": "rule",
|
||||
"name": "Salsa Pronta Ciliegini",
|
||||
"location": "dispensa",
|
||||
"ts": 1778419086
|
||||
},
|
||||
"188634f49edb8b014a46942ee9fad689": {
|
||||
"days": 180,
|
||||
"source": "rule",
|
||||
"name": "Farina Barilla",
|
||||
"location": "dispensa",
|
||||
"ts": 1778419204
|
||||
},
|
||||
"c8db359d8709c69a95f0e6f68216d220": {
|
||||
"days": 9999,
|
||||
"source": "rule",
|
||||
"name": "Bicarbonato",
|
||||
"location": "dispensa",
|
||||
"ts": 1778419205
|
||||
},
|
||||
"a6d16a09fd9a6bfbd0a915f05dd71780": {
|
||||
"days": 7,
|
||||
"source": "ai",
|
||||
"name": "Salsa Pronta Ciliegini",
|
||||
"location": "frigo",
|
||||
"ts": 1778419205
|
||||
},
|
||||
"4f8f1bb04a00e5fc62d7a9cfb21e1796": {
|
||||
"days": 365,
|
||||
"source": "rule",
|
||||
"name": "Riso Chicchi Ricchi Gran Risparmio",
|
||||
"location": "dispensa",
|
||||
"ts": 1778419206
|
||||
},
|
||||
"e116e4c11084a463f9aaac02e1749fe7": {
|
||||
"days": 90,
|
||||
"source": "rule",
|
||||
"name": "Salsa di soia",
|
||||
"location": "dispensa",
|
||||
"ts": 1778419207
|
||||
},
|
||||
"b1ad9afd4139b3f225b79af4dae256ce": {
|
||||
"days": 60,
|
||||
"source": "rule",
|
||||
"name": "Tè Al limone",
|
||||
"location": "dispensa",
|
||||
"ts": 1778419504
|
||||
},
|
||||
"7ff2b7d326dcba52a664cebbf12f78a2": {
|
||||
"days": 3,
|
||||
"source": "ai",
|
||||
"name": "Piselli fini 1\/2 vapore",
|
||||
"location": "frigo",
|
||||
"ts": 1778419505
|
||||
},
|
||||
"71062dc7ffd82b3ee4f40bad076a7c91": {
|
||||
"days": 60,
|
||||
"source": "rule",
|
||||
"name": "Cioccolato bianco",
|
||||
"location": "frigo",
|
||||
"ts": 1778419506
|
||||
},
|
||||
"38a0eaea422dfe970eba125494e75981": {
|
||||
"days": 180,
|
||||
"source": "rule",
|
||||
"name": "Zucca a pezzi",
|
||||
"location": "freezer",
|
||||
"ts": 1778419506
|
||||
},
|
||||
"cde21270e1cd50c431742e49117b225d": {
|
||||
"days": 7,
|
||||
"source": "rule",
|
||||
"name": "Pancetta Dolce",
|
||||
"location": "frigo",
|
||||
"ts": 1778419507
|
||||
},
|
||||
"9e4189bd3f8cb1121e7389967dd4f74c": {
|
||||
"days": 180,
|
||||
"source": "rule",
|
||||
"name": "Farina di grano tenero tipo rossa",
|
||||
"location": "dispensa",
|
||||
"ts": 1778427005
|
||||
},
|
||||
"e3472dd051ed13ae18fc96bbebedc1ba": {
|
||||
"days": 60,
|
||||
"source": "rule",
|
||||
"name": "Lievito di birra",
|
||||
"location": "dispensa",
|
||||
"ts": 1778427005
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId = "it.dadaloop.evershelf.kiosk"
|
||||
minSdk = 24
|
||||
targetSdk = 34
|
||||
versionCode = 15
|
||||
versionName = "1.7.14"
|
||||
versionCode = 17
|
||||
versionName = "1.7.16"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
||||
@@ -113,7 +113,9 @@ class KioskActivity : AppCompatActivity() {
|
||||
private const val KEY_SCREENSAVER = "screensaver_enabled"
|
||||
private const val KIOSK_DOWNLOAD_URL = "https://github.com/dadaloop82/EverShelf/releases/download/kiosk-latest/evershelf-kiosk.apk"
|
||||
private const val SPLASH_DURATION = 1500L
|
||||
private const val GITHUB_RELEASES_API = "https://api.github.com/repos/dadaloop82/EverShelf/releases/latest"
|
||||
// Use the kiosk-specific rolling release tag so version comparison is always
|
||||
// against the KIOSK version, not the webapp version (they diverge).
|
||||
private const val GITHUB_RELEASES_API = "https://api.github.com/repos/dadaloop82/EverShelf/releases/tags/kiosk-latest"
|
||||
// Keys for persisting a pending update across restarts
|
||||
private const val KEY_PENDING_UPDATE_VERSION = "pending_update_version"
|
||||
private const val KEY_PENDING_UPDATE_URL = "pending_update_url"
|
||||
@@ -627,10 +629,16 @@ class KioskActivity : AppCompatActivity() {
|
||||
packageManager.getPackageInfo(packageName, 0).versionName ?: ""
|
||||
} catch (_: Exception) { "" }
|
||||
|
||||
// Strip any non-numeric prefix so "kiosk-1.7.0", "v1.7.0", "kiosk-v1.7.1"
|
||||
// all normalise to "1.7.0" / "1.7.1" for comparison.
|
||||
// The kiosk-latest release uses a non-semver tag ("kiosk-latest").
|
||||
// Extract the actual kiosk version from the release body text.
|
||||
// Body format: "Alias automatico → kiosk-X.Y.Z" or just "kiosk-X.Y.Z".
|
||||
// Fall back to stripping the tag prefix if body parsing fails.
|
||||
val bodyText = json.optString("body", "")
|
||||
val norm = { v: String -> v.replace(Regex("^[^0-9]*"), "") }
|
||||
val isSemver = norm(latestTag).matches(Regex("\\d+\\.\\d+.*"))
|
||||
val remoteKioskVersion = Regex("""kiosk-v?(\d+\.\d+(?:\.\d+)?)""")
|
||||
.find(bodyText)?.groupValues?.get(1)
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: norm(latestTag)
|
||||
|
||||
// Compare semver: returns true if `remote` is strictly greater than `local`
|
||||
fun semverNewer(remote: String, local: String): Boolean {
|
||||
@@ -645,29 +653,31 @@ class KioskActivity : AppCompatActivity() {
|
||||
return false
|
||||
}
|
||||
|
||||
val isSemver = remoteKioskVersion.matches(Regex("\\d+\\.\\d+.*"))
|
||||
|
||||
// Get APK URL from assets; fall back to the hardcoded KIOSK_DOWNLOAD_URL
|
||||
val assets = json.optJSONArray("assets")
|
||||
var kioskApkUrl = ""
|
||||
if (assets != null) {
|
||||
for (i in 0 until assets.length()) {
|
||||
val a = assets.getJSONObject(i)
|
||||
val name = a.optString("name", "").lowercase()
|
||||
val url = a.optString("browser_download_url", "")
|
||||
if (name.contains("kiosk") && url.isNotEmpty()) kioskApkUrl = url
|
||||
val a = assets.getJSONObject(i)
|
||||
val url = a.optString("browser_download_url", "")
|
||||
if (url.endsWith(".apk", ignoreCase = true) && url.isNotEmpty()) {
|
||||
kioskApkUrl = url; break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (kioskApkUrl.isEmpty()) kioskApkUrl = KIOSK_DOWNLOAD_URL
|
||||
|
||||
// Only flag an update when the remote tag is parseable as semver AND
|
||||
// the remote version is strictly greater than the installed version.
|
||||
// Non-semver tags (e.g. "kiosk-latest", "rolling") cannot be compared
|
||||
// numerically → treat as "no update" to avoid false positives.
|
||||
val kioskNeedsUpdate = currentKiosk.isNotEmpty() &&
|
||||
isSemver && semverNewer(norm(latestTag), norm(currentKiosk))
|
||||
// Only flag an update when the remote version is parseable as semver AND
|
||||
// strictly greater than the installed version.
|
||||
val kioskNeedsUpdate = currentKiosk.isNotEmpty() && isSemver &&
|
||||
semverNewer(remoteKioskVersion, currentKiosk)
|
||||
|
||||
val result = JSONObject()
|
||||
.put("has_update", kioskNeedsUpdate)
|
||||
.put("current", currentKiosk)
|
||||
.put("latest", latestTag)
|
||||
.put("latest", remoteKioskVersion)
|
||||
.put("apk_url", kioskApkUrl)
|
||||
|
||||
notifyJs(result)
|
||||
@@ -680,12 +690,11 @@ class KioskActivity : AppCompatActivity() {
|
||||
|
||||
// Persist the pending update so the banner reappears after a crash/restart
|
||||
prefs.edit()
|
||||
.putString(KEY_PENDING_UPDATE_VERSION, latestTag)
|
||||
.putString(KEY_PENDING_UPDATE_VERSION, remoteKioskVersion)
|
||||
.putString(KEY_PENDING_UPDATE_URL, kioskApkUrl)
|
||||
.apply()
|
||||
|
||||
val label = if (isSemver) "$currentKiosk → $latestTag" else latestTag
|
||||
runOnUiThread { showNativeUpdateBanner("🔄 Kiosk $label", kioskApkUrl) }
|
||||
runOnUiThread { showNativeUpdateBanner("🔄 Kiosk $currentKiosk → $remoteKioskVersion", kioskApkUrl) }
|
||||
} catch (e: Exception) {
|
||||
notifyJs(JSONObject().put("has_update", false).put("error", e.message ?: "network error"))
|
||||
}
|
||||
@@ -802,6 +811,52 @@ class KioskActivity : AppCompatActivity() {
|
||||
file.delete()
|
||||
return
|
||||
}
|
||||
// ── Pre-install validation via PackageManager ──────────────────────
|
||||
// This catches version-downgrade or same-version attempts before PackageInstaller
|
||||
// gets them (which would silently fail with STATUS_FAILURE=1 on many OEMs).
|
||||
@Suppress("DEPRECATION")
|
||||
val apkInfo = try { packageManager.getPackageArchiveInfo(file.absolutePath, 0) } catch (_: Exception) { null }
|
||||
if (apkInfo != null) {
|
||||
// Wrong package: would always fail with STATUS_FAILURE=1
|
||||
if (apkInfo.packageName != packageName) {
|
||||
val detail = "APK package=${apkInfo.packageName}, expected=$packageName"
|
||||
setInstallUI("\u274C", "APK non valido", detail, 0xFFf87171.toInt(), btnEnabled = true, progress = -2)
|
||||
runOnUiThread { activeInstallBtn?.text = getString(R.string.install_btn_retry) }
|
||||
ErrorReporter.reportMessage("install_wrong_package", detail, mapOf("apk_pkg" to apkInfo.packageName, "expected" to packageName), forceReport = true)
|
||||
file.delete()
|
||||
return
|
||||
}
|
||||
// Version downgrade or same versionCode: Android rejects it
|
||||
@Suppress("DEPRECATION")
|
||||
val apkVc: Long = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||
apkInfo.longVersionCode
|
||||
else
|
||||
apkInfo.versionCode.toLong()
|
||||
val installedVc: Long = try {
|
||||
@Suppress("DEPRECATION")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||
packageManager.getPackageInfo(packageName, 0).longVersionCode
|
||||
else
|
||||
packageManager.getPackageInfo(packageName, 0).versionCode.toLong()
|
||||
} catch (_: Exception) { -1L }
|
||||
|
||||
if (installedVc >= 0 && apkVc <= installedVc) {
|
||||
// Same or older version — no real update, dismiss banner silently
|
||||
runOnUiThread {
|
||||
updateBanner.visibility = View.GONE
|
||||
bannerProgressBar.visibility = View.GONE
|
||||
prefs.edit().remove(KEY_PENDING_UPDATE_VERSION).remove(KEY_PENDING_UPDATE_URL).apply()
|
||||
}
|
||||
ErrorReporter.reportMessage(
|
||||
"install_no_upgrade",
|
||||
"APK versionCode=$apkVc (${apkInfo.versionName}) ≤ installed=$installedVc — not an upgrade",
|
||||
mapOf("apk_vc" to apkVc, "apk_ver" to (apkInfo.versionName ?: ""), "installed_vc" to installedVc),
|
||||
forceReport = true
|
||||
)
|
||||
file.delete()
|
||||
return
|
||||
}
|
||||
}
|
||||
// Only kiosk self-update is handled; gateway is now integrated
|
||||
val targetPkg = packageName
|
||||
installWithPackageInstaller(file, targetPkg)
|
||||
|
||||
@@ -58,8 +58,10 @@ import javax.net.ssl.X509TrustManager
|
||||
* 2 — Permissions rationale + grant
|
||||
* 3 — Server URL + auto-discovery + connection test
|
||||
* 4 — Smart scale question → gateway info + install
|
||||
* 5 — Screensaver toggle (NEW)
|
||||
* 6 — Done
|
||||
* 5 — Features (screensaver / prices / meal-plan / zero-waste)
|
||||
* 6 — Gemini AI key (optional, auto-skipped if already set)
|
||||
* 7 — Bring! credentials (optional, auto-skipped if already set)
|
||||
* 8 — Done
|
||||
*/
|
||||
class SetupActivity : AppCompatActivity() {
|
||||
|
||||
@@ -73,6 +75,8 @@ class SetupActivity : AppCompatActivity() {
|
||||
private lateinit var stepServer: LinearLayout
|
||||
private lateinit var stepScale: LinearLayout
|
||||
private lateinit var stepScreensaver: LinearLayout
|
||||
private lateinit var stepGemini: LinearLayout
|
||||
private lateinit var stepBring: LinearLayout
|
||||
private lateinit var stepDone: LinearLayout
|
||||
|
||||
// Progress dots
|
||||
@@ -110,6 +114,14 @@ class SetupActivity : AppCompatActivity() {
|
||||
|
||||
// Screensaver step
|
||||
private lateinit var setupSwitchScreensaver: SwitchMaterial
|
||||
private lateinit var setupSwitchPrices: SwitchMaterial
|
||||
private lateinit var setupSwitchMealPlan: SwitchMaterial
|
||||
private lateinit var setupSwitchZeroWaste: SwitchMaterial
|
||||
|
||||
// Gemini + Bring steps
|
||||
private lateinit var setupGeminiKeyEdit: EditText
|
||||
private lateinit var setupBringEmailEdit: EditText
|
||||
private lateinit var setupBringPasswordEdit: EditText
|
||||
|
||||
// Done step
|
||||
private lateinit var summaryText: TextView
|
||||
@@ -128,6 +140,12 @@ class SetupActivity : AppCompatActivity() {
|
||||
private const val KEY_HAS_SCALE = "has_scale"
|
||||
private const val KEY_LANGUAGE = "kiosk_language"
|
||||
private const val KEY_SCREENSAVER = "screensaver_enabled"
|
||||
private const val KEY_PRICE_ENABLED = "price_enabled"
|
||||
private const val KEY_MEAL_PLAN = "meal_plan_enabled"
|
||||
private const val KEY_ZEROWASTE_TIPS = "zerowaste_tips_enabled"
|
||||
private const val KEY_GEMINI_KEY = "gemini_api_key"
|
||||
private const val KEY_BRING_EMAIL = "bring_email"
|
||||
private const val KEY_BRING_PASSWORD = "bring_password"
|
||||
private const val PERMISSION_REQUEST_CODE = 2004
|
||||
private const val BLE_PERMISSION_REQUEST = 2006
|
||||
|
||||
@@ -178,8 +196,11 @@ class SetupActivity : AppCompatActivity() {
|
||||
|
||||
override fun onBackPressed() {
|
||||
when (currentStep) {
|
||||
0 -> confirmExit()
|
||||
1 -> showStep(0) // back to language
|
||||
0 -> confirmExit()
|
||||
1 -> showStep(0) // back to language
|
||||
8 -> showStep(7) // done → bring
|
||||
7 -> showStep(6) // bring → gemini
|
||||
6 -> showStep(5) // gemini → features
|
||||
else -> showStep(currentStep - 1)
|
||||
}
|
||||
}
|
||||
@@ -215,8 +236,18 @@ class SetupActivity : AppCompatActivity() {
|
||||
stepServer = findViewById(R.id.stepServer)
|
||||
stepScale = findViewById(R.id.stepScale)
|
||||
stepScreensaver = findViewById(R.id.stepScreensaver)
|
||||
stepGemini = findViewById(R.id.stepGemini)
|
||||
stepBring = findViewById(R.id.stepBring)
|
||||
stepDone = findViewById(R.id.stepDone)
|
||||
|
||||
// Gemini + Bring fields
|
||||
setupGeminiKeyEdit = findViewById(R.id.setupGeminiKeyEdit)
|
||||
setupBringEmailEdit = findViewById(R.id.setupBringEmailEdit)
|
||||
setupBringPasswordEdit = findViewById(R.id.setupBringPasswordEdit)
|
||||
// Pre-fill from saved prefs
|
||||
(prefs.getString(KEY_GEMINI_KEY, "") ?: "").takeIf { it.isNotEmpty() }?.let { setupGeminiKeyEdit.setText(it) }
|
||||
(prefs.getString(KEY_BRING_EMAIL, "") ?: "").takeIf { it.isNotEmpty() }?.let { setupBringEmailEdit.setText(it) }
|
||||
|
||||
// Server step
|
||||
urlEdit = findViewById(R.id.setupUrlEdit)
|
||||
urlStatus = findViewById(R.id.setupUrlStatus)
|
||||
@@ -238,10 +269,17 @@ class SetupActivity : AppCompatActivity() {
|
||||
tvTestWeight = findViewById(R.id.tvTestWeight)
|
||||
testWeightBox = findViewById(R.id.testWeightBox)
|
||||
|
||||
// Screensaver step
|
||||
// Features step — bind all four toggles
|
||||
setupSwitchScreensaver = findViewById(R.id.setupSwitchScreensaver)
|
||||
// Pre-fill saved screensaver pref
|
||||
setupSwitchScreensaver.isChecked = prefs.getBoolean(KEY_SCREENSAVER, false)
|
||||
setupSwitchPrices = findViewById(R.id.setupSwitchPrices)
|
||||
setupSwitchMealPlan = findViewById(R.id.setupSwitchMealPlan)
|
||||
setupSwitchZeroWaste = findViewById(R.id.setupSwitchZeroWaste)
|
||||
// Pre-fill from saved prefs only if each key was previously configured
|
||||
// ("se non sono impostati, chiedi!" — fresh install → all start at false)
|
||||
setupSwitchScreensaver.isChecked = if (prefs.contains(KEY_SCREENSAVER)) prefs.getBoolean(KEY_SCREENSAVER, false) else false
|
||||
setupSwitchPrices.isChecked = if (prefs.contains(KEY_PRICE_ENABLED)) prefs.getBoolean(KEY_PRICE_ENABLED, false) else false
|
||||
setupSwitchMealPlan.isChecked = if (prefs.contains(KEY_MEAL_PLAN)) prefs.getBoolean(KEY_MEAL_PLAN, false) else false
|
||||
setupSwitchZeroWaste.isChecked = if (prefs.contains(KEY_ZEROWASTE_TIPS)) prefs.getBoolean(KEY_ZEROWASTE_TIPS, false) else false
|
||||
|
||||
// Done step
|
||||
summaryText = findViewById(R.id.setupSummaryText)
|
||||
@@ -260,6 +298,8 @@ class SetupActivity : AppCompatActivity() {
|
||||
findViewById<MaterialButton>(R.id.btnLangIt).setOnClickListener { selectLanguage("it") }
|
||||
findViewById<MaterialButton>(R.id.btnLangEn).setOnClickListener { selectLanguage("en") }
|
||||
findViewById<MaterialButton>(R.id.btnLangDe).setOnClickListener { selectLanguage("de") }
|
||||
findViewById<MaterialButton>(R.id.btnLangEs).setOnClickListener { selectLanguage("es") }
|
||||
findViewById<MaterialButton>(R.id.btnLangFr).setOnClickListener { selectLanguage("fr") }
|
||||
|
||||
// ── Welcome ──────────────────────────────────────────────────────
|
||||
findViewById<MaterialButton>(R.id.btnSetupExit).setOnClickListener { confirmExit() }
|
||||
@@ -362,10 +402,10 @@ class SetupActivity : AppCompatActivity() {
|
||||
bleSetupCard.visibility = View.VISIBLE
|
||||
tvSelectedScale.text = ""
|
||||
tvSelectedScale.visibility = View.GONE
|
||||
tvScanStatus.text = "Bilancia non confermata. Riprova la scansione."
|
||||
tvScanStatus.text = getString(R.string.ble_not_confirmed)
|
||||
tvScanStatus.setTextColor(0xFFfbbf24.toInt())
|
||||
btnScanBle.isEnabled = true
|
||||
btnScanBle.text = "🔍 Cerca bilancia"
|
||||
btnScanBle.text = getString(R.string.ble_scan_again)
|
||||
findViewById<MaterialButton>(R.id.btnScaleNext).isEnabled = false
|
||||
}
|
||||
findViewById<MaterialButton>(R.id.btnTestSkip).setOnClickListener {
|
||||
@@ -381,13 +421,38 @@ class SetupActivity : AppCompatActivity() {
|
||||
findViewById<MaterialButton>(R.id.btnScaleNext).isEnabled = true
|
||||
}
|
||||
|
||||
// ── Screensaver ───────────────────────────────────────────────────
|
||||
// ── Features step (screensaver / prices / meal plan / zero-waste) ────
|
||||
findViewById<MaterialButton>(R.id.btnScreensaverBack).setOnClickListener { showStep(4) }
|
||||
findViewById<MaterialButton>(R.id.btnScreensaverNext).setOnClickListener {
|
||||
prefs.edit().putBoolean(KEY_SCREENSAVER, setupSwitchScreensaver.isChecked).apply()
|
||||
prefs.edit()
|
||||
.putBoolean(KEY_SCREENSAVER, setupSwitchScreensaver.isChecked)
|
||||
.putBoolean(KEY_PRICE_ENABLED, setupSwitchPrices.isChecked)
|
||||
.putBoolean(KEY_MEAL_PLAN, setupSwitchMealPlan.isChecked)
|
||||
.putBoolean(KEY_ZEROWASTE_TIPS, setupSwitchZeroWaste.isChecked)
|
||||
.apply()
|
||||
showStep(6)
|
||||
}
|
||||
|
||||
// ── Gemini step ───────────────────────────────────────────────────
|
||||
findViewById<MaterialButton>(R.id.btnGeminiBack).setOnClickListener { showStep(5) }
|
||||
findViewById<MaterialButton>(R.id.btnGeminiSkip).setOnClickListener { showStep(7) }
|
||||
findViewById<MaterialButton>(R.id.btnGeminiNext).setOnClickListener {
|
||||
val key = setupGeminiKeyEdit.text.toString().trim()
|
||||
if (key.isNotEmpty()) prefs.edit().putString(KEY_GEMINI_KEY, key).apply()
|
||||
showStep(7)
|
||||
}
|
||||
|
||||
// ── Bring step ────────────────────────────────────────────────────
|
||||
findViewById<MaterialButton>(R.id.btnBringBack).setOnClickListener { showStep(6) }
|
||||
findViewById<MaterialButton>(R.id.btnBringSkip).setOnClickListener { showStep(8) }
|
||||
findViewById<MaterialButton>(R.id.btnBringNext).setOnClickListener {
|
||||
val email = setupBringEmailEdit.text.toString().trim()
|
||||
val pass = setupBringPasswordEdit.text.toString().trim()
|
||||
if (email.isNotEmpty()) prefs.edit().putString(KEY_BRING_EMAIL, email).apply()
|
||||
if (pass.isNotEmpty()) prefs.edit().putString(KEY_BRING_PASSWORD, pass).apply()
|
||||
showStep(8)
|
||||
}
|
||||
|
||||
// ── Done ──────────────────────────────────────────────────────────
|
||||
findViewById<MaterialButton>(R.id.btnLaunch).setOnClickListener { finishSetup() }
|
||||
}
|
||||
@@ -403,20 +468,27 @@ class SetupActivity : AppCompatActivity() {
|
||||
|
||||
private fun highlightSelectedLang() {
|
||||
val saved = prefs.getString(KEY_LANGUAGE, null) ?: return
|
||||
val (btnIt, btnEn, btnDe) = Triple(
|
||||
findViewById<MaterialButton>(R.id.btnLangIt),
|
||||
findViewById<MaterialButton>(R.id.btnLangEn),
|
||||
findViewById<MaterialButton>(R.id.btnLangDe)
|
||||
)
|
||||
val btnIt = findViewById<MaterialButton>(R.id.btnLangIt)
|
||||
val btnEn = findViewById<MaterialButton>(R.id.btnLangEn)
|
||||
val btnDe = findViewById<MaterialButton>(R.id.btnLangDe)
|
||||
val btnEs = findViewById<MaterialButton>(R.id.btnLangEs)
|
||||
val btnFr = findViewById<MaterialButton>(R.id.btnLangFr)
|
||||
// Add checkmark to selected
|
||||
btnIt.text = if (saved == "it") "✅ 🇮🇹 Italiano" else "🇮🇹 Italiano"
|
||||
btnEn.text = if (saved == "en") "✅ 🇬🇧 English" else "🇬🇧 English"
|
||||
btnDe.text = if (saved == "de") "✅ 🇩🇪 Deutsch" else "🇩🇪 Deutsch"
|
||||
btnEs.text = if (saved == "es") "✅ 🇪🇸 Español" else "🇪🇸 Español"
|
||||
btnFr.text = if (saved == "fr") "✅ 🇫🇷 Français" else "🇫🇷 Français"
|
||||
}
|
||||
|
||||
// ── Step navigation ───────────────────────────────────────────────────
|
||||
|
||||
private fun showStep(step: Int) {
|
||||
// Auto-skip Gemini step if already configured
|
||||
if (step == 6 && !(prefs.getString(KEY_GEMINI_KEY, "") ?: "").isNullOrEmpty()) { showStep(7); return }
|
||||
// Auto-skip Bring step if already configured
|
||||
if (step == 7 && !(prefs.getString(KEY_BRING_EMAIL, "") ?: "").isNullOrEmpty()) { showStep(8); return }
|
||||
|
||||
currentStep = step
|
||||
stepLanguage.visibility = if (step == 0) View.VISIBLE else View.GONE
|
||||
stepWelcome.visibility = if (step == 1) View.VISIBLE else View.GONE
|
||||
@@ -424,7 +496,9 @@ class SetupActivity : AppCompatActivity() {
|
||||
stepServer.visibility = if (step == 3) View.VISIBLE else View.GONE
|
||||
stepScale.visibility = if (step == 4) View.VISIBLE else View.GONE
|
||||
stepScreensaver.visibility = if (step == 5) View.VISIBLE else View.GONE
|
||||
stepDone.visibility = if (step == 6) View.VISIBLE else View.GONE
|
||||
stepGemini.visibility = if (step == 6) View.VISIBLE else View.GONE
|
||||
stepBring.visibility = if (step == 7) View.VISIBLE else View.GONE
|
||||
stepDone.visibility = if (step == 8) View.VISIBLE else View.GONE
|
||||
|
||||
updateProgressDots()
|
||||
|
||||
@@ -460,7 +534,7 @@ class SetupActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
// Build summary when entering done step
|
||||
if (step == 6) buildSummary()
|
||||
if (step == 8) buildSummary()
|
||||
|
||||
// Cancel auto-discover when leaving server step
|
||||
if (step != 3) discoverCancelled.set(true)
|
||||
@@ -471,11 +545,11 @@ class SetupActivity : AppCompatActivity() {
|
||||
|
||||
private fun updateProgressDots() {
|
||||
progressDots.removeAllViews()
|
||||
// Show 5 dots for steps 1-5; step 0 (language) and step 6 (done) have no dots
|
||||
if (currentStep == 0 || currentStep == 6) return
|
||||
val active = currentStep // 1..5
|
||||
// Show 7 dots for steps 1-7; step 0 (language) and step 8 (done) have no dots
|
||||
if (currentStep == 0 || currentStep == 8) return
|
||||
val active = currentStep // 1..7
|
||||
val density = resources.displayMetrics.density
|
||||
for (i in 1..5) {
|
||||
for (i in 1..7) {
|
||||
val dot = View(this)
|
||||
val sizeDp = if (i == active) 10 else 7
|
||||
val px = (sizeDp * density).toInt()
|
||||
@@ -819,7 +893,7 @@ class SetupActivity : AppCompatActivity() {
|
||||
}
|
||||
discoveredDevices.clear()
|
||||
deviceAdapter?.notifyDataSetChanged()
|
||||
tvScanStatus.text = "🔍 Scansione in corso…"
|
||||
tvScanStatus.text = getString(R.string.ble_scanning)
|
||||
tvScanStatus.setTextColor(0xFF94a3b8.toInt())
|
||||
btnScanBle.isEnabled = false
|
||||
mgr.startScan()
|
||||
@@ -832,7 +906,7 @@ class SetupActivity : AppCompatActivity() {
|
||||
tvSelectedScale.text = "✅ ${info.name}"
|
||||
tvSelectedScale.visibility = View.VISIBLE
|
||||
btnScanBle.isEnabled = true
|
||||
btnScanBle.text = "🔄 Scansiona di nuovo"
|
||||
btnScanBle.text = getString(R.string.ble_scan_again)
|
||||
// Start connection test
|
||||
startScaleTest(info)
|
||||
}
|
||||
@@ -845,7 +919,7 @@ class SetupActivity : AppCompatActivity() {
|
||||
scaleTestCard.visibility = View.VISIBLE
|
||||
testWeightBox.visibility = View.GONE
|
||||
step3NextButtons.visibility = View.GONE
|
||||
tvTestStatus.text = "🔗 Connessione a ${info.name}…"
|
||||
tvTestStatus.text = getString(R.string.ble_connecting_to).format(info.name)
|
||||
tvTestStatus.setTextColor(0xFF94a3b8.toInt())
|
||||
tvTestWeight.text = "— g"
|
||||
// Disable confirm/retry until we have data
|
||||
@@ -869,19 +943,19 @@ class SetupActivity : AppCompatActivity() {
|
||||
}
|
||||
override fun onConnecting(device: BluetoothDevice) {
|
||||
if (!isInTestMode) return
|
||||
tvTestStatus.text = "🔗 Connessione in corso…"
|
||||
tvTestStatus.text = getString(R.string.ble_connecting)
|
||||
tvTestStatus.setTextColor(0xFF94a3b8.toInt())
|
||||
}
|
||||
override fun onConnected(deviceName: String) {
|
||||
if (!isInTestMode) return
|
||||
tvTestStatus.text = "⚖️ Connesso! Posiziona un oggetto sulla bilancia…"
|
||||
tvTestStatus.text = getString(R.string.ble_connected)
|
||||
tvTestStatus.setTextColor(0xFF34d399.toInt())
|
||||
testWeightBox.visibility = View.VISIBLE
|
||||
findViewById<MaterialButton>(R.id.btnTestRetry).isEnabled = true
|
||||
}
|
||||
override fun onDisconnected() {
|
||||
if (!isInTestMode) return
|
||||
tvTestStatus.text = "⚠️ Connessione persa. Riprova."
|
||||
tvTestStatus.text = getString(R.string.ble_disconnected)
|
||||
tvTestStatus.setTextColor(0xFFfbbf24.toInt())
|
||||
testWeightBox.visibility = View.GONE
|
||||
testHasWeight = false
|
||||
@@ -896,7 +970,7 @@ class SetupActivity : AppCompatActivity() {
|
||||
"%g ${reading.unit}".format(reading.value)
|
||||
tvTestWeight.text = display
|
||||
testWeightBox.visibility = View.VISIBLE
|
||||
tvTestStatus.text = "Peso ricevuto — coincide con quello sulla bilancia?"
|
||||
tvTestStatus.text = getString(R.string.ble_weight_received)
|
||||
tvTestStatus.setTextColor(0xFF94a3b8.toInt())
|
||||
findViewById<MaterialButton>(R.id.btnTestConfirm).isEnabled = true
|
||||
findViewById<MaterialButton>(R.id.btnTestRetry).isEnabled = true
|
||||
@@ -918,10 +992,10 @@ class SetupActivity : AppCompatActivity() {
|
||||
override fun onScanStopped() {
|
||||
btnScanBle.isEnabled = true
|
||||
if (discoveredDevices.isEmpty()) {
|
||||
tvScanStatus.text = "Nessuna bilancia trovata. Assicurati che sia accesa e vicina, poi riprova."
|
||||
tvScanStatus.text = getString(R.string.ble_no_scale_found)
|
||||
tvScanStatus.setTextColor(0xFFfbbf24.toInt())
|
||||
} else {
|
||||
tvScanStatus.text = "Seleziona la tua bilancia dall'elenco."
|
||||
tvScanStatus.text = getString(R.string.ble_select_from_list)
|
||||
tvScanStatus.setTextColor(0xFF94a3b8.toInt())
|
||||
}
|
||||
}
|
||||
@@ -971,22 +1045,32 @@ class SetupActivity : AppCompatActivity() {
|
||||
// ── Summary / Finish ─────────────────────────────────────────────────
|
||||
|
||||
private fun buildSummary() {
|
||||
val url = prefs.getString(KEY_URL, "") ?: ""
|
||||
val hasScale = prefs.getBoolean(KEY_HAS_SCALE, false)
|
||||
val screensOn = setupSwitchScreensaver.isChecked
|
||||
val scaleName = bleManager?.getSavedDeviceName()
|
||||
val scaleOk = hasScale && scaleName != null
|
||||
val lang = prefs.getString(KEY_LANGUAGE, "it") ?: "it"
|
||||
val langLabel = when (lang) { "en" -> "English 🇬🇧"; "de" -> "Deutsch 🇩🇪"; else -> "Italiano 🇮🇹" }
|
||||
val url = prefs.getString(KEY_URL, "") ?: ""
|
||||
val hasScale = prefs.getBoolean(KEY_HAS_SCALE, false)
|
||||
val screensOn = setupSwitchScreensaver.isChecked
|
||||
val pricesOn = setupSwitchPrices.isChecked
|
||||
val mealPlanOn = setupSwitchMealPlan.isChecked
|
||||
val zeroWasteOn = setupSwitchZeroWaste.isChecked
|
||||
val scaleName = bleManager?.getSavedDeviceName()
|
||||
val scaleOk = hasScale && scaleName != null
|
||||
val lang = prefs.getString(KEY_LANGUAGE, "it") ?: "it"
|
||||
val langLabel = when (lang) { "en" -> "English 🇬🇧"; "de" -> "Deutsch 🇩🇪"; "es" -> "Español 🇪🇸"; "fr" -> "Français 🇫🇷"; else -> "Italiano 🇮🇹" }
|
||||
val sb = StringBuilder()
|
||||
sb.appendLine("🌐 ${getString(R.string.summary_lang)}: $langLabel")
|
||||
if (url.isNotEmpty()) sb.appendLine("🖥️ Server: $url")
|
||||
sb.appendLine(when {
|
||||
scaleOk -> "✅ Bilancia: $scaleName"
|
||||
hasScale -> "⚠️ Bilancia: da configurare"
|
||||
scaleOk -> getString(R.string.summary_scale_ok).format(scaleName)
|
||||
hasScale -> "⚠️ ${getString(R.string.summary_scale_warn)}"
|
||||
else -> "⏭ ${getString(R.string.summary_scale_skip)}"
|
||||
})
|
||||
sb.appendLine(if (screensOn) "🌙 ${getString(R.string.summary_screensaver_on)}" else "💡 ${getString(R.string.summary_screensaver_off)}")
|
||||
sb.appendLine(if (screensOn) getString(R.string.summary_screensaver_on) else getString(R.string.summary_screensaver_off))
|
||||
if (pricesOn) sb.appendLine(getString(R.string.summary_prices_on))
|
||||
if (mealPlanOn) sb.appendLine(getString(R.string.summary_mealplan_on))
|
||||
if (zeroWasteOn) sb.appendLine(getString(R.string.summary_zerowaste_on))
|
||||
val geminiSet = !(prefs.getString(KEY_GEMINI_KEY, "") ?: "").isNullOrEmpty()
|
||||
val bringSet = !(prefs.getString(KEY_BRING_EMAIL, "") ?: "").isNullOrEmpty()
|
||||
sb.appendLine(if (geminiSet) getString(R.string.summary_gemini_set) else getString(R.string.summary_gemini_skip))
|
||||
sb.appendLine(if (bringSet) getString(R.string.summary_bring_set) else getString(R.string.summary_bring_skip))
|
||||
summaryText.text = sb.toString().trimEnd()
|
||||
}
|
||||
|
||||
@@ -994,19 +1078,29 @@ class SetupActivity : AppCompatActivity() {
|
||||
prefs.edit().putBoolean(KEY_SETUP_COMPLETE, true).apply()
|
||||
val baseUrl = (prefs.getString(KEY_URL, "") ?: "").trimEnd('/')
|
||||
if (baseUrl.isNotEmpty()) {
|
||||
val hasScale = prefs.getBoolean(KEY_HAS_SCALE, false) && (bleManager?.getSavedDeviceAddress() != null)
|
||||
val screensaver = prefs.getBoolean(KEY_SCREENSAVER, false)
|
||||
val hasScale = prefs.getBoolean(KEY_HAS_SCALE, false) && (bleManager?.getSavedDeviceAddress() != null)
|
||||
val screensaver = prefs.getBoolean(KEY_SCREENSAVER, false)
|
||||
val priceEnabled = prefs.getBoolean(KEY_PRICE_ENABLED, false)
|
||||
val mealPlan = prefs.getBoolean(KEY_MEAL_PLAN, false)
|
||||
val zeroWaste = prefs.getBoolean(KEY_ZEROWASTE_TIPS, false)
|
||||
Thread {
|
||||
try {
|
||||
val url = "$baseUrl/api/index.php?action=save_settings"
|
||||
val geminiKey = prefs.getString(KEY_GEMINI_KEY, "") ?: ""
|
||||
val bringEmail = prefs.getString(KEY_BRING_EMAIL, "") ?: ""
|
||||
val bringPassword = prefs.getString(KEY_BRING_PASSWORD, "") ?: ""
|
||||
val body = buildString {
|
||||
append("{\"screensaver_enabled\":$screensaver")
|
||||
append(",\"price_enabled\":$priceEnabled")
|
||||
append(",\"meal_plan_enabled\":$mealPlan")
|
||||
append(",\"zerowaste_tips_enabled\":$zeroWaste")
|
||||
if (hasScale) {
|
||||
// Use the tablet's actual LAN IP so the EverShelf server
|
||||
// (potentially on a different machine) can reach the gateway.
|
||||
val lanIp = getDeviceLanIp() ?: "127.0.0.1"
|
||||
append(",\"scale_enabled\":true,\"scale_gateway_url\":\"ws://$lanIp:8765\"")
|
||||
}
|
||||
if (geminiKey.isNotEmpty()) append(",\"gemini_api_key\":\"${geminiKey.replace("\"", "\\\"\")}\"")
|
||||
if (bringEmail.isNotEmpty()) append(",\"bring_email\":\"${bringEmail.replace("\"", "\\\"\")}\"")
|
||||
if (bringPassword.isNotEmpty()) append(",\"bring_password\":\"${bringPassword.replace("\"", "\\\"\")}\"")
|
||||
append("}")
|
||||
}
|
||||
val conn = (java.net.URL(url).openConnection() as java.net.HttpURLConnection).apply {
|
||||
|
||||
@@ -43,17 +43,18 @@
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- Settings gear (shown after setup, over WebView) — top-right corner to avoid overlapping modals -->
|
||||
<!-- Settings gear (shown after setup, over WebView) — bottom-right corner so it never
|
||||
overlaps the webapp header buttons (e.g. the 📷 scan button at top-right) -->
|
||||
<ImageButton
|
||||
android:id="@+id/btnSettings"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginBottom="80dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@android:drawable/ic_menu_manage"
|
||||
android:alpha="0.12"
|
||||
android:alpha="0.28"
|
||||
android:contentDescription="Settings"
|
||||
android:scaleType="centerInside"
|
||||
android:visibility="gone" />
|
||||
|
||||
@@ -78,11 +78,11 @@
|
||||
android:layout_marginBottom="24dp"
|
||||
android:contentDescription="EverShelf" />
|
||||
|
||||
<!-- Title shown in all 3 languages so it's always readable -->
|
||||
<!-- Title shown in all 5 languages so it's always readable -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Scegli la lingua\nChoose your language\nSprache wählen"
|
||||
android:text="Scegli la lingua · Choose your language\nSprache wählen · Elige el idioma\nChoisissez votre langue"
|
||||
android:textColor="#f1f5f9"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold"
|
||||
@@ -117,7 +117,27 @@
|
||||
android:text="🇩🇪 Deutsch"
|
||||
android:textSize="18sp"
|
||||
android:textAllCaps="false"
|
||||
android:backgroundTint="#b91c1c" />
|
||||
android:backgroundTint="#b91c1c"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnLangEs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:text="🇪🇸 Español"
|
||||
android:textSize="18sp"
|
||||
android:textAllCaps="false"
|
||||
android:backgroundTint="#c2410c"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnLangFr"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:text="🇫🇷 Français"
|
||||
android:textSize="18sp"
|
||||
android:textAllCaps="false"
|
||||
android:backgroundTint="#1d4ed8" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -1050,7 +1070,7 @@
|
||||
</LinearLayout>
|
||||
|
||||
<!-- ════════════════════════════════════════════
|
||||
STEP 5 — Screensaver
|
||||
STEP 5 — Features
|
||||
════════════════════════════════════════════ -->
|
||||
<LinearLayout
|
||||
android:id="@+id/stepScreensaver"
|
||||
@@ -1063,66 +1083,58 @@
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🌙"
|
||||
android:text="⚡"
|
||||
android:textSize="52sp"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/tvScreensaverTitle"
|
||||
android:text="@string/setup_screensaver_title"
|
||||
android:text="@string/setup_features_title"
|
||||
android:textColor="#f1f5f9"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvScreensaverDesc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Dopo 5 minuti di inattività mostra un overlay con l'orologio e informazioni utili (statistiche, piano pasti). Lo schermo rimane SEMPRE acceso — questa opzione riguarda solo l'overlay visivo in-app."
|
||||
android:text="@string/setup_features_desc"
|
||||
android:textColor="#94a3b8"
|
||||
android:textSize="15sp"
|
||||
android:gravity="center"
|
||||
android:lineSpacingExtra="4dp"
|
||||
android:layout_marginBottom="28dp" />
|
||||
android:layout_marginBottom="20dp" />
|
||||
|
||||
<!-- Toggle card -->
|
||||
<!-- Toggle: Screensaver -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/card_background"
|
||||
android:padding="20dp"
|
||||
android:padding="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="32dp">
|
||||
|
||||
android:layout_marginBottom="10dp">
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvScreensaverToggleLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/setup_screensaver_toggle_label"
|
||||
android:textColor="#f1f5f9"
|
||||
android:textSize="16sp"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
android:layout_marginBottom="3dp" />
|
||||
<TextView
|
||||
android:id="@+id/tvScreensaverToggleHint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/setup_screensaver_toggle_hint"
|
||||
android:textColor="#64748b"
|
||||
android:textSize="13sp" />
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/setupSwitchScreensaver"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -1130,6 +1142,114 @@
|
||||
android:checked="false" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Toggle: Prezzi lista spesa -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/card_background"
|
||||
android:padding="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="10dp">
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/setup_prices_toggle_label"
|
||||
android:textColor="#f1f5f9"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="3dp" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/setup_prices_toggle_hint"
|
||||
android:textColor="#64748b"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/setupSwitchPrices"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="false" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Toggle: Piano pasti -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/card_background"
|
||||
android:padding="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="10dp">
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/setup_mealplan_toggle_label"
|
||||
android:textColor="#f1f5f9"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="3dp" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/setup_mealplan_toggle_hint"
|
||||
android:textColor="#64748b"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/setupSwitchMealPlan"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="false" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Toggle: Suggerimenti zero-waste -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/card_background"
|
||||
android:padding="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="24dp">
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/setup_zerowaste_toggle_label"
|
||||
android:textColor="#f1f5f9"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="3dp" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/setup_zerowaste_toggle_hint"
|
||||
android:textColor="#64748b"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/setupSwitchZeroWaste"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="false" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Navigation -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -1141,7 +1261,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_weight="1"
|
||||
android:text="← Indietro"
|
||||
android:text="@string/setup_step_back"
|
||||
android:textSize="14sp"
|
||||
android:textAllCaps="false"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
@@ -1154,7 +1274,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_weight="2"
|
||||
android:text="Avanti →"
|
||||
android:text="@string/setup_step_next"
|
||||
android:textSize="15sp"
|
||||
android:textAllCaps="false"
|
||||
android:backgroundTint="#7c3aed" />
|
||||
@@ -1162,7 +1282,230 @@
|
||||
</LinearLayout>
|
||||
|
||||
<!-- ════════════════════════════════════════════
|
||||
STEP 6 — Done
|
||||
STEP 6 — Gemini AI key
|
||||
════════════════════════════════════════════ -->
|
||||
<LinearLayout
|
||||
android:id="@+id/stepGemini"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🤖"
|
||||
android:textSize="52sp"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/setup_gemini_title"
|
||||
android:textColor="#f1f5f9"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/setup_gemini_desc"
|
||||
android:textColor="#94a3b8"
|
||||
android:textSize="15sp"
|
||||
android:gravity="center"
|
||||
android:lineSpacingExtra="4dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- How-to card -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/card_background"
|
||||
android:padding="14dp"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="💡"
|
||||
android:textSize="20sp"
|
||||
android:layout_marginEnd="10dp" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/setup_gemini_how"
|
||||
android:textColor="#7dd3fc"
|
||||
android:textSize="13sp"
|
||||
android:lineSpacingExtra="3dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/setupGeminiKeyEdit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/setup_gemini_hint"
|
||||
android:textColor="#f1f5f9"
|
||||
android:textColorHint="#475569"
|
||||
android:backgroundTint="#334155"
|
||||
android:inputType="textVisiblePassword"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:layout_marginBottom="20dp" />
|
||||
|
||||
<!-- Navigation -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnGeminiBack"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/setup_step_back"
|
||||
android:textSize="14sp"
|
||||
android:textAllCaps="false"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:strokeColor="#334155"
|
||||
android:textColor="#64748b"
|
||||
android:layout_marginEnd="8dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnGeminiSkip"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/setup_skip_later"
|
||||
android:textSize="13sp"
|
||||
android:textAllCaps="false"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:strokeColor="#475569"
|
||||
android:textColor="#94a3b8"
|
||||
android:layout_marginEnd="8dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnGeminiNext"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/setup_confirm"
|
||||
android:textSize="14sp"
|
||||
android:textAllCaps="false"
|
||||
android:backgroundTint="#7c3aed" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- ════════════════════════════════════════════
|
||||
STEP 7 — Bring! credentials
|
||||
════════════════════════════════════════════ -->
|
||||
<LinearLayout
|
||||
android:id="@+id/stepBring"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🛒"
|
||||
android:textSize="52sp"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/setup_bring_title"
|
||||
android:textColor="#f1f5f9"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/setup_bring_desc"
|
||||
android:textColor="#94a3b8"
|
||||
android:textSize="15sp"
|
||||
android:gravity="center"
|
||||
android:lineSpacingExtra="4dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/setupBringEmailEdit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/setup_bring_email_hint"
|
||||
android:textColor="#f1f5f9"
|
||||
android:textColorHint="#475569"
|
||||
android:backgroundTint="#334155"
|
||||
android:inputType="textEmailAddress"
|
||||
android:textSize="15sp"
|
||||
android:layout_marginBottom="10dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/setupBringPasswordEdit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/setup_bring_pass_hint"
|
||||
android:textColor="#f1f5f9"
|
||||
android:textColorHint="#475569"
|
||||
android:backgroundTint="#334155"
|
||||
android:inputType="textPassword"
|
||||
android:textSize="15sp"
|
||||
android:layout_marginBottom="20dp" />
|
||||
|
||||
<!-- Navigation -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnBringBack"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/setup_step_back"
|
||||
android:textSize="14sp"
|
||||
android:textAllCaps="false"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:strokeColor="#334155"
|
||||
android:textColor="#64748b"
|
||||
android:layout_marginEnd="8dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnBringSkip"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/setup_skip_later"
|
||||
android:textSize="13sp"
|
||||
android:textAllCaps="false"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:strokeColor="#475569"
|
||||
android:textColor="#94a3b8"
|
||||
android:layout_marginEnd="8dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnBringNext"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/setup_confirm"
|
||||
android:textSize="14sp"
|
||||
android:textAllCaps="false"
|
||||
android:backgroundTint="#059669" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- ════════════════════════════════════════════
|
||||
STEP 8 — Done
|
||||
════════════════════════════════════════════ -->
|
||||
<LinearLayout
|
||||
android:id="@+id/stepDone"
|
||||
@@ -1182,7 +1525,7 @@
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Tutto pronto!"
|
||||
android:text="@string/setup_done_title"
|
||||
android:textColor="#f1f5f9"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
@@ -1191,7 +1534,7 @@
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="La configurazione è completa. Premi il pulsante per avviare EverShelf in modalità kiosk."
|
||||
android:text="@string/setup_done_desc"
|
||||
android:textColor="#94a3b8"
|
||||
android:textSize="15sp"
|
||||
android:gravity="center"
|
||||
@@ -1210,10 +1553,10 @@
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Riepilogo configurazione"
|
||||
android:text="@string/setup_done_summary_label"
|
||||
android:textColor="#94a3b8"
|
||||
android:textSize="13sp"
|
||||
android:textAllCaps="true"
|
||||
android:textAllCaps="false"
|
||||
android:letterSpacing="0.08"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
@@ -1231,7 +1574,7 @@
|
||||
android:id="@+id/btnLaunch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:text="🚀 Avvia EverShelf"
|
||||
android:text="@string/btn_launch"
|
||||
android:textSize="18sp"
|
||||
android:textAllCaps="false"
|
||||
android:backgroundTint="#059669" />
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">EverShelf Kiosk</string>
|
||||
|
||||
<!-- Setup-Assistent Zeichenfolgen -->
|
||||
<string name="setup_enter_url">Bitte zuerst eine URL eingeben</string>
|
||||
<string name="setup_testing">Verbindung wird getestet…</string>
|
||||
<string name="setup_server_found">EverShelf-Server gefunden und API aktiv!</string>
|
||||
<string name="setup_api_not_found">Server erreichbar, aber EverShelf-API nicht gefunden. Pfad prüfen.</string>
|
||||
<string name="setup_unreachable">Server nicht erreichbar</string>
|
||||
<string name="setup_discover_btn">🔍 Lokales Netzwerk durchsuchen</string> <string name="setup_perms_granted_next">✅ Berechtigungen erteilt — Weiter →</string> <string name="setup_discovering">Suche läuft…</string>
|
||||
<string name="setup_discover_btn">🔍 Lokales Netzwerk durchsuchen</string>
|
||||
<string name="setup_perms_granted_next">✅ Berechtigungen erteilt — Weiter →</string>
|
||||
<string name="setup_discovering">Suche läuft…</string>
|
||||
<string name="setup_discovering_detail">Suche nach EverShelf-Servern im lokalen Netzwerk…</string>
|
||||
<string name="setup_discover_not_found">Kein EverShelf-Server automatisch gefunden. URL manuell eingeben.</string>
|
||||
<string name="setup_exit_title">Setup beenden?</string>
|
||||
<string name="setup_exit_message">Die Einrichtung kann später beim erneuten Öffnen der App abgeschlossen werden.</string>
|
||||
<string name="setup_exit_confirm">Beenden</string>
|
||||
<string name="setup_exit_cancel">Weiter</string>
|
||||
|
||||
<!-- Wizard Schritt 3: Smart-Waage -->
|
||||
<string name="setup_step_back">← Zurück</string>
|
||||
<string name="setup_step_next">Weiter →</string>
|
||||
<string name="setup_skip_later">Später einrichten</string>
|
||||
<string name="setup_confirm">Bestätigen →</string>
|
||||
<string name="wizard_step3_title">Smart-Waage (Optional)</string>
|
||||
<string name="wizard_step3_description">Um eine Bluetooth-Küchenwaage zu verwenden, musst du die EverShelf Scale Gateway App separat installieren.</string>
|
||||
<string name="wizard_step3_question">Hast du eine Bluetooth-Küchenwaage?</string>
|
||||
<string name="wizard_step3_yes">✅ Ja, ich habe eine Waage</string>
|
||||
<string name="wizard_step3_no">➡️ Nein, überspringen</string>
|
||||
|
||||
<!-- Gateway-Statusmeldungen -->
|
||||
<string name="ble_scanning">🔍 Suche läuft…</string>
|
||||
<string name="ble_connected">Verbunden! Gegenstand auf die Waage legen…</string>
|
||||
<string name="ble_disconnected">Verbindung getrennt. Erneut versuchen.</string>
|
||||
<string name="ble_no_scale_found">Keine Waage gefunden. Sicherstellen, dass sie eingeschaltet und in der Nähe ist, und erneut versuchen.</string>
|
||||
<string name="ble_select_from_list">Waage aus der Liste auswählen.</string>
|
||||
<string name="ble_not_confirmed">Waage nicht bestätigt. Erneut scannen.</string>
|
||||
<string name="ble_scan_again">🔄 Erneut scannen</string>
|
||||
<string name="ble_weight_received">Gewicht empfangen — Stimmt es mit der Anzeige überein?</string>
|
||||
<string name="wizard_gateway_installed">Scale Gateway installiert ✅</string>
|
||||
<string name="wizard_gateway_installed_detail">Wird beim Fortfahren im Hintergrund gestartet.</string>
|
||||
<string name="wizard_gateway_not_installed">Scale Gateway nicht installiert</string>
|
||||
@@ -32,8 +40,6 @@
|
||||
<string name="wizard_gateway_up_to_date">Scale Gateway ist aktuell.</string>
|
||||
<string name="wizard_gateway_update_available">Update für Scale Gateway verfügbar</string>
|
||||
<string name="wizard_gateway_update_detail">Tippe auf den Button, um jetzt zu aktualisieren.</string>
|
||||
|
||||
<!-- Download- / Installationsfortschritt -->
|
||||
<string name="install_downloading">Download läuft…</string>
|
||||
<string name="install_downloading_detail">Bitte warten, die Datei wird heruntergeladen.</string>
|
||||
<string name="install_installing">Installation läuft…</string>
|
||||
@@ -43,31 +49,56 @@
|
||||
<string name="install_error_download">Download fehlgeschlagen</string>
|
||||
<string name="install_error_download_detail">Verbindung prüfen und erneut versuchen.</string>
|
||||
<string name="install_error_install">Installation fehlgeschlagen</string>
|
||||
<string name="install_perm_detail">Aktiviere \'Unbekannte Apps installieren\' in den Einstellungen, dann komm zurück.</string>
|
||||
<string name="install_perm_detail">Aktiviere 'Unbekannte Apps installieren' in den Einstellungen, dann komm zurück.</string>
|
||||
<string name="install_btn_retry">↩ Nochmal versuchen</string>
|
||||
|
||||
<!-- Schaltflächen -->
|
||||
<string name="btn_back">Zurück</string>
|
||||
<string name="btn_launch">🚀 EverShelf starten</string>
|
||||
<string name="btn_launch_no_scale">🚀 Ohne Waage starten</string>
|
||||
<string name="btn_download_gateway">📥 Scale Gateway installieren</string>
|
||||
<string name="btn_update_gateway">📥 Scale Gateway aktualisieren</string>
|
||||
|
||||
<!-- Server-Erreichbarkeit prüfen (Wizard Schritt 3) -->
|
||||
<string name="wizard_server_checking">Server-Verbindung wird geprüft…</string>
|
||||
<string name="wizard_server_ok">Server erreichbar ✅</string>
|
||||
<string name="wizard_server_ok_detail">Fehlerberichterstattung aktiv — Installationsfehler werden automatisch an GitHub Issues gesendet.</string>
|
||||
<string name="wizard_server_error">Server nicht erreichbar ⚠️</string>
|
||||
<string name="wizard_server_error_detail">Fehler werden GitHub Issues nicht erreichen. URL in Schritt 2 prüfen.</string>
|
||||
<!-- Bildschirmschoner-Schritt -->
|
||||
<string name="setup_screensaver_title">Bildschirmschoner</string>
|
||||
<string name="setup_screensaver_desc">Zeigt nach 5 Minuten Inaktivität eine Uhr mit nützlichen Fakten. Standardmäßig deaktiviert (Bildschirm bleibt immer an).</string>
|
||||
<string name="setup_screensaver_toggle_label">Bildschirmschoner aktivieren</string>
|
||||
<string name="setup_screensaver_toggle_hint">Wenn deaktiviert, bleibt der Bildschirm immer an.</string>
|
||||
<string name="setup_features_title">Funktionen</string>
|
||||
<string name="setup_features_desc">Aktiviere die gewünschten Funktionen. Du kannst sie später jederzeit in den Servereinstellungen ändern.</string>
|
||||
<string name="setup_screensaver_toggle_label">Uhr-Bildschirmschoner</string>
|
||||
<string name="setup_screensaver_toggle_hint">Zeigt eine Uhranzeige nach 5 Min. Inaktivität.</string>
|
||||
<string name="setup_prices_toggle_label">Einkaufslisten-Preise</string>
|
||||
<string name="setup_prices_toggle_hint">KI-gestützte automatische Kostensätzung für jeden Artikel.</string>
|
||||
<string name="setup_mealplan_toggle_label">Mahlzeitenplan</string>
|
||||
<string name="setup_mealplan_toggle_hint">Plane die Wöchentliche Mahlzeiten mit Rezepten aus deiner Vorratskammer.</string>
|
||||
<string name="setup_zerowaste_toggle_label">Zero-Waste-Tipps</string>
|
||||
<string name="setup_zerowaste_toggle_hint">Beim Kochen Tipps zur Wiederverwendung von Resten anzeigen (Schalen, Kochwasser usw.).</string>
|
||||
<string name="setup_gemini_title">Google Gemini AI</string>
|
||||
<string name="setup_gemini_desc">EverShelf nutzt Google Gemini AI für Rezeptvorschläge, smarte Einkaufsschätzungen und mehr.
|
||||
|
||||
<!-- Zusammenfassung -->
|
||||
Zum Aktivieren den kostenlosen Gemini API-Schlüssel eingeben.</string>
|
||||
<string name="setup_gemini_how">Kostenlosen Schlüssel unter: aistudio.google.com → "API-Schlüssel erhalten"</string>
|
||||
<string name="setup_gemini_hint">API-Schlüssel einfügen (beginnt mit AIza…)</string>
|
||||
<string name="setup_bring_title">Bring! Einkaufsliste</string>
|
||||
<string name="setup_bring_desc">EverShelf kann die Einkaufsliste mit der Bring!-App synchronisieren.
|
||||
|
||||
Bring!-Zugangsdaten eingeben, um die Integration zu aktivieren.</string>
|
||||
<string name="setup_bring_email_hint">Bring!-E-Mail-Adresse</string>
|
||||
<string name="setup_bring_pass_hint">Bring!-Passwort</string>
|
||||
<string name="setup_done_title">Alles bereit!</string>
|
||||
<string name="setup_done_desc">Die Einrichtung ist abgeschlossen. Auf den Button tippen, um EverShelf im Kiosk-Modus zu starten.</string>
|
||||
<string name="setup_done_summary_label">KONFIGURATIONSSÜBERSICHT</string>
|
||||
<string name="summary_lang">Sprache</string>
|
||||
<string name="summary_scale_skip">Waage: nicht konfiguriert</string>
|
||||
<string name="summary_screensaver_on">Bildschirmschoner: aktiv</string>
|
||||
<string name="summary_screensaver_off">Bildschirm immer an (Bildschirmschoner deaktiviert)</string>
|
||||
</resources>
|
||||
<string name="summary_prices_on">Einkaufslisten-Preise: aktiviert</string>
|
||||
<string name="summary_mealplan_on">Mahlzeitenplan: aktiviert</string>
|
||||
<string name="summary_zerowaste_on">Zero-Waste-Tipps: aktiviert</string>
|
||||
<string name="summary_gemini_set">Gemini AI: aktiviert</string>
|
||||
<string name="summary_gemini_skip">Gemini AI: nicht konfiguriert</string>
|
||||
<string name="summary_bring_set">Bring!: verbunden</string>
|
||||
<string name="summary_bring_skip">Bring!: nicht konfiguriert</string>
|
||||
<string name="ble_connecting_to">🔗 Verbinde mit %s…</string>
|
||||
<string name="ble_connecting">🔗 Verbindung wird hergestellt…</string>
|
||||
<string name="summary_scale_ok">Waage: %s</string>
|
||||
<string name="summary_scale_warn">Waage: nicht bestätigt</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,104 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">EverShelf Kiosk</string>
|
||||
<string name="setup_enter_url">Introduce primero una URL</string>
|
||||
<string name="setup_testing">Probando conexión…</string>
|
||||
<string name="setup_server_found">¡Servidor EverShelf encontrado y API activa!</string>
|
||||
<string name="setup_api_not_found">Servidor accesible pero API EverShelf no encontrada. Comprueba la ruta.</string>
|
||||
<string name="setup_unreachable">No se puede alcanzar el servidor</string>
|
||||
<string name="setup_discover_btn">🔍 Buscar en la red local</string>
|
||||
<string name="setup_perms_granted_next">✅ Permisos concedidos — Continuar →</string>
|
||||
<string name="setup_discovering">Escaneando…</string>
|
||||
<string name="setup_discovering_detail">Buscando servidores EverShelf en la red local…</string>
|
||||
<string name="setup_discover_not_found">Ningún servidor EverShelf encontrado automáticamente. Introduce la URL manualmente.</string>
|
||||
<string name="setup_exit_title">¿Salir de la configuración?</string>
|
||||
<string name="setup_exit_message">Puedes completar la configuración más tarde cuando vuelvas a abrir la app.</string>
|
||||
<string name="setup_exit_confirm">Salir</string>
|
||||
<string name="setup_exit_cancel">Continuar</string>
|
||||
<string name="setup_step_back">← Atrás</string>
|
||||
<string name="setup_step_next">Siguiente →</string>
|
||||
<string name="setup_skip_later">Configurar después</string>
|
||||
<string name="setup_confirm">Confirmar →</string>
|
||||
<string name="wizard_step3_title">Báscula inteligente</string>
|
||||
<string name="wizard_step3_description">EverShelf Kiosk incluye una pasarela Bluetooth integrada — no necesitas ninguna app externa. Selecciona tu báscula abajo.</string>
|
||||
<string name="wizard_step3_question">¿Tienes una báscula inteligente Bluetooth?</string>
|
||||
<string name="wizard_step3_yes">✅ Sí, tengo una báscula</string>
|
||||
<string name="wizard_step3_no">➡️ No, saltar este paso</string>
|
||||
<string name="ble_scanning">🔍 Escaneando…</string>
|
||||
<string name="ble_connected">¡Conectado! Coloca un objeto en la báscula…</string>
|
||||
<string name="ble_disconnected">Conexión perdida. Reintentar.</string>
|
||||
<string name="ble_no_scale_found">No se encontró ninguna báscula. Asegúrate de que esté encendida y cerca, e inténtalo de nuevo.</string>
|
||||
<string name="ble_select_from_list">Selecciona tu báscula de la lista.</string>
|
||||
<string name="ble_not_confirmed">Báscula no confirmada. Vuelve a escanear.</string>
|
||||
<string name="ble_scan_again">🔄 Volver a escanear</string>
|
||||
<string name="ble_weight_received">Peso recibido — ¿coincide con el mostrado en la báscula?</string>
|
||||
<string name="wizard_gateway_installed">Báscula guardada ✅</string>
|
||||
<string name="wizard_gateway_installed_detail">La pasarela BLE integrada se conectará automáticamente al inicio.</string>
|
||||
<string name="wizard_gateway_not_installed">Ninguna báscula seleccionada</string>
|
||||
<string name="wizard_gateway_not_installed_detail">Escanea las básculas BLE cercanas y toca una para seleccionarla.</string>
|
||||
<string name="wizard_gateway_checking">Escaneando básculas BLE…</string>
|
||||
<string name="wizard_gateway_up_to_date">Servicio BLE de báscula listo.</string>
|
||||
<string name="wizard_gateway_update_available">Báscula BLE encontrada</string>
|
||||
<string name="wizard_gateway_update_detail">Toca la báscula en la lista para conectarte.</string>
|
||||
<string name="install_downloading">Descargando…</string>
|
||||
<string name="install_downloading_detail">Por favor, espera mientras se descarga el archivo.</string>
|
||||
<string name="install_installing">Instalando…</string>
|
||||
<string name="install_confirm_detail">Confirma la instalación en el diálogo que se ha abierto.</string>
|
||||
<string name="install_success">¡Instalado correctamente!</string>
|
||||
<string name="install_success_detail">La app ha sido actualizada.</string>
|
||||
<string name="install_error_download">Descarga fallida</string>
|
||||
<string name="install_error_download_detail">Comprueba la conexión e inténtalo de nuevo.</string>
|
||||
<string name="install_error_install">Instalación fallida</string>
|
||||
<string name="install_perm_detail">Habilita 'Instalar apps desconocidas' en los ajustes y vuelve aquí.</string>
|
||||
<string name="install_btn_retry">↩ Reintentar</string>
|
||||
<string name="btn_back">Atrás</string>
|
||||
<string name="btn_launch">🚀 Iniciar EverShelf</string>
|
||||
<string name="btn_launch_no_scale">🚀 Iniciar sin báscula</string>
|
||||
<string name="btn_download_gateway">📥 Instalar Scale Gateway</string>
|
||||
<string name="btn_update_gateway">📥 Actualizar Scale Gateway</string>
|
||||
<string name="wizard_server_checking">Comprobando conexión al servidor…</string>
|
||||
<string name="wizard_server_ok">Servidor accesible ✅</string>
|
||||
<string name="wizard_server_ok_detail">Informe de errores activo — los fallos de instalación se enviarán automáticamente a GitHub Issues.</string>
|
||||
<string name="wizard_server_error">Servidor no accesible ⚠️</string>
|
||||
<string name="wizard_server_error_detail">Los errores no llegarán a GitHub Issues. Comprueba la URL introducida en el paso 2.</string>
|
||||
<string name="setup_features_title">Funcionalidades</string>
|
||||
<string name="setup_features_desc">Activa las funciones que quieras usar. Puedes cambiarlas en cualquier momento desde los ajustes del servidor.</string>
|
||||
<string name="setup_screensaver_toggle_label">Salvapantallas reloj</string>
|
||||
<string name="setup_screensaver_toggle_hint">Muestra un reloj después de 5 min de inactividad.</string>
|
||||
<string name="setup_prices_toggle_label">Precios lista de la compra</string>
|
||||
<string name="setup_prices_toggle_hint">Estimación automática del coste de cada artículo mediante IA.</string>
|
||||
<string name="setup_mealplan_toggle_label">Plan de comidas</string>
|
||||
<string name="setup_mealplan_toggle_hint">Planifica las comidas de la semana con recetas basadas en tu despensa.</string>
|
||||
<string name="setup_zerowaste_toggle_label">Consejos zero-waste</string>
|
||||
<string name="setup_zerowaste_toggle_hint">Muestra consejos para reutilizar restos (cáscaras, agua de cocción, etc.) al cocinar.</string>
|
||||
<string name="setup_gemini_title">Google Gemini AI</string>
|
||||
<string name="setup_gemini_desc">EverShelf usa Google Gemini AI para sugerencias de recetas, estimaciones inteligentes de la compra y más.
|
||||
|
||||
Para activarla, introduce tu clave API de Gemini gratuita.</string>
|
||||
<string name="setup_gemini_how">Obtén tu clave gratuita en: aistudio.google.com → "Obtener clave API"</string>
|
||||
<string name="setup_gemini_hint">Pega la clave API aquí (empieza por AIza…)</string>
|
||||
<string name="setup_bring_title">Bring! Lista de la compra</string>
|
||||
<string name="setup_bring_desc">EverShelf puede sincronizar tu lista de la compra con la app Bring!.
|
||||
|
||||
Introduce tus credenciales de Bring! para activar la integración.</string>
|
||||
<string name="setup_bring_email_hint">Correo electrónico de Bring!</string>
|
||||
<string name="setup_bring_pass_hint">Contraseña de Bring!</string>
|
||||
<string name="setup_done_title">¡Todo listo!</string>
|
||||
<string name="setup_done_desc">La configuración está completa. Pulsa el botón para iniciar EverShelf en modo quiosco.</string>
|
||||
<string name="setup_done_summary_label">RESUMEN DE CONFIGURACIÓN</string>
|
||||
<string name="summary_lang">Idioma</string>
|
||||
<string name="summary_scale_skip">Báscula: no configurada</string>
|
||||
<string name="summary_screensaver_on">Salvapantallas: activo</string>
|
||||
<string name="summary_screensaver_off">Pantalla siempre encendida (salvapantallas desactivado)</string>
|
||||
<string name="summary_prices_on">Precios lista de la compra: activados</string>
|
||||
<string name="summary_mealplan_on">Plan de comidas: activado</string>
|
||||
<string name="summary_zerowaste_on">Consejos zero-waste: activados</string>
|
||||
<string name="summary_gemini_set">Gemini AI: activada</string>
|
||||
<string name="summary_gemini_skip">Gemini AI: no configurada</string>
|
||||
<string name="summary_bring_set">Bring!: conectada</string>
|
||||
<string name="summary_bring_skip">Bring!: no configurada</string>
|
||||
<string name="ble_connecting_to">🔗 Conectando con %s…</string>
|
||||
<string name="ble_connecting">🔗 Estableciendo conexión…</string>
|
||||
<string name="summary_scale_ok">Báscula: %s</string>
|
||||
<string name="summary_scale_warn">Báscula: no confirmada</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,104 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">EverShelf Kiosk</string>
|
||||
<string name="setup_enter_url">Veuillez d'abord saisir une URL</string>
|
||||
<string name="setup_testing">Test de connexion…</string>
|
||||
<string name="setup_server_found">Serveur EverShelf trouvé et API active !</string>
|
||||
<string name="setup_api_not_found">Serveur accessible mais API EverShelf introuvable. Vérifiez le chemin.</string>
|
||||
<string name="setup_unreachable">Impossible d'atteindre le serveur</string>
|
||||
<string name="setup_discover_btn">🔍 Rechercher sur le réseau local</string>
|
||||
<string name="setup_perms_granted_next">✅ Permissions accordées — Continuer →</string>
|
||||
<string name="setup_discovering">Analyse en cours…</string>
|
||||
<string name="setup_discovering_detail">Recherche de serveurs EverShelf sur le réseau local…</string>
|
||||
<string name="setup_discover_not_found">Aucun serveur EverShelf trouvé automatiquement. Entrez l'URL manuellement.</string>
|
||||
<string name="setup_exit_title">Quitter la configuration ?</string>
|
||||
<string name="setup_exit_message">Vous pouvez terminer la configuration plus tard en rouvrant l'app.</string>
|
||||
<string name="setup_exit_confirm">Quitter</string>
|
||||
<string name="setup_exit_cancel">Continuer</string>
|
||||
<string name="setup_step_back">← Retour</string>
|
||||
<string name="setup_step_next">Suivant →</string>
|
||||
<string name="setup_skip_later">Configurer plus tard</string>
|
||||
<string name="setup_confirm">Confirmer →</string>
|
||||
<string name="wizard_step3_title">Balance intelligente</string>
|
||||
<string name="wizard_step3_description">EverShelf Kiosk inclut une passerelle Bluetooth intégrée — aucune app externe nécessaire. Sélectionnez votre balance ci-dessous.</string>
|
||||
<string name="wizard_step3_question">Avez-vous une balance intelligente Bluetooth ?</string>
|
||||
<string name="wizard_step3_yes">✅ Oui, j'ai une balance</string>
|
||||
<string name="wizard_step3_no">➡️ Non, ignorer cette étape</string>
|
||||
<string name="ble_scanning">🔍 Scan en cours…</string>
|
||||
<string name="ble_connected">Connecté ! Posez un objet sur la balance…</string>
|
||||
<string name="ble_disconnected">Connexion perdue. Réessayer.</string>
|
||||
<string name="ble_no_scale_found">Aucune balance trouvée. Vérifiez qu'elle est allumée et à proximité, puis réessayez.</string>
|
||||
<string name="ble_select_from_list">Sélectionnez votre balance dans la liste.</string>
|
||||
<string name="ble_not_confirmed">Balance non confirmée. Relancer le scan.</string>
|
||||
<string name="ble_scan_again">🔄 Scanner à nouveau</string>
|
||||
<string name="ble_weight_received">Poids reçu — correspond-il à l'affichage de la balance ?</string>
|
||||
<string name="wizard_gateway_installed">Balance enregistrée ✅</string>
|
||||
<string name="wizard_gateway_installed_detail">La passerelle BLE intégrée se connectera automatiquement au démarrage.</string>
|
||||
<string name="wizard_gateway_not_installed">Aucune balance sélectionnée</string>
|
||||
<string name="wizard_gateway_not_installed_detail">Scannez les balances BLE à proximité et appuyez sur l'une d'elles pour la sélectionner.</string>
|
||||
<string name="wizard_gateway_checking">Scan des balances BLE en cours…</string>
|
||||
<string name="wizard_gateway_up_to_date">Service BLE de la balance prêt.</string>
|
||||
<string name="wizard_gateway_update_available">Balance BLE trouvée</string>
|
||||
<string name="wizard_gateway_update_detail">Appuyez sur la balance dans la liste pour vous connecter.</string>
|
||||
<string name="install_downloading">Téléchargement en cours…</string>
|
||||
<string name="install_downloading_detail">Veuillez patienter, le fichier est en cours de téléchargement.</string>
|
||||
<string name="install_installing">Installation en cours…</string>
|
||||
<string name="install_confirm_detail">Confirmez l'installation dans la boîte de dialogue ouverte.</string>
|
||||
<string name="install_success">Installé avec succès !</string>
|
||||
<string name="install_success_detail">L'app a été mise à jour.</string>
|
||||
<string name="install_error_download">Téléchargement échoué</string>
|
||||
<string name="install_error_download_detail">Vérifiez la connexion et réessayez.</string>
|
||||
<string name="install_error_install">Installation échouée</string>
|
||||
<string name="install_perm_detail">Activez 'Installer des apps inconnues' dans les paramètres, puis revenez ici.</string>
|
||||
<string name="install_btn_retry">↩ Réessayer</string>
|
||||
<string name="btn_back">Retour</string>
|
||||
<string name="btn_launch">🚀 Lancer EverShelf</string>
|
||||
<string name="btn_launch_no_scale">🚀 Lancer sans balance</string>
|
||||
<string name="btn_download_gateway">📥 Installer Scale Gateway</string>
|
||||
<string name="btn_update_gateway">📥 Mettre à jour Scale Gateway</string>
|
||||
<string name="wizard_server_checking">Vérification de la connexion au serveur…</string>
|
||||
<string name="wizard_server_ok">Serveur accessible ✅</string>
|
||||
<string name="wizard_server_ok_detail">Rapport d'erreurs actif — les échecs d'installation seront envoyés automatiquement aux GitHub Issues.</string>
|
||||
<string name="wizard_server_error">Serveur inaccessible ⚠️</string>
|
||||
<string name="wizard_server_error_detail">Les erreurs n'atteindront pas GitHub Issues. Vérifiez l'URL saisie à l'étape 2.</string>
|
||||
<string name="setup_features_title">Fonctionnalités</string>
|
||||
<string name="setup_features_desc">Activez les fonctions que vous souhaitez utiliser. Vous pourrez les modifier plus tard dans les paramètres du serveur.</string>
|
||||
<string name="setup_screensaver_toggle_label">Horloge écran de veille</string>
|
||||
<string name="setup_screensaver_toggle_hint">Affiche une horloge après 5 min d'inactivité.</string>
|
||||
<string name="setup_prices_toggle_label">Prix liste de courses</string>
|
||||
<string name="setup_prices_toggle_hint">Estimation automatique du coût de chaque article via IA.</string>
|
||||
<string name="setup_mealplan_toggle_label">Plan de repas</string>
|
||||
<string name="setup_mealplan_toggle_hint">Planifiez les repas de la semaine avec des recettes basées sur votre garde-manger.</string>
|
||||
<string name="setup_zerowaste_toggle_label">Conseils zéro déchet</string>
|
||||
<string name="setup_zerowaste_toggle_hint">Affiche des conseils pour réutiliser les restes (peaux, eau de cuisson, etc.) pendant la cuisson.</string>
|
||||
<string name="setup_gemini_title">Google Gemini AI</string>
|
||||
<string name="setup_gemini_desc">EverShelf utilise Google Gemini AI pour les suggestions de recettes, les estimations intelligentes des courses et plus encore.
|
||||
|
||||
Pour l'activer, entrez votre clé API Gemini gratuite.</string>
|
||||
<string name="setup_gemini_how">Obtenez votre clé gratuite sur : aistudio.google.com → "Obtenir une clé API"</string>
|
||||
<string name="setup_gemini_hint">Collez la clé API ici (commence par AIza…)</string>
|
||||
<string name="setup_bring_title">Bring! Liste de courses</string>
|
||||
<string name="setup_bring_desc">EverShelf peut synchroniser votre liste de courses avec l'app Bring!.
|
||||
|
||||
Entrez vos identifiants Bring! pour activer l'intégration.</string>
|
||||
<string name="setup_bring_email_hint">Adresse e-mail Bring!</string>
|
||||
<string name="setup_bring_pass_hint">Mot de passe Bring!</string>
|
||||
<string name="setup_done_title">Tout est prêt !</string>
|
||||
<string name="setup_done_desc">La configuration est terminée. Appuyez sur le bouton pour lancer EverShelf en mode kiosque.</string>
|
||||
<string name="setup_done_summary_label">RÉSUMÉ DE CONFIGURATION</string>
|
||||
<string name="summary_lang">Langue</string>
|
||||
<string name="summary_scale_skip">Balance : non configurée</string>
|
||||
<string name="summary_screensaver_on">Écran de veille : actif</string>
|
||||
<string name="summary_screensaver_off">Écran toujours allumé (écran de veille désactivé)</string>
|
||||
<string name="summary_prices_on">Prix liste de courses : activés</string>
|
||||
<string name="summary_mealplan_on">Plan de repas : activé</string>
|
||||
<string name="summary_zerowaste_on">Conseils zéro déchet : activés</string>
|
||||
<string name="summary_gemini_set">Gemini AI : activée</string>
|
||||
<string name="summary_gemini_skip">Gemini AI : non configurée</string>
|
||||
<string name="summary_bring_set">Bring! : connectée</string>
|
||||
<string name="summary_bring_skip">Bring! : non configurée</string>
|
||||
<string name="ble_connecting_to">🔗 Connexion à %s…</string>
|
||||
<string name="ble_connecting">🔗 Connexion en cours…</string>
|
||||
<string name="summary_scale_ok">Balance : %s</string>
|
||||
<string name="summary_scale_warn">Balance : non confirmée</string>
|
||||
</resources>
|
||||
@@ -1,73 +1,104 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">EverShelf Kiosk</string>
|
||||
|
||||
<!-- Stringhe setup wizard -->
|
||||
<string name="setup_enter_url">Inserisci prima un URL</string>
|
||||
<string name="setup_testing">Verifica connessione…</string>
|
||||
<string name="setup_server_found">Server EverShelf trovato e API attiva!</string>
|
||||
<string name="setup_api_not_found">Server raggiungibile ma API EverShelf non trovata. Verifica il percorso.</string>
|
||||
<string name="setup_unreachable">Impossibile raggiungere il server</string>
|
||||
<string name="setup_discover_btn">🔍 Cerca nella rete locale</string> <string name="setup_perms_granted_next">✅ Permessi concessi — Continua →</string> <string name="setup_discovering">Scansione in corso…</string>
|
||||
<string name="setup_discover_btn">🔍 Cerca nella rete locale</string>
|
||||
<string name="setup_perms_granted_next">✅ Permessi concessi — Continua →</string>
|
||||
<string name="setup_discovering">Scansione in corso…</string>
|
||||
<string name="setup_discovering_detail">Ricerca server EverShelf nella rete locale…</string>
|
||||
<string name="setup_discover_not_found">Nessun server EverShelf trovato automaticamente. Inserisci l\'URL manualmente.</string>
|
||||
<string name="setup_discover_not_found">Nessun server EverShelf trovato automaticamente. Inserisci l'URL manualmente.</string>
|
||||
<string name="setup_exit_title">Uscire dalla configurazione?</string>
|
||||
<string name="setup_exit_message">Puoi completare la configurazione più tardi riaprendo l\'app.</string>
|
||||
<string name="setup_exit_message">Puoi completare la configurazione più tardi riaprendo l'app.</string>
|
||||
<string name="setup_exit_confirm">Esci</string>
|
||||
<string name="setup_exit_cancel">Continua</string>
|
||||
|
||||
<!-- Wizard Step 3: Bilancia smart -->
|
||||
<string name="setup_step_back">← Indietro</string>
|
||||
<string name="setup_step_next">Avanti →</string>
|
||||
<string name="setup_skip_later">Lo faccio dopo</string>
|
||||
<string name="setup_confirm">Conferma →</string>
|
||||
<string name="wizard_step3_title">Bilancia Smart</string>
|
||||
<string name="wizard_step3_description">EverShelf Kiosk include un gateway Bluetooth integrato — nessuna app esterna necessaria. Seleziona la tua bilancia qui sotto.</string>
|
||||
<string name="wizard_step3_question">Hai una bilancia smart Bluetooth?</string>
|
||||
<string name="wizard_step3_yes">✅ Sì, ho una bilancia</string>
|
||||
<string name="wizard_step3_no">➡️ No, salta questo passaggio</string>
|
||||
|
||||
<!-- Messaggi stato gateway -->
|
||||
<string name="ble_scanning">🔍 Scansione in corso…</string>
|
||||
<string name="ble_connected">Connesso! Posiziona un oggetto sulla bilancia…</string>
|
||||
<string name="ble_disconnected">Connessione persa. Riprova.</string>
|
||||
<string name="ble_no_scale_found">Nessuna bilancia trovata. Assicurati che sia accesa e vicina, poi riprova.</string>
|
||||
<string name="ble_select_from_list">Seleziona la tua bilancia dall'elenco.</string>
|
||||
<string name="ble_not_confirmed">Bilancia non confermata. Riprova la scansione.</string>
|
||||
<string name="ble_scan_again">🔄 Scansiona di nuovo</string>
|
||||
<string name="ble_weight_received">Peso ricevuto — coincide con quello sulla bilancia?</string>
|
||||
<string name="wizard_gateway_installed">Bilancia salvata ✅</string>
|
||||
<string name="wizard_gateway_installed_detail">Il gateway BLE integrato si collegherà automaticamente all\'avvio.</string>
|
||||
<string name="wizard_gateway_installed_detail">Il gateway BLE integrato si collegherà automaticamente all'avvio.</string>
|
||||
<string name="wizard_gateway_not_installed">Nessuna bilancia selezionata</string>
|
||||
<string name="wizard_gateway_not_installed_detail">Scansiona le bilance BLE nelle vicinanze e tocca una per selezionarla.</string>
|
||||
<string name="wizard_gateway_checking">Scansione bilance BLE in corso…</string>
|
||||
<string name="wizard_gateway_up_to_date">Servizio BLE bilancia pronto.</string>
|
||||
<string name="wizard_gateway_update_available">Bilancia BLE trovata</string>
|
||||
<string name="wizard_gateway_update_detail">Tocca la bilancia nell\'elenco per connettersi.</string>
|
||||
|
||||
<!-- Stati scaricamento / installazione -->
|
||||
<string name="wizard_gateway_update_detail">Tocca la bilancia nell'elenco per connettersi.</string>
|
||||
<string name="install_downloading">Scaricamento in corso…</string>
|
||||
<string name="install_downloading_detail">Attendi, il file viene scaricato.</string>
|
||||
<string name="install_installing">Installazione in corso…</string>
|
||||
<string name="install_confirm_detail">Conferma l\'installazione nel dialog che si è aperto.</string>
|
||||
<string name="install_confirm_detail">Conferma l'installazione nel dialog che si è aperto.</string>
|
||||
<string name="install_success">Installato con successo!</string>
|
||||
<string name="install_success_detail">L\'app è stata aggiornata.</string>
|
||||
<string name="install_success_detail">L'app è stata aggiornata.</string>
|
||||
<string name="install_error_download">Download fallito</string>
|
||||
<string name="install_error_download_detail">Controlla la connessione e riprova.</string>
|
||||
<string name="install_error_install">Installazione fallita</string>
|
||||
<string name="install_perm_detail">Abilita \'Installa app sconosciute\' nelle impostazioni, poi torna qui.</string>
|
||||
<string name="install_perm_detail">Abilita 'Installa app sconosciute' nelle impostazioni, poi torna qui.</string>
|
||||
<string name="install_btn_retry">↩ Riprova</string>
|
||||
|
||||
<!-- Pulsanti -->
|
||||
<string name="btn_back">Indietro</string>
|
||||
<string name="btn_launch">🚀 Avvia EverShelf</string>
|
||||
<string name="btn_launch_no_scale">🚀 Avvia senza bilancia</string>
|
||||
<string name="btn_download_gateway">📥 Installa Scale Gateway</string>
|
||||
<string name="btn_update_gateway">📥 Aggiorna Scale Gateway</string>
|
||||
|
||||
<!-- Verifica raggiungibilità server (step 3 wizard) -->
|
||||
<string name="wizard_server_checking">Verifica connessione server…</string>
|
||||
<string name="wizard_server_ok">Server raggiungibile ✅</string>
|
||||
<string name="wizard_server_ok_detail">Segnalazione errori attiva — i problemi di installazione vengono inviati automaticamente alle GitHub Issues.</string>
|
||||
<string name="wizard_server_error">Server non raggiungibile ⚠️</string>
|
||||
<string name="wizard_server_error_detail">Gli errori non raggiungeranno GitHub Issues. Verifica l\'URL inserito al passaggio 2.</string>
|
||||
<!-- Passo salvaschermo -->
|
||||
<string name="setup_screensaver_title">Salvaschermo</string>
|
||||
<string name="setup_screensaver_desc">Mostra un orologio con fatti utili dopo 5 minuti di inattività. Di default è disattivato (lo schermo resta sempre acceso).</string>
|
||||
<string name="setup_screensaver_toggle_label">Attiva salvaschermo</string>
|
||||
<string name="setup_screensaver_toggle_hint">Se disattivo, lo schermo resta sempre acceso.</string>
|
||||
<string name="wizard_server_error_detail">Gli errori non raggiungeranno GitHub Issues. Verifica l'URL inserito al passaggio 2.</string>
|
||||
<string name="setup_features_title">Funzionalità</string>
|
||||
<string name="setup_features_desc">Attiva le funzioni che vuoi usare. Puoi sempre cambiarle in seguito dalle impostazioni del server.</string>
|
||||
<string name="setup_screensaver_toggle_label">Salvaschermo orologio</string>
|
||||
<string name="setup_screensaver_toggle_hint">Mostra l'overlay orologio dopo 5 min di inattività.</string>
|
||||
<string name="setup_prices_toggle_label">Prezzi lista spesa</string>
|
||||
<string name="setup_prices_toggle_hint">Stima automatica del costo di ogni articolo in lista tramite AI.</string>
|
||||
<string name="setup_mealplan_toggle_label">Piano pasti</string>
|
||||
<string name="setup_mealplan_toggle_hint">Pianifica i pasti della settimana suggerendo ricette basate sulla dispensa.</string>
|
||||
<string name="setup_zerowaste_toggle_label">Suggerimenti zero-waste</string>
|
||||
<string name="setup_zerowaste_toggle_hint">Durante la cottura mostra consigli per riutilizzare scarti (bucce, acqua di cottura, ecc.).</string>
|
||||
<string name="setup_gemini_title">Google Gemini AI</string>
|
||||
<string name="setup_gemini_desc">EverShelf usa Google Gemini AI per suggerimenti di ricette, stime intelligenti della spesa e altro ancora.
|
||||
|
||||
<!-- Riepilogo -->
|
||||
Per abilitarla, inserisci la tua chiave API Gemini gratuita.</string>
|
||||
<string name="setup_gemini_how">Ottieni la chiave gratuita su: aistudio.google.com → "Ottieni chiave API"</string>
|
||||
<string name="setup_gemini_hint">Incolla la chiave API (inizia con AIza…)</string>
|
||||
<string name="setup_bring_title">Bring! Lista della spesa</string>
|
||||
<string name="setup_bring_desc">EverShelf può sincronizzare la lista della spesa con l'app Bring!.
|
||||
|
||||
Inserisci le credenziali del tuo account Bring! per abilitare l'integrazione.</string>
|
||||
<string name="setup_bring_email_hint">Email Bring!</string>
|
||||
<string name="setup_bring_pass_hint">Password Bring!</string>
|
||||
<string name="setup_done_title">Tutto pronto!</string>
|
||||
<string name="setup_done_desc">La configurazione è completa. Premi il pulsante per avviare EverShelf in modalità kiosk.</string>
|
||||
<string name="setup_done_summary_label">RIEPILOGO CONFIGURAZIONE</string>
|
||||
<string name="summary_lang">Lingua</string>
|
||||
<string name="summary_scale_skip">Bilancia: non configurata</string>
|
||||
<string name="summary_screensaver_on">Salvaschermo: attivo</string>
|
||||
<string name="summary_screensaver_off">Schermo sempre acceso (salvaschermo disattivato)</string>
|
||||
</resources>
|
||||
<string name="summary_prices_on">Prezzi lista spesa: abilitati</string>
|
||||
<string name="summary_mealplan_on">Piano pasti: abilitato</string>
|
||||
<string name="summary_zerowaste_on">Suggerimenti zero-waste: abilitati</string>
|
||||
<string name="summary_gemini_set">Gemini AI: abilitata</string>
|
||||
<string name="summary_gemini_skip">Gemini AI: non configurata</string>
|
||||
<string name="summary_bring_set">Bring!: connessa</string>
|
||||
<string name="summary_bring_skip">Bring!: non configurata</string>
|
||||
<string name="ble_connecting_to">🔗 Connessione a %s…</string>
|
||||
<string name="ble_connecting">🔗 Connessione in corso…</string>
|
||||
<string name="summary_scale_ok">Bilancia: %s</string>
|
||||
<string name="summary_scale_warn">Bilancia: da configurare</string>
|
||||
</resources>
|
||||
@@ -1,28 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">EverShelf Kiosk</string>
|
||||
|
||||
<!-- Setup wizard strings -->
|
||||
<!-- ── Setup wizard ─────────────────────────────────────────────────── -->
|
||||
<string name="setup_enter_url">Please enter a URL first</string>
|
||||
<string name="setup_testing">Testing connection…</string>
|
||||
<string name="setup_server_found">EverShelf server found and API active!</string>
|
||||
<string name="setup_api_not_found">Server reachable but EverShelf API not found. Check the path.</string>
|
||||
<string name="setup_unreachable">Cannot reach server</string>
|
||||
<string name="setup_discover_btn">🔍 Search local network</string> <string name="setup_perms_granted_next">✅ Permissions granted — Continue →</string> <string name="setup_discovering">Scanning…</string>
|
||||
<string name="setup_discover_btn">🔍 Search local network</string>
|
||||
<string name="setup_perms_granted_next">✅ Permissions granted — Continue →</string>
|
||||
<string name="setup_discovering">Scanning…</string>
|
||||
<string name="setup_discovering_detail">Searching for EverShelf servers on the local network…</string>
|
||||
<string name="setup_discover_not_found">No EverShelf server found automatically. Enter the URL manually.</string>
|
||||
<string name="setup_exit_title">Exit setup?</string>
|
||||
<string name="setup_exit_message">You can complete setup later when you reopen the app.</string>
|
||||
<string name="setup_exit_confirm">Exit</string>
|
||||
<string name="setup_exit_cancel">Continue</string>
|
||||
<string name="setup_step_back">← Back</string>
|
||||
<string name="setup_step_next">Next →</string>
|
||||
<string name="setup_skip_later">Set up later</string>
|
||||
<string name="setup_confirm">Confirm →</string>
|
||||
|
||||
<!-- Wizard Step 3: Smart scale -->
|
||||
<!-- ── Wizard Step 4: Smart scale ───────────────────────────────────── -->
|
||||
<string name="wizard_step3_title">Smart Scale</string>
|
||||
<string name="wizard_step3_description">EverShelf Kiosk includes a built-in Bluetooth gateway — no external app needed. Select your scale below.</string>
|
||||
<string name="wizard_step3_question">Do you have a Bluetooth smart scale?</string>
|
||||
<string name="wizard_step3_yes">✅ Yes, I have a scale</string>
|
||||
<string name="wizard_step3_no">➡️ No, skip this step</string>
|
||||
|
||||
<!-- Gateway status messages -->
|
||||
<!-- BLE scan / test feedback (previously hardcoded) -->
|
||||
<string name="ble_scanning">🔍 Scanning…</string>
|
||||
<string name="ble_connected">Connected! Place an object on the scale…</string>
|
||||
<string name="ble_disconnected">Connection lost. Retry.</string>
|
||||
<string name="ble_no_scale_found">No scale found. Make sure it is on and nearby, then retry.</string>
|
||||
<string name="ble_select_from_list">Select your scale from the list.</string>
|
||||
<string name="ble_not_confirmed">Scale not confirmed. Retry scan.</string>
|
||||
<string name="ble_scan_again">🔄 Scan again</string>
|
||||
<string name="ble_weight_received">Weight received — does it match the display?</string>
|
||||
|
||||
<!-- ── Gateway status messages ──────────────────────────────────────── -->
|
||||
<string name="wizard_gateway_installed">Scale device saved ✅</string>
|
||||
<string name="wizard_gateway_installed_detail">The integrated BLE gateway will connect automatically on startup.</string>
|
||||
<string name="wizard_gateway_not_installed">No scale selected</string>
|
||||
@@ -32,41 +49,76 @@
|
||||
<string name="wizard_gateway_update_available">BLE scale found</string>
|
||||
<string name="wizard_gateway_update_detail">Tap the scale in the list to connect.</string>
|
||||
|
||||
<!-- Install / download progress states -->
|
||||
<string name="install_downloading">Scaricamento in corso…</string>
|
||||
<string name="install_downloading_detail">Attendi, il file viene scaricato.</string>
|
||||
<string name="install_installing">Installazione in corso…</string>
|
||||
<string name="install_confirm_detail">Conferma l\'installazione nel dialog che si è aperto.</string>
|
||||
<string name="install_success">Installato con successo!</string>
|
||||
<string name="install_success_detail">L\'app è stata aggiornata.</string>
|
||||
<string name="install_error_download">Download fallito</string>
|
||||
<string name="install_error_download_detail">Controlla la connessione e riprova.</string>
|
||||
<string name="install_error_install">Installazione fallita</string>
|
||||
<string name="install_perm_detail">Abilita \'Installa app sconosciute\' nelle impostazioni, poi torna qui.</string>
|
||||
<string name="install_btn_retry">↩ Riprova</string>
|
||||
<!-- ── Install / download progress states ───────────────────────────── -->
|
||||
<string name="install_downloading">Downloading…</string>
|
||||
<string name="install_downloading_detail">Please wait, the file is being downloaded.</string>
|
||||
<string name="install_installing">Installing…</string>
|
||||
<string name="install_confirm_detail">Confirm the installation in the dialog that has opened.</string>
|
||||
<string name="install_success">Installed successfully!</string>
|
||||
<string name="install_success_detail">The app has been updated.</string>
|
||||
<string name="install_error_download">Download failed</string>
|
||||
<string name="install_error_download_detail">Check your connection and try again.</string>
|
||||
<string name="install_error_install">Installation failed</string>
|
||||
<string name="install_perm_detail">Enable \'Install unknown apps\' in settings, then come back here.</string>
|
||||
<string name="install_btn_retry">↩ Retry</string>
|
||||
|
||||
<!-- Buttons -->
|
||||
<!-- ── Buttons ───────────────────────────────────────────────────────── -->
|
||||
<string name="btn_back">Back</string>
|
||||
<string name="btn_launch">🚀 Launch EverShelf</string>
|
||||
<string name="btn_launch_no_scale">🚀 Launch without scale</string>
|
||||
<string name="btn_download_gateway">📥 Install Scale Gateway</string>
|
||||
<string name="btn_update_gateway">📥 Update Scale Gateway</string>
|
||||
|
||||
<!-- Server reachability check (wizard step 3) -->
|
||||
<!-- ── Server reachability check ────────────────────────────────────── -->
|
||||
<string name="wizard_server_checking">Checking server connection…</string>
|
||||
<string name="wizard_server_ok">Server reachable ✅</string>
|
||||
<string name="wizard_server_ok_detail">Error reporting is active — install failures will be sent to GitHub Issues automatically.</string>
|
||||
<string name="wizard_server_error">Server not reachable ⚠️</string>
|
||||
<string name="wizard_server_error_detail">Install errors won\'t reach GitHub Issues. Check the URL entered in step 2.</string>
|
||||
<!-- Screensaver step -->
|
||||
<string name="setup_screensaver_title">Salvaschermo in-app</string>
|
||||
<string name="setup_screensaver_desc">Shows a clock with useful facts after 5 minutes of inactivity. Off by default (screen stays always on).</string>
|
||||
<string name="setup_screensaver_toggle_label">Abilita salvaschermo orologio</string>
|
||||
<string name="setup_screensaver_toggle_hint">Mostra l\'overlay orologio dopo 5 min. Lo schermo resta sempre acceso.</string>
|
||||
|
||||
<!-- Summary -->
|
||||
<!-- ── Step 5 — Features ─────────────────────────────────────────────── -->
|
||||
<string name="setup_features_title">Features</string>
|
||||
<string name="setup_features_desc">Enable the features you want to use. You can always change them later in the server settings.</string>
|
||||
<string name="setup_screensaver_toggle_label">Clock screensaver</string>
|
||||
<string name="setup_screensaver_toggle_hint">Shows a clock overlay after 5 min of inactivity.</string>
|
||||
<string name="setup_prices_toggle_label">Shopping list prices</string>
|
||||
<string name="setup_prices_toggle_hint">AI-powered automatic cost estimate for each item in the list.</string>
|
||||
<string name="setup_mealplan_toggle_label">Meal plan</string>
|
||||
<string name="setup_mealplan_toggle_hint">Plan the week\'s meals with recipes based on your pantry.</string>
|
||||
<string name="setup_zerowaste_toggle_label">Zero-waste tips</string>
|
||||
<string name="setup_zerowaste_toggle_hint">Show tips for reusing scraps (peels, cooking water, etc.) while cooking.</string>
|
||||
|
||||
<!-- ── Step 6 — Gemini AI key ─────────────────────────────────────────── -->
|
||||
<string name="setup_gemini_title">Google Gemini AI</string>
|
||||
<string name="setup_gemini_desc">EverShelf uses Google Gemini AI for recipe suggestions, smart shopping estimates and more.\n\nTo enable it, enter your free Gemini API key below.</string>
|
||||
<string name="setup_gemini_how">Get your free key at: aistudio.google.com → \"Get API key\"</string>
|
||||
<string name="setup_gemini_hint">Paste your API key here (starts with AIza…)</string>
|
||||
|
||||
<!-- ── Step 7 — Bring! credentials ──────────────────────────────────── -->
|
||||
<string name="setup_bring_title">Bring! Shopping List</string>
|
||||
<string name="setup_bring_desc">EverShelf can sync your shopping list with the Bring! app.\n\nEnter your Bring! account credentials to enable this integration.</string>
|
||||
<string name="setup_bring_email_hint">Bring! email address</string>
|
||||
<string name="setup_bring_pass_hint">Bring! password</string>
|
||||
|
||||
<!-- ── Step 8 — Done ─────────────────────────────────────────────────── -->
|
||||
<string name="setup_done_title">All set!</string>
|
||||
<string name="setup_done_desc">Setup is complete. Press the button below to launch EverShelf in kiosk mode.</string>
|
||||
<string name="setup_done_summary_label">CONFIGURATION SUMMARY</string>
|
||||
|
||||
<!-- ── Summary lines ─────────────────────────────────────────────────── -->
|
||||
<string name="summary_lang">Language</string>
|
||||
<string name="summary_scale_skip">Scale: not configured</string>
|
||||
<string name="summary_screensaver_on">Screensaver: enabled</string>
|
||||
<string name="summary_screensaver_off">Screen always on (screensaver disabled)</string>
|
||||
<string name="summary_prices_on">Shopping list prices: enabled</string>
|
||||
<string name="summary_mealplan_on">Meal plan: enabled</string>
|
||||
<string name="summary_zerowaste_on">Zero-waste tips: enabled</string>
|
||||
<string name="summary_gemini_set">Gemini AI: enabled</string>
|
||||
<string name="summary_gemini_skip">Gemini AI: not configured</string>
|
||||
<string name="summary_bring_set">Bring!: connected</string>
|
||||
<string name="summary_bring_skip">Bring!: not configured</string>
|
||||
<string name="ble_connecting_to">🔗 Connecting to %s…</string>
|
||||
<string name="ble_connecting">🔗 Connecting…</string>
|
||||
<string name="summary_scale_ok">Scale: %s</string>
|
||||
<string name="summary_scale_warn">Scale: not confirmed</string>
|
||||
</resources>
|
||||
|
||||
+146
-54
@@ -11,7 +11,7 @@
|
||||
<title>EverShelf</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="icon" type="image/png" href="assets/img/logo/logo_icon.png">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=20260516b">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=20260517a">
|
||||
<!-- QuaggaJS for barcode scanning -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
|
||||
<!-- @xenova/transformers: ES-module bootstrap that exposes a lazy category-classifier as window._categoryPipelinePromise -->
|
||||
@@ -54,8 +54,17 @@
|
||||
<div id="app-preloader" aria-hidden="true">
|
||||
<div class="app-preloader-inner">
|
||||
<img src="assets/img/logo/logo.png" alt="EverShelf" class="app-preloader-logo" />
|
||||
<div class="app-preloader-spinner"></div>
|
||||
<span class="app-preloader-version" id="preloader-version">v1.7.15</span>
|
||||
<div class="app-preloader-spinner" id="preloader-spinner"></div>
|
||||
<div id="preloader-progress-wrap" class="preloader-progress-wrap" style="display:none">
|
||||
<div class="preloader-bar-track">
|
||||
<div id="preloader-bar" class="preloader-bar"></div>
|
||||
</div>
|
||||
<div id="check-ticker" class="check-ticker"></div>
|
||||
</div>
|
||||
<div id="preloader-warnings" class="preloader-warnings" style="display:none"></div>
|
||||
<div id="preloader-error-msg" class="preloader-error-msg" style="display:none"></div>
|
||||
<button id="preloader-retry-btn" class="preloader-retry-btn" style="display:none" onclick="_startupRetry()">🔄 <span data-i18n="startup.retry">Riprova</span></button>
|
||||
<span class="app-preloader-version" id="preloader-version">v1.7.23</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,7 +77,7 @@
|
||||
<!-- Title — left-aligned; grows to fill space -->
|
||||
<div class="header-title-wrap">
|
||||
<h1 class="header-title" onclick="showPage('dashboard')">
|
||||
<img src="assets/img/logo/logo_icon.png" alt="" class="header-logo-icon" aria-hidden="true" /><span data-i18n="nav.title">EverShelf</span><span class="header-version">v1.7.15</span>
|
||||
<img src="assets/img/logo/logo_icon.png" alt="" class="header-logo-icon" aria-hidden="true" /><span data-i18n="nav.title">EverShelf</span><span class="header-version">v1.7.23</span>
|
||||
</h1>
|
||||
<!-- Update badge — shown alongside title, never replaces it -->
|
||||
<span class="header-update-badge" id="header-update-badge" style="display:none"></span>
|
||||
@@ -185,6 +194,7 @@
|
||||
<div class="page-header">
|
||||
<button class="back-btn" onclick="showPage('dashboard')" data-i18n="btn.back">← Indietro</button>
|
||||
<h2 id="inventory-title" data-i18n="inventory.title">Dispensa</h2>
|
||||
<button class="page-header-action-btn" onclick="_showExportModal()" title="Export" data-i18n-title="export.btn_title">📤</button>
|
||||
</div>
|
||||
<div class="location-tabs" id="location-tabs">
|
||||
<button class="tab active" onclick="filterLocation('')" data-loc="" data-i18n="inventory.filter_all">Tutti</button>
|
||||
@@ -821,7 +831,8 @@
|
||||
<h2 data-i18n="settings.title">⚙️ Configurazione</h2>
|
||||
</div>
|
||||
<div class="settings-tabs">
|
||||
<button class="settings-tab active" onclick="switchSettingsTab(this, 'tab-api')" data-tab="tab-api" title="API Keys">🔑</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-bring')" data-tab="tab-bring" title="Bring!">🛒</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>
|
||||
@@ -829,12 +840,108 @@
|
||||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-camera')" data-tab="tab-camera" title="Fotocamera">📷</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-language')" data-tab="tab-language" title="Lingua" data-i18n-title="settings.tab_language">🌐</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-info'); _loadInfoTab();" data-tab="tab-info" data-i18n-title="settings.info.tab" title="Info">ℹ️</button>
|
||||
</div>
|
||||
<div class="settings-panels">
|
||||
<!-- Generali Tab -->
|
||||
<div class="settings-panel active" id="tab-general">
|
||||
<div class="settings-card">
|
||||
<h4 data-i18n="settings.language.title">🌐 Lingua / Language</h4>
|
||||
<p class="settings-hint" data-i18n="settings.language.hint">Seleziona la lingua dell'interfaccia. Select the interface language.</p>
|
||||
<div class="form-group">
|
||||
<select id="setting-language" class="form-input" onchange="changeLanguage(this.value)">
|
||||
</select>
|
||||
<p class="settings-hint mt-2" data-i18n="settings.language.restart_notice">La pagina verrà ricaricata per applicare la nuova lingua.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-card">
|
||||
<h4 data-i18n="settings.info.currency_title">💱 Valuta</h4>
|
||||
<p class="settings-hint" data-i18n="settings.info.currency_hint">La valuta usata per tutti i costi e i prezzi nell'app.</p>
|
||||
<div class="form-group" style="margin-top:8px">
|
||||
<select id="setting-price-currency" class="form-input">
|
||||
<option value="EUR">€ Euro (EUR)</option>
|
||||
<option value="USD">$ Dollaro USA (USD)</option>
|
||||
<option value="GBP">£ Sterlina (GBP)</option>
|
||||
<option value="CHF">CHF Franco Svizzero</option>
|
||||
<option value="CAD">CA$ Dollaro Canadese</option>
|
||||
<option value="AUD">A$ Dollaro Australiano</option>
|
||||
<option value="BRL">R$ Real Brasiliano</option>
|
||||
<option value="JPY">¥ Yen Giapponese</option>
|
||||
<option value="SEK">kr Corona Svedese</option>
|
||||
<option value="NOK">kr Corona Norvegese</option>
|
||||
<option value="DKK">kr Corona Danese</option>
|
||||
<option value="PLN">zł Zloty Polacco</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top:10px">
|
||||
<button class="btn btn-primary" onclick="saveSettings()" data-i18n="btn.save">Salva</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-card">
|
||||
<h4 data-i18n="settings.theme.title">🌙 Tema / Aspetto</h4>
|
||||
<p class="settings-hint" data-i18n="settings.theme.hint">Scegli il tema dell'interfaccia.</p>
|
||||
<div class="form-group">
|
||||
<select id="setting-dark-mode" class="form-input" onchange="_setThemeMode(this.value)">
|
||||
<option value="off" data-i18n="settings.theme.off">☀️ Chiaro</option>
|
||||
<option value="auto" selected data-i18n="settings.theme.auto">🔄 Automatico (orario)</option>
|
||||
<option value="on" data-i18n="settings.theme.on">🌙 Scuro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-card">
|
||||
<h4 data-i18n="settings.screensaver.card_title">🌙 Salvaschermo</h4>
|
||||
<p class="settings-hint" data-i18n="settings.screensaver.card_hint">Mostra un orologio con fatti utili dopo un periodo di inattività. Di default è disattivato.</p>
|
||||
<div class="form-group">
|
||||
<label class="toggle-row">
|
||||
<span data-i18n="settings.screensaver.label">Attiva salvaschermo</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="setting-screensaver-enabled">
|
||||
<span class="toggle-slider"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group" id="screensaver-timeout-row" style="margin-top:10px">
|
||||
<label for="setting-screensaver-timeout" style="font-size:0.85rem;font-weight:600;color:var(--text-secondary)" data-i18n="settings.screensaver.start_after">⏱️ Avvia dopo</label>
|
||||
<select id="setting-screensaver-timeout" class="form-input" style="margin-top:6px;max-width:200px">
|
||||
<option value="1" data-i18n="settings.screensaver.timeout_1">1 minuto</option>
|
||||
<option value="2" data-i18n="settings.screensaver.timeout_2">2 minuti</option>
|
||||
<option value="5" selected data-i18n="settings.screensaver.timeout_5">5 minuti</option>
|
||||
<option value="10" data-i18n="settings.screensaver.timeout_10">10 minuti</option>
|
||||
<option value="15" data-i18n="settings.screensaver.timeout_15">15 minuti</option>
|
||||
<option value="30" data-i18n="settings.screensaver.timeout_30">30 minuti</option>
|
||||
<option value="60" data-i18n="settings.screensaver.timeout_60">1 ora</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-card">
|
||||
<h4 data-i18n="settings.zerowaste.card_title">♻️ Suggerimenti zero-waste</h4>
|
||||
<p class="settings-hint" data-i18n="settings.zerowaste.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.</p>
|
||||
<div class="form-group">
|
||||
<label class="toggle-row">
|
||||
<span data-i18n="settings.zerowaste.label">Mostra suggerimenti durante la cottura</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="setting-zerowaste-tips">
|
||||
<span class="toggle-slider"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-card">
|
||||
<h4 data-i18n="export.title">📤 Esporta inventario</h4>
|
||||
<p class="settings-hint" data-i18n="export.hint">Scarica l'inventario corrente in CSV o apri una versione stampabile (PDF).</p>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
<button class="btn btn-outline" onclick="exportInventory('csv')" style="flex:1;min-width:120px">
|
||||
📊 <span data-i18n="export.btn_csv">CSV</span>
|
||||
</button>
|
||||
<button class="btn btn-outline" onclick="exportInventory('html')" style="flex:1;min-width:120px">
|
||||
🖨️ <span data-i18n="export.btn_pdf">PDF / Stampa</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- API Keys Tab -->
|
||||
<div class="settings-panel active" id="tab-api">
|
||||
<div class="settings-panel" id="tab-api">
|
||||
<div class="settings-card">
|
||||
<h4 data-i18n="settings.gemini.title">🤖 Google Gemini AI</h4>
|
||||
<p class="settings-hint" data-i18n="settings.gemini.hint">Chiave API per identificazione prodotti, scadenze e ricette.</p>
|
||||
@@ -893,23 +1000,6 @@
|
||||
<option value="Japan">🇯🇵 Giappone</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-i18n="settings.price.currency_label">💱 Valuta</label>
|
||||
<select id="setting-price-currency" class="form-input">
|
||||
<option value="EUR">€ Euro (EUR)</option>
|
||||
<option value="USD">$ Dollaro USA (USD)</option>
|
||||
<option value="GBP">£ Sterlina (GBP)</option>
|
||||
<option value="CHF">CHF Franco Svizzero</option>
|
||||
<option value="CAD">CA$ Dollaro Canadese</option>
|
||||
<option value="AUD">A$ Dollaro Australiano</option>
|
||||
<option value="BRL">R$ Real Brasiliano</option>
|
||||
<option value="JPY">¥ Yen Giapponese</option>
|
||||
<option value="SEK">kr Corona Svedese</option>
|
||||
<option value="NOK">kr Corona Norvegese</option>
|
||||
<option value="DKK">kr Corona Danese</option>
|
||||
<option value="PLN">zł Zloty Polacco</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-i18n="settings.price.update_label">🔄 Aggiorna prezzi ogni</label>
|
||||
<div class="qty-control">
|
||||
@@ -1251,40 +1341,36 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- Language Tab -->
|
||||
<div class="settings-panel" id="tab-language">
|
||||
|
||||
<!-- Info Tab -->
|
||||
<div class="settings-panel" id="tab-info">
|
||||
<!-- Gemini AI Usage card -->
|
||||
<div class="settings-card">
|
||||
<h4 data-i18n="settings.language.title">🌐 Lingua / Language</h4>
|
||||
<p class="settings-hint" data-i18n="settings.language.hint">Seleziona la lingua dell'interfaccia. Select the interface language.</p>
|
||||
<div class="form-group">
|
||||
<label data-i18n="settings.language.label">🌐 Lingua</label>
|
||||
<select id="setting-language" class="form-input" onchange="changeLanguage(this.value)">
|
||||
</select>
|
||||
<p class="settings-hint mt-2" data-i18n="settings.language.restart_notice">La pagina verrà ricaricata per applicare la nuova lingua.</p>
|
||||
<h4 data-i18n="settings.info.ai_title">Gemini AI — Token Usage</h4>
|
||||
<p class="settings-hint info-ai-subtitle" data-i18n="settings.info.ai_overview">Utilizzo AI, inventario e sistema</p>
|
||||
<div id="info-ai-content" style="margin-top:10px">
|
||||
<p class="settings-hint" data-i18n="settings.info.loading">Caricamento…</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Inventory card -->
|
||||
<div class="settings-card">
|
||||
<h4 data-i18n="settings.screensaver.card_title">🌙 Salvaschermo</h4>
|
||||
<p class="settings-hint" data-i18n="settings.screensaver.card_hint">Mostra un orologio con fatti utili dopo un periodo di inattività. Di default è disattivato.</p>
|
||||
<div class="form-group">
|
||||
<label class="toggle-row">
|
||||
<span data-i18n="settings.screensaver.label">Attiva salvaschermo</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="setting-screensaver-enabled">
|
||||
<span class="toggle-slider"></span>
|
||||
</span>
|
||||
</label>
|
||||
<h4 data-i18n="settings.info.inv_title">Inventario</h4>
|
||||
<div id="info-inv-content" style="margin-top:10px">
|
||||
<p class="settings-hint" data-i18n="settings.info.loading">Caricamento…</p>
|
||||
</div>
|
||||
<div class="form-group" id="screensaver-timeout-row" style="margin-top:10px">
|
||||
<label for="setting-screensaver-timeout" style="font-size:0.85rem;font-weight:600;color:var(--text-secondary)" data-i18n="settings.screensaver.start_after">⏱️ Avvia dopo</label>
|
||||
<select id="setting-screensaver-timeout" class="form-control" style="margin-top:6px;max-width:200px">
|
||||
<option value="1" data-i18n="settings.screensaver.timeout_1">1 minuto</option>
|
||||
<option value="2" data-i18n="settings.screensaver.timeout_2">2 minuti</option>
|
||||
<option value="5" selected data-i18n="settings.screensaver.timeout_5">5 minuti</option>
|
||||
<option value="10" data-i18n="settings.screensaver.timeout_10">10 minuti</option>
|
||||
<option value="15" data-i18n="settings.screensaver.timeout_15">15 minuti</option>
|
||||
<option value="30" data-i18n="settings.screensaver.timeout_30">30 minuti</option>
|
||||
<option value="60" data-i18n="settings.screensaver.timeout_60">1 ora</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Activity card -->
|
||||
<div class="settings-card">
|
||||
<h4 data-i18n="settings.info.act_title">Attività del mese</h4>
|
||||
<div id="info-act-content" style="margin-top:10px">
|
||||
<p class="settings-hint" data-i18n="settings.info.loading">Caricamento…</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- System Info card -->
|
||||
<div class="settings-card">
|
||||
<h4 data-i18n="settings.info.system_title">Sistema</h4>
|
||||
<div id="info-system-content" style="margin-top:10px">
|
||||
<p class="settings-hint" data-i18n="settings.info.loading">Caricamento…</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1553,6 +1639,12 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="cooking-step-ings" id="cooking-step-ings" style="display:none"></div>
|
||||
<div id="cooking-zerowaste-tip" class="cooking-zerowaste-tip" style="display:none">
|
||||
<span class="cooking-zerowaste-label" data-i18n="cooking.zerowaste_label">♻️ Scarto</span>
|
||||
<span id="cooking-zerowaste-scrap" class="cooking-zerowaste-scrap"></span>
|
||||
<p id="cooking-zerowaste-text" class="cooking-zerowaste-text"></p>
|
||||
<button class="cooking-zerowaste-close" onclick="_dismissZeroWasteTip()" aria-label="Chiudi">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cooking-nav">
|
||||
<button class="cooking-nav-btn cooking-prev-btn" id="cooking-prev" onclick="navigateCookingStep(-1)">◀ Precedente</button>
|
||||
@@ -1560,6 +1652,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/app.js?v=20260516b"></script>
|
||||
<script src="assets/js/app.js?v=20260518c"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# logs/
|
||||
|
||||
This directory contains EverShelf runtime log files.
|
||||
|
||||
Files are generated automatically by `api/logger.php` and follow the naming pattern:
|
||||
|
||||
```
|
||||
evershelf_YYYY-MM-DD_HH.log
|
||||
```
|
||||
|
||||
The directory is tracked in git (via this README) but `.log` files are ignored via `.gitignore`.
|
||||
|
||||
## Configuration (`.env`)
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `LOG_LEVEL` | `INFO` | Minimum log level: `DEBUG`, `INFO`, `WARN`, `ERROR` |
|
||||
| `LOG_ROTATE_HOURS` | `24` | Hours per file before rotating |
|
||||
| `LOG_MAX_FILES` | `14` | Maximum number of rotated files to keep |
|
||||
|
||||
## Format
|
||||
|
||||
```
|
||||
[2026-05-18 14:23:11] [INFO ] [rid=a1b2c3d4] [action] Message {"ctx":"value"}
|
||||
```
|
||||
|
||||
## Remote inspection
|
||||
|
||||
```
|
||||
GET /api/?action=get_logs&lines=100&level=WARN
|
||||
```
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "EverShelf",
|
||||
"short_name": "EverShelf",
|
||||
"description": "Gestione completa della dispensa di casa con scansione barcode",
|
||||
"version": "1.7.15",
|
||||
"version": "1.7.23",
|
||||
"start_url": "/evershelf/",
|
||||
"display": "standalone",
|
||||
"background_color": "#f0f4e8",
|
||||
|
||||
+109
-7
@@ -113,6 +113,8 @@
|
||||
"banner_expired_action_finished": "Habe ich verbraucht!",
|
||||
"banner_expired_action_throw": "Habe ich weggeworfen",
|
||||
"banner_expired_action_edit": "Datum korrigieren",
|
||||
"banner_expired_action_modify": "Bearbeiten",
|
||||
"banner_expired_action_vacuum": "Vakuumieren",
|
||||
"banner_anomaly_action_edit": "Bestand korrigieren",
|
||||
"banner_anomaly_action_dismiss": "Menge ist korrekt",
|
||||
"banner_no_expiry_title": "Ablaufdatum fehlt: {name}",
|
||||
@@ -217,7 +219,7 @@
|
||||
"title": "Was möchtest du tun?",
|
||||
"add_btn": "📥 HINZUFÜGEN",
|
||||
"add_sub": "in Vorrat/Kühlschrank",
|
||||
"use_btn": "📤 VERWENDEN / VERBRAUCHEN",
|
||||
"use_btn": "VERWENDEN",
|
||||
"use_sub": "aus Vorrat/Kühlschrank",
|
||||
"have_title": "📦 Schon auf Lager!",
|
||||
"add_more_sub": "weitere Menge",
|
||||
@@ -500,7 +502,8 @@
|
||||
"undo_success": "↩ Vorgang rückgängig gemacht für {name}",
|
||||
"already_undone": "Vorgang bereits rückgängig gemacht",
|
||||
"too_old": "Vorgänge älter als 24 Stunden können nicht rückgängig gemacht werden",
|
||||
"undo_error": "Fehler beim Rückgängigmachen"
|
||||
"undo_error": "Fehler beim Rückgängigmachen",
|
||||
"recipe_prefix": "Rezept"
|
||||
},
|
||||
"chat": {
|
||||
"title": "Gemini Chef",
|
||||
@@ -532,14 +535,16 @@
|
||||
"prev": "◀ Zurück",
|
||||
"next": "Weiter ▶",
|
||||
"ingredient_used": "✔️ Abgezogen",
|
||||
"ingredient_use_btn": "📦 Verwenden",
|
||||
"ingredient_use_btn": "Usa",
|
||||
"ingredient_deduct_title": "Von Vorrat abziehen",
|
||||
"timer_expired_tts": "Timer {label} abgelaufen!",
|
||||
"timer_warning_tts": "Achtung! {label}: noch 10 Sekunden!",
|
||||
"recipe_done_tts": "Rezept abgeschlossen! Guten Appetit!",
|
||||
"expires_chip": "läuft ab {date}",
|
||||
"finish": "✅ Fertig",
|
||||
"step_fallback": "Schritt {n}"
|
||||
"step_fallback": "Schritt {n}",
|
||||
"zerowaste_label": "♻️ Abfall",
|
||||
"zerowaste_tip_title": "Zero-Waste-Tipp"
|
||||
},
|
||||
"settings": {
|
||||
"title": "⚙️ Einstellungen",
|
||||
@@ -743,7 +748,61 @@
|
||||
},
|
||||
"saved": "✅ Konfiguration gespeichert!",
|
||||
"saved_local": "✅ Konfiguration lokal gespeichert",
|
||||
"saved_local_error": "⚠️ Lokal gespeichert, Serverfehler: {error}"
|
||||
"saved_local_error": "⚠️ Lokal gespeichert, Serverfehler: {error}",
|
||||
"theme": {
|
||||
"title": "🌙 Erscheinungsbild",
|
||||
"hint": "Wähle das Interface-Design.",
|
||||
"label": "🌙 Design",
|
||||
"off": "☀️ Hell",
|
||||
"on": "🌙 Dunkel",
|
||||
"auto": "🔄 Automatisch (Tageszeit)"
|
||||
},
|
||||
"zerowaste": {
|
||||
"card_title": "♻️ Zero-Waste-Tipps",
|
||||
"card_hint": "Zeige während des Kochens Tipps zur Wiederverwendung von Abfällen (Schalen, Kochwasser usw.). Standardmäßig deaktiviert.",
|
||||
"label": "Tipps beim Kochen anzeigen"
|
||||
},
|
||||
"info": {
|
||||
"tab": "Info",
|
||||
"ai_title": "Gemini AI — Token-Nutzung",
|
||||
"ai_hint": "Monatlicher Verbrauch und geschätzte Kosten für den aktuellen API-Schlüssel.",
|
||||
"loading": "Laden…",
|
||||
"total_tokens": "Token gesamt",
|
||||
"est_cost": "Gesch. Kosten",
|
||||
"input_tok": "Eingabe-Token",
|
||||
"output_tok": "Ausgabe-Token",
|
||||
"ai_calls": "Aufrufe",
|
||||
"by_action": "Aufschlüsselung nach Funktion",
|
||||
"by_model": "Aufschlüsselung nach Modell",
|
||||
"pricing_note": "Gemini Referenzpreise: 2.5-flash $0.15/M in · $0.60/M out — 2.0-flash $0.10/M in · $0.40/M out.",
|
||||
"system_title": "System",
|
||||
"db_size": "Datenbank",
|
||||
"log_size": "Protokolle",
|
||||
"log_level": "Log-Level",
|
||||
"ai_overview": "KI-Nutzungsübersicht, Inventar und Systemstatus",
|
||||
"calls_unit": "Aufrufe",
|
||||
"inv_title": "Inventar",
|
||||
"inv_active": "Aktiv",
|
||||
"inv_products": "Produkte gesamt",
|
||||
"inv_expiring": "Ablaufend (7T)",
|
||||
"inv_expired": "Abgelaufen",
|
||||
"inv_finished": "Leer",
|
||||
"act_title": "Monatliche Aktivität",
|
||||
"act_tx_month": "Bewegungen",
|
||||
"act_restock": "Einkäufe",
|
||||
"act_use": "Verbrauch",
|
||||
"act_new_products": "Neue Produkte",
|
||||
"act_tx_year": "Jährl. Bewegungen",
|
||||
"price_cache": "Preiscache",
|
||||
"cache_entries": "Produkte",
|
||||
"last_backup": "Letztes Backup",
|
||||
"bring_days": "Token läuft in {n} Tagen ab",
|
||||
"bring_expired": "Token abgelaufen",
|
||||
"year_label": "Jahr {year}",
|
||||
"currency_title": "Währung",
|
||||
"currency_hint": "Die Währung, die für alle Kosten und Preise in der App verwendet wird."
|
||||
},
|
||||
"tab_general": "Allgemein"
|
||||
},
|
||||
"expiry": {
|
||||
"today": "HEUTE",
|
||||
@@ -815,6 +874,7 @@
|
||||
"thrown_away": "🗑️ {name} weggeworfen!",
|
||||
"thrown_away_partial": "🗑️ {qty} {unit} von {name} weggeworfen",
|
||||
"finished_all": "📤 {name} aufgebraucht!",
|
||||
"vacuum_sealed": "{name} als vakuumversiegelt gespeichert",
|
||||
"product_finished_confirmed": "✅ Entfernt — wieder hinzufügen, wenn du nachkaufst",
|
||||
"appliance_added": "Gerät hinzugefügt",
|
||||
"item_added": "{name} hinzugefügt"
|
||||
@@ -991,8 +1051,8 @@
|
||||
"thing_rest": "den Rest",
|
||||
"stay_btn": "Nein, bleibt in {location}",
|
||||
"moved_toast": "📦 Offene Packung bewegt nach {location}",
|
||||
"vacuum_restore": "🫙 Vakuum wiederherstellen",
|
||||
"vacuum_seal_rest": "🔒 Rest vakuumieren"
|
||||
"vacuum_restore": "Vakuum wiederherstellen",
|
||||
"vacuum_seal_rest": "Rest vakuumieren"
|
||||
},
|
||||
"nova": {
|
||||
"1": "Unverarbeitet",
|
||||
@@ -1178,5 +1238,47 @@
|
||||
"report_bug_error": "Bericht konnte nicht gesendet werden. Verbindung prüfen.",
|
||||
"changelog": "Changelog",
|
||||
"github": "GitHub-Repository"
|
||||
},
|
||||
"export": {
|
||||
"title": "Inventar exportieren",
|
||||
"hint": "Lade das aktuelle Inventar als CSV herunter oder öffne die druckfertige Version (PDF).",
|
||||
"btn_csv": "CSV herunterladen",
|
||||
"btn_pdf": "PDF / Drucken",
|
||||
"btn_title": "Exportieren"
|
||||
},
|
||||
"startup": {
|
||||
"connecting": "Serververbindung wird hergestellt...",
|
||||
"check_php_memory": "PHP-Speicher",
|
||||
"check_php_timeout": "PHP-Timeout",
|
||||
"check_php_upload": "PHP-Upload",
|
||||
"check_data_dir": "Datenverzeichnis",
|
||||
"check_rate_limits": "Rate-Limits-Verzeichnis",
|
||||
"check_backups": "Backup-Verzeichnis",
|
||||
"check_write_test": "Schreibtest",
|
||||
"check_disk_space": "Speicherplatz",
|
||||
"check_db_legacy": "Legacy-DB (dispensa.db)",
|
||||
"check_db_connect": "Datenbankverbindung",
|
||||
"check_db_tables": "Datenbanktabellen",
|
||||
"check_db_integrity": "Datenbankintegrität",
|
||||
"check_db_wal": "WAL-Modus",
|
||||
"check_db_size": "Datenbankgröße",
|
||||
"check_db_rows": "Inventardaten",
|
||||
"check_env": ".env-Datei",
|
||||
"check_gemini": "Gemini-AI-Schlüssel",
|
||||
"check_bring_creds": "Bring!-Anmeldedaten",
|
||||
"check_bring_token": "Bring!-Token",
|
||||
"check_tts": "Text-to-Speech-URL",
|
||||
"check_scale": "Waagen-Gateway",
|
||||
"check_curl_ssl": "cURL-SSL",
|
||||
"check_internet": "Internetverbindung",
|
||||
"fresh_install": "Neuinstallation",
|
||||
"warnings_found": "Warnungen",
|
||||
"all_ok": "System OK",
|
||||
"critical_error_short": "Kritischer Fehler",
|
||||
"critical_error": "Kritischer Fehler: Die App kann nicht gestartet werden. Prüfe die Serverlogs.",
|
||||
"critical_error_intro": "Die App kann aufgrund folgender Probleme nicht gestartet werden:",
|
||||
"error_network": "Server nicht erreichbar.",
|
||||
"error_network_detail": "Der Browser kann den PHP-Server nicht erreichen.\n\nMögliche Ursachen:\n• Apache/PHP-Server läuft nicht\n• Netzwerk- oder Firewall-Problem\n• Falsche App-URL\n\nBitte Server starten und erneut versuchen.",
|
||||
"retry": "Erneut versuchen"
|
||||
}
|
||||
}
|
||||
+107
-6
@@ -113,6 +113,8 @@
|
||||
"banner_expired_action_finished": "I finished it!",
|
||||
"banner_expired_action_throw": "I threw it away",
|
||||
"banner_expired_action_edit": "Fix date",
|
||||
"banner_expired_action_modify": "Edit",
|
||||
"banner_expired_action_vacuum": "Put in vacuum seal",
|
||||
"banner_anomaly_action_edit": "Fix inventory",
|
||||
"banner_anomaly_action_dismiss": "Quantity is correct",
|
||||
"banner_no_expiry_title": "Missing expiry: {name}",
|
||||
@@ -217,7 +219,7 @@
|
||||
"title": "What do you want to do?",
|
||||
"add_btn": "📥 ADD",
|
||||
"add_sub": "to pantry/fridge",
|
||||
"use_btn": "📤 USE / CONSUME",
|
||||
"use_btn": "USE",
|
||||
"use_sub": "from pantry/fridge",
|
||||
"have_title": "📦 Already in stock!",
|
||||
"add_more_sub": "add more",
|
||||
@@ -533,14 +535,16 @@
|
||||
"prev": "◀ Previous",
|
||||
"next": "Next ▶",
|
||||
"ingredient_used": "✔️ Deducted",
|
||||
"ingredient_use_btn": "📦 Use",
|
||||
"ingredient_use_btn": "Use",
|
||||
"ingredient_deduct_title": "Deduct from pantry",
|
||||
"timer_expired_tts": "Timer {label} expired!",
|
||||
"timer_warning_tts": "Heads up! {label}: 10 seconds left!",
|
||||
"recipe_done_tts": "Recipe complete! Enjoy your meal!",
|
||||
"expires_chip": "exp. {date}",
|
||||
"finish": "✅ Finish",
|
||||
"step_fallback": "Step {n}"
|
||||
"step_fallback": "Step {n}",
|
||||
"zerowaste_label": "♻️ Scrap",
|
||||
"zerowaste_tip_title": "Zero-waste tip"
|
||||
},
|
||||
"settings": {
|
||||
"title": "⚙️ Settings",
|
||||
@@ -744,7 +748,61 @@
|
||||
},
|
||||
"saved": "✅ Configuration saved!",
|
||||
"saved_local": "✅ Configuration saved locally",
|
||||
"saved_local_error": "⚠️ Saved locally, server error: {error}"
|
||||
"saved_local_error": "⚠️ Saved locally, server error: {error}",
|
||||
"theme": {
|
||||
"title": "🌙 Appearance",
|
||||
"hint": "Choose the interface theme.",
|
||||
"label": "🌙 Theme",
|
||||
"off": "☀️ Light",
|
||||
"on": "🌙 Dark",
|
||||
"auto": "🔄 Automatic (time of day)"
|
||||
},
|
||||
"zerowaste": {
|
||||
"card_title": "♻️ Zero-waste tips",
|
||||
"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"
|
||||
},
|
||||
"info": {
|
||||
"tab": "Info",
|
||||
"ai_title": "Gemini AI — Token Usage",
|
||||
"ai_hint": "Monthly consumption and estimated cost for the current API key.",
|
||||
"loading": "Loading…",
|
||||
"total_tokens": "Total tokens",
|
||||
"est_cost": "Est. cost",
|
||||
"input_tok": "Input tokens",
|
||||
"output_tok": "Output tokens",
|
||||
"ai_calls": "Calls",
|
||||
"by_action": "Breakdown by function",
|
||||
"by_model": "Breakdown by model",
|
||||
"pricing_note": "Gemini reference pricing: 2.5-flash $0.15/M in · $0.60/M out — 2.0-flash $0.10/M in · $0.40/M out.",
|
||||
"system_title": "System",
|
||||
"db_size": "Database",
|
||||
"log_size": "Logs",
|
||||
"log_level": "Log level",
|
||||
"ai_overview": "AI usage overview, inventory and system status",
|
||||
"calls_unit": "calls",
|
||||
"inv_title": "Inventory",
|
||||
"inv_active": "Active",
|
||||
"inv_products": "Total products",
|
||||
"inv_expiring": "Expiring (7d)",
|
||||
"inv_expired": "Expired",
|
||||
"inv_finished": "Finished",
|
||||
"act_title": "Monthly activity",
|
||||
"act_tx_month": "Movements",
|
||||
"act_restock": "Restocks",
|
||||
"act_use": "Usages",
|
||||
"act_new_products": "New products",
|
||||
"act_tx_year": "Yearly movements",
|
||||
"price_cache": "Price cache",
|
||||
"cache_entries": "products",
|
||||
"last_backup": "Last backup",
|
||||
"bring_days": "token expires in {n} days",
|
||||
"bring_expired": "token expired",
|
||||
"year_label": "Year {year}",
|
||||
"currency_title": "Currency",
|
||||
"currency_hint": "The currency used for all costs and prices in the app."
|
||||
},
|
||||
"tab_general": "General"
|
||||
},
|
||||
"expiry": {
|
||||
"today": "TODAY",
|
||||
@@ -816,6 +874,7 @@
|
||||
"thrown_away": "🗑️ {name} thrown away!",
|
||||
"thrown_away_partial": "🗑️ Thrown away {qty} {unit} of {name}",
|
||||
"finished_all": "📤 {name} finished!",
|
||||
"vacuum_sealed": "{name} saved as vacuum sealed",
|
||||
"product_finished_confirmed": "✅ Removed — add it again when you restock",
|
||||
"appliance_added": "Appliance added",
|
||||
"item_added": "{name} added"
|
||||
@@ -992,8 +1051,8 @@
|
||||
"thing_rest": "rest",
|
||||
"stay_btn": "No, stay in {location}",
|
||||
"moved_toast": "📦 Opened package moved to {location}",
|
||||
"vacuum_restore": "🫙 Restore vacuum sealed",
|
||||
"vacuum_seal_rest": "🔒 Vacuum seal the rest"
|
||||
"vacuum_restore": "Restore vacuum sealed",
|
||||
"vacuum_seal_rest": "Vacuum seal the rest"
|
||||
},
|
||||
"nova": {
|
||||
"1": "Unprocessed",
|
||||
@@ -1179,5 +1238,47 @@
|
||||
"report_bug_error": "Could not send the report. Check your connection.",
|
||||
"changelog": "Changelog",
|
||||
"github": "GitHub Repository"
|
||||
},
|
||||
"export": {
|
||||
"title": "Export inventory",
|
||||
"hint": "Download the current inventory as CSV or open a print-ready version (PDF).",
|
||||
"btn_csv": "Download CSV",
|
||||
"btn_pdf": "PDF / Print",
|
||||
"btn_title": "Export"
|
||||
},
|
||||
"startup": {
|
||||
"connecting": "Connecting to server...",
|
||||
"check_php_memory": "PHP memory",
|
||||
"check_php_timeout": "PHP timeout",
|
||||
"check_php_upload": "PHP upload",
|
||||
"check_data_dir": "Data directory",
|
||||
"check_rate_limits": "Rate limits dir",
|
||||
"check_backups": "Backup dir",
|
||||
"check_write_test": "Disk write test",
|
||||
"check_disk_space": "Disk space",
|
||||
"check_db_legacy": "Legacy DB (dispensa.db)",
|
||||
"check_db_connect": "Database connection",
|
||||
"check_db_tables": "Database tables",
|
||||
"check_db_integrity": "Database integrity",
|
||||
"check_db_wal": "WAL mode",
|
||||
"check_db_size": "Database size",
|
||||
"check_db_rows": "Inventory data",
|
||||
"check_env": ".env file",
|
||||
"check_gemini": "Gemini AI key",
|
||||
"check_bring_creds": "Bring! credentials",
|
||||
"check_bring_token": "Bring! token",
|
||||
"check_tts": "Text-to-Speech URL",
|
||||
"check_scale": "Scale gateway",
|
||||
"check_curl_ssl": "cURL SSL",
|
||||
"check_internet": "Internet connection",
|
||||
"fresh_install": "fresh install",
|
||||
"warnings_found": "warnings found",
|
||||
"all_ok": "System OK",
|
||||
"critical_error_short": "Critical error",
|
||||
"critical_error": "Critical error: the app cannot start. Check your server logs.",
|
||||
"critical_error_intro": "The app cannot start due to the following issues:",
|
||||
"error_network": "Cannot reach the server.",
|
||||
"error_network_detail": "The browser cannot reach the PHP server.\n\nPossible causes:\n• Apache/PHP server is not running\n• Network or firewall issue\n• Incorrect app URL\n\nMake sure the server is started and try again.",
|
||||
"retry": "Retry"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+107
-6
@@ -113,6 +113,8 @@
|
||||
"banner_expired_action_finished": "L'ho finito!",
|
||||
"banner_expired_action_throw": "L'ho buttato",
|
||||
"banner_expired_action_edit": "Correggi data",
|
||||
"banner_expired_action_modify": "Modifica",
|
||||
"banner_expired_action_vacuum": "Metti sottovuoto",
|
||||
"banner_anomaly_action_edit": "Correggi inventario",
|
||||
"banner_anomaly_action_dismiss": "La quantità è giusta",
|
||||
"banner_no_expiry_title": "Scadenza mancante: {name}",
|
||||
@@ -217,7 +219,7 @@
|
||||
"title": "Cosa vuoi fare?",
|
||||
"add_btn": "📥 AGGIUNGI",
|
||||
"add_sub": "in dispensa/frigo",
|
||||
"use_btn": "📤 USA / CONSUMA",
|
||||
"use_btn": "USA",
|
||||
"use_sub": "dalla dispensa/frigo",
|
||||
"have_title": "📦 Ce l'hai già!",
|
||||
"add_more_sub": "altra quantità",
|
||||
@@ -533,14 +535,16 @@
|
||||
"prev": "◀ Precedente",
|
||||
"next": "Successivo ▶",
|
||||
"ingredient_used": "✔️ Scalato",
|
||||
"ingredient_use_btn": "📦 Usa",
|
||||
"ingredient_use_btn": "Usa",
|
||||
"ingredient_deduct_title": "Scala dalla dispensa",
|
||||
"timer_expired_tts": "Timer {label} scaduto!",
|
||||
"timer_warning_tts": "Attenzione! {label}: mancano 10 secondi!",
|
||||
"recipe_done_tts": "Ricetta completata! Buon appetito!",
|
||||
"expires_chip": "scade {date}",
|
||||
"finish": "✅ Fine",
|
||||
"step_fallback": "Passo {n}"
|
||||
"step_fallback": "Passo {n}",
|
||||
"zerowaste_label": "♻️ Scarto",
|
||||
"zerowaste_tip_title": "Consiglio anti-spreco"
|
||||
},
|
||||
"settings": {
|
||||
"title": "⚙️ Configurazione",
|
||||
@@ -744,7 +748,61 @@
|
||||
},
|
||||
"saved": "✅ Configurazione salvata!",
|
||||
"saved_local": "✅ Configurazione salvata localmente",
|
||||
"saved_local_error": "⚠️ Salvato localmente, errore server: {error}"
|
||||
"saved_local_error": "⚠️ Salvato localmente, errore server: {error}",
|
||||
"theme": {
|
||||
"title": "🌙 Tema / Aspetto",
|
||||
"hint": "Scegli il tema dell interfaccia.",
|
||||
"label": "🌙 Tema",
|
||||
"off": "☀️ Chiaro",
|
||||
"on": "🌙 Scuro",
|
||||
"auto": "🔄 Automatico (orario)"
|
||||
},
|
||||
"zerowaste": {
|
||||
"card_title": "♻️ Suggerimenti zero-waste",
|
||||
"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"
|
||||
},
|
||||
"info": {
|
||||
"tab": "Info",
|
||||
"ai_title": "Gemini AI — Utilizzo Token",
|
||||
"ai_hint": "Consumo mensile e costo stimato per la chiave API corrente.",
|
||||
"loading": "Caricamento…",
|
||||
"total_tokens": "Token totali",
|
||||
"est_cost": "Costo stimato",
|
||||
"input_tok": "Token input",
|
||||
"output_tok": "Token output",
|
||||
"ai_calls": "Chiamate",
|
||||
"by_action": "Dettaglio per funzione",
|
||||
"by_model": "Dettaglio per modello",
|
||||
"pricing_note": "Prezzi di riferimento Gemini: 2.5-flash $0.15/M in · $0.60/M out — 2.0-flash $0.10/M in · $0.40/M out.",
|
||||
"system_title": "Sistema",
|
||||
"db_size": "Database",
|
||||
"log_size": "Log",
|
||||
"log_level": "Livello log",
|
||||
"ai_overview": "Prospetto utilizzo AI, inventario e stato del sistema",
|
||||
"calls_unit": "call",
|
||||
"inv_title": "Inventario",
|
||||
"inv_active": "Attivi",
|
||||
"inv_products": "Prodotti totali",
|
||||
"inv_expiring": "In scadenza (7gg)",
|
||||
"inv_expired": "Scaduti",
|
||||
"inv_finished": "Finiti",
|
||||
"act_title": "Attività del mese",
|
||||
"act_tx_month": "Movimenti",
|
||||
"act_restock": "Acquisti",
|
||||
"act_use": "Consumi",
|
||||
"act_new_products": "Nuovi prodotti",
|
||||
"act_tx_year": "Movimenti anno",
|
||||
"price_cache": "Cache prezzi",
|
||||
"cache_entries": "prodotti",
|
||||
"last_backup": "Ultimo backup",
|
||||
"bring_days": "token scade tra {n} giorni",
|
||||
"bring_expired": "token scaduto",
|
||||
"year_label": "Anno {year}",
|
||||
"currency_title": "Valuta",
|
||||
"currency_hint": "La valuta usata per tutti i costi e i prezzi nell'app."
|
||||
},
|
||||
"tab_general": "Generali"
|
||||
},
|
||||
"expiry": {
|
||||
"today": "OGGI",
|
||||
@@ -816,6 +874,7 @@
|
||||
"thrown_away": "🗑️ {name} buttato!",
|
||||
"thrown_away_partial": "🗑️ Buttato {qty} {unit} di {name}",
|
||||
"finished_all": "📤 {name} terminato!",
|
||||
"vacuum_sealed": "{name} salvato come sottovuoto",
|
||||
"product_finished_confirmed": "✅ Rimosso — riaggiungi quando ne ricompri",
|
||||
"appliance_added": "Elettrodomestico aggiunto",
|
||||
"item_added": "{name} aggiunto"
|
||||
@@ -992,8 +1051,8 @@
|
||||
"thing_rest": "il resto",
|
||||
"stay_btn": "No, resta in {location}",
|
||||
"moved_toast": "📦 Confezione aperta spostata in {location}",
|
||||
"vacuum_restore": "🫙 Torna sotto vuoto",
|
||||
"vacuum_seal_rest": "🔒 Metti sotto vuoto il resto"
|
||||
"vacuum_restore": "Torna sotto vuoto",
|
||||
"vacuum_seal_rest": "Metti sotto vuoto il resto"
|
||||
},
|
||||
"nova": {
|
||||
"1": "Non trasformato",
|
||||
@@ -1179,5 +1238,47 @@
|
||||
"report_bug_error": "Impossibile inviare la segnalazione. Controlla la connessione.",
|
||||
"changelog": "Changelog",
|
||||
"github": "Repository GitHub"
|
||||
},
|
||||
"export": {
|
||||
"title": "Esporta inventario",
|
||||
"hint": "Scarica l inventario corrente in CSV o apri la versione stampabile (PDF).",
|
||||
"btn_csv": "Scarica CSV",
|
||||
"btn_pdf": "PDF / Stampa",
|
||||
"btn_title": "Esporta"
|
||||
},
|
||||
"startup": {
|
||||
"connecting": "Connessione al server...",
|
||||
"check_php_memory": "Memoria PHP",
|
||||
"check_php_timeout": "Timeout PHP",
|
||||
"check_php_upload": "Upload PHP",
|
||||
"check_data_dir": "Cartella dati",
|
||||
"check_rate_limits": "Dir rate limits",
|
||||
"check_backups": "Dir backup",
|
||||
"check_write_test": "Test scrittura disco",
|
||||
"check_disk_space": "Spazio disco",
|
||||
"check_db_legacy": "DB legacy (dispensa.db)",
|
||||
"check_db_connect": "Connessione database",
|
||||
"check_db_tables": "Tabelle database",
|
||||
"check_db_integrity": "Integrità database",
|
||||
"check_db_wal": "WAL mode",
|
||||
"check_db_size": "Dimensione database",
|
||||
"check_db_rows": "Dati inventario",
|
||||
"check_env": "File .env",
|
||||
"check_gemini": "Chiave Gemini AI",
|
||||
"check_bring_creds": "Credenziali Bring!",
|
||||
"check_bring_token": "Token Bring!",
|
||||
"check_tts": "URL Text-to-Speech",
|
||||
"check_scale": "Gateway bilancia",
|
||||
"check_curl_ssl": "cURL SSL",
|
||||
"check_internet": "Connessione internet",
|
||||
"fresh_install": "nuovo impianto",
|
||||
"warnings_found": "avvisi rilevati",
|
||||
"all_ok": "Sistema OK",
|
||||
"critical_error_short": "Errore critico",
|
||||
"critical_error": "Errore critico: l'app non può avviarsi. Controlla i log del server.",
|
||||
"critical_error_intro": "L'app non può avviarsi a causa dei seguenti problemi:",
|
||||
"error_network": "Impossibile contattare il server.",
|
||||
"error_network_detail": "Il browser non riesce a raggiungere il server PHP.\n\nPossibili cause:\n• Il server Apache/PHP non è in esecuzione\n• Problema di rete o firewall\n• URL dell'app non corretta\n\nControlla che il server sia avviato e riprova.",
|
||||
"retry": "Riprova"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user