/**
* General file to control dynamic interactions with the user.
*
* @author HugoFara <Hugo.Farajallah@protonmail.com>
* @license Unlicense <http://unlicense.org/>
* @since 2.0.3-fork
*/
/**
* Redirect the user to a specific page depending on the value
*/
function quickMenuRedirection (value) {
const qm = document.getElementById('quickmenu');
qm.selectedIndex = 0;
if (value == '') { return; }
if (value == 'INFO') {
top.location.href = 'docs/info.html';
} else if (value == 'rss_import') {
top.location.href = 'do_feeds.php?check_autoupdate=1';
} else {
top.location.href = value + '.php';
}
}
/**
* Create an interactable to add a new expression.
*
* WARNING! This function was not properly tested!
*
* @param {string[]} text An array of words forming the expression
* @param {string} attrs A group of attributes to add
* @param {int} length Number of words, should correspond to WoWordCount
* @param {string} hex Lowercase formatted version of the text.
* @param {bool} showallwords true: multi-word is a superscript, show mw index + words
* false: only show the multiword, hide the words
* @returns {undefined}
*
* @since 2.5.2-fork Don't hide multi-word index when inserting new multi-word.
*/
function newExpressionInteractable (text, attrs, length, hex, showallwords) {
const context = window.parent.document;
// From each multi-word group
for (key in text) {
// Remove any previous multi-word of same length + same position
$('#ID-' + key + '-' + length, context).remove();
// From text, select the first mword smaller than this one, or the first
// word in this mword
let next_term_key = '';
for (let j = length - 1; j > 0; j--) {
if (j == 1) { next_term_key = '#ID-' + key + '-1'; }
if ($('#ID-' + key + '-' + j, context).length) {
next_term_key = '#ID-' + key + '-' + j;
break;
}
}
// Add the multi-word marker before
$(next_term_key, context)
.before(
'<span id="ID-' + key + '-' + length + '"' + attrs + '>' + text[key] +
'</span>'
);
// Change multi-word properties
const multi_word = $('#ID-' + key + '-' + length, context);
multi_word.addClass('order' + key).attr('data_order', key);
const txt = multi_word
.nextUntil(
$('#ID-' + (parseInt(key) + length * 2 - 1) + '-1', context),
'[id$="-1"]'
)
.map(function () {
return $(this).text();
})
.get().join('');
const pos = $('#ID-' + key + '-1', context).attr('data_pos');
multi_word.attr('data_text', txt).attr('data_pos', pos);
// Hide the next words if necessary
if (showallwords) {
return;
}
const next_words = [];
// TODO: overlapsing multi-words
for (let i = 0; i < length * 2 - 1; i++) {
next_words.push('span[id="ID-' + (parseInt(key) + i) + '-1"]');
}
$(next_words.join(','), context).hide();
}
}
/**
* Scroll to a specific reading position
*
* @since 2.0.3-fork
*/
function goToLastPosition () {
// Last registered position to go to
const lookPos = LWT_DATA.text.reading_position;
// Position to scroll to
let pos = 0;
if (lookPos > 0) {
const posObj = $('.wsty[data_pos=' + lookPos + ']').not('.hide').eq(0);
if (posObj.attr('data_pos') === undefined) {
pos = $('.wsty').not('.hide').filter(function () {
return $(this).attr('data_pos') <= lookPos;
}).eq(-1);
}
}
$(document).scrollTo(pos);
focus();
setTimeout(overlib, 10);
setTimeout(cClick, 100);
}
/**
* Save the current reading position.
*
* @param {int} text_id Text id
* @param {int} position Position to save
*
* @since 2.9.0-fork
*/
function saveReadingPosition (text_id, position) {
$.post(
'api.php/v1/texts/' + text_id + '/reading-position',
{ position: position }
);
}
/**
* Save audio position
*/
function saveAudioPosition (text_id, pos) {
$.post(
'api.php/v1/texts/' + text_id + '/audio-position',
{ position: pos }
);
}
/**
* Get the phonetic version of a text.
*
* @param {string} text Text to convert to phonetics.
* @param {string} lang Language, either two letters code or four letters (BCP 47).
*
* @deprecated Since 2.10.0 use getPhoneticTextAsync
*/
function getPhoneticText (text, lang) {
let phoneticText;
$.ajax(
'api.php/v1/phonetic-reading',
{
async: false,
data: {
text: text,
lang: lang
},
dataType: 'json',
type: 'GET'
}
)
.done(
function (data) {
phoneticText = data.phonetic_reading;
}
);
return phoneticText;
}
/**
* Get the phonetic version of a text, asynchronous.
*
* @param {string} text Text to convert to phonetics.
* @param {string|number} lang Language, either two letters code or four letters (BCP 47), or language ID
*/
async function getPhoneticTextAsync (text, lang) {
const parameters = {
text: text
};
if (typeof lang == 'number') {
parameters.lang_id = lang;
} else {
parameters.lang = lang;
}
return $.getJSON(
'api.php/v1/phonetic-reading',
parameters
);
}
/**
* Replace any searchValue on object value by replaceValue with deepth.
*
* @param {dict} obj Object to search in
* @param {string} searchValue Value to find
* @param {string} replaceValue Value to replace with
* */
function deepReplace (obj, searchValue, replaceValue) {
for (let key in obj) {
if (typeof obj[key] === 'object') {
// Recursively search nested objects
deepReplace(obj[key], searchValue, replaceValue);
} else if (typeof obj[key] === 'string' && obj[key].includes(searchValue)) {
// If the property is a string and contains the searchValue, replace it
obj[key] = obj[key].replace(searchValue, replaceValue);
}
}
}
/**
* Find the first string starting with searchValue in object.
*
* @param {dict} obj Object to search in
* @param {string} searchValue Value to search
*/
function deepFindValue (obj, searchValue) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'string' && obj[key].startsWith(searchValue)) {
return obj[key];
} else if (typeof obj[key] === 'object') {
const result = deepFindValue(obj[key], searchValue);
if (result) {
return result;
}
}
}
}
return null; // Return null if no matching string is found
}
function readTextWithExternal (text, voice_api, lang) {
const fetchRequest = JSON.parse(voice_api);
// TODO: can expose more vars to Request
deepReplace(fetchRequest, 'lwt_term', text)
deepReplace(fetchRequest, 'lwt_lang', lang)
fetchRequest.options.body = JSON.stringify(fetchRequest.options.body)
fetch(fetchRequest.input, fetchRequest.options)
.then(response => response.json())
.then(data => {
const encodeString = deepFindValue(data, 'data:')
const utter = new Audio(encodeString)
utter.play()
})
.catch(error => {
console.error(error)
});
}
function cookieTTSSettings (language) {
const prefix = 'tts[' + language;
const lang_settings = {};
const num_vals = ['Rate', 'Pitch'];
const cookies = ['Rate', 'Pitch', 'Voice'];
let cookie_val;
for (const cook in cookies) {
cookie_val = getCookie(prefix + cook + ']');
if (cookie_val) {
if (num_vals.includes(cook)) {
lang_settings[cook.toLowerCase()] = parseFloat(cookie_val);
} else {
lang_settings[cook.toLowerCase()] = cookie_val;
}
}
}
return lang_settings;
}
/**
* Read a text aloud, works with a phonetic version only.
*
* @param {string} text Text to read, won't be parsed further.
* @param {string} lang Language code with BCP 47 convention
* (e. g. "en-US" for English with an American accent)
* @param {number} rate Reading rate
* @param {number} pitch Pitch value
*
* @return {SpeechSynthesisUtterance} The spoken message object
*
* @since 2.9.0 Accepts "voice" as a new optional argument
*/
function readRawTextAloud (text, lang, rate, pitch, voice) {
const msg = new SpeechSynthesisUtterance();
const tts_settings = cookieTTSSettings(lang.substring(0, 2));
msg.text = text;
if (lang) {
msg.lang = lang;
}
// Voice is a string but we have to assign a SpeechSynthesysVoice
const useVoice = voice || tts_settings.voice;
if (useVoice) {
const voices = window.speechSynthesis.getVoices();
for (let i = 0; i < voices.length; i++) {
if (voices[i].name === useVoice) {
msg.voice = voices[i];
}
}
}
if (rate) {
msg.rate = rate;
} else if (tts_settings.rate) {
msg.rate = tts_settings.rate;
}
if (pitch) {
msg.pitch = pitch;
} else if (tts_settings.pitch) {
msg.pitch = tts_settings.pitch;
}
window.speechSynthesis.speak(msg);
return msg;
}
/**
* Read a text aloud, may parse the text to get a phonetic version.
*
* @param {string} text Text to read, do not need to be phonetic
* @param {string} lang Language code with BCP 47 convention
* (e. g. "en-US" for English with an American accent)
* @param {number} rate Reading rate
* @param {number} pitch Pitch value
* @param {string} voice Optional voice, the result will depend on the browser used
*
* @since 2.9.0 Accepts "voice" as a new optional argument
*/
function readTextAloud (text, lang, rate, pitch, voice, convert_to_phonetic) {
if (convert_to_phonetic) {
getPhoneticTextAsync(text, lang)
.then(
function (data) {
readRawTextAloud(
data.phonetic_reading, lang, rate, pitch, voice
);
}
);
} else {
readRawTextAloud(text, lang, rate, pitch, voice);
}
}
function handleReadingConfiguration(language, term, lang_id) {
if (language.reading_mode == "direct" || language.reading_mode == "internal") {
const lang_settings = cookieTTSSettings(language.name);
if (language.reading_mode == "direct") {
// No reparsing needed
readRawTextAloud(
term,
language.abbreviation,
lang_settings.rate,
lang_settings.pitch,
lang_settings.voice
);
} else {
// Server handled reparsing
getPhoneticTextAsync(term, parseInt(lang_id, 10))
.then(
function (reparsed_text) {
readRawTextAloud(
reparsed_text.phonetic_reading,
language.abbreviation,
lang_settings.rate,
lang_settings.pitch,
lang_settings.voice
);
}
);
}
} else if (language.reading_mode == "external") {
// Use external API
readTextWithExternal(term, language.voiceapi, language.name);
}
}
function speechDispatcher (term, lang_id) {
return $.getJSON(
'api.php/v1/languages/' + lang_id + '/reading-configuration',
{ lang_id },
(data) => handleReadingConfiguration(data, term, parseInt(lang_id, 10))
);
}