// Voice Reminder Plugin - Periodically speaks the current task title via Web Speech API if (!window.speechSynthesis) { console.warn('[voice-reminder] Web Speech API not available. Plugin disabled.'); } else { // --- plugin body (runs only when speechSynthesis is available) --- var _vrInterval = null; var _vrCurrentTask = null; var _vrDefaultTtsRate = 0.7; var _vrUnloaded = false; var _vrStartGen = 0; var _vrDefaults = { isEnabled: false, text: 'Your current task is: ${currentTaskTitle}', interval: 300000, volume: 75, voice: '', }; function _vrT(key, params) { return PluginAPI.translate(key, params); } async function _vrLoadConfig() { var raw = await PluginAPI.loadSyncedData(); if (!raw) return Object.assign({}, _vrDefaults); try { return Object.assign({}, _vrDefaults, JSON.parse(raw)); } catch (e) { return Object.assign({}, _vrDefaults); } } async function _vrSaveConfig(cfg) { await PluginAPI.persistDataSynced(JSON.stringify(cfg)); } function _vrSpeak(text, volume, voiceName) { // the settings dialog can outlive the plugin (Test button) — stay silent if (_vrUnloaded) return; var synth = window.speechSynthesis; if (!synth) { console.error('[voice-reminder] No window.speechSynthesis available.'); return; } synth.cancel(); var voices = synth.getVoices(); var utter = new SpeechSynthesisUtterance(); utter.text = text; utter.voice = voices.find(function (v) { return voiceName && v.voiceURI === voiceName; }) || voices.find(function (v) { return voiceName && v.name === voiceName; }) || voices.find(function (v) { return v.default; }) || null; utter.volume = volume / 100; utter.rate = _vrDefaultTtsRate; synth.speak(utter); } function _vrStopTimer() { if (_vrInterval) { clearInterval(_vrInterval); _vrInterval = null; } } async function _vrStartTimer() { var myGen = ++_vrStartGen; _vrStopTimer(); var cfg = await _vrLoadConfig(); // overlapping calls (e.g. Save racing the initial load) resume with stale // config — only the latest call may touch the timer, and only while the // plugin is still loaded (#8281) if (myGen !== _vrStartGen) return; _vrStopTimer(); if (!cfg.isEnabled || _vrUnloaded) return; var intervalMs = Math.max(cfg.interval || 300000, 5000); _vrInterval = setInterval(function () { if (!_vrCurrentTask) return; var txt = cfg.text.replace('${currentTaskTitle}', _vrCurrentTask.title); if (txt.length <= 1) { txt = _vrCurrentTask.title; } _vrSpeak(txt, cfg.volume, cfg.voice); }, intervalMs); } // Track current task via hook. The host emits { current, previous }. PluginAPI.registerHook('currentTaskChange', function (payload) { _vrCurrentTask = payload && payload.current ? payload.current : null; }); // Stop the reminder timer and any in-flight speech when the plugin is // disabled/reloaded — without this the interval survives unload (#8281). // shortcut: cancel() clears ALL renderer TTS, not just ours — fine while // this is the only plugin using speechSynthesis PluginAPI.onUnload(function () { _vrUnloaded = true; _vrStopTimer(); window.speechSynthesis.cancel(); }); // Initial load and start _vrLoadConfig().then(function (cfg) { if (cfg.isEnabled) { _vrStartTimer(); } }); // Config dialog helpers function _vrEscapeHtml(str) { var div = document.createElement('div'); div.appendChild(document.createTextNode(str)); return div.innerHTML.replace(/"/g, '"').replace(/'/g, '''); } function _vrBuildVoiceOptions(voiceList, selectedVoice) { var opts = ''; for (var i = 0; i < voiceList.length; i++) { var v = voiceList[i]; var selected = v.voiceURI === selectedVoice ? ' selected' : ''; opts += ''; } return opts; } async function openVoiceReminderSettings() { var cfg = await _vrLoadConfig(); var voices = []; if (window.speechSynthesis) { voices = window.speechSynthesis.getVoices(); } var voiceOptions = _vrBuildVoiceOptions(voices, cfg.voice); var html = '
' + _vrEscapeHtml(_vrT('SETTINGS.DESCRIPTION')) + '
' + '