fix: cooking timer — AudioContext.resume() on mobile; always play beep; show done card 3s before dismiss

This commit is contained in:
dadaloop82
2026-05-20 18:30:25 +00:00
parent 66f5a03503
commit 3a1f6cfd1e
+34 -19
View File
@@ -13644,7 +13644,6 @@ function _playCookingTimerSound(type = 'done') {
const Ctx = window.AudioContext || window.webkitAudioContext; const Ctx = window.AudioContext || window.webkitAudioContext;
if (!Ctx) return; if (!Ctx) return;
const ctx = new Ctx(); const ctx = new Ctx();
const now = ctx.currentTime;
const pattern = type === 'warning' const pattern = type === 'warning'
? [{ f: 880, d: 0.08, o: 0.00 }, { f: 1046, d: 0.10, o: 0.14 }] ? [{ f: 880, d: 0.08, o: 0.00 }, { f: 1046, d: 0.10, o: 0.14 }]
: [ : [
@@ -13653,22 +13652,32 @@ function _playCookingTimerSound(type = 'done') {
{ f: 1318, d: 0.14, o: 0.38 } { f: 1318, d: 0.14, o: 0.38 }
]; ];
for (const p of pattern) { const doPlay = () => {
const osc = ctx.createOscillator(); const now = ctx.currentTime;
const gain = ctx.createGain(); for (const p of pattern) {
osc.type = 'sine'; const osc = ctx.createOscillator();
osc.frequency.value = p.f; const gain = ctx.createGain();
gain.gain.setValueAtTime(0.0001, now + p.o); osc.type = 'sine';
gain.gain.exponentialRampToValueAtTime(0.12, now + p.o + 0.02); osc.frequency.value = p.f;
gain.gain.exponentialRampToValueAtTime(0.0001, now + p.o + p.d); gain.gain.setValueAtTime(0.0001, now + p.o);
osc.connect(gain); gain.gain.exponentialRampToValueAtTime(0.12, now + p.o + 0.02);
gain.connect(ctx.destination); gain.gain.exponentialRampToValueAtTime(0.0001, now + p.o + p.d);
osc.start(now + p.o); osc.connect(gain);
osc.stop(now + p.o + p.d + 0.02); gain.connect(ctx.destination);
} osc.start(now + p.o);
osc.stop(now + p.o + p.d + 0.02);
}
const endAt = now + Math.max(...pattern.map(p => p.o + p.d)) + 0.08;
setTimeout(() => { try { ctx.close(); } catch (_) { /* ignore */ } }, Math.max(120, Math.round((endAt - now) * 1000)));
};
const endAt = now + Math.max(...pattern.map(p => p.o + p.d)) + 0.08; // AudioContext starts suspended on mobile/Android after autoplay policy —
setTimeout(() => { try { ctx.close(); } catch (_) { /* ignore */ } }, Math.max(120, Math.round((endAt - now) * 1000))); // must call resume() before scheduling nodes, even outside a user gesture.
if (ctx.state === 'suspended') {
ctx.resume().then(doPlay).catch(() => {});
} else {
doPlay();
}
} catch (_) { /* ignore */ } } catch (_) { /* ignore */ }
} }
@@ -13679,10 +13688,12 @@ function _notifyCookingTimer(type, label) {
const hasBrowserTts = typeof window !== 'undefined' && 'speechSynthesis' in window; const hasBrowserTts = typeof window !== 'undefined' && 'speechSynthesis' in window;
const hasCustomTts = (s.tts_engine === 'custom' && !!s.tts_url); const hasCustomTts = (s.tts_engine === 'custom' && !!s.tts_url);
// Always play the beep — reliable even when speech synthesis fails silently
// (autoplay policy, no recent user gesture, Android WebView restrictions).
_playCookingTimerSound(type === 'warning' ? 'warning' : 'done');
if (_cookingTTS && (hasBrowserTts || hasCustomTts)) { if (_cookingTTS && (hasBrowserTts || hasCustomTts)) {
speakCookingStep(msg); speakCookingStep(msg);
} else {
_playCookingTimerSound(type === 'warning' ? 'warning' : 'done');
} }
} }
@@ -13888,8 +13899,12 @@ function _cookingTimerDoneById(id) {
timer.running = false; timer.running = false;
timer.seconds = 0; timer.seconds = 0;
// Show the done state in the card before removing it
_updateTimerCard(id);
_updateScreenFlash();
_notifyCookingTimer('done', timer.label); _notifyCookingTimer('done', timer.label);
removeCookingTimer(id); // auto-cancel finished timer (do not continue past 00:00) // Keep the done card visible for 3 s so the user sees which timer finished
setTimeout(() => removeCookingTimer(id), 3000);
} }
function _updateTimerCard(id) { function _updateTimerCard(id) {