fix: cooking timer — AudioContext.resume() on mobile; always play beep; show done card 3s before dismiss
This commit is contained in:
+34
-19
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user