Compare commits

...

40 Commits

Author SHA1 Message Date
dadaloop82 45dc79e5b7 chore(kiosk): trigger CI build for v1.7.14 with openNativeSettings 2026-05-16 13:32:28 +00:00
dadaloop82 8508993441 chore(kiosk): trigger CI build for v1.7.14 2026-05-16 13:32:03 +00:00
dadaloop82 a3147d704e chore: bump to v1.7.14 — kiosk versionCode 15, CHANGELOG 2026-05-16 13:31:54 +00:00
dadaloop82 834d8efab4 chore: bump to v1.7.14 — kiosk versionCode 15, CHANGELOG 2026-05-16 13:31:31 +00:00
github-actions[bot] 8894a5a2c7 chore: auto-merge develop → main
Triggered by: 5f4c29b feat: in-app bug report form (replaces GitHub link)
2026-05-16 13:27:29 +00:00
dadaloop82 5f4c29bd5a feat: in-app bug report form (replaces GitHub link) 2026-05-16 13:25:51 +00:00
github-actions[bot] 460875430b chore: auto-merge develop → main
Triggered by: 8a596cb fix: openNativeSettings uses try/catch instead of fragile typeof check
2026-05-16 13:19:44 +00:00
dadaloop82 8a596cb7d8 fix: openNativeSettings uses try/catch instead of fragile typeof check 2026-05-16 13:18:07 +00:00
github-actions[bot] 99b8953ccf chore: auto-merge develop → main
Triggered by: c87d7d2 fix: bump manifest.json version to 1.7.13 (was showing false update badge)
2026-05-16 13:14:25 +00:00
dadaloop82 c87d7d2cde fix: bump manifest.json version to 1.7.13 (was showing false update badge) 2026-05-16 13:12:49 +00:00
github-actions[bot] 424fc7bbe3 chore: auto-merge develop → main
Triggered by: 61a2372 feat(kiosk): add native settings shortcut in webapp settings page
2026-05-16 13:09:08 +00:00
dadaloop82 61a2372caa feat(kiosk): add native settings shortcut in webapp settings page 2026-05-16 13:07:29 +00:00
github-actions[bot] ad9be3b705 chore: auto-merge develop → main
Triggered by: bd8dc05 fix(kiosk): restore native settings gear — remove JS ⚙️ (opens wrong settings), restore visibility on modal close
2026-05-16 13:04:28 +00:00
dadaloop82 bd8dc0501a fix(kiosk): restore native settings gear — remove JS ⚙️ (opens wrong settings), restore visibility on modal close 2026-05-16 13:02:49 +00:00
github-actions[bot] c9a6f8ec42 chore: auto-merge develop → main
Triggered by: 0afdf60 fix(kiosk): settings gear lost when Kotlin pre-injects #_kiosk_overlay before JS runs
2026-05-16 12:59:52 +00:00
dadaloop82 0afdf60d38 fix(kiosk): settings gear lost when Kotlin pre-injects #_kiosk_overlay before JS runs 2026-05-16 12:58:10 +00:00
dadaloop82 6ab1da4bd5 ci(kiosk): trigger APK build — versionName 1.7.13 fix 2026-05-16 12:51:43 +00:00
dadaloop82 1566e32a85 ci(kiosk): trigger APK build for v1.7.13 (versionName fix) 2026-05-16 12:50:59 +00:00
github-actions[bot] fe7a047656 chore: auto-merge develop → main
Triggered by: 9c285b4 fix(tts): guard getVoices() against browser extension crash (Brave anti-fingerprinting, issue #61)
2026-05-16 12:48:12 +00:00
dadaloop82 9c285b426f fix(tts): guard getVoices() against browser extension crash (Brave anti-fingerprinting, issue #61) 2026-05-16 12:46:31 +00:00
github-actions[bot] c58705f35c chore: auto-merge develop → main
Triggered by: 8d87494 fix(kiosk): versionName 1.7.2→1.7.13, versionCode 13→14 (stops false update loop)
2026-05-16 12:44:27 +00:00
dadaloop82 8d874944b5 fix(kiosk): versionName 1.7.2→1.7.13, versionCode 13→14 (stops false update loop) 2026-05-16 12:42:46 +00:00
github-actions[bot] b6f85b8e29 chore: auto-merge develop → main
Triggered by: 68693e7 fix(expiry): sealed potatoes shelf life 14→30 days (aligns with JS)
2026-05-16 12:33:04 +00:00
dadaloop82 68693e7168 fix(expiry): sealed potatoes shelf life 14→30 days (aligns with JS) 2026-05-16 12:31:26 +00:00
github-actions[bot] 84c3bb6e4c chore: auto-merge develop → main
Triggered by: d8aec91 fix(cooking): extract tools from step text as fallback for old cached recipes
2026-05-16 10:02:40 +00:00
dadaloop82 d8aec91599 fix(cooking): extract tools from step text as fallback for old cached recipes 2026-05-16 10:01:05 +00:00
github-actions[bot] 11d3209482 chore: auto-merge develop → main
Triggered by: e19c256 feat(cooking): show required tools/appliances bar in cooking mode
2026-05-16 10:00:18 +00:00
dadaloop82 e19c2564f6 feat(cooking): show required tools/appliances bar in cooking mode 2026-05-16 09:58:39 +00:00
github-actions[bot] 6c0ae6627b chore: auto-merge develop → main
Triggered by: 8928c75 feat(recipes): add tools_needed field — appliances shown as chips above ingredients
2026-05-16 09:57:43 +00:00
dadaloop82 8928c75a9d feat(recipes): add tools_needed field — appliances shown as chips above ingredients 2026-05-16 09:56:10 +00:00
dadaloop82 b09b485e80 Merge branch 'main' of github-evershelf:dadaloop82/EverShelf 2026-05-16 09:36:15 +00:00
dadaloop82 9e9528054e merge: develop → main (v1.7.13 — cooking mode kiosk fix, potato shelf life, move-after-use preference) 2026-05-16 09:36:05 +00:00
github-actions[bot] 12cbcb1a29 chore: auto-merge develop → main
Triggered by: 9b9a196 fix(ux): skip move-after-use modal after 2 consistent choices; hide single-location picker
2026-05-16 09:34:22 +00:00
dadaloop82 9b9a196f73 fix(ux): skip move-after-use modal after 2 consistent choices; hide single-location picker 2026-05-16 09:32:46 +00:00
github-actions[bot] 9ce3fbcb9e chore: auto-merge develop → main
Triggered by: 3065b80 fix(expiry): potato shelf life 14→30 days in pantry; add explicit rules for onion/garlic/carrot
2026-05-16 09:26:41 +00:00
dadaloop82 3065b80370 fix(expiry): potato shelf life 14→30 days in pantry; add explicit rules for onion/garlic/carrot 2026-05-16 09:25:04 +00:00
github-actions[bot] 93acc58191 chore: auto-merge develop → main
Triggered by: d9f7755 fix(ux): hide kiosk overlay during cooking mode
2026-05-16 09:21:28 +00:00
dadaloop82 d9f775562f fix(ux): hide kiosk overlay during cooking mode 2026-05-16 09:19:51 +00:00
github-actions[bot] 85d957be2b chore: auto-merge develop → main
Triggered by: 7774fc4 docs: remove stale scale-gateway reference from README
2026-05-16 09:13:50 +00:00
dadaloop82 7774fc4cc8 docs: remove stale scale-gateway reference from README 2026-05-16 09:12:18 +00:00
14 changed files with 546 additions and 76 deletions
+17
View File
@@ -5,6 +5,23 @@ All notable changes to EverShelf will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] — Ideas & Roadmap
> Ideas collected during development. No priority or date implied.
- **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.14] - 2026-05-16
### Added
- **In-app bug report form** — "Segnala un problema" now opens a modal form instead of redirecting to GitHub. Users can select type (Bug / Feature / Question), write title and description, optionally add reproduction steps. A GitHub issue is created directly with labels and app metadata attached.
### Fixed
- **Kiosk settings button** — "Apri configurazione kiosk" in webapp settings was showing a toast asking to tap a gear icon that no longer exists. Now calls `openNativeSettings()` bridge directly (opens Android SettingsActivity). Fallback for old APKs shows a proper "update the kiosk app" hint.
- **False update badge** — `manifest.json` version was `1.7.12` while the app header showed `v1.7.13`, causing the server to report an older deployed version and triggering a spurious update notification.
- **Kiosk settings gear disappeared** — Race condition where Kotlin's `onPageFinished` injects `#_kiosk_overlay` before JS runs; JS found the element already present and returned early without ever restoring the native gear button. Fixed: JS no longer hides the native gear on load; `closeModal()` restores it with `setNativeSettingsVisible(true)`.
- **`openNativeSettings()` fragile typeof check** — Android `@JavascriptInterface` methods are not always detected as `'function'` by typeof; replaced with try/catch.
## [1.7.13] - 2026-05-16 ## [1.7.13] - 2026-05-16
### Fixed ### Fixed
+1 -1
View File
@@ -104,7 +104,7 @@
- **Stability + auto-confirm** — 10s stable wait + 5s countdown before confirming - **Stability + auto-confirm** — 10s stable wait + 5s countdown before confirming
- **Real-time status** — Scale connection indicator always visible in the header - **Real-time status** — Scale connection indicator always visible in the header
- **Multi-protocol** — Supports Bluetooth SIG Weight Scale, Body Composition, Xiaomi Mi Scale 2 and 100+ models - **Multi-protocol** — Supports Bluetooth SIG Weight Scale, Body Composition, Xiaomi Mi Scale 2 and 100+ models
- **Built into kiosk (v1.6.0+)** — BLE gateway runs as an integrated foreground service inside the [EverShelf Kiosk](evershelf-kiosk/) app; no separate APK needed. The standalone gateway app in [`evershelf-scale-gateway/`](evershelf-scale-gateway/) is deprecated but kept for non-kiosk use cases. - **Built into kiosk (v1.6.0+)** — BLE gateway runs as an integrated foreground service inside the [EverShelf Kiosk](evershelf-kiosk/) app; no separate APK needed.
### 📺 Android Kiosk Mode (Add-on) ### 📺 Android Kiosk Mode (Add-on)
- **Dedicated tablet app** — Full-screen WebView wrapper for wall-mounted kitchen tablets - **Dedicated tablet app** — Full-screen WebView wrapper for wall-mounted kitchen tablets
+5 -1
View File
@@ -363,6 +363,10 @@ function estimateOpenedExpiryDaysPHP(string $name, string $category, string $loc
if (preg_match('/\b(yogurt|yaourt|yoghurt)\b/', $n)) return 2; if (preg_match('/\b(yogurt|yaourt|yoghurt)\b/', $n)) return 2;
if (preg_match('/\blatte\b/', $n)) return 1; if (preg_match('/\blatte\b/', $n)) return 1;
if (preg_match('/\bformaggio\b/', $n)) return 2; if (preg_match('/\bformaggio\b/', $n)) return 2;
// Root vegetables / tubers in pantry: sfusi in un sacchetto, durano 3-5 settimane
if (preg_match('/\b(patata|patate|tubero)\b/', $n)) return 30;
if (preg_match('/\b(cipolla|cipolle|aglio|scalogno|porro)\b/', $n)) return 30;
if (preg_match('/\b(carota|carote)\b/', $n)) return 14;
return 60; // generic pantry fallback return 60; // generic pantry fallback
} }
@@ -470,7 +474,7 @@ function estimateSealedExpiryDaysPHP(string $name, string $category, string $loc
elseif (preg_match('/carota|carote|zucchina|zucchine|peperoni|melanzane/', $n)) $days = 7; elseif (preg_match('/carota|carote|zucchina|zucchine|peperoni|melanzane/', $n)) $days = 7;
elseif (preg_match('/broccoli|cavolfiore|cavolo|spinaci|bietola/', $n)) $days = 5; elseif (preg_match('/broccoli|cavolfiore|cavolo|spinaci|bietola/', $n)) $days = 5;
elseif (preg_match('/cipolla|cipolle/', $n)) $days = 10; elseif (preg_match('/cipolla|cipolle/', $n)) $days = 10;
elseif (preg_match('/patata|patate/', $n)) $days = 14; elseif (preg_match('/patata|patate/', $n)) $days = 30; // whole tubers in a bag, pantry: 3-5 weeks
elseif (preg_match('/biscott|cracker|grissini|fette\s+biscott/', $n)) $days = 180; elseif (preg_match('/biscott|cracker|grissini|fette\s+biscott/', $n)) $days = 180;
elseif (preg_match('/nutella|marmellata|miele/', $n)) $days = 365; elseif (preg_match('/nutella|marmellata|miele/', $n)) $days = 365;
elseif (preg_match('/passata|pelati|pomodor/', $n)) $days = 730; elseif (preg_match('/passata|pelati|pomodor/', $n)) $days = 730;
+93 -4
View File
@@ -438,6 +438,10 @@ try {
reportError(); reportError();
break; break;
case 'report_bug':
reportBugManual();
break;
case 'check_update': case 'check_update':
checkUpdate(); checkUpdate();
break; break;
@@ -3020,6 +3024,7 @@ PROMPT;
'error_empty_reply' => 'Risposta vuota da Gemini', 'error_empty_reply' => 'Risposta vuota da Gemini',
'prompt_lang_rule' => 'IMPORTANTE: scrivi tutti i campi testuali della ricetta in Italiano.', 'prompt_lang_rule' => 'IMPORTANTE: scrivi tutti i campi testuali della ricetta in Italiano.',
'prompt_step_example' => 'Passo 1…', 'prompt_step_example' => 'Passo 1…',
'tools_title' => 'Strumenti necessari',
], ],
'en' => [ 'en' => [
'status_analyze_pantry' => '📦 Analyzing pantry...', 'status_analyze_pantry' => '📦 Analyzing pantry...',
@@ -3042,6 +3047,7 @@ PROMPT;
'error_empty_reply' => 'Empty response from Gemini', 'error_empty_reply' => 'Empty response from Gemini',
'prompt_lang_rule' => 'IMPORTANT: write all textual recipe fields in English only. Do not use Italian or German.', 'prompt_lang_rule' => 'IMPORTANT: write all textual recipe fields in English only. Do not use Italian or German.',
'prompt_step_example' => 'Step 1…', 'prompt_step_example' => 'Step 1…',
'tools_title' => 'Equipment needed',
], ],
'de' => [ 'de' => [
'status_analyze_pantry' => '📦 Vorrat wird analysiert...', 'status_analyze_pantry' => '📦 Vorrat wird analysiert...',
@@ -3064,6 +3070,7 @@ PROMPT;
'error_empty_reply' => 'Leere Antwort von Gemini', 'error_empty_reply' => 'Leere Antwort von Gemini',
'prompt_lang_rule' => 'WICHTIG: schreibe alle textuellen Rezeptfelder nur auf Deutsch. Verwende kein Italienisch oder Englisch.', 'prompt_lang_rule' => 'WICHTIG: schreibe alle textuellen Rezeptfelder nur auf Deutsch. Verwende kein Italienisch oder Englisch.',
'prompt_step_example' => 'Schritt 1…', 'prompt_step_example' => 'Schritt 1…',
'tools_title' => 'Benötigte Geräte',
], ],
]; ];
$text = $dict[$lang][$key] ?? $dict['it'][$key] ?? $key; $text = $dict[$lang][$key] ?? $dict['it'][$key] ?? $key;
@@ -3441,14 +3448,15 @@ REGOLE:
4. "qty_number": valore NUMERICO nella STESSA unità della dispensa (g/ml/pz/conf, MAI kg o litri). Per non-dispensa: 0. IMPORTANTE: per ingredienti con unità "pz" scrivi qty_number come numero di PEZZI (es. 2, non 200g). 4. "qty_number": valore NUMERICO nella STESSA unità della dispensa (g/ml/pz/conf, MAI kg o litri). Per non-dispensa: 0. IMPORTANTE: per ingredienti con unità "pz" scrivi qty_number come numero di PEZZI (es. 2, non 200g).
5. "name": usa ESATTAMENTE il nome dalla lista (il sistema lo usa per scalare l'inventario). 5. "name": usa ESATTAMENTE il nome dalla lista (il sistema lo usa per scalare l'inventario).
6. Includi nella lista ingredienti TUTTI quelli citati nei passi (tranne acqua/sale/pepe/olio). 6. Includi nella lista ingredienti TUTTI quelli citati nei passi (tranne acqua/sale/pepe/olio).
7. Language rule: {$recipeLangName} only for all textual fields (`title`, `tags`, `expiry_note`, `ingredients.qty`, `steps`, `nutrition_note`). Keep `meal` unchanged. 7. Language rule: {$recipeLangName} only for all textual fields (`title`, `tags`, `expiry_note`, `ingredients.qty`, `steps`, `nutrition_note`, `tools_needed`). Keep `meal` unchanged.
8. `tools_needed`: array of kitchen tools/appliances actually required by this recipe (e.g. ["Forno","Frullatore"]). Use the same language as all other text fields. Empty array [] if only stovetop/knife/pan needed.
DISPENSA: DISPENSA:
$ingredientsText $ingredientsText
Rispondi SOLO JSON valido (no markdown): Rispondi SOLO JSON valido (no markdown):
{$promptLanguageRule} {$promptLanguageRule}
{"title":"","meal":"$mealType","persons":$persons,"prep_time":"","cook_time":"","tags":[""],"expiry_note":"","ingredients":[{"name":"","qty":"200 g","qty_number":200,"from_pantry":true}],"steps":["{$promptStepExample}"],"nutrition_note":""} {"title":"","meal":"$mealType","persons":$persons,"prep_time":"","cook_time":"","tags":[""],"expiry_note":"","tools_needed":[""],"ingredients":[{"name":"","qty":"200 g","qty_number":200,"from_pantry":true}],"steps":["{$promptStepExample}"],"nutrition_note":""}
PROMPT; PROMPT;
$payload = [ $payload = [
@@ -4317,14 +4325,15 @@ REGOLE:
4. "qty_number": valore NUMERICO nella STESSA unità della dispensa (g/ml/pz/conf, MAI kg o litri). Per non-dispensa: 0. IMPORTANTE: per ingredienti con unità "pz" scrivi qty_number come numero di PEZZI (es. 2, non 200g). 4. "qty_number": valore NUMERICO nella STESSA unità della dispensa (g/ml/pz/conf, MAI kg o litri). Per non-dispensa: 0. IMPORTANTE: per ingredienti con unità "pz" scrivi qty_number come numero di PEZZI (es. 2, non 200g).
5. "name": usa ESATTAMENTE il nome dalla lista (il sistema lo usa per scalare l'inventario). 5. "name": usa ESATTAMENTE il nome dalla lista (il sistema lo usa per scalare l'inventario).
6. Includi nella lista ingredienti TUTTI quelli citati nei passi (tranne acqua/sale/pepe/olio). 6. Includi nella lista ingredienti TUTTI quelli citati nei passi (tranne acqua/sale/pepe/olio).
7. Language rule: {$recipeLangName} only for all textual fields (`title`, `tags`, `expiry_note`, `ingredients.qty`, `steps`, `nutrition_note`). Keep `meal` unchanged. 7. Language rule: {$recipeLangName} only for all textual fields (`title`, `tags`, `expiry_note`, `ingredients.qty`, `steps`, `nutrition_note`, `tools_needed`). Keep `meal` unchanged.
8. `tools_needed`: array of kitchen tools/appliances actually required by this recipe (e.g. ["Forno","Frullatore"]). Use the same language as all other text fields. Empty array [] if only stovetop/knife/pan needed.
DISPENSA: DISPENSA:
$ingredientsText $ingredientsText
Rispondi SOLO JSON valido (no markdown): Rispondi SOLO JSON valido (no markdown):
{$promptLanguageRule} {$promptLanguageRule}
{"title":"","meal":"$mealType","persons":$persons,"prep_time":"","cook_time":"","tags":[""],"expiry_note":"","ingredients":[{"name":"","qty":"200 g","qty_number":200,"from_pantry":true}],"steps":["{$promptStepExample}"],"nutrition_note":""} {"title":"","meal":"$mealType","persons":$persons,"prep_time":"","cook_time":"","tags":[""],"expiry_note":"","tools_needed":[""],"ingredients":[{"name":"","qty":"200 g","qty_number":200,"from_pantry":true}],"steps":["{$promptStepExample}"],"nutrition_note":""}
PROMPT; PROMPT;
$genConfig = [ $genConfig = [
@@ -6892,6 +6901,86 @@ function reportError(): void {
echo json_encode(['ok' => true]); echo json_encode(['ok' => true]);
} }
/**
* POST /api/?action=report_bug
*
* Manual bug/feature/question report submitted by the user via the in-app form.
* Creates a GitHub issue directly with the provided title and description.
*
* Expected JSON body:
* type string 'bug'|'feature'|'question'
* title string Issue title (required, max 150 chars)
* description string Main description (required, max 3000 chars)
* steps string? Steps to reproduce (optional, max 2000 chars)
* lang string? UI language the user is running
* url string? Page URL
* user_agent string? Navigator UA
* version string? App version
*/
function reportBugManual(): void {
$input = json_decode(file_get_contents('php://input'), true) ?: [];
$allowedTypes = ['bug', 'feature', 'question'];
$type = in_array($input['type'] ?? '', $allowedTypes, true) ? $input['type'] : 'bug';
$title = substr(trim($input['title'] ?? ''), 0, 150);
$desc = substr(trim($input['description'] ?? ''), 0, 3000);
$steps = substr(trim($input['steps'] ?? ''), 0, 2000);
$ua = substr(trim($input['user_agent'] ?? ($_SERVER['HTTP_USER_AGENT'] ?? '')), 0, 300);
$url = substr(trim($input['url'] ?? ''), 0, 300);
$ver = substr(trim($input['version'] ?? ''), 0, 50);
$lang = preg_replace('/[^a-z\-]/', '', strtolower($input['lang'] ?? 'it'));
if (empty($title) || empty($desc)) {
echo json_encode(['ok' => false, 'error' => 'title and description required']);
return;
}
$token = _ghToken();
if (!$token) {
// No GitHub token configured — log locally and return ok so the UX is not broken
_appendErrorLog('pwa', 'manual_report', $title, $desc, $url, $ua, ['type' => $type, 'version' => $ver, 'lang' => $lang]);
echo json_encode(['ok' => true, 'issue' => null]);
return;
}
// Labels: always 'user-report' + type-specific label
$labelMap = [
'bug' => ['bug', 'user-report'],
'feature' => ['enhancement', 'user-report'],
'question' => ['question', 'user-report'],
];
$labels = $labelMap[$type];
$typeEmoji = ['bug' => '🐛', 'feature' => '💡', 'question' => '❓'][$type];
$ts = date('Y-m-d H:i:s T');
$body = "## {$typeEmoji} User Report\n\n";
$body .= "**Description:**\n{$desc}\n\n";
if ($steps) {
$body .= "**Steps to reproduce:**\n{$steps}\n\n";
}
$body .= "---\n";
$body .= "**Version:** `{$ver}` \n";
$body .= "**Language:** `{$lang}` \n";
if ($url) $body .= "**URL:** `{$url}` \n";
if ($ua) $body .= "**User-Agent:** `{$ua}` \n";
$body .= "**Reported at:** {$ts}\n\n";
$body .= "_This issue was submitted via the in-app bug report form._";
$res = _githubRequest($token, 'POST',
'https://api.github.com/repos/' . GH_REPO . '/issues',
['title' => $title, 'body' => $body, 'labels' => $labels]
);
$issueNum = $res['body']['number'] ?? null;
$issueUrl = $res['body']['html_url'] ?? null;
if ($issueNum) {
echo json_encode(['ok' => true, 'issue' => $issueNum, 'url' => $issueUrl]);
} else {
echo json_encode(['ok' => false, 'error' => 'github_api_error']);
}
}
/** /**
* Append to data/error_reports.log (local safety net, max 500 KB) * Append to data/error_reports.log (local safety net, max 500 KB)
*/ */
+75
View File
@@ -3103,6 +3103,36 @@ body.server-offline .bottom-nav {
justify-content: center; justify-content: center;
} }
/* ── Bug report form ── */
.bug-type-pills {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.bug-type-pill {
flex: 1;
min-width: 80px;
padding: 7px 10px;
border: 1.5px solid #cbd5e1;
border-radius: 20px;
background: #fff;
color: #475569;
font-size: 0.88rem;
cursor: pointer;
transition: border-color 0.15s, background 0.15s, color 0.15s;
-webkit-tap-highlight-color: transparent;
}
.bug-type-pill.active {
border-color: var(--primary, #2d5016);
background: var(--primary, #2d5016);
color: #fff;
font-weight: 600;
}
.bug-type-pill:not(.active):hover {
border-color: var(--primary, #2d5016);
color: var(--primary, #2d5016);
}
.modal-detail { .modal-detail {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -3918,6 +3948,29 @@ body.server-offline .bottom-nav {
line-height: 1.5; line-height: 1.5;
} }
.recipe-tools-banner {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
background: #f0f4ff;
border: 1px solid #c7d2fe;
border-radius: var(--radius-sm);
padding: 8px 12px;
font-size: 0.85rem;
color: #3730a3;
margin-bottom: 12px;
line-height: 1.5;
}
.recipe-tool-chip {
background: #e0e7ff;
border-radius: 20px;
padding: 2px 10px;
font-size: 0.8rem;
color: #3730a3;
white-space: nowrap;
}
/* Recipe ingredient use buttons */ /* Recipe ingredient use buttons */
.recipe-ingredients { .recipe-ingredients {
list-style: none; list-style: none;
@@ -4480,6 +4533,28 @@ body.cooking-mode-active .app-header {
} }
.cooking-timers-bar::-webkit-scrollbar { display: none; } .cooking-timers-bar::-webkit-scrollbar { display: none; }
.cooking-tools-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
padding: 7px 14px;
background: rgba(99,102,241,0.15);
border-bottom: 1px solid rgba(99,102,241,0.25);
flex-shrink: 0;
font-size: 0.78rem;
color: #c7d2fe;
}
.cooking-tool-chip {
background: rgba(99,102,241,0.25);
border: 1px solid rgba(99,102,241,0.4);
border-radius: 20px;
padding: 2px 9px;
font-size: 0.76rem;
color: #e0e7ff;
white-space: nowrap;
}
.cooking-timer-card { .cooking-timer-card {
display: flex; display: flex;
align-items: center; align-items: center;
+261 -55
View File
@@ -1553,7 +1553,7 @@ function estimateExpiryDays(product, location) {
else if (/carota|carote|zucchina|zucchine|peperoni|melanzane/.test(name)) days = 7; else if (/carota|carote|zucchina|zucchine|peperoni|melanzane/.test(name)) days = 7;
else if (/broccoli|cavolfiore|cavolo|spinaci|bietola/.test(name)) days = 5; else if (/broccoli|cavolfiore|cavolo|spinaci|bietola/.test(name)) days = 5;
else if (/cipolla|cipolle/.test(name)) days = 10; else if (/cipolla|cipolle/.test(name)) days = 10;
else if (/patata|patate/.test(name)) days = 14; else if (/patata|patate/.test(name)) days = 30; // whole tubers in a bag, pantry: 3-5 weeks
else if (/biscott|cracker|grissini|fette\s+biscott/.test(name)) days = 180; else if (/biscott|cracker|grissini|fette\s+biscott/.test(name)) days = 180;
else if (/nutella|marmellata|miele/.test(name)) days = 365; else if (/nutella|marmellata|miele/.test(name)) days = 365;
else if (/passata|pelati|pomodor/.test(name)) days = 730; else if (/passata|pelati|pomodor/.test(name)) days = 730;
@@ -1573,7 +1573,7 @@ function estimateExpiryDays(product, location) {
else if (/arancia|arance|agrumi|mandarini|limone|limoni/.test(name)) days = Math.max(days, 21); else if (/arancia|arance|agrumi|mandarini|limone|limoni/.test(name)) days = Math.max(days, 21);
else if (/carota|carote/.test(name)) days = Math.max(days, 21); else if (/carota|carote/.test(name)) days = Math.max(days, 21);
else if (/cipolla/.test(name)) days = Math.max(days, 14); else if (/cipolla/.test(name)) days = Math.max(days, 14);
else if (/patata|patate/.test(name)) days = Math.max(days, 21); else if (/patata|patate/.test(name)) days = Math.max(days, 30);
else if (/pera|pere/.test(name)) days = Math.max(days, 21); else if (/pera|pere/.test(name)) days = Math.max(days, 21);
else if (/kiwi/.test(name)) days = Math.max(days, 28); else if (/kiwi/.test(name)) days = Math.max(days, 28);
else if (/uva/.test(name)) days = Math.max(days, 14); else if (/uva/.test(name)) days = Math.max(days, 14);
@@ -2100,51 +2100,124 @@ async function _loadAboutSection() {
* Manually triggered bug report from the About section in Settings. * Manually triggered bug report from the About section in Settings.
* Collects basic info and submits via the existing report_error endpoint. * Collects basic info and submits via the existing report_error endpoint.
*/ */
async function reportBugManual() { function reportBugManual() {
const btn = document.getElementById('btn-report-bug'); const mc = document.getElementById('modal-content');
const statusEl = document.getElementById('report-bug-status'); if (!mc) return;
if (!btn || !statusEl) return;
btn.disabled = true; mc.innerHTML = `
<div class="modal-header">
<h3>${t('about.report_bug_modal_title')}</h3>
<button class="modal-close" onclick="closeModal()"></button>
</div>
<div style="padding:16px 20px 20px">
<div style="margin-bottom:14px">
<div class="bug-type-pills">
<button type="button" class="bug-type-pill active" data-btype="bug">🐛 ${t('about.report_type_bug')}</button>
<button type="button" class="bug-type-pill" data-btype="feature">💡 ${t('about.report_type_feature')}</button>
<button type="button" class="bug-type-pill" data-btype="question"> ${t('about.report_type_question')}</button>
</div>
</div>
<div style="margin-bottom:12px">
<label class="settings-label" style="display:block;margin-bottom:4px">${t('about.report_field_title')} *</label>
<input type="text" id="bug-form-title" maxlength="150" autocomplete="off"
placeholder="${t('about.report_field_title_ph')}"
style="width:100%;box-sizing:border-box;padding:9px 11px;border:1.5px solid #cbd5e1;border-radius:8px;font-size:0.95rem;background:#fff;color:#1e293b;outline:none">
</div>
<div style="margin-bottom:12px">
<label class="settings-label" style="display:block;margin-bottom:4px">${t('about.report_field_desc')} *</label>
<textarea id="bug-form-desc" rows="4" maxlength="3000"
placeholder="${t('about.report_field_desc_ph')}"
style="width:100%;box-sizing:border-box;padding:9px 11px;border:1.5px solid #cbd5e1;border-radius:8px;font-size:0.95rem;resize:vertical;background:#fff;color:#1e293b;outline:none;font-family:inherit"></textarea>
</div>
<div id="bug-form-steps-group" style="margin-bottom:12px">
<label class="settings-label" style="display:block;margin-bottom:4px">${t('about.report_field_steps')}</label>
<textarea id="bug-form-steps" rows="3" maxlength="2000"
placeholder="${t('about.report_field_steps_ph')}"
style="width:100%;box-sizing:border-box;padding:9px 11px;border:1.5px solid #cbd5e1;border-radius:8px;font-size:0.95rem;resize:vertical;background:#fff;color:#1e293b;outline:none;font-family:inherit"></textarea>
</div>
<p class="settings-hint" style="margin:0 0 14px;font-size:0.78rem">
${t('about.report_auto_info').replace('{version}', _loadedVersion || '—').replace('{lang}', _currentLang || '—')}
</p>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button type="button" class="btn btn-secondary" onclick="closeModal()">${t('common.cancel') || 'Annulla'}</button>
<button type="button" class="btn btn-primary" id="bug-form-submit" onclick="_submitBugReport()">
${t('about.report_send_btn')}
</button>
</div>
<div id="bug-form-status" style="display:none;margin-top:10px;text-align:center;font-size:0.88rem;padding:8px;border-radius:6px"></div>
</div>
`;
document.getElementById('modal-overlay').style.display = 'flex';
// Pill click: switch type, show/hide steps field
mc.querySelectorAll('.bug-type-pill').forEach(pill => {
pill.addEventListener('click', () => {
mc.querySelectorAll('.bug-type-pill').forEach(p => p.classList.remove('active'));
pill.classList.add('active');
const stepsGroup = document.getElementById('bug-form-steps-group');
if (stepsGroup) stepsGroup.style.display = (pill.dataset.btype === 'bug') ? '' : 'none';
});
});
}
async function _submitBugReport() {
const submitBtn = document.getElementById('bug-form-submit');
const statusEl = document.getElementById('bug-form-status');
const titleEl = document.getElementById('bug-form-title');
const descEl = document.getElementById('bug-form-desc');
const stepsEl = document.getElementById('bug-form-steps');
const activePill = document.querySelector('#modal-content .bug-type-pill.active');
const title = titleEl?.value.trim() || '';
const desc = descEl?.value.trim() || '';
const steps = stepsEl?.value.trim() || '';
const type = activePill?.dataset.btype || 'bug';
// Inline validation
if (!title) {
titleEl.style.borderColor = '#dc2626';
titleEl.focus();
return;
}
if (!desc) {
descEl.style.borderColor = '#dc2626';
descEl.focus();
return;
}
submitBtn.disabled = true;
statusEl.style.display = ''; statusEl.style.display = '';
statusEl.style.background = '#f1f5f9';
statusEl.style.color = '#64748b'; statusEl.style.color = '#64748b';
statusEl.textContent = t('about.report_bug_sending'); statusEl.textContent = t('about.report_bug_sending');
const manifest = await fetch('manifest.json?_=' + Date.now()).then(r => r.json()).catch(() => ({}));
try { try {
const res = await fetch(API_BASE + '?action=report_error', { const res = await api('report_bug', null, 'POST', {
method: 'POST', type,
headers: { 'Content-Type': 'application/json' }, title,
body: JSON.stringify({ description: desc,
source: 'pwa', steps,
type: 'manual_report', user_agent: navigator.userAgent,
message: 'Manual bug report submitted from Settings → About', url: location.href,
stack: '', version: _loadedVersion || '',
url: location.href, lang: _currentLang || 'it',
user_agent: navigator.userAgent,
version: manifest.version || '',
context: {
lang: _currentLang,
online: navigator.onLine,
version_guard_bypass: true,
}
})
}); });
const json = await res.json();
if (json.ok) { if (res.ok) {
statusEl.style.background = '#dcfce7';
statusEl.style.color = '#15803d'; statusEl.style.color = '#15803d';
statusEl.textContent = t('about.report_bug_sent'); const issueRef = res.issue ? ` (#${res.issue})` : '';
// Open GitHub issues so user can add details statusEl.textContent = t('about.report_bug_sent') + issueRef;
setTimeout(() => window.open('https://github.com/dadaloop82/EverShelf/issues', '_blank', 'noopener'), 800); submitBtn.style.display = 'none';
setTimeout(() => closeModal(), 3500);
} else { } else {
throw new Error(json.error || 'error'); throw new Error(res.error || 'error');
} }
} catch(e) { } catch(e) {
statusEl.style.background = '#fee2e2';
statusEl.style.color = '#dc2626'; statusEl.style.color = '#dc2626';
statusEl.textContent = t('about.report_bug_error'); statusEl.textContent = t('about.report_bug_error');
} finally { submitBtn.disabled = false;
btn.disabled = false;
} }
} }
@@ -2337,6 +2410,9 @@ async function loadSettingsUI() {
// Show kiosk self-update panel // Show kiosk self-update panel
const updatePanel = document.getElementById('kiosk-update-panel'); const updatePanel = document.getElementById('kiosk-update-panel');
if (updatePanel) updatePanel.style.display = ''; if (updatePanel) updatePanel.style.display = '';
// Show kiosk native settings shortcut panel
const nativePanel = document.getElementById('kiosk-native-settings-panel');
if (nativePanel) nativePanel.style.display = '';
} }
// Populate About section version // Populate About section version
@@ -2356,6 +2432,19 @@ function _kioskReconfigureScale() {
} }
} }
// ── Kiosk: open native SettingsActivity (server URL, BLE, screensaver) ──
function _openKioskNativeSettings() {
if (typeof _kioskBridge === 'undefined') return;
// Use try/catch directly: Android @JavascriptInterface methods are not always
// detected as 'function' by typeof, so we just call and catch if unavailable.
try {
_kioskBridge.openNativeSettings();
} catch(e) {
// Older APK without openNativeSettings bridge — inform user to update
showToast(t('settings.kiosk.native_update_hint') || 'Aggiorna l\'app kiosk per usare questa funzione', 'warning', 4000);
}
}
// ── Kiosk: manual update check ──────────────────────────────────────── // ── Kiosk: manual update check ────────────────────────────────────────
let _kioskPendingApkUrl = ''; let _kioskPendingApkUrl = '';
@@ -2466,6 +2555,10 @@ function _injectKioskOverlay() {
const appHeader = document.querySelector('.app-header'); const appHeader = document.querySelector('.app-header');
if (appHeader) appHeader.classList.add('kiosk-mode'); if (appHeader) appHeader.classList.add('kiosk-mode');
const btnStyle = 'background:rgba(255,255,255,0.2);border:none;color:#fff;width:34px;height:34px;border-radius:50%;font-size:15px;cursor:pointer;display:flex;align-items:center;justify-content:center;-webkit-tap-highlight-color:transparent;touch-action:manipulation;';
// If the Kotlin onPageFinished already injected #_kiosk_overlay (with only ✕ and ↻),
// nothing more to do — the native Android btnSettings (top-right) opens SettingsActivity.
if (document.getElementById('_kiosk_overlay')) return; if (document.getElementById('_kiosk_overlay')) return;
const headerLeft = document.getElementById('header-left'); const headerLeft = document.getElementById('header-left');
@@ -2475,8 +2568,6 @@ function _injectKioskOverlay() {
wrap.id = '_kiosk_overlay'; wrap.id = '_kiosk_overlay';
wrap.style.cssText = 'display:flex;gap:6px;align-items:center;'; wrap.style.cssText = 'display:flex;gap:6px;align-items:center;';
const btnStyle = 'background:rgba(255,255,255,0.2);border:none;color:#fff;width:34px;height:34px;border-radius:50%;font-size:15px;cursor:pointer;display:flex;align-items:center;justify-content:center;-webkit-tap-highlight-color:transparent;touch-action:manipulation;';
// Exit button // Exit button
const exitBtn = document.createElement('button'); const exitBtn = document.createElement('button');
exitBtn.id = '_kiosk_exit_btn'; exitBtn.id = '_kiosk_exit_btn';
@@ -2499,24 +2590,13 @@ function _injectKioskOverlay() {
_kioskBridge.hardReload(); _kioskBridge.hardReload();
}); });
// Settings button — replaces the native Android settings button // NOTE: No ⚙️ button here — the native Android settings button (top-right, injected by
const settBtn = document.createElement('button'); // Kotlin) opens SettingsActivity (server URL, BLE scale, screensaver). Do NOT call
settBtn.id = '_kiosk_settings_btn'; // setNativeSettingsVisible(false) — that would hide the only way to reconfigure the kiosk.
settBtn.textContent = '\u2699\uFE0F';
settBtn.title = t('settings.title') || 'Settings';
settBtn.style.cssText = btnStyle.replace('font-size:15px', 'font-size:16px');
settBtn.addEventListener('click', (e) => {
e.stopPropagation();
showPage('settings');
});
wrap.appendChild(exitBtn); wrap.appendChild(exitBtn);
wrap.appendChild(refBtn); wrap.appendChild(refBtn);
wrap.appendChild(settBtn);
headerLeft.appendChild(wrap); headerLeft.appendChild(wrap);
// Permanently hide the native Android settings button — replaced by the web overlay button above.
try { _kioskBridge.setNativeSettingsVisible(false); } catch(_) {}
} }
function renderAppliances(appliances) { function renderAppliances(appliances) {
@@ -4910,8 +4990,8 @@ function showItemDetail(inventoryId, productId) {
function closeModal() { function closeModal() {
document.getElementById('modal-overlay').style.display = 'none'; document.getElementById('modal-overlay').style.display = 'none';
clearMoveModalTimer(); clearMoveModalTimer();
// Native kiosk settings button is permanently replaced by the web overlay button — keep hidden. // Restore the native kiosk settings button when the modal closes.
try { if (typeof _kioskBridge !== 'undefined') _kioskBridge.setNativeSettingsVisible(false); } catch (_) {} try { if (typeof _kioskBridge !== 'undefined') _kioskBridge.setNativeSettingsVisible(true); } catch (_) {}
_cancelScaleAutoConfirm(false); _cancelScaleAutoConfirm(false);
_scaleRecipeAutoFillPaused = false; _scaleRecipeAutoFillPaused = false;
_scaleUserDismissed = false; _scaleUserDismissed = false;
@@ -7626,6 +7706,9 @@ async function loadUseInventoryInfo() {
// Build location buttons only for locations where the product exists // Build location buttons only for locations where the product exists
const productLocations = [...new Set(items.map(i => i.location))]; const productLocations = [...new Set(items.map(i => i.location))];
const locSelector = document.getElementById('use-location-selector'); const locSelector = document.getElementById('use-location-selector');
// Hide the location row when the product is in only one location (nothing to choose)
const locGroup = document.getElementById('use-location-group');
if (locGroup) locGroup.style.display = productLocations.length > 1 ? '' : 'none';
// Prefer the remembered location (if confirmed), else use the opened-package heuristic // Prefer the remembered location (if confirmed), else use the opened-package heuristic
const prefLoc = _getPreferredUseLocation(currentProduct.id); const prefLoc = _getPreferredUseLocation(currentProduct.id);
@@ -7824,6 +7907,42 @@ function selectUseLocation(btn, loc) {
const _PREF_LOC_KEY = '_prefUseLoc'; const _PREF_LOC_KEY = '_prefUseLoc';
const _PREF_LOC_NEEDED = 2; // choices needed to confirm a preference const _PREF_LOC_NEEDED = 2; // choices needed to confirm a preference
// ── PREFERRED MOVE-AFTER-USE LOCATION ────────────────────────────────────
// Tracks where the user puts the remainder after using a product.
// After _PREF_MOVE_NEEDED consistent choices, the modal is skipped entirely.
const _PREF_MOVE_KEY = '_prefMoveLoc';
const _PREF_MOVE_NEEDED = 2;
let _pendingMoveCtx = null; // { productId, fromLoc, openedId } — set before showing modal
function _getMoveLocHistory(productId, fromLoc) {
try {
const all = JSON.parse(localStorage.getItem(_PREF_MOVE_KEY) || '{}');
return all[`${productId}|${fromLoc}`] || [];
} catch { return []; }
}
function _recordMoveLocChoice(productId, fromLoc, toLoc) {
try {
const all = JSON.parse(localStorage.getItem(_PREF_MOVE_KEY) || '{}');
const key = `${productId}|${fromLoc}`;
const hist = all[key] || [];
hist.push(toLoc);
if (hist.length > 8) hist.splice(0, hist.length - 8);
all[key] = hist;
localStorage.setItem(_PREF_MOVE_KEY, JSON.stringify(all));
} catch { }
}
function _getPreferredMoveLoc(productId, fromLoc) {
const hist = _getMoveLocHistory(productId, fromLoc);
if (hist.length < _PREF_MOVE_NEEDED) return null;
const recent = hist.slice(-5);
const counts = {};
for (const loc of recent) counts[loc] = (counts[loc] || 0) + 1;
const [topLoc, topCount] = Object.entries(counts).sort((a, b) => b[1] - a[1])[0];
return topCount >= _PREF_MOVE_NEEDED ? topLoc : null;
}
function _getPrefLocHistory(productId) { function _getPrefLocHistory(productId) {
try { try {
const all = JSON.parse(localStorage.getItem(_PREF_LOC_KEY) || '{}'); const all = JSON.parse(localStorage.getItem(_PREF_LOC_KEY) || '{}');
@@ -8176,6 +8295,22 @@ function startMoveModalCountdown(btnId, onExpire) {
} }
function showMoveAfterUseModal(product, fromLoc, remaining, openedId, openedVacuumSealed, unit) { function showMoveAfterUseModal(product, fromLoc, remaining, openedId, openedVacuumSealed, unit) {
// Store context so _saveVacuumAndStay can record the choice
_pendingMoveCtx = { productId: product.id, fromLoc, openedId };
// If a preference is established, skip the modal entirely and auto-apply
const prefMoveLoc = _getPreferredMoveLoc(product.id, fromLoc);
if (prefMoveLoc) {
if (prefMoveLoc === fromLoc) {
// Preference: stay in place — silent, no modal
_saveVacuumAndStay(openedId || 0);
} else {
// Preference: move to another location — apply silently
confirmMoveAfterUse(product.id, fromLoc, prefMoveLoc, openedId || 0, !!(openedVacuumSealed ?? product.vacuum_sealed));
}
return;
}
const otherLocs = Object.entries(LOCATIONS).filter(([k]) => k !== fromLoc); const otherLocs = Object.entries(LOCATIONS).filter(([k]) => k !== fromLoc);
const locButtons = otherLocs.map(([k, v]) => const locButtons = otherLocs.map(([k, v]) =>
`<button type="button" class="loc-btn" onclick="clearMoveModalTimer();confirmMoveAfterUse(${product.id}, '${fromLoc}', '${k}', ${openedId || 0})">${v.icon} ${v.label}</button>` `<button type="button" class="loc-btn" onclick="clearMoveModalTimer();confirmMoveAfterUse(${product.id}, '${fromLoc}', '${k}', ${openedId || 0})">${v.icon} ${v.label}</button>`
@@ -8209,6 +8344,11 @@ function showMoveAfterUseModal(product, fromLoc, remaining, openedId, openedVacu
/** Save vacuum state when user chooses to keep the item at the current location. */ /** Save vacuum state when user chooses to keep the item at the current location. */
async function _saveVacuumAndStay(openedId) { async function _saveVacuumAndStay(openedId) {
// Record the "stay" preference before closing
if (_pendingMoveCtx) {
_recordMoveLocChoice(_pendingMoveCtx.productId, _pendingMoveCtx.fromLoc, _pendingMoveCtx.fromLoc);
_pendingMoveCtx = null;
}
closeModal(); closeModal();
if (openedId) { if (openedId) {
const isVacuum = document.getElementById('move-vacuum-check')?.checked ? 1 : 0; const isVacuum = document.getElementById('move-vacuum-check')?.checked ? 1 : 0;
@@ -8220,9 +8360,14 @@ async function _saveVacuumAndStay(openedId) {
showPage('dashboard'); showPage('dashboard');
} }
async function confirmMoveAfterUse(productId, fromLoc, toLoc, openedId) { async function confirmMoveAfterUse(productId, fromLoc, toLoc, openedId, forcedVacuum) {
clearMoveModalTimer(); clearMoveModalTimer();
const newVacuum = document.getElementById('move-vacuum-check')?.checked ? 1 : 0; const newVacuum = forcedVacuum !== undefined ? (forcedVacuum ? 1 : 0) : (document.getElementById('move-vacuum-check')?.checked ? 1 : 0);
// Record preference
if (_pendingMoveCtx && _pendingMoveCtx.productId === productId) {
_recordMoveLocChoice(productId, fromLoc, toLoc);
_pendingMoveCtx = null;
}
closeModal(); closeModal();
showLoading(true); showLoading(true);
try { try {
@@ -11872,6 +12017,36 @@ async function confirmRecipeMove(productId, fromLoc, toLoc, openedId) {
} }
} }
/**
* Extract tools/appliances from recipe steps text when tools_needed is absent (old cached recipes).
* Returns an array of localised tool names found in the steps.
*/
function _extractToolsFromSteps(steps) {
const text = (steps || []).join(' ').toLowerCase();
// Map: regex keyword → display name per language
const patterns = [
{ re: /\bforn[oi]\b|oven|backofen/, it: 'Forno', en: 'Oven', de: 'Backofen' },
{ re: /\bmicroond[ea]\b|microwave|mikrowelle/, it: 'Microonde', en: 'Microwave', de: 'Mikrowelle' },
{ re: /\bfrullator[ei]\b|blender|mixer\b|pimer|frullatore a immersione|stabmixer/,
it: 'Frullatore', en: 'Blender', de: 'Mixer' },
{ re: /\bfritteuse\b|friggitrici[ae]\b|air\s*fry|friggitric[ae]\b|friggi\b/, it: 'Friggitrice', en: 'Air fryer', de: 'Fritteuse' },
{ re: /\bpentola\s+a\s+pressione\b|pressure\s+cook|schnellkochtopf|cookeo|instant\s*pot/, it: 'Pentola a pressione', en: 'Pressure cooker', de: 'Schnellkochtopf' },
{ re: /\bbimby\b|thermomix\b|monsieur\s+cuisine/,it: 'Bimby/Thermomix', en: 'Thermomix', de: 'Thermomix' },
{ re: /\bimpastatric[ae]\b|planetari[ao]\b|stand\s*mixer|knetmaschine/, it: 'Impastatrice', en: 'Stand mixer', de: 'Knetmaschine' },
{ re: /\bvapore\b|steamer\b|dampfgarer\b/, it: 'Vaporiera', en: 'Steamer', de: 'Dampfgarer' },
{ re: /\bslow\s*cook|cottura\s+lenta\b|schongarer/, it: 'Slow cooker', en: 'Slow cooker', de: 'Schongarer' },
{ re: /\bgrill[eo]?\b|griglia\b|grillpfanne/, it: 'Griglia', en: 'Grill', de: 'Grill' },
{ re: /\bmacchina\s+del\s+pane\b|bread\s*machine|brotbackautomat/, it: 'Macchina del pane', en: 'Bread machine', de: 'Brotbackautomat' },
{ re: /\bessiccator[ei]\b|dehydrator\b|dörrgerät/, it: 'Essiccatore', en: 'Dehydrator', de: 'Dörrgerät' },
];
const lang = _currentLang || 'it';
const found = [];
for (const p of patterns) {
if (p.re.test(text)) found.push(p[lang] || p.it);
}
return found;
}
function renderRecipe(r) { function renderRecipe(r) {
let html = `<h2>${r.title}</h2>`; let html = `<h2>${r.title}</h2>`;
@@ -11889,6 +12064,14 @@ function renderRecipe(r) {
html += `<div class="recipe-expiry-note">⚠️ ${r.expiry_note}</div>`; html += `<div class="recipe-expiry-note">⚠️ ${r.expiry_note}</div>`;
} }
// Tools/appliances banner (shown only when specific equipment is needed)
const tools = (r.tools_needed && r.tools_needed.length > 0)
? r.tools_needed.filter(t => t && t.trim())
: _extractToolsFromSteps(r.steps);
if (tools.length > 0) {
html += `<div class="recipe-tools-banner">🔧 <strong>${t('recipes.tools_title')}:</strong> ${tools.map(t => `<span class="recipe-tool-chip">${t}</span>`).join('')}</div>`;
}
// Ingredients // Ingredients
html += `<h3>${t('recipes.ingredients_title')}</h3><ul class="recipe-ingredients">`; html += `<h3>${t('recipes.ingredients_title')}</h3><ul class="recipe-ingredients">`;
(r.ingredients || []).forEach((ing, idx) => { (r.ingredients || []).forEach((ing, idx) => {
@@ -12024,8 +12207,25 @@ function startCookingMode() {
_cookingTTS = true; _cookingTTS = true;
document.getElementById('cooking-title').textContent = _cookingRecipe.title || ''; document.getElementById('cooking-title').textContent = _cookingRecipe.title || '';
document.getElementById('cooking-tts-btn').textContent = '🔊'; document.getElementById('cooking-tts-btn').textContent = '🔊';
// Tools bar
const toolsBar = document.getElementById('cooking-tools-bar');
if (toolsBar) {
const tools = (_cookingRecipe.tools_needed && _cookingRecipe.tools_needed.length > 0)
? _cookingRecipe.tools_needed.filter(t => t && t.trim())
: _extractToolsFromSteps(_cookingRecipe.steps);
if (tools.length > 0) {
toolsBar.innerHTML = '🔧 ' + tools.map(t => `<span class="cooking-tool-chip">${t}</span>`).join('');
toolsBar.style.display = '';
} else {
toolsBar.style.display = 'none';
toolsBar.innerHTML = '';
}
}
document.getElementById('cooking-overlay').style.display = 'flex'; document.getElementById('cooking-overlay').style.display = 'flex';
document.body.classList.add('cooking-mode-active'); document.body.classList.add('cooking-mode-active');
// Hide kiosk overlay — it lives outside <body> with z-index:2147483647 and would overlap cooking UI
const _kioskOvl = document.getElementById('_kiosk_overlay');
if (_kioskOvl) _kioskOvl.style.display = 'none';
_bindCookingWheelControls(); _bindCookingWheelControls();
const wheelEl = document.getElementById('cooking-wheel'); const wheelEl = document.getElementById('cooking-wheel');
if (wheelEl) setTimeout(() => wheelEl.focus(), 20); if (wheelEl) setTimeout(() => wheelEl.focus(), 20);
@@ -12039,6 +12239,9 @@ function startCookingMode() {
function closeCookingMode() { function closeCookingMode() {
document.getElementById('cooking-overlay').style.display = 'none'; document.getElementById('cooking-overlay').style.display = 'none';
document.body.classList.remove('cooking-mode-active'); document.body.classList.remove('cooking-mode-active');
// Restore kiosk overlay
const _kioskOvl = document.getElementById('_kiosk_overlay');
if (_kioskOvl) _kioskOvl.style.display = 'flex';
// NOTE: intentionally keep _cookingRecipe, _cookingStep, _cookingVisited // NOTE: intentionally keep _cookingRecipe, _cookingStep, _cookingVisited
// so the user can resume from the same step when they reopen // so the user can resume from the same step when they reopen
try { screen.orientation?.unlock().catch(() => {}); } catch (_) { /* ignore */ } try { screen.orientation?.unlock().catch(() => {}); } catch (_) { /* ignore */ }
@@ -12371,7 +12574,10 @@ function _initBrowserTtsVoices(selectedVoice) {
sel.innerHTML = '<option value="">— Caricamento voci… —</option>'; sel.innerHTML = '<option value="">— Caricamento voci… —</option>';
const populate = () => { const populate = () => {
const voices = (window.speechSynthesis.getVoices() || []).filter(v => v != null && v.lang); let voices = [];
try {
voices = (window.speechSynthesis.getVoices() || []).filter(v => v != null && v.lang);
} catch (_) { return false; }
if (!voices.length) return false; if (!voices.length) return false;
// Italian voices first, then others // Italian voices first, then others
const it = voices.filter(v => v.lang.startsWith('it')); const it = voices.filter(v => v.lang.startsWith('it'));
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId = "it.dadaloop.evershelf.kiosk" applicationId = "it.dadaloop.evershelf.kiosk"
minSdk = 24 minSdk = 24
targetSdk = 34 targetSdk = 34
versionCode = 13 versionCode = 15
versionName = "1.7.2" versionName = "1.7.14"
} }
signingConfigs { signingConfigs {
@@ -515,6 +515,17 @@ class KioskActivity : AppCompatActivity() {
btnSettings.visibility = if (visible) View.VISIBLE else View.GONE btnSettings.visibility = if (visible) View.VISIBLE else View.GONE
} }
} }
/**
* Open the native SettingsActivity from the webapp settings page.
* Allows configuring server URL, BLE scale and screensaver without
* the user having to find the native gear button.
*/
@JavascriptInterface
fun openNativeSettings() {
runOnUiThread {
startActivity(Intent(this@KioskActivity, SettingsActivity::class.java))
}
}
}, "_kioskBridge") }, "_kioskBridge")
val url = prefs.getString(KEY_URL, "http://evershelf.local") ?: "http://evershelf.local" val url = prefs.getString(KEY_URL, "http://evershelf.local") ?: "http://evershelf.local"
+2
View File
@@ -1,3 +1,5 @@
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
# Build trigger: versionName 1.7.13 fix (8d87494)
# Build trigger: TTS bridge fix (95389eb) # Build trigger: TTS bridge fix (95389eb)
# Build trigger: v1.7.14 with openNativeSettings fix (834d8ef)
+18 -6
View File
@@ -11,7 +11,7 @@
<title>EverShelf</title> <title>EverShelf</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<link rel="icon" type="image/png" href="assets/img/logo/logo_icon.png"> <link rel="icon" type="image/png" href="assets/img/logo/logo_icon.png">
<link rel="stylesheet" href="assets/css/style.css?v=20260513a"> <link rel="stylesheet" href="assets/css/style.css?v=20260516a">
<!-- QuaggaJS for barcode scanning --> <!-- QuaggaJS for barcode scanning -->
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script> <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 --> <!-- @xenova/transformers: ES-module bootstrap that exposes a lazy category-classifier as window._categoryPipelinePromise -->
@@ -67,7 +67,7 @@
<!-- Title — left-aligned; grows to fill space --> <!-- Title — left-aligned; grows to fill space -->
<div class="header-title-wrap"> <div class="header-title-wrap">
<h1 class="header-title" onclick="showPage('dashboard')"> <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.13</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.14</span>
</h1> </h1>
<!-- Update badge — shown alongside title, never replaces it --> <!-- Update badge — shown alongside title, never replaces it -->
<span class="header-update-badge" id="header-update-badge" style="display:none"></span> <span class="header-update-badge" id="header-update-badge" style="display:none"></span>
@@ -407,7 +407,7 @@
<div class="use-inventory-info" id="use-inventory-info"></div> <div class="use-inventory-info" id="use-inventory-info"></div>
<div id="use-expiry-hint" style="display:none"></div> <div id="use-expiry-hint" style="display:none"></div>
<form class="form" onsubmit="submitUse(event)"> <form class="form" onsubmit="submitUse(event)">
<div class="form-group"> <div class="form-group" id="use-location-group">
<label>📍 Da dove?</label> <label>📍 Da dove?</label>
<div class="location-selector" id="use-location-selector"> <div class="location-selector" id="use-location-selector">
<button type="button" class="loc-btn active" onclick="selectUseLocation(this, 'dispensa')">🗄️ Dispensa</button> <button type="button" class="loc-btn active" onclick="selectUseLocation(this, 'dispensa')">🗄️ Dispensa</button>
@@ -1302,6 +1302,18 @@
<p class="settings-hint" style="margin-top:8px" data-i18n="settings.kiosk.download_sub">Modalità kiosk full-screen + gateway bilancia integrato. Sorgente: <code>evershelf-kiosk/</code></p> <p class="settings-hint" style="margin-top:8px" data-i18n="settings.kiosk.download_sub">Modalità kiosk full-screen + gateway bilancia integrato. Sorgente: <code>evershelf-kiosk/</code></p>
</div> </div>
<!-- Kiosk native settings panel (visible only inside kiosk WebView) -->
<div id="kiosk-native-settings-panel" style="display:none;background:rgba(99,102,241,0.06);border:1.5px solid rgba(99,102,241,0.2);border-radius:12px;padding:16px;margin-top:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">
<span style="font-size:1.4rem">🖥️</span>
<div>
<p style="margin:0;font-weight:700;font-size:0.9rem" data-i18n="settings.kiosk.native_title">Configurazione Kiosk</p>
<p class="settings-hint" style="margin:2px 0 0" data-i18n="settings.kiosk.native_hint">URL server, bilancia BLE, salvaschermo e setup wizard.</p>
</div>
</div>
<button class="btn btn-secondary full-width" onclick="_openKioskNativeSettings()">⚙️ <span data-i18n="settings.kiosk.native_btn">Apri configurazione kiosk</span></button>
</div>
<!-- Kiosk self-update panel (visible only inside kiosk WebView) --> <!-- Kiosk self-update panel (visible only inside kiosk WebView) -->
<div id="kiosk-update-panel" style="display:none;background:rgba(16,185,129,0.06);border:1.5px solid rgba(16,185,129,0.2);border-radius:12px;padding:16px;margin-top:16px"> <div id="kiosk-update-panel" style="display:none;background:rgba(16,185,129,0.06);border:1.5px solid rgba(16,185,129,0.2);border-radius:12px;padding:16px;margin-top:16px">
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px"> <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px">
@@ -1332,7 +1344,7 @@
<button class="btn btn-outline full-width" onclick="reportBugManual()" id="btn-report-bug"> <button class="btn btn-outline full-width" onclick="reportBugManual()" id="btn-report-bug">
🐛 <span data-i18n="about.report_bug">Segnala un problema</span> 🐛 <span data-i18n="about.report_bug">Segnala un problema</span>
</button> </button>
<p class="settings-hint" style="text-align:center;margin:0" data-i18n="about.report_bug_hint">Qualcosa non funziona? Apri una segnalazione su GitHub.</p> <p class="settings-hint" style="text-align:center;margin:0" data-i18n="about.report_bug_hint">Qualcosa non funziona? Inviaci una segnalazione direttamente dall'app.</p>
<div style="display:flex;gap:8px"> <div style="display:flex;gap:8px">
<a class="btn btn-outline full-width" style="text-decoration:none;text-align:center" <a class="btn btn-outline full-width" style="text-decoration:none;text-align:center"
href="https://github.com/dadaloop82/EverShelf/blob/main/CHANGELOG.md" href="https://github.com/dadaloop82/EverShelf/blob/main/CHANGELOG.md"
@@ -1342,7 +1354,6 @@
target="_blank" rel="noopener" data-i18n="about.github">GitHub</a> target="_blank" rel="noopener" data-i18n="about.github">GitHub</a>
</div> </div>
</div> </div>
<div id="report-bug-status" style="display:none;margin-top:8px;text-align:center;font-size:0.85rem"></div>
</div> </div>
</section> </section>
@@ -1522,6 +1533,7 @@
<button class="cooking-tts-btn" id="cooking-tts-btn" onclick="toggleCookingTTS()" title="Leggi ad alta voce">🔊</button> <button class="cooking-tts-btn" id="cooking-tts-btn" onclick="toggleCookingTTS()" title="Leggi ad alta voce">🔊</button>
</div> </div>
<div id="cooking-timers-bar" class="cooking-timers-bar" style="display:none"></div> <div id="cooking-timers-bar" class="cooking-timers-bar" style="display:none"></div>
<div id="cooking-tools-bar" class="cooking-tools-bar" style="display:none"></div>
<div class="cooking-body"> <div class="cooking-body">
<div class="cooking-step-header"> <div class="cooking-step-header">
<div class="cooking-step-num" id="cooking-step-num">1 / 1</div> <div class="cooking-step-num" id="cooking-step-num">1 / 1</div>
@@ -1547,6 +1559,6 @@
</div> </div>
</div> </div>
<script src="assets/js/app.js?v=20260513a"></script> <script src="assets/js/app.js?v=20260516a"></script>
</body> </body>
</html> </html>
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "EverShelf", "name": "EverShelf",
"short_name": "EverShelf", "short_name": "EverShelf",
"description": "Gestione completa della dispensa di casa con scansione barcode", "description": "Gestione completa della dispensa di casa con scansione barcode",
"version": "1.7.12", "version": "1.7.14",
"start_url": "/evershelf/", "start_url": "/evershelf/",
"display": "standalone", "display": "standalone",
"background_color": "#f0f4e8", "background_color": "#f0f4e8",
+20 -2
View File
@@ -341,6 +341,7 @@
"regenerate": "🔄 Noch eins generieren", "regenerate": "🔄 Noch eins generieren",
"close_btn": "✅ Schließen", "close_btn": "✅ Schließen",
"ingredients_title": "🧾 Zutaten", "ingredients_title": "🧾 Zutaten",
"tools_title": "Benötigte Geräte",
"steps_title": "👨‍🍳 Zubereitung", "steps_title": "👨‍🍳 Zubereitung",
"no_steps": "Keine Zubereitungsschritte verfügbar", "no_steps": "Keine Zubereitungsschritte verfügbar",
"generate_error": "Fehler bei der Generierung", "generate_error": "Fehler bei der Generierung",
@@ -676,7 +677,12 @@
"kiosk": { "kiosk": {
"hint": "Verwandeln Sie ein Android-Tablet in ein EverShelf-Panel mit integriertem BLE-Waagen-Gateway.", "hint": "Verwandeln Sie ein Android-Tablet in ein EverShelf-Panel mit integriertem BLE-Waagen-Gateway.",
"download_btn": "📥 EverShelf Kiosk herunterladen (APK)", "download_btn": "📥 EverShelf Kiosk herunterladen (APK)",
"download_sub": "Vollbild-Kioskmodus + integriertes Waagen-Gateway. Quellcode: evershelf-kiosk/" "download_sub": "Vollbild-Kioskmodus + integriertes Waagen-Gateway. Quellcode: evershelf-kiosk/",
"native_title": "Kiosk-Konfiguration",
"native_hint": "Server-URL, BLE-Waage, Bildschirmschoner und Einrichtungsassistent.",
"native_btn": "Kiosk-Konfiguration öffnen",
"native_tap_hint": "Zahnrad oben rechts antippen",
"native_update_hint": "Kiosk-App aktualisieren, um diese Funktion zu nutzen"
}, },
"saved": "✅ Konfiguration gespeichert!", "saved": "✅ Konfiguration gespeichert!",
"saved_local": "✅ Konfiguration lokal gespeichert", "saved_local": "✅ Konfiguration lokal gespeichert",
@@ -1091,7 +1097,19 @@
"title": "Über", "title": "Über",
"version": "Version", "version": "Version",
"report_bug": "Fehler melden", "report_bug": "Fehler melden",
"report_bug_hint": "Etwas funktioniert nicht? Öffne ein Issue auf GitHub.", "report_bug_hint": "Etwas funktioniert nicht? Sende uns direkt aus der App eine Meldung.",
"report_bug_modal_title": "Fehler melden",
"report_type_bug": "Fehler",
"report_type_feature": "Funktion",
"report_type_question": "Frage",
"report_field_title": "Titel",
"report_field_title_ph": "Kurze Beschreibung des Problems",
"report_field_desc": "Beschreibung",
"report_field_desc_ph": "Problem detailliert beschreiben…",
"report_field_steps": "Schritte zum Reproduzieren (optional)",
"report_field_steps_ph": "1. Gehe zu…\n2. Tippe auf…\n3. Fehler erscheint…",
"report_auto_info": "Automatisch beigefügt: Version {version}, Sprache {lang}.",
"report_send_btn": "Bericht senden",
"report_bug_sending": "Wird gesendet…", "report_bug_sending": "Wird gesendet…",
"report_bug_sent": "Bericht gesendet — danke!", "report_bug_sent": "Bericht gesendet — danke!",
"report_bug_error": "Bericht konnte nicht gesendet werden. Verbindung prüfen.", "report_bug_error": "Bericht konnte nicht gesendet werden. Verbindung prüfen.",
+20 -2
View File
@@ -341,6 +341,7 @@
"regenerate": "🔄 Generate another one", "regenerate": "🔄 Generate another one",
"close_btn": "✅ Close", "close_btn": "✅ Close",
"ingredients_title": "🧾 Ingredients", "ingredients_title": "🧾 Ingredients",
"tools_title": "Equipment needed",
"steps_title": "👨‍🍳 Steps", "steps_title": "👨‍🍳 Steps",
"no_steps": "No steps available", "no_steps": "No steps available",
"generate_error": "Generation error", "generate_error": "Generation error",
@@ -676,7 +677,12 @@
"kiosk": { "kiosk": {
"hint": "Turn an Android tablet into an always-on EverShelf panel with built-in BLE scale gateway.", "hint": "Turn an Android tablet into an always-on EverShelf panel with built-in BLE scale gateway.",
"download_btn": "📥 Download EverShelf Kiosk (APK)", "download_btn": "📥 Download EverShelf Kiosk (APK)",
"download_sub": "Full-screen kiosk mode + integrated scale gateway. Source: evershelf-kiosk/" "download_sub": "Full-screen kiosk mode + integrated scale gateway. Source: evershelf-kiosk/",
"native_title": "Kiosk Configuration",
"native_hint": "Server URL, BLE scale, screensaver and setup wizard.",
"native_btn": "Open kiosk configuration",
"native_tap_hint": "Tap the gear button at the top right",
"native_update_hint": "Update the kiosk app to use this feature"
}, },
"saved": "✅ Configuration saved!", "saved": "✅ Configuration saved!",
"saved_local": "✅ Configuration saved locally", "saved_local": "✅ Configuration saved locally",
@@ -1091,7 +1097,19 @@
"title": "About", "title": "About",
"version": "Version", "version": "Version",
"report_bug": "Report a Bug", "report_bug": "Report a Bug",
"report_bug_hint": "Something not working? Open an issue on GitHub.", "report_bug_hint": "Something not working? Send us a report directly from the app.",
"report_bug_modal_title": "Report a Bug",
"report_type_bug": "Bug",
"report_type_feature": "Feature",
"report_type_question": "Question",
"report_field_title": "Title",
"report_field_title_ph": "Brief description of the issue",
"report_field_desc": "Description",
"report_field_desc_ph": "Describe the issue in detail…",
"report_field_steps": "Steps to reproduce (optional)",
"report_field_steps_ph": "1. Go to…\n2. Tap…\n3. See the error…",
"report_auto_info": "Automatically attached: version {version}, language {lang}.",
"report_send_btn": "Send report",
"report_bug_sending": "Sending…", "report_bug_sending": "Sending…",
"report_bug_sent": "Report sent — thank you!", "report_bug_sent": "Report sent — thank you!",
"report_bug_error": "Could not send the report. Check your connection.", "report_bug_error": "Could not send the report. Check your connection.",
+20 -2
View File
@@ -341,6 +341,7 @@
"regenerate": "🔄 Generane un'altra", "regenerate": "🔄 Generane un'altra",
"close_btn": "✅ Chiudi", "close_btn": "✅ Chiudi",
"ingredients_title": "🧾 Ingredienti", "ingredients_title": "🧾 Ingredienti",
"tools_title": "Strumenti necessari",
"steps_title": "👨‍🍳 Procedimento", "steps_title": "👨‍🍳 Procedimento",
"no_steps": "Nessun procedimento disponibile", "no_steps": "Nessun procedimento disponibile",
"generate_error": "Errore nella generazione", "generate_error": "Errore nella generazione",
@@ -676,7 +677,12 @@
"kiosk": { "kiosk": {
"hint": "Trasforma un tablet Android in un pannello EverShelf sempre acceso, con bilancia BLE integrata.", "hint": "Trasforma un tablet Android in un pannello EverShelf sempre acceso, con bilancia BLE integrata.",
"download_btn": "📥 Scarica EverShelf Kiosk (APK)", "download_btn": "📥 Scarica EverShelf Kiosk (APK)",
"download_sub": "Modalità kiosk full-screen + gateway bilancia integrato. Sorgente: evershelf-kiosk/" "download_sub": "Modalità kiosk full-screen + gateway bilancia integrato. Sorgente: evershelf-kiosk/",
"native_title": "Configurazione Kiosk",
"native_hint": "URL server, bilancia BLE, salvaschermo e setup wizard.",
"native_btn": "Apri configurazione kiosk",
"native_tap_hint": "Tocca la rotella in alto a destra",
"native_update_hint": "Aggiorna l'app kiosk per usare questa funzione"
}, },
"saved": "✅ Configurazione salvata!", "saved": "✅ Configurazione salvata!",
"saved_local": "✅ Configurazione salvata localmente", "saved_local": "✅ Configurazione salvata localmente",
@@ -1091,7 +1097,19 @@
"title": "Informazioni", "title": "Informazioni",
"version": "Versione", "version": "Versione",
"report_bug": "Segnala un problema", "report_bug": "Segnala un problema",
"report_bug_hint": "Qualcosa non funziona? Apri una segnalazione su GitHub.", "report_bug_hint": "Qualcosa non funziona? Inviaci una segnalazione direttamente dall'app.",
"report_bug_modal_title": "Segnala un problema",
"report_type_bug": "Bug",
"report_type_feature": "Funzionalità",
"report_type_question": "Domanda",
"report_field_title": "Titolo",
"report_field_title_ph": "Breve descrizione del problema",
"report_field_desc": "Descrizione",
"report_field_desc_ph": "Descrivi il problema in dettaglio…",
"report_field_steps": "Passi per riprodurlo (opzionale)",
"report_field_steps_ph": "1. Vai su…\n2. Tocca…\n3. Vedi l'errore…",
"report_auto_info": "Saranno allegati automaticamente: versione {version}, lingua {lang}.",
"report_send_btn": "Invia segnalazione",
"report_bug_sending": "Invio in corso…", "report_bug_sending": "Invio in corso…",
"report_bug_sent": "Segnalazione inviata — grazie!", "report_bug_sent": "Segnalazione inviata — grazie!",
"report_bug_error": "Impossibile inviare la segnalazione. Controlla la connessione.", "report_bug_error": "Impossibile inviare la segnalazione. Controlla la connessione.",