Compare commits
24 Commits
v1.7.13
...
kiosk-1.7.13
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ab1da4bd5 | |||
| 1566e32a85 | |||
| fe7a047656 | |||
| 9c285b426f | |||
| c58705f35c | |||
| 8d874944b5 | |||
| b6f85b8e29 | |||
| 68693e7168 | |||
| 84c3bb6e4c | |||
| d8aec91599 | |||
| 11d3209482 | |||
| e19c2564f6 | |||
| 6c0ae6627b | |||
| 8928c75a9d | |||
| b09b485e80 | |||
| 9e9528054e | |||
| 12cbcb1a29 | |||
| 9b9a196f73 | |||
| 9ce3fbcb9e | |||
| 3065b80370 | |||
| 93acc58191 | |||
| d9f775562f | |||
| 85d957be2b | |||
| 7774fc4cc8 |
@@ -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
@@ -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;
|
||||||
|
|||||||
+9
-4
@@ -3020,6 +3020,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 +3043,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 +3066,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 +3444,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 +4321,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 = [
|
||||||
|
|||||||
@@ -3918,6 +3918,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 +4503,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;
|
||||||
|
|||||||
+131
-5
@@ -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);
|
||||||
@@ -7626,6 +7626,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 +7827,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 +8215,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 +8264,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 +8280,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 +11937,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 +11984,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 +12127,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 +12159,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 +12494,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'));
|
||||||
|
|||||||
@@ -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 = 14
|
||||||
versionName = "1.7.2"
|
versionName = "1.7.13"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
# Build trigger: TTS bridge fix (95389eb)
|
# Build trigger: versionName 1.7.13 fix (8d87494)
|
||||||
|
|||||||
+2
-1
@@ -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>
|
||||||
@@ -1522,6 +1522,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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user