From 7be6ae8cd7b6ef33f576ac78087522c53de84586 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Sun, 29 Mar 2026 16:09:12 +0000 Subject: [PATCH] feat: timer integrato nella modalita cucina - Rileva automaticamente durate nel testo dello step (minuti, ore, secondi, mezz'ora, un quarto d'ora, qualche minuto, un paio di minuti, ecc.) - Mostra countdown grande con Avvia/Pausa/Reset - Ultimi 30 secondi in arancione, scaduto in rosso pulsante - Allo scadere: vibrazione + TTS 'Tempo scaduto!' - Timer continua a contare in overtime (+00:XX) dopo lo zero - Timer si resetta automaticamente cambiando step o chiudendo --- assets/css/style.css | 70 ++++++++++++++++++++++ assets/js/app.js | 138 +++++++++++++++++++++++++++++++++++++++++++ data/dispensa.db | Bin 266240 -> 266240 bytes index.html | 9 ++- 4 files changed, 216 insertions(+), 1 deletion(-) diff --git a/assets/css/style.css b/assets/css/style.css index bf312e8..f38fccf 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -3067,6 +3067,76 @@ body { background: rgba(255,255,255,0.22); } +.cooking-timer-wrap { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + width: 100%; + max-width: 360px; + margin-top: 4px; +} + +.cooking-timer-display { + font-size: clamp(2.4rem, 10vw, 4rem); + font-weight: 700; + font-variant-numeric: tabular-nums; + color: #fff; + letter-spacing: 0.04em; + text-align: center; + transition: color 0.4s; +} + +.cooking-timer-display.timer-warning { + color: #f97316; +} + +.cooking-timer-display.timer-done { + color: #ef4444; + animation: timerPulse 0.8s ease-in-out infinite alternate; +} + +@keyframes timerPulse { + from { opacity: 1; transform: scale(1); } + to { opacity: 0.5; transform: scale(1.06); } +} + +.cooking-timer-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; + justify-content: center; +} + +.cooking-timer-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; + 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-step-ings { width: 100%; max-width: 480px; diff --git a/assets/js/app.js b/assets/js/app.js index f299fed..1fb7514 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -6043,6 +6043,7 @@ function closeCookingMode() { document.getElementById('cooking-overlay').style.display = 'none'; document.body.classList.remove('cooking-mode-active'); if ('speechSynthesis' in window) window.speechSynthesis.cancel(); + clearCookingTimer(); try { screen.orientation?.unlock(); } catch (_) { /* ignore */ } } @@ -6088,6 +6089,9 @@ function renderCookingStep() { prevBtn.disabled = _cookingStep === 0; nextBtn.textContent = _cookingStep === total - 1 ? '✅ Fine' : 'Successivo ▶'; + // Timer: detect durations in step text + setupCookingTimer(cleanStep); + // Speak step if (_cookingTTS) speakCookingStep(cleanStep); } @@ -6131,6 +6135,140 @@ function replayCookingTTS() { if (text) speakCookingStep(text); } +// ===== COOKING TIMER ===== +let _cookingTimerInterval = null; +let _cookingTimerSeconds = 0; +let _cookingTimerRunning = false; +let _cookingTimerTotal = 0; // original total seconds + +/** + * 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 ? '-' : ''; + return `${sign}${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; +} + +function setupCookingTimer(stepText) { + clearCookingTimer(); + const seconds = _parseStepTimer(stepText); + const wrap = document.getElementById('cooking-timer-wrap'); + if (seconds <= 0) { + wrap.style.display = 'none'; + 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'; +} + +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)); + + 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)); + } + }, 1000); + } +} + +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 clearCookingTimer() { + if (_cookingTimerInterval) clearInterval(_cookingTimerInterval); + _cookingTimerInterval = null; + _cookingTimerRunning = false; + _cookingTimerSeconds = 0; + _cookingTimerTotal = 0; +} + +function _cookingTimerDone() { + // Vibrate if supported + 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 +} +// ===== END COOKING TIMER ===== + function toggleCookingTTS() { _cookingTTS = !_cookingTTS; const btn = document.getElementById('cooking-tts-btn'); diff --git a/data/dispensa.db b/data/dispensa.db index b2730b9e09b9ec1abca257b473b46cf55754b18b..639b7a7364957376113960b0a2bb75f9f244b8d4 100644 GIT binary patch delta 1106 zcmZvbU1$_n6vywmXC`-McC)+C=ms@)7qdbrA+s}Xj8oa!iA`!v1e1q^LP-6Jl9qNC z6QNY4$yy4wrDPfLV(=l*2MdKrJJBJrj+o^$^H%N?6aj7=pbHp_bnA(sCYDvhtg-mee#nX&eWfScjO(2?QN(8!>3(<~(F z3v86cm*Y#ynYb7KU71q$$%olvR<;wR4R*_cUnv{4qdgjBr|f*s0%a9j?cGG#IcHyQ zO3c(C&<#r!UH8F**QYgAYi(9{G;3|EE4?|Zrm|T>%^7Oz z+LpE}mCYG?&d65VvVU5f5yS=3X^+c$oZElC9|Poj5VkAfv7BmXt?mfIRS|acCK)o3 z)qFd48fw;kQ--gez{_0p;FY>sMUKT5B$Kh48{&1*ip)w^+}AufR3or1vZZ3x^BYU> zi6DJUI{a#uJMkBEim=mP>s#I!X9}UoP$(glV<(1>7tQAvgE7hXzgX)>*9=W}`~HOi zplyWrc#!7{E6^$U4$k?BZhk~(P)Ol^%y5>%ih}ca8BYfPj+H)3k7vs55`mW>S*v#x zPmT&Z2K;5P?E&@zBY4sA5zubL* zk#uricUL~e73Al?rCa$n4;uobtZNe}6tIs?ayE}&C~U=l7(XzFP5OtQ)bO_lX!OI? zYYx4D_IlLmdXqx&VEsQ3YKB6UK%f!KDuF-@%w4>nyqXZaxIG~!_#d;lmA~cT zAlLda>CSoBBJiS%N#1=6>)egcuot;)V49!#8}IXXmuQmT`3*Pm@#pxA-|og*-hT-< NxS#!lMNtf8{Re%IE_(m~ delta 762 zcmX|9TSyd97(Tag=FBX+>vb&^cX2JYkY;9GFJq;vB2hl5lw}BMrX^7>ZV`c!W(1X$ zGUUmggdPk6p++4dEsTgH%3dO)A}H;lK(;lFkfd=zAAbG|AKwN3t|4dFkh3q1mH_~Q zYt`>9f@KdHYK!f=B!bgmdvjB3s5x9`JTESBrWEmmz_H?37CRjg#}9Ex+)I!0^SU#X ztSeQYb!WAzmsdwOOQ{l-p!h)?5uLO~SQ7t=zeIr^rdh`R8aJo+(H+L%!IP=*tj>kz z_@B0Kwh7y)5edDM;EMjDBU|7xLSO*&8jBrvOvDjjA-m@IN&1iu(;G&l>y02{7iwv5 zJ>F)aUS26|k+>-E;u(p{B0YvJ(u6IU27P96w4xk|0A@gs-!A}stTh~JJ{@jEs;mZb z6g5ZoTT`CZaunIED4HD51@g@>hGQJ8 zU_J`7B}H{B1)7qtDY<4Hh4;V1OvXjv_9RCEwcDdYF~5)J&Grchd`I z?LSx#Q72&45xAR0S7AkFX*a*lC;-qOV_XX#i>%5l{s2mJZS47OC7`K(O;)XMh;d#* zqnNny9s(He#{EXA=L8=2Vp@-vkk1HuGLY%Jh!-LJWO(FRp56mhJn*}`V25~;$QBUsvJrs{up^IA`oI|ragLHj X*1Z#_n7txt<2bcY%T1 / 1
+
@@ -990,6 +997,6 @@
- +