// 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 _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) {
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() {
_vrStopTimer();
var cfg = await _vrLoadConfig();
if (!cfg.isEnabled) 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
PluginAPI.registerHook('currentTaskChange', function (task) {
_vrCurrentTask = task;
});
// 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')) +
'
' +
'
' +
'' +
'
' +
'
' +
'' +
'' +
'
' +
'
' +
'' +
'' +
'
' +
'
' +
'
' +
'
' +
'' +
'' +
_vrEscapeHtml(String(cfg.volume)) +
'' +
'' +
'
' +
'
' +
'
' +
'' +
'' +
'
' +
'
';
PluginAPI.openDialog({
title: _vrT('SETTINGS.TITLE'),
htmlContent: html,
buttons: [
{
label: _vrT('SETTINGS.CANCEL'),
onClick: function () {},
},
{
label: _vrT('SETTINGS.SAVE'),
color: 'primary',
icon: 'save',
raised: true,
onClick: function () {
return saveSettings();
},
},
],
});
// Post-render setup
setTimeout(function () {
var volumeSlider = document.getElementById('vr-volume');
var volumeVal = document.getElementById('vr-volume-val');
if (volumeSlider && volumeVal) {
volumeSlider.addEventListener('input', function () {
volumeVal.textContent = volumeSlider.value;
});
}
var testBtn = document.getElementById('vr-test-btn');
if (testBtn) {
testBtn.addEventListener('click', function () {
var textInput = document.getElementById('vr-text');
var voiceSelect = document.getElementById('vr-voice');
var txt = textInput
? textInput.value.replace(
'${currentTaskTitle}',
_vrT('SETTINGS.TEST_PLACEHOLDER'),
)
: 'Test';
if (txt.length <= 1) txt = _vrT('SETTINGS.TEST_NO_TEXT');
var vol = volumeSlider ? parseInt(volumeSlider.value, 10) : 75;
var voice = voiceSelect ? voiceSelect.value : '';
_vrSpeak(txt, vol, voice);
});
}
// Handle async voice loading on some platforms
if (window.speechSynthesis && voices.length === 0) {
window.speechSynthesis.addEventListener(
'voiceschanged',
function () {
var newVoices = window.speechSynthesis.getVoices();
var voiceSelect = document.getElementById('vr-voice');
if (voiceSelect) {
voiceSelect.innerHTML = _vrBuildVoiceOptions(newVoices, cfg.voice);
}
},
{ once: true },
);
}
}, 100);
async function saveSettings() {
var enabledCb = document.getElementById('vr-enabled');
var textInput = document.getElementById('vr-text');
var intervalInput = document.getElementById('vr-interval');
var volumeSlider = document.getElementById('vr-volume');
var voiceSelect = document.getElementById('vr-voice');
var newCfg = {
isEnabled: enabledCb ? enabledCb.checked : false,
text: textInput ? textInput.value : cfg.text,
interval: intervalInput ? parseInt(intervalInput.value, 10) * 1000 : cfg.interval,
volume: volumeSlider ? parseInt(volumeSlider.value, 10) : cfg.volume,
voice: voiceSelect ? voiceSelect.value : cfg.voice,
};
await _vrSaveConfig(newCfg);
await _vrStartTimer();
PluginAPI.showSnack({
msg: _vrT('SETTINGS.SAVED_MSG'),
type: 'SUCCESS',
ico: 'check',
});
}
}
// Register config handler for settings button on plugin card
PluginAPI.registerConfigHandler(openVoiceReminderSettings);
} // end speechSynthesis guard