Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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,4 @@ data/error_reports.log
|
||||
data/latest_release_cache.json
|
||||
data/food_facts_cache.json
|
||||
data/category_ai_cache.json
|
||||
assets/img/logo/*_backup.*
|
||||
|
||||
@@ -20,11 +20,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
|
||||
|
||||
|
||||
@@ -352,35 +352,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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+25
-5
@@ -2273,6 +2273,12 @@ function getServerSettings(): void {
|
||||
'tts_content_type' => env('TTS_CONTENT_TYPE', 'application/json'),
|
||||
'tts_payload_key' => env('TTS_PAYLOAD_KEY', 'message'),
|
||||
'tts_enabled' => env('TTS_ENABLED', 'false') === 'true',
|
||||
'tts_engine' => env('TTS_ENGINE', ''),
|
||||
'tts_rate' => (float)env('TTS_RATE', '1'),
|
||||
'tts_pitch' => (float)env('TTS_PITCH', '1'),
|
||||
'tts_auth_header_name' => env('TTS_AUTH_HEADER_NAME', ''),
|
||||
'tts_auth_header_value' => env('TTS_AUTH_HEADER_VALUE', ''),
|
||||
'tts_extra_fields' => env('TTS_EXTRA_FIELDS', ''),
|
||||
// User preferences (now server-side)
|
||||
'default_persons' => intval(env('DEFAULT_PERSONS', '1')),
|
||||
'pref_veloce' => env('PREF_VELOCE', 'false') === 'true',
|
||||
@@ -2323,11 +2329,15 @@ function saveSettings(): void {
|
||||
'tts_auth_type' => 'TTS_AUTH_TYPE',
|
||||
'tts_content_type'=> 'TTS_CONTENT_TYPE',
|
||||
'tts_payload_key' => 'TTS_PAYLOAD_KEY',
|
||||
'camera_facing' => 'CAMERA_FACING',
|
||||
'dietary' => 'DIETARY',
|
||||
'scale_gateway_url' => 'SCALE_GATEWAY_URL',
|
||||
'price_country' => 'PRICE_COUNTRY',
|
||||
'price_currency' => 'PRICE_CURRENCY',
|
||||
'camera_facing' => 'CAMERA_FACING',
|
||||
'dietary' => 'DIETARY',
|
||||
'scale_gateway_url' => 'SCALE_GATEWAY_URL',
|
||||
'price_country' => 'PRICE_COUNTRY',
|
||||
'price_currency' => 'PRICE_CURRENCY',
|
||||
'tts_engine' => 'TTS_ENGINE',
|
||||
'tts_auth_header_name' => 'TTS_AUTH_HEADER_NAME',
|
||||
'tts_auth_header_value' => 'TTS_AUTH_HEADER_VALUE',
|
||||
'tts_extra_fields' => 'TTS_EXTRA_FIELDS',
|
||||
];
|
||||
// Boolean keys
|
||||
$boolMap = [
|
||||
@@ -2349,6 +2359,11 @@ function saveSettings(): void {
|
||||
'screensaver_timeout' => 'SCREENSAVER_TIMEOUT',
|
||||
'price_update_months' => 'PRICE_UPDATE_MONTHS',
|
||||
];
|
||||
// Float keys
|
||||
$floatMap = [
|
||||
'tts_rate' => 'TTS_RATE',
|
||||
'tts_pitch' => 'TTS_PITCH',
|
||||
];
|
||||
|
||||
foreach ($keyMap as $inKey => $envKey) {
|
||||
if (array_key_exists($inKey, $input)) {
|
||||
@@ -2365,6 +2380,11 @@ function saveSettings(): void {
|
||||
$envVars[$envKey] = (string)intval($input[$inKey]);
|
||||
}
|
||||
}
|
||||
foreach ($floatMap as $inKey => $envKey) {
|
||||
if (array_key_exists($inKey, $input)) {
|
||||
$envVars[$envKey] = (string)(float)$input[$inKey];
|
||||
}
|
||||
}
|
||||
// Arrays stored as comma-separated
|
||||
if (array_key_exists('appliances', $input)) {
|
||||
$envVars['APPLIANCES'] = is_array($input['appliances']) ? implode(',', $input['appliances']) : (string)$input['appliances'];
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.7 MiB |
@@ -279,6 +279,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);
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
+101
-7
@@ -2038,12 +2038,25 @@ async function syncSettingsFromDB() {
|
||||
// Primary: load from server .env (only when not already done via _applySyncedSettings)
|
||||
const serverSettings = await api('get_settings');
|
||||
_applySyncedSettings(serverSettings);
|
||||
// Also load review_confirmed from DB
|
||||
// Also load review_confirmed, meal_plan, tts_voice from DB (cross-device shared)
|
||||
const res = await api('app_settings_get');
|
||||
if (res.success && res.settings) {
|
||||
if (res.settings.review_confirmed) {
|
||||
_reviewConfirmedCache = res.settings.review_confirmed;
|
||||
}
|
||||
// meal_plan is stored in SQLite app_settings so all devices stay in sync
|
||||
if (res.settings.meal_plan) {
|
||||
const s = getSettings();
|
||||
s.meal_plan = res.settings.meal_plan;
|
||||
_settingsCache = s;
|
||||
localStorage.setItem('evershelf_settings', JSON.stringify(s));
|
||||
if (document.getElementById('meal-plan-grid')) renderMealPlanEditor();
|
||||
}
|
||||
// tts_voice preference (best-effort cross-device — falls back if voice unavailable)
|
||||
if (res.settings.tts_voice) {
|
||||
const s = getSettings();
|
||||
if (!s.tts_voice) { s.tts_voice = res.settings.tts_voice; _settingsCache = s; localStorage.setItem('evershelf_settings', JSON.stringify(s)); }
|
||||
}
|
||||
}
|
||||
} catch(e) { /* offline, use local */ }
|
||||
}
|
||||
@@ -2064,6 +2077,7 @@ function _applySyncedSettings(serverSettings) {
|
||||
'camera_facing','scale_enabled','scale_gateway_url',
|
||||
'meal_plan_enabled','tts_enabled','tts_url','tts_token',
|
||||
'tts_method','tts_auth_type','tts_content_type','tts_payload_key',
|
||||
'tts_engine','tts_rate','tts_pitch','tts_auth_header_name','tts_auth_header_value','tts_extra_fields',
|
||||
'screensaver_enabled','screensaver_timeout',
|
||||
'price_enabled','price_country','price_currency','price_update_months'];
|
||||
let changed = false;
|
||||
@@ -2599,6 +2613,53 @@ function _injectKioskOverlay() {
|
||||
headerLeft.appendChild(wrap);
|
||||
}
|
||||
|
||||
const _APPLIANCE_KEY_MAP = {
|
||||
'forno': 'settings.appliances.oven',
|
||||
'oven': 'settings.appliances.oven',
|
||||
'backofen': 'settings.appliances.oven',
|
||||
'microonde': 'settings.appliances.microwave',
|
||||
'microwave': 'settings.appliances.microwave',
|
||||
'mikrowelle': 'settings.appliances.microwave',
|
||||
'friggitrice ad aria': 'settings.appliances.air_fryer',
|
||||
'air fryer': 'settings.appliances.air_fryer',
|
||||
'heißluftfritteuse': 'settings.appliances.air_fryer',
|
||||
'macchina del pane': 'settings.appliances.bread_maker',
|
||||
'macchina pane': 'settings.appliances.bread_maker',
|
||||
'bread maker': 'settings.appliances.bread_maker',
|
||||
'bread machine': 'settings.appliances.bread_maker',
|
||||
'brotbackmaschine': 'settings.appliances.bread_maker',
|
||||
'brotbackautomat': 'settings.appliances.bread_maker',
|
||||
'bimby/moulinex cookeo': 'settings.appliances.bimby',
|
||||
'moulinex cookeo': 'settings.appliances.bimby',
|
||||
'bimby/cookeo': 'settings.appliances.bimby',
|
||||
'bimby': 'settings.appliances.bimby',
|
||||
'thermomix': 'settings.appliances.bimby',
|
||||
'thermomix/cookeo': 'settings.appliances.bimby',
|
||||
'planetaria': 'settings.appliances.mixer',
|
||||
'stand mixer': 'settings.appliances.mixer',
|
||||
'küchenmaschine': 'settings.appliances.mixer',
|
||||
'vaporiera': 'settings.appliances.steamer',
|
||||
'steamer': 'settings.appliances.steamer',
|
||||
'dampfgarer': 'settings.appliances.steamer',
|
||||
'pentola a pressione': 'settings.appliances.pressure_cooker',
|
||||
'pentola pressione': 'settings.appliances.pressure_cooker',
|
||||
'pressure cooker': 'settings.appliances.pressure_cooker',
|
||||
'schnellkochtopf': 'settings.appliances.pressure_cooker',
|
||||
'tostapane': 'settings.appliances.toaster',
|
||||
'toaster': 'settings.appliances.toaster',
|
||||
'frullatore/mixer': 'settings.appliances.blender',
|
||||
'frullatore': 'settings.appliances.blender',
|
||||
'blender': 'settings.appliances.blender',
|
||||
'mixer': 'settings.appliances.blender',
|
||||
};
|
||||
|
||||
function _applianceDisplayName(name) {
|
||||
const key = _APPLIANCE_KEY_MAP[name.toLowerCase().trim()];
|
||||
if (!key) return name;
|
||||
// Strip leading emoji/symbols from the translated button label (e.g. "🔥 Oven" → "Oven")
|
||||
return t(key).replace(/^[^\p{L}]+/u, '').trim() || name;
|
||||
}
|
||||
|
||||
function renderAppliances(appliances) {
|
||||
const container = document.getElementById('appliances-list');
|
||||
if (!appliances || appliances.length === 0) {
|
||||
@@ -2607,8 +2668,8 @@ function renderAppliances(appliances) {
|
||||
}
|
||||
container.innerHTML = appliances.map((a, i) => `
|
||||
<div class="appliance-item">
|
||||
<span>🔌 ${escapeHtml(a)}</span>
|
||||
<button class="appliance-remove" onclick="removeAppliance(${i})" title="Rimuovi">✕</button>
|
||||
<span>🔌 ${escapeHtml(_applianceDisplayName(a))}</span>
|
||||
<button class="appliance-remove" onclick="removeAppliance(${i})" title="${t('btn.delete')}">✕</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
@@ -2657,7 +2718,7 @@ function addApplianceQuick(name) {
|
||||
s.appliances.push(name);
|
||||
saveSettingsToStorage(s);
|
||||
renderAppliances(s.appliances);
|
||||
showToast(`${name} aggiunto`, 'success');
|
||||
showToast(t('toast.appliance_added'), 'success');
|
||||
}
|
||||
|
||||
function removeAppliance(idx) {
|
||||
@@ -2670,7 +2731,9 @@ function removeAppliance(idx) {
|
||||
|
||||
async function saveSettings() {
|
||||
const s = getSettings();
|
||||
s.gemini_key = document.getElementById('setting-gemini-key').value.trim();
|
||||
// Only update gemini_key if user actually typed something; preserve existing key otherwise
|
||||
const _newGeminiKey = document.getElementById('setting-gemini-key').value.trim();
|
||||
if (_newGeminiKey) s.gemini_key = _newGeminiKey;
|
||||
s.bring_email = document.getElementById('setting-bring-email').value.trim();
|
||||
s.bring_password = document.getElementById('setting-bring-password').value.trim();
|
||||
s.default_persons = parseInt(document.getElementById('setting-default-persons').value) || 1;
|
||||
@@ -2765,6 +2828,12 @@ async function saveSettings() {
|
||||
tts_auth_type: s.tts_auth_type,
|
||||
tts_content_type: s.tts_content_type,
|
||||
tts_payload_key: s.tts_payload_key,
|
||||
tts_engine: s.tts_engine || '',
|
||||
tts_rate: s.tts_rate || 1,
|
||||
tts_pitch: s.tts_pitch || 1,
|
||||
tts_auth_header_name: s.tts_auth_header_name || '',
|
||||
tts_auth_header_value: s.tts_auth_header_value || '',
|
||||
tts_extra_fields: s.tts_extra_fields || '',
|
||||
price_enabled: s.price_enabled,
|
||||
price_country: s.price_country,
|
||||
price_currency: s.price_currency,
|
||||
@@ -2790,6 +2859,21 @@ async function saveSettings() {
|
||||
statusEl.style.display = 'block';
|
||||
setTimeout(() => statusEl.style.display = 'none', 4000);
|
||||
}
|
||||
// Re-sync _geminiAvailable after save (key may have been set/confirmed on server)
|
||||
try {
|
||||
const refreshed = await api('get_settings');
|
||||
if (refreshed && refreshed.gemini_key_set !== undefined) {
|
||||
_geminiAvailable = !!(refreshed.gemini_key_set);
|
||||
_updateGeminiButtonState();
|
||||
}
|
||||
} catch(e) {}
|
||||
// Persist meal_plan and tts_voice to SQLite for cross-device sync
|
||||
try {
|
||||
const appData = {};
|
||||
if (s.meal_plan) appData.meal_plan = s.meal_plan;
|
||||
if (s.tts_voice) appData.tts_voice = s.tts_voice;
|
||||
if (Object.keys(appData).length) await api('app_settings_save', {}, 'POST', { settings: appData });
|
||||
} catch(e) {}
|
||||
// Re-init screensaver watcher in case it was just enabled
|
||||
initInactivityWatcher();
|
||||
}
|
||||
@@ -11413,6 +11497,8 @@ function selectMealPlanType(dow, slot, typeId) {
|
||||
saveSettingsToStorage(s);
|
||||
closeMealPlanPicker();
|
||||
renderMealPlanEditor();
|
||||
// Persist to server for cross-device sync
|
||||
api('app_settings_save', {}, 'POST', { settings: { meal_plan: s.meal_plan } }).catch(() => {});
|
||||
}
|
||||
function resetMealPlan() {
|
||||
const s = getSettings();
|
||||
@@ -11420,6 +11506,7 @@ function resetMealPlan() {
|
||||
saveSettingsToStorage(s);
|
||||
renderMealPlanEditor();
|
||||
showToast(t('meal_plan.reset_success'), 'success');
|
||||
api('app_settings_save', {}, 'POST', { settings: { meal_plan: s.meal_plan } }).catch(() => {});
|
||||
}
|
||||
|
||||
// ===== RECIPE GENERATION =====
|
||||
@@ -12576,7 +12663,10 @@ function _initBrowserTtsVoices(selectedVoice) {
|
||||
const populate = () => {
|
||||
let voices = [];
|
||||
try {
|
||||
voices = (window.speechSynthesis.getVoices() || []).filter(v => v != null && v.lang);
|
||||
voices = (window.speechSynthesis.getVoices() || []).filter(v => {
|
||||
try { return v != null && typeof v.lang === 'string' && v.lang.length > 0; }
|
||||
catch (_) { return false; }
|
||||
});
|
||||
} catch (_) { return false; }
|
||||
if (!voices.length) return false;
|
||||
// Italian voices first, then others
|
||||
@@ -14186,6 +14276,8 @@ function initSpesaMode() {
|
||||
if (!btn) return;
|
||||
|
||||
btn.addEventListener('pointerdown', (e) => {
|
||||
e.preventDefault(); // prevent browser-generated synthetic click + 300ms delay
|
||||
btn.setPointerCapture(e.pointerId); // ensure pointerup always fires on this element even if finger drifts
|
||||
_longPressTimer = setTimeout(() => {
|
||||
_longPressTimer = null;
|
||||
startSpesaMode();
|
||||
@@ -14199,12 +14291,14 @@ function initSpesaMode() {
|
||||
showPage('scan');
|
||||
}
|
||||
});
|
||||
btn.addEventListener('pointerleave', () => {
|
||||
btn.addEventListener('pointercancel', () => {
|
||||
// OS cancelled gesture (e.g. home swipe) — discard timer, do nothing
|
||||
if (_longPressTimer) {
|
||||
clearTimeout(_longPressTimer);
|
||||
_longPressTimer = null;
|
||||
}
|
||||
});
|
||||
// Note: no pointerleave handler needed — setPointerCapture prevents it from firing during touch
|
||||
}
|
||||
|
||||
function startSpesaMode() {
|
||||
|
||||
@@ -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 = 16
|
||||
versionName = "1.7.15"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user