Modalità cucina: timer multipli persistenti con etichetta, riprendi dal passo salvato, progress dots, pulsante Ricomincia; priorità ricette basata su scadenze con ingredienti obbligatori

This commit is contained in:
dadaloop82
2026-03-31 15:55:35 +00:00
parent 2be6643104
commit fb7bb4d675
4 changed files with 400 additions and 169 deletions
+77 -24
View File
@@ -1359,8 +1359,37 @@ function generateRecipe(PDO $db): void {
return;
}
// Build ingredient list with expiry info
$ingredientLines = [];
// Helper to compute priority group for an item:
// 1=scaduto, 2=scadenza imminente ≤3gg, 3=scadenza ravvicinata ≤7gg, 4=scadenza lontana, 5=confezione aperta, 6=chiuso
$getItemPriority = function($item) {
$daysLeft = floatval($item['days_left']);
$isOpen = (floatval($item['quantity']) > 0 && floatval($item['quantity']) < 1 && $item['unit'] === 'conf');
if (!empty($item['expiry_date']) && $daysLeft < 0) return 1;
if (!empty($item['expiry_date']) && $daysLeft <= 3) return 2;
if (!empty($item['expiry_date']) && $daysLeft <= 7) return 3;
if (!empty($item['expiry_date'])) return 4;
if ($isOpen) return 5;
return 6;
};
// Sort by priority group, then by days_left within each group
usort($items, function($a, $b) use ($getItemPriority) {
$pa = $getItemPriority($a);
$pb = $getItemPriority($b);
if ($pa !== $pb) return $pa - $pb;
return floatval($a['days_left']) - floatval($b['days_left']);
});
// Build ingredient list grouped by priority
$priorityHeaders = [
1 => '⚠️ PRODOTTI SCADUTI (PRIORITÀ MASSIMA - USA SUBITO SE ANCORA COMMESTIBILI)',
2 => '🔴 SCADENZA IMMINENTE (entro 3 giorni - USA PER PRIMI)',
3 => '🟠 SCADENZA RAVVICINATA (entro 7 giorni)',
4 => '🟡 ALTRI PRODOTTI CON SCADENZA',
5 => '📦 CONFEZIONI APERTE (da consumare prima dei chiusi)',
6 => '🟢 ALTRI PRODOTTI',
];
$priorityGroups = [];
foreach ($items as $item) {
$line = "- {$item['name']}";
if ($item['brand']) $line .= " ({$item['brand']})";
@@ -1371,27 +1400,44 @@ function generateRecipe(PDO $db): void {
if ($item['expiry_date']) {
$daysLeft = intval($item['days_left']);
if ($daysLeft < 0) {
$line .= " [SCADUTO da " . abs($daysLeft) . " giorni!]";
} elseif ($daysLeft <= 3) {
$line .= " [SCADE TRA $daysLeft GIORNI - PRIORITÀ ALTA!]";
} elseif ($daysLeft <= 7) {
$line .= " [scade tra $daysLeft giorni - priorità media]";
$line .= " [SCADUTO da " . abs($daysLeft) . " giorni]";
} else {
$line .= " [scade tra $daysLeft giorni]";
}
}
// Flag fridge items for priority
if (strtolower($item['location']) === 'frigo') {
$line .= " [IN FRIGO - PRIORITÀ]";
$line .= " [FRIGO]";
}
// Flag opened packages (fractional quantity = already opened)
$qty = floatval($item['quantity']);
if ($qty > 0 && $qty < 1 && $item['unit'] === 'conf') {
$line .= " [CONFEZIONE APERTA - USA PRIMA]";
$line .= " [APERTO]";
}
$line .= " (in {$item['location']})";
$ingredientLines[] = $line;
$group = $getItemPriority($item);
$priorityGroups[$group][] = $line;
}
$ingredientsText = implode("\n", $ingredientLines);
$ingredientSections = [];
foreach ($priorityHeaders as $g => $header) {
if (!empty($priorityGroups[$g])) {
$ingredientSections[] = "=== {$header} ===\n" . implode("\n", $priorityGroups[$g]);
}
}
$ingredientsText = implode("\n\n", $ingredientSections);
// Build a mandatory-use list from the most urgent items (groups 1 + 2)
$urgentItems = [];
foreach ($items as $item) {
$g = $getItemPriority($item);
if ($g <= 2) {
$urgentItems[] = $item['name'] . ($item['brand'] ? " ({$item['brand']})" : '') . " — scade: {$item['expiry_date']}";
}
}
$mustUseText = '';
if (!empty($urgentItems)) {
$mustUseText = "\n\n⚠️⚠️⚠️ INGREDIENTI OBBLIGATORI (SCADUTI O IN SCADENZA IMMINENTE) ⚠️⚠️⚠️\nLa ricetta DEVE usare ALMENO uno (meglio se tutti) di questi ingredienti come ingrediente PRINCIPALE della ricetta. Non sono opzionali!\n" . implode("\n", array_map(fn($n) => "$n", $urgentItems));
}
$mealLabels = [
'colazione' => 'colazione (mattina)',
@@ -1409,7 +1455,7 @@ function generateRecipe(PDO $db): void {
'pocafame' => 'L\'utente ha POCA FAME: proponi una porzione leggera, magari uno snack, un\'insalata o qualcosa di semplice e poco abbondante.',
'scadenze' => 'PRIORITÀ SCADENZE: usa ASSOLUTAMENTE per primi gli ingredienti più vicini alla scadenza o già scaduti (se ancora commestibili).',
'salutare' => 'Ricetta EXTRA SALUTARE: prediligi ingredienti integrali, tante verdure, pochi grassi, cotture leggere.',
'opened' => 'PRIORITÀ COSE APERTE: dai la MASSIMA PRIORITÀ ai prodotti con confezione aperta (contrassegnati [CONFEZIONE APERTA]) e a quelli in FRIGO (contrassegnati [IN FRIGO]). Questi prodotti si deteriorano più in fretta e DEVONO essere usati per primi. Costruisci la ricetta attorno a questi ingredienti.',
'opened' => 'PRIORITÀ COSE APERTE: dai la MASSIMA PRIORITÀ ai prodotti con confezione aperta (contrassegnati [APERTO]) e a quelli in FRIGO (contrassegnati [FRIGO]). Questi prodotti si deteriorano più in fretta e DEVONO essere usati per primi. Costruisci la ricetta attorno a questi ingredienti.',
'zerowaste' => 'ZERO SPRECHI: cerca di usare quanti più ingredienti in scadenza possibile, combina anche ingredienti insoliti pur di non sprecare nulla.'
];
foreach ($options as $opt) {
@@ -1474,18 +1520,25 @@ function generateRecipe(PDO $db): void {
$prompt = <<<PROMPT
Sei un nutrizionista e chef italiano esperto. Genera UNA ricetta per $mealLabel per $persons persona/e usando PRINCIPALMENTE gli ingredienti disponibili nella dispensa dell'utente.
{$extraRulesText}{$appliancesText}{$dietaryText}{$varietyText}
{$extraRulesText}{$appliancesText}{$dietaryText}{$varietyText}{$mustUseText}
REGOLE IMPORTANTI:
1. PRIORITÀ ASSOLUTA: usa prima gli ingredienti in scadenza o già scaduti (se ancora utilizzabili)
2. SUGGERIMENTO (non obbligatorio): quando possibile, preferisci ingredienti in FRIGO (contrassegnati [IN FRIGO]) e quelli con CONFEZIONE APERTA (contrassegnati [CONFEZIONE APERTA]) perché si deteriorano più in fretta. Ma se la ricetta migliore usa altri ingredienti, va benissimo.
3. Prediligi una ricetta SANA, EQUILIBRATA e NUTRIENTE
4. Usa SOLO ingredienti dalla lista sotto, più al massimo acqua, sale, pepe e olio che si presumono sempre disponibili
5. Adatta le quantità per $persons persona/e
6. Se non ci sono abbastanza ingredienti per una ricetta completa, suggerisci la migliore combinazione possibile
7. La ricetta deve essere adatta al pasto: $mealLabel
8. IMPORTANTE - QUANTITÀ NUMERICHE: per ogni ingrediente dalla dispensa, il campo "qty_number" DEVE contenere il valore NUMERICO da scalare dall'inventario, espresso nella STESSA unità di misura della dispensa. Le unità ammesse sono SOLO: g (grammi), ml (millilitri), pz (pezzi), conf (confezioni). NON usare mai kg o litri. Esempio: se in dispensa c'è "Farina: 1000 g" e la ricetta richiede 200g, qty_number = 200. Se "Riso: 2000 g" e servono 300g, qty_number = 300. Per ingredienti non dalla dispensa, qty_number = 0.
9. GESTIONE SMART QUANTITÀ: NON lasciare rimasugli poco usabili in dispensa. Se un ingrediente ha una quantità piccola (es. 50g di formaggio, 1 uovo, 100ml di latte), preferisci usarlo TUTTO piuttosto che lasciarne una quantità inutilizzabile. Se invece la quantità è abbondante, usa solo il necessario lasciando abbastanza per un altro pasto. Pensa sempre: "quello che resta sarà sufficiente per un altro utilizzo?"
1. ORDINE DI PRIORITÀ INGREDIENTI (dal più urgente al meno urgente) — gli ingredienti nella lista sono già ordinati per priorità:
a) PRODOTTI SCADUTI: se ancora commestibili, usali SUBITO — hanno la priorità massima assoluta
b) PRODOTTI IN SCADENZA IMMINENTE (≤3 giorni): usa questi per primi dopo gli scaduti
c) PRODOTTI IN SCADENZA RAVVICINATA (≤7 giorni): poi questi
d) PRODOTTI CON SCADENZA PIÙ LONTANA: poi questi
e) CONFEZIONI APERTE (contrassegnate [APERTO]): preferiscile rispetto a quelle chiuse
f) PRODOTTI CHIUSI SENZA SCADENZA: usa per ultimi
Costruisci la ricetta partendo dagli ingredienti delle categorie più urgenti! Usa il maggior numero possibile di ingredienti ad alta priorità.
*** OBBLIGO: se nella sezione "INGREDIENTI OBBLIGATORI" sopra ci sono prodotti, la ricetta DEVE contenere ALMENO UNO di quei prodotti come ingrediente principale. Se li ignori, la ricetta è SBAGLIATA. ***
2. Prediligi una ricetta SANA, EQUILIBRATA e NUTRIENTE
3. Usa SOLO ingredienti dalla lista sotto, più al massimo acqua, sale, pepe e olio che si presumono sempre disponibili
4. Adatta le quantità per $persons persona/e
5. Se non ci sono abbastanza ingredienti per una ricetta completa, suggerisci la migliore combinazione possibile
6. La ricetta deve essere adatta al pasto: $mealLabel
7. IMPORTANTE - QUANTITÀ NUMERICHE: per ogni ingrediente dalla dispensa, il campo "qty_number" DEVE contenere il valore NUMERICO da scalare dall'inventario, espresso nella STESSA unità di misura della dispensa. Le unità ammesse sono SOLO: g (grammi), ml (millilitri), pz (pezzi), conf (confezioni). NON usare mai kg o litri. Esempio: se in dispensa c'è "Farina: 1000 g" e la ricetta richiede 200g, qty_number = 200. Se "Riso: 2000 g" e servono 300g, qty_number = 300. Per ingredienti non dalla dispensa, qty_number = 0.
8. GESTIONE SMART QUANTITÀ: NON lasciare rimasugli poco usabili in dispensa. Se un ingrediente ha una quantità piccola (es. 50g di formaggio, 1 uovo, 100ml di latte), preferisci usarlo TUTTO piuttosto che lasciarne una quantità inutilizzabile. Se invece la quantità è abbondante, usa solo il necessario lasciando abbastanza per un altro pasto. Pensa sempre: "quello che resta sarà sufficiente per un altro utilizzo?"
INGREDIENTI DISPONIBILI IN DISPENSA:
$ingredientsText
+141 -43
View File
@@ -3029,6 +3029,15 @@ body {
-webkit-overflow-scrolling: touch;
}
.cooking-step-header {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
flex-shrink: 0;
width: 100%;
}
.cooking-step-num {
font-size: 0.85rem;
font-weight: 700;
@@ -3038,6 +3047,51 @@ body {
flex-shrink: 0;
}
.cooking-restart-btn {
background: rgba(255,255,255,0.07);
border: 1px solid rgba(255,255,255,0.14);
color: rgba(255,255,255,0.45);
border-radius: 14px;
padding: 4px 11px;
font-size: 0.72rem;
cursor: pointer;
transition: background 0.2s, color 0.2s;
white-space: nowrap;
}
.cooking-restart-btn:active {
background: rgba(255,255,255,0.18);
color: #fff;
}
/* Progress dots */
.cooking-progress-dots {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 6px;
flex-shrink: 0;
max-width: 320px;
width: 100%;
}
.cprog-dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: rgba(255,255,255,0.18);
border: 1.5px solid rgba(255,255,255,0.18);
transition: background 0.25s, border-color 0.25s, transform 0.2s;
flex-shrink: 0;
}
.cprog-dot.visited {
background: rgba(255,255,255,0.50);
border-color: rgba(255,255,255,0.50);
}
.cprog-dot.current {
background: #f59e0b;
border-color: #fbbf24;
transform: scale(1.35);
}
.cooking-step-text {
font-size: clamp(1.4rem, 5vw, 2.2rem);
line-height: 1.5;
@@ -3068,30 +3122,55 @@ body {
}
.cooking-timer-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
width: 100%;
max-width: 360px;
margin-top: 4px;
display: none; /* legacy - replaced by cooking-timers-bar */
}
.cooking-timer-display {
font-size: clamp(2.4rem, 10vw, 4rem);
/* ===== Persistent timers bar (fixed below header, survives step changes) ===== */
.cooking-timers-bar {
display: flex;
gap: 8px;
padding: 8px 12px;
overflow-x: auto;
background: rgba(255,255,255,0.04);
border-bottom: 1px solid rgba(255,255,255,0.10);
flex-shrink: 0;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.cooking-timers-bar::-webkit-scrollbar { display: none; }
.cooking-timer-card {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255,255,255,0.09);
border: 1px solid rgba(255,255,255,0.18);
border-radius: 14px;
padding: 6px 10px 6px 14px;
flex-shrink: 0;
white-space: nowrap;
}
.ctimer-label {
font-size: 0.78rem;
font-weight: 600;
color: rgba(255,255,255,0.60);
max-width: 85px;
overflow: hidden;
text-overflow: ellipsis;
}
.ctimer-display {
font-size: 1.15rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: #fff;
letter-spacing: 0.04em;
letter-spacing: 0.03em;
min-width: 48px;
text-align: center;
transition: color 0.4s;
}
.cooking-timer-display.timer-warning {
color: #f97316;
}
.cooking-timer-display.timer-done {
.ctimer-display.ctimer-warning { color: #f97316; }
.ctimer-display.ctimer-done {
color: #ef4444;
animation: timerPulse 0.8s ease-in-out infinite alternate;
}
@@ -3101,41 +3180,60 @@ body {
to { opacity: 0.5; transform: scale(1.06); }
}
.cooking-timer-actions {
.ctimer-btns {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
gap: 4px;
align-items: center;
}
.ctimer-btn {
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.15);
color: #fff;
border-radius: 8px;
width: 28px;
height: 28px;
font-size: 0.80rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.15s;
flex-shrink: 0;
}
.ctimer-btn:active { background: rgba(255,255,255,0.25); }
.ctimer-toggle.running {
background: rgba(239,68,68,0.28);
border-color: rgba(239,68,68,0.55);
}
.ctimer-remove {
background: rgba(255,255,255,0.04);
border-color: rgba(255,255,255,0.10);
color: rgba(255,255,255,0.45);
font-size: 0.65rem;
}
.ctimer-remove:active { background: rgba(239,68,68,0.25); }
.cooking-timer-btn {
/* ===== Step timer suggestion button ===== */
.cooking-timer-suggest {
display: flex;
justify-content: center;
width: 100%;
}
.cooking-timer-add-btn {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(255,255,255,0.10);
border: 1px solid rgba(255,255,255,0.20);
color: #fff;
border-radius: 20px;
padding: 8px 20px;
font-size: 1rem;
background: rgba(251,191,36,0.12);
border: 1.5px dashed rgba(251,191,36,0.45);
color: #fbbf24;
border-radius: 22px;
padding: 10px 22px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.cooking-timer-btn:active {
background: rgba(255,255,255,0.22);
}
.cooking-timer-btn.timer-running {
background: rgba(239,68,68,0.25);
border-color: rgba(239,68,68,0.5);
}
.cooking-timer-reset {
background: rgba(255,255,255,0.05);
border-color: rgba(255,255,255,0.12);
font-size: 0.9rem;
}
.cooking-timer-add-btn:active { background: rgba(251,191,36,0.26); }
.cooking-step-ings {
width: 100%;
+171 -94
View File
@@ -6095,6 +6095,7 @@ function renderRecipe(r) {
let _cookingRecipe = null;
let _cookingStep = 0;
let _cookingTTS = true;
let _cookingVisited = new Set(); // indices of steps already seen
function startCookingMode() {
const recipe = _cachedRecipe && _cachedRecipe.recipe ? _cachedRecipe.recipe : null;
@@ -6102,8 +6103,14 @@ function startCookingMode() {
showToast('Nessun procedimento disponibile', 'info');
return;
}
_cookingRecipe = JSON.parse(JSON.stringify(recipe)); // deep copy so we can track .used
// Resume if same recipe; otherwise start fresh
const isSame = _cookingRecipe && _cookingRecipe.title === recipe.title;
if (!isSame) {
_cookingRecipe = JSON.parse(JSON.stringify(recipe));
_cookingStep = 0;
_cookingVisited = new Set();
clearAllCookingTimers();
}
_cookingTTS = true;
document.getElementById('cooking-title').textContent = _cookingRecipe.title || '';
document.getElementById('cooking-tts-btn').textContent = '🔊';
@@ -6138,10 +6145,18 @@ function closeCookingMode() {
document.getElementById('cooking-overlay').style.display = 'none';
document.body.classList.remove('cooking-mode-active');
if ('speechSynthesis' in window) window.speechSynthesis.cancel();
clearCookingTimer();
// NOTE: intentionally keep _cookingRecipe, _cookingStep, _cookingVisited
// so the user can resume from the same step when they reopen
try { screen.orientation?.unlock(); } catch (_) { /* ignore */ }
}
function restartCookingMode() {
_cookingStep = 0;
_cookingVisited = new Set();
clearAllCookingTimers();
renderCookingStep();
}
function renderCookingStep() {
if (!_cookingRecipe) return;
const steps = _cookingRecipe.steps || [];
@@ -6149,9 +6164,23 @@ function renderCookingStep() {
const cleanStep = step.replace(/^Passo\s*\d+\s*[:.]\s*/i, '');
const total = steps.length;
// Mark current step as visited
_cookingVisited.add(_cookingStep);
document.getElementById('cooking-step-num').textContent = `${_cookingStep + 1} / ${total}`;
document.getElementById('cooking-step-text').textContent = cleanStep;
// Progress dots
const dotsEl = document.getElementById('cooking-progress-dots');
if (dotsEl) {
dotsEl.innerHTML = Array.from({ length: total }, (_, i) => {
let cls = 'cprog-dot';
if (i === _cookingStep) cls += ' current';
else if (_cookingVisited.has(i)) cls += ' visited';
return `<span class="${cls}"></span>`;
}).join('');
}
// Find pantry ingredients that appear in this step's text and haven't been used yet
const stepLower = cleanStep.toLowerCase();
const ings = (_cookingRecipe.ingredients || [])
@@ -6184,8 +6213,8 @@ function renderCookingStep() {
prevBtn.disabled = _cookingStep === 0;
nextBtn.textContent = _cookingStep === total - 1 ? '✅ Fine' : 'Successivo ▶';
// Timer: detect durations in step text
setupCookingTimer(cleanStep);
// Timer: detect duration in step text and show suggestion
setupCookingTimerSuggestion(cleanStep);
// Speak step
if (_cookingTTS) speakCookingStep(cleanStep);
@@ -6230,139 +6259,185 @@ function replayCookingTTS() {
if (text) speakCookingStep(text);
}
// ===== COOKING TIMER =====
let _cookingTimerInterval = null;
let _cookingTimerSeconds = 0;
let _cookingTimerRunning = false;
let _cookingTimerTotal = 0; // original total seconds
// ===== COOKING TIMER SYSTEM =====
let _cookingTimers = []; // { id, label, total, seconds, running, interval }
let _cookingTimerIdCounter = 0;
let _cookingSuggestedSeconds = 0;
let _cookingSuggestedLabel = '';
/**
* Parse time durations from step text.
* Returns total seconds or 0 if no time found.
* Handles: "10 minuti", "1 ora", "1 ora e 30 minuti", "30 secondi",
* "un'ora", "mezz'ora", "un paio di minuti", "qualche minuto"
*/
function _parseStepTimer(text) {
const t = text.toLowerCase();
let totalSec = 0;
// "mezz'ora" / "mezzora"
if (/mezz['']?\s*ora/i.test(t)) totalSec += 30 * 60;
// "un quarto d'ora"
if (/un\s+quarto\s+d['']?\s*ora/i.test(t)) totalSec += 15 * 60;
// "un'ora" (without other numbers)
if (/un['']?\s*ora(?!\s*e)/i.test(t) && !/\d\s*or[ae]/i.test(t)) totalSec += 60 * 60;
if (totalSec > 0) return totalSec;
// Numeric patterns: "N ore/ora [e M minuti]", "N minuti/min", "N secondi/sec"
const reOre = /(\d+(?:[.,]\d+)?)\s*or[ae]/gi;
const reMin = /(\d+(?:[.,]\d+)?)\s*min(?:ut[oi])?/gi;
const reSec = /(\d+(?:[.,]\d+)?)\s*second[oi]/gi;
let m;
while ((m = reOre.exec(t)) !== null) totalSec += parseFloat(m[1].replace(',', '.')) * 3600;
while ((m = reMin.exec(t)) !== null) totalSec += parseFloat(m[1].replace(',', '.')) * 60;
while ((m = reSec.exec(t)) !== null) totalSec += parseFloat(m[1].replace(',', '.'));
// "un paio di minuti" / "qualche minuto" / "pochi minuti"
if (totalSec === 0 && /(?:un\s+paio\s+di|qualche|pochi)\s+minut/i.test(t)) totalSec = 2 * 60;
// "qualche secondo"
if (totalSec === 0 && /qualche\s+second/i.test(t)) totalSec = 15;
return Math.round(totalSec);
}
function _formatTimerDisplay(sec) {
const m = Math.floor(Math.abs(sec) / 60);
const s = Math.abs(sec) % 60;
const sign = sec < 0 ? '-' : '';
const abs = Math.abs(sec);
const m = Math.floor(abs / 60);
const s = abs % 60;
const sign = sec < 0 ? '+' : '';
return `${sign}${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
}
function setupCookingTimer(stepText) {
clearCookingTimer();
/** Extract a short 2-3 word label from the step text for the timer. */
function _extractTimerLabel(text, stepNum) {
const fillers = new Set(['il','la','lo','le','gli','i','dell','della','dello','delle','degli','dei',
'un','una','uno','del','al','alla','allo','alle','agli','ai','nel','nella','nello','nelle',
'negli','nei','per','con','che','poi','e','o','non','se','in','di','a','da','fino','mentre',
'quando','dopo','prima','circa','bene','ancora','subito','su','ad','ed','più','meno','tutto','tutta']);
const timePatterns = [/mezz['']?\s*ora/i, /\bor[ae]\b/i, /\bmin(?:ut[oi])?\b/i, /\bsecond[oi]\b/i, /\bquarto\s+d['']?\s*ora/i];
let timeIdx = text.length;
for (const p of timePatterns) { const r = p.exec(text); if (r && r.index < timeIdx) timeIdx = r.index; }
const beforeTime = (text.slice(0, timeIdx).trim() || text);
const words = beforeTime.replace(/[.,!?;:'"()\[\]]/g, '').split(/\s+/).filter(w => w.length > 2 && !/^\d+$/.test(w));
const meaningful = words.filter(w => !fillers.has(w.toLowerCase()));
if (meaningful.length >= 1) return meaningful.slice(0, 3).join(' ');
return `Passo ${stepNum + 1}`;
}
function setupCookingTimerSuggestion(stepText) {
const seconds = _parseStepTimer(stepText);
const wrap = document.getElementById('cooking-timer-wrap');
const suggestEl = document.getElementById('cooking-timer-suggest');
if (seconds <= 0) {
wrap.style.display = 'none';
suggestEl.style.display = 'none';
_cookingSuggestedSeconds = 0;
_cookingSuggestedLabel = '';
return;
}
_cookingTimerTotal = seconds;
_cookingTimerSeconds = seconds;
_cookingTimerRunning = false;
wrap.style.display = 'flex';
document.getElementById('cooking-timer-display').textContent = _formatTimerDisplay(seconds);
document.getElementById('cooking-timer-display').className = 'cooking-timer-display';
document.getElementById('cooking-timer-start').textContent = '⏱️ Avvia timer';
document.getElementById('cooking-timer-start').classList.remove('timer-running');
document.getElementById('cooking-timer-reset').style.display = 'none';
_cookingSuggestedSeconds = seconds;
_cookingSuggestedLabel = _extractTimerLabel(stepText, _cookingStep);
document.getElementById('cooking-timer-suggest-text').textContent =
`⏱️ ${_formatTimerDisplay(seconds)} · ${_cookingSuggestedLabel}`;
suggestEl.style.display = 'flex';
}
function toggleCookingTimer() {
if (_cookingTimerRunning) {
// Pause
clearInterval(_cookingTimerInterval);
_cookingTimerInterval = null;
_cookingTimerRunning = false;
document.getElementById('cooking-timer-start').textContent = '▶️ Riprendi';
document.getElementById('cooking-timer-start').classList.remove('timer-running');
document.getElementById('cooking-timer-reset').style.display = 'inline-flex';
} else {
// Start / Resume
_cookingTimerRunning = true;
document.getElementById('cooking-timer-start').textContent = '⏸️ Pausa';
document.getElementById('cooking-timer-start').classList.add('timer-running');
document.getElementById('cooking-timer-reset').style.display = 'inline-flex';
_cookingTimerInterval = setInterval(() => {
_cookingTimerSeconds--;
const display = document.getElementById('cooking-timer-display');
display.textContent = _formatTimerDisplay(Math.abs(_cookingTimerSeconds));
function addSuggestedCookingTimer() {
if (_cookingSuggestedSeconds <= 0) return;
addCookingTimer(_cookingSuggestedSeconds, _cookingSuggestedLabel);
document.getElementById('cooking-timer-suggest').style.display = 'none';
_cookingSuggestedSeconds = 0;
}
if (_cookingTimerSeconds <= 0 && _cookingTimerSeconds > -1) {
// Timer just finished
display.classList.add('timer-done');
display.classList.remove('timer-warning');
_cookingTimerDone();
} else if (_cookingTimerSeconds > 0 && _cookingTimerSeconds <= 30) {
display.classList.add('timer-warning');
}
if (_cookingTimerSeconds < 0) {
// Counting overtime (negative)
display.textContent = '+' + _formatTimerDisplay(Math.abs(_cookingTimerSeconds));
}
function addCookingTimer(seconds, label) {
const id = ++_cookingTimerIdCounter;
_cookingTimers.push({ id, label, total: seconds, seconds, running: false, interval: null });
renderTimersBar();
toggleCookingTimerById(id); // auto-start
}
function removeCookingTimer(id) {
const t = _cookingTimers.find(t => t.id === id);
if (t && t.interval) clearInterval(t.interval);
_cookingTimers = _cookingTimers.filter(t => t.id !== id);
renderTimersBar();
}
function toggleCookingTimerById(id) {
const t = _cookingTimers.find(t => t.id === id);
if (!t) return;
if (t.running) {
clearInterval(t.interval);
t.interval = null;
t.running = false;
} else {
t.running = true;
t.interval = setInterval(() => {
t.seconds--;
if (t.seconds === 0) _cookingTimerDoneById(id);
_updateTimerCard(id);
}, 1000);
}
_updateTimerCard(id);
}
function resetCookingTimer() {
clearInterval(_cookingTimerInterval);
_cookingTimerInterval = null;
_cookingTimerRunning = false;
_cookingTimerSeconds = _cookingTimerTotal;
document.getElementById('cooking-timer-display').textContent = _formatTimerDisplay(_cookingTimerTotal);
document.getElementById('cooking-timer-display').className = 'cooking-timer-display';
document.getElementById('cooking-timer-start').textContent = '⏱️ Avvia timer';
document.getElementById('cooking-timer-start').classList.remove('timer-running');
document.getElementById('cooking-timer-reset').style.display = 'none';
function resetCookingTimerById(id) {
const t = _cookingTimers.find(t => t.id === id);
if (!t) return;
clearInterval(t.interval);
t.interval = null;
t.running = false;
t.seconds = t.total;
_updateTimerCard(id);
}
function clearCookingTimer() {
if (_cookingTimerInterval) clearInterval(_cookingTimerInterval);
_cookingTimerInterval = null;
_cookingTimerRunning = false;
_cookingTimerSeconds = 0;
_cookingTimerTotal = 0;
}
function _cookingTimerDone() {
// Vibrate if supported
function _cookingTimerDoneById(id) {
if (navigator.vibrate) navigator.vibrate([300, 100, 300, 100, 300]);
// TTS announcement
if (_cookingTTS) speakCookingStep('Tempo scaduto!');
// Visual alert already handled by .timer-done CSS animation
const t = _cookingTimers.find(t => t.id === id);
if (_cookingTTS && t) speakCookingStep(`Timer ${t.label} scaduto!`);
}
// ===== END COOKING TIMER =====
function _updateTimerCard(id) {
const t = _cookingTimers.find(t => t.id === id);
if (!t) return;
const card = document.getElementById(`ctimer-${id}`);
if (!card) { renderTimersBar(); return; }
const sec = t.seconds;
const dispEl = card.querySelector('.ctimer-display');
const toggleBtn = card.querySelector('.ctimer-toggle');
dispEl.textContent = _formatTimerDisplay(sec);
if (sec <= 0) {
dispEl.className = 'ctimer-display ctimer-done';
} else if (sec <= 30) {
dispEl.className = 'ctimer-display ctimer-warning';
} else {
dispEl.className = 'ctimer-display';
}
toggleBtn.textContent = t.running ? '⏸' : '▶';
toggleBtn.classList.toggle('running', t.running);
}
function renderTimersBar() {
const bar = document.getElementById('cooking-timers-bar');
if (!bar) return;
if (_cookingTimers.length === 0) {
bar.style.display = 'none';
bar.innerHTML = '';
return;
}
bar.style.display = 'flex';
bar.innerHTML = _cookingTimers.map(t => {
const sec = t.seconds;
const doneClass = sec <= 0 ? ' ctimer-done' : sec <= 30 ? ' ctimer-warning' : '';
const runClass = t.running ? ' running' : '';
return `<div class="cooking-timer-card" id="ctimer-${t.id}">
<span class="ctimer-label">${escapeHtml(t.label)}</span>
<span class="ctimer-display${doneClass}">${_formatTimerDisplay(sec)}</span>
<div class="ctimer-btns">
<button class="ctimer-btn ctimer-toggle${runClass}" onclick="toggleCookingTimerById(${t.id})">${t.running ? '⏸' : '▶'}</button>
<button class="ctimer-btn ctimer-reset" onclick="resetCookingTimerById(${t.id})"></button>
<button class="ctimer-btn ctimer-remove" onclick="removeCookingTimer(${t.id})"></button>
</div>
</div>`;
}).join('');
}
function clearAllCookingTimers() {
_cookingTimers.forEach(t => { if (t.interval) clearInterval(t.interval); });
_cookingTimers = [];
_cookingTimerIdCounter = 0;
_cookingSuggestedSeconds = 0;
_cookingSuggestedLabel = '';
const bar = document.getElementById('cooking-timers-bar');
if (bar) { bar.style.display = 'none'; bar.innerHTML = ''; }
}
// ===== END COOKING TIMER SYSTEM =====
function toggleCookingTTS() {
_cookingTTS = !_cookingTTS;
@@ -6383,6 +6458,8 @@ function navigateCookingStep(delta) {
const next = _cookingStep + delta;
if (next < 0) return;
if (next >= total) {
// All steps done: mark all visited, close overlay
for (let i = 0; i < total; i++) _cookingVisited.add(i);
closeCookingMode();
return;
}
+9 -6
View File
@@ -970,16 +970,19 @@
<span class="cooking-title" id="cooking-title"></span>
<button class="cooking-tts-btn" id="cooking-tts-btn" onclick="toggleCookingTTS()" title="Leggi ad alta voce">🔊</button>
</div>
<div id="cooking-timers-bar" class="cooking-timers-bar" style="display:none"></div>
<div class="cooking-body">
<div class="cooking-step-header">
<div class="cooking-step-num" id="cooking-step-num">1 / 1</div>
<button class="cooking-restart-btn" onclick="restartCookingMode()" title="Ricomincia dall'inizio">↺ Ricomincia</button>
</div>
<div class="cooking-progress-dots" id="cooking-progress-dots"></div>
<div class="cooking-step-text" id="cooking-step-text"></div>
<button class="cooking-replay-btn" id="cooking-replay" onclick="replayCookingTTS()" title="Rileggi questo passo">🔊 Rileggi</button>
<div class="cooking-timer-wrap" id="cooking-timer-wrap" style="display:none">
<div class="cooking-timer-display" id="cooking-timer-display">00:00</div>
<div class="cooking-timer-actions">
<button class="cooking-timer-btn" id="cooking-timer-start" onclick="toggleCookingTimer()">⏱️ Avvia timer</button>
<button class="cooking-timer-btn cooking-timer-reset" id="cooking-timer-reset" onclick="resetCookingTimer()" style="display:none">↩ Reset</button>
</div>
<div class="cooking-timer-suggest" id="cooking-timer-suggest" style="display:none">
<button class="cooking-timer-add-btn" onclick="addSuggestedCookingTimer()">
<span id="cooking-timer-suggest-text">⏱️ 00:00 · Timer</span>
</button>
</div>
<div class="cooking-step-ings" id="cooking-step-ings" style="display:none"></div>
</div>