// ==UserScript==
// @name Duolingo HearEverything
// @namespace http://tampermonkey.net/
// @version 0.62
// @description Reads aloud most sentences in Duo's challenges.
// @author Esh
// @match https://*.duolingo.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @downloadURL none
// ==/UserScript==
const VERSION = '0.62 --- 1 ---';
/*
// 0.62: auto play challenge form, gap-fill, tap-complete
// 0.61: mute Duo
// 0.60: add challenge listen
// 0.59.1: fix listen tap bug with '
// 0.59: add challenge read comprehension
// 0.58: read again challenge listen tap
// 0.57: remove interception calls
// 0.56: read again challenge translate
// 0.55: Fix challenge gap-fill
// 0.54: Simplify code (addConfig())
// 0.53: Auto play for speak challenge
// 0.52: Duo mute removed
// 0.51: Duo will be muted correctly
// 0.50: fixed challenge form
// 0.49: fixed challenge tap-complete
// 0.48: fixed challenge listen comprehension, included config
// 0.7: Mutation Observer instead of setInterval
// 0.6: Add voice to choices on click
// 0.6.1: check why not the innerText of the answer is displayed in the full sentence?
// 0.8.1: fix speaking numbers for options
// 0.9: Move speak button near the continue button
// 0.10.2: set better newPage = true - deleted
// 0.10.3: debug quirks from setting newPage
// 0.11: cleaned up some code
// 0.12: finally got rid of the new page problem
// 0.13: show some debug infos on the page
// 0.14: more working reading
// 0.15: added more challenges to read
// 0.16: Challenges, which work (some partially) // FORM, TRANSLATE, DIALOGUE, GAP_FILL, COMPLETE_REVERSE_TRANSLATION, TAP_COMPLETE
// 0.17: added shortcut ALT+l
// 0.18: listening button for DIALOGUE and bugfixing TRANSLATE
// 0.19: better listening button
// 0.20: Voice selection
// 0.21: cleaned up code
// 0.22: challenge translate (tap) working
// 0.22.1: no speaker button with translate from learning language
// 0.23: Alt + l for Duo buttons, too
// 0.24: tap-complete working
// 0.25: form challenge working
// 0.25.1: bugfix: challenge-translate
// 0.26: challenge read-comprehension
// 0.27: challenge name
// 0.28: autoplay for challenge translate
// 0.29: replace prompt at challenge gap fill
// 0.30: gap fill auto play
// 0.30.1: fixed playback stops
// 0.31: stops playback on new page
// 0.31.1: fixed gap fill not reading whole answer
// 0.32: auto play for complete reverse translation
// 0.33: toggle options readout at gap fill challenge
// 0.34: challenge dialogue auto play, auto intro, play options
// 0.34.1: bug fix only render intro button at challenge dialogue
// 0.35: challenge tapComplete
// 0.36: challenge form
// 0.37: challenge gap fill - extended
// bug fix challenge Tap Complete
// 0.38: better looking config menu
// 0.38.1: removed unused code
// 0.39: hide config on mouse click outside
// 0.40: challenge-name
// 0.41: get challenges data
// 0.42: get typo answers
// 0.43: mute most Duo speech
// 0.44: challenge hint in config popup, minor bugfixes
// 0.44.1: eventHandler for unmute - everything beta
// 0.44.2: gray borders for challenge hint
// 0.45: show more debug information
// 0.46: fixed autoIntro with challenge dialogue
// 0.47: fixed reading english challenge translate
*/
/*
// TODO: muto Duo with tap-tokens
// TODO: close debugs
// TODO: tips
// TODO: auto play for all challenges
// TODO: add translation after check
// TODO: speak english
*/
const LOG_STRING = 'Duolingo HearEverything: ';
const buttonPosition = 'bottom'; // bottom / top allowed
let voiceSelect;
let config = {};
const DEBUG = false;
// for config mouse hover
let hover = true;
let synth = window.speechSynthesis;
let voices = [];
let newPage = false;
let addedSpeech = false;
let challengesUrls = [];
let challengesReads = [];
let timeout;
let howlPlay = false;
let speakerButton = `
`;
// Element definitions
const WRONG_ANSWER_CLASS = '._1UqAr._1sqiF';
const RIGHT_ANSWER_CLASS = '._1UqAr._1Nmv6';
const RIGHT_CLASS = '._1Nmv6';
const WRONG_CLASS = '._1sqiF';
const ANSWER_HEADLINE = '._1x6Dk';
const ANSWER_CONTAINER = '._2ez4I';
const DIALOGUE_SPEAKER_CLASS = '_29e-M _39MJv _2Hg6H';
// currently used
const ANSWER_CLASS = '._1UqAr';
const ANSWER = 'blame';
const ANSWER_QS = '[data-test~="' + ANSWER + '"]';
const RIGHT_ANSWER = 'blame-correct';
const RIGHT_ANSWER_QS = '[data-test~="' + RIGHT_ANSWER + '"]';
const RIGHT_ANSWER_TYPO_QS = '[data-test~="' + RIGHT_ANSWER + '"] ._3gI0Y';
const WRONG_ANSWER = 'blame-incorrect';
const WRONG_ANSWER_QS = '[data-test~="' + WRONG_ANSWER + '"]';
const CHALLENGE_TAP_TOKEN = 'challenge-tap-token'; // challenge-translate (tap)
const CHALLENGE_TAP_TOKEN_QS = '[data-test="' + CHALLENGE_TAP_TOKEN + '"]';
const WORD_BANK = 'word-bank'; // if exists it's tap instead of keyboard (challenge-translate)
const WORD_BANK_QS = '[data-test="' + WORD_BANK + '"]';
const TRANSLATE_INPUT = 'challenge-translate-input';
const TRANSLATE_INPUT_QS = '[data-test="' + TRANSLATE_INPUT + '"]';
// const SPEAKER_BUTTON = 'speaker-button';
// const SPEAKER_BUTTON_QS = '[data-test="' + SPEAKER_BUTTON + '"]';
const SPEAKER_BUTTON = '._1KXUd._1I13x._2kfEr._1nlVc._2fOC9.UCrz7.t5wFJ';
const SPEAKER_BUTTON_QS = SPEAKER_BUTTON;
// const HINT_SENTENCE = 'hint-sentence';
// const HINT_SENTENCE_QS = '[data-test="' + HINT_SENTENCE + '"]';
const HINT_SENTENCE = '._29e-M._39MJv._2Hg6H';
const HINT_SENTENCE_QS = HINT_SENTENCE;
// const HINT_SENTENCE_CTC = '._3NgMa._2Hg6H'; // hint sentence replacement for challenge tap complete
// const HINT_SENTENCE_CTC_QS = HINT_SENTENCE_CTC;
const CHALLENGE_JUDGE = 'challenge-judge-text';
const CHALLENGE_JUDGE_QS = '[data-test="' + CHALLENGE_JUDGE + '"]';
const CHALLENGE_JUDGE_INTRO = 'hint-token';
const CHALLENGE_JUDGE_INTRO_QS = '[data-test="' + CHALLENGE_JUDGE_INTRO + '"]';
// const FORM_PROMPT = 'challenge-form-prompt';
// const FORM_PROMPT_QS = '[data-test="' + FORM_PROMPT + '"]';
const FORM_PROMPT = '._2SfAl._2Hg6H';
const FORM_PROMPT_QS = FORM_PROMPT;
// const CHALLENGE_CHOICE = 'challenge-choice';
// const CHALLENGE_CHOICE_QS = '[data-test="' + CHALLENGE_CHOICE + '"]';
const RIGHT_OPTION_QS = '[aria-checked="true"] div';
const TEXT_INPUT = 'challenge-text-input';
const TEXT_INPUT_QS = '[data-test="' + TEXT_INPUT + '"]';
const SPEAK_INTRO = 'speakIntro';
const SPEAK_INTRO_QS = '#' + SPEAK_INTRO;
const NEXT_BUTTON = 'player-next';
const NEXT_BUTTON_QS = '[data-test="' + NEXT_BUTTON + '"]';
const HINT_TOKEN = 'hint-token';
const HINT_TOKEN_QS = '[data-test="' + HINT_TOKEN + '"]';
// used page types
const FORM = 'challenge-form';
const TRANSLATE = 'challenge-translate';
const DIALOGUE = 'challenge-dialogue';
const GAP_FILL = 'challenge-gapFill';
const COMPLETE_REVERSE_TRANSLATION = 'challenge-completeReverseTranslation';
const TAP_COMPLETE = 'challenge-tapComplete';
const LISTEN_COMPREHENSION = 'challenge-listenComprehension';
const READ_COMPREHENSION = 'challenge-readComprehension';
const NAME = 'challenge-name';
const SPEAK = 'challenge-speak';
const LISTEN_TAP = 'challenge-listenTap';
const LISTEN = 'challenge-listen';
// duo reads aloud
const SELECT_TRANSCRIPTION = 'challenge-selectTranscription';
// allowed challenge types
const TEST = [FORM, TRANSLATE, DIALOGUE, GAP_FILL, COMPLETE_REVERSE_TRANSLATION, TAP_COMPLETE, LISTEN_COMPREHENSION, READ_COMPREHENSION, NAME, SPEAK, LISTEN_TAP, LISTEN];
// challenges where duo has to read
const NO_MUTE = [LISTEN, SELECT_TRANSCRIPTION, GAP_FILL, LISTEN_TAP, LISTEN_COMPREHENSION];
var buttonDisabled = true;
function debug(s) {
console.debug(LOG_STRING + s);
}
// needed for Duo Mute
// intercept xmlhttprequest to get session json and extract challenges
(function(open) {
XMLHttpRequest.prototype.open = function() {
this.addEventListener("readystatechange", function() {
if (this.readyState === 4 && this.responseURL.includes('sessions')) {
if (this.response.challenges) {
this.response.challenges.map((challenge) => {
if (typeof challenge.tts !== 'undefined') {
challengesUrls.push(challenge.tts);
challengesReads.push(challenge.prompt);
}
});
}
// console.log(challengesUrls);
// console.log(challengesReads);
}
}, false);
open.apply(this, arguments);
};
})(XMLHttpRequest.prototype.open);
// needed for Duo Muto
// Intercept Howl.play (which plays the sound over Howl.js).
// of course the linter doesn't know the object Howl, but it is there ;)
(function(play) {
Howl.prototype.play = function() {
if (config.he_muteduo) {
debug('intercepting Duo speaking');
//if (challengesUrls.includes(this._src)) {
let read = challengesReads[challengesUrls.indexOf(this._src)];
if (DEBUG) document.querySelector('#mySentence').innerText = read;
if (typeof read != 'undefined') {
debug('read = ' + read);
howlPlay = true;
clearTimeout(timeout);
let utter = generateUtter(read);
synth.cancel();
synth.speak(utter);
} else {
play.apply(this, arguments);
}
} else {
play.apply(this, arguments);
}
}
})(Howl.prototype.play);
window.onload = function() {
'use strict';
debug(VERSION);
// console.log('---------------------onload-------------------------');
voices = window.speechSynthesis.getVoices();
//debug(voices);
//setVoice();
readConfig();
new MutationObserver(start).observe(document.body, {
// attributes: true,
childList: true,
subtree: true
});
debug('MutationObserver running');
}
function setVoice() {
voiceSelect = GM_getValue('voiceSelect', 1000);
//debug('stored voice = ' + voiceSelect);
var duoState = JSON.parse(localStorage.getItem('duo.state'));
config.lang = duoState.user.learningLanguage;
if(voiceSelect == 1000) {
for (let i = 0; i < voices.length; i++) {
if(voices[i].lang.includes(config.lang)) {
voiceSelect = i;
//debug('auto set voice');
}
}
}
//debug(`voice = ${voiceSelect}, learning language = ${config.lang}`);
}
// toggles visibility
function togglePopout(id) {
let popout = document.getElementById(id);
// popout.style.display === "none" ? popout.style.display = "block" : popout.style.display = "none";
if (popout.style.display === 'none') {
popout.style.display = 'block';
document.addEventListener('click', closePopout);
popout.addEventListener('mouseenter', setHover);
popout.addEventListener('mouseleave', removeHover);
} else {
popout.style.display = 'none';
document.removeEventListener('click', closePopout);
popout.removeEventListener('mouseenter', setHover);
popout.removeEventListener('mouseleave', removeHover);
}
}
function setHover() {
hover = true;
// debug('hover = true');
}
function removeHover() {
setTimeout(function() { hover = false; }, '100');
}
function closePopout() {
// debug('closePopout: hover = ' + hover);
if (!hover) {
hover = true;
togglePopout('hearEverythingConfig');
}
}
function readConfig() {
config.ap_timeout = 1000; // GM_getValue('he_auto_timeout', 1000);
config.he_ct_auto = GM_getValue('he_ct_auto', true);
config.he_cgf_auto = GM_getValue('he_cgf_auto', true);
config.he_cgf_click = GM_getValue('he_cgf_click', true);
config.he_cgf_autointro = GM_getValue('he_cgf_autointro', true);
config.he_cd_auto = GM_getValue('he_cd_auto', false);
config.he_cd_click = GM_getValue('he_cd_click', true);
config.he_cd_autointro = GM_getValue('he_cd_autointro', true);
config.he_cf_auto = GM_getValue('he_cf_auto', true);
config.he_cf_click = GM_getValue('he_cf_click', true);
config.he_cf_autointro = GM_getValue('he_cf_autointro', false);
config.he_ctc_auto = GM_getValue('he_ctc_auto', true);
config.he_ctc_click = GM_getValue('he_ctc_click', false);
config.he_ctc_autointro = GM_getValue('he_ctc_autointro', true);
config.he_cn_auto = GM_getValue('he_cn_auto', true);
config.he_muteduo = GM_getValue('he_muteduo', false);
config.he_lc_click = GM_getValue('he_lc_click', true);
config.he_cs_auto = GM_getValue('he_cs_auto', true);
config.he_clt_auto = GM_getValue('he_clt_auto', true);
config.he_crc_auto = GM_getValue('he_crc_auto', true);
config.he_crc_click = GM_getValue('he_crc_click', false);
config.he_crc_autointro = GM_getValue('he_crc_autointro', false);
config.he_cl_auto = GM_getValue('he_cl_auto', true);
setVoice();
// console.debug(config);
}
// builds a configBlock
// auto = autoplay, click = read options, intro = read intro
function createConfigOption(challenge, prefix, auto, click, intro) {
let name = challenge.split('-');
for (let i = 0; i < name.length; i++) {
name[i] = name[i][0].toUpperCase() + name[i].substr(1);
}
name = name.join(' ');
let styleCheckbox = 'style="vertical-align: bottom;"';
let clickSpan = '';
let autoSpan = '';
let introSpan = '';
if (auto === true) autoSpan = ``;
if (click === true) clickSpan = ``;
if (intro === true) introSpan = ``;
let config = `
${name}:
${autoSpan}
${clickSpan}
${introSpan}
`;
return config;
}
// adds config to the page
function addConfig() {
if(!document.querySelector('#hearEverythingGear') && document.querySelector('[role="progressbar"]')) {
let configButton = document.createElement('button');
configButton.setAttribute('id', 'hearEverythingGear');
configButton.setAttribute('class', '_2hiHn _2kfEr _1nlVc _2fOC9 UCrz7 t5wFJ _1DC8p _2jNpf');
configButton.setAttribute('style', `grid-column: 3/3; background-image:url(//d35aaqx5ub95lt.cloudfront.net/images/gear.svg);
background-position: 0px 0px; background-repeat: no-repeat; background-size: contain;`);
let configDiv = document.createElement('div');
configDiv.setAttribute('class','_3yqw1 np6Tv _1Xlh1');
configDiv.setAttribute('style','display: none; position: fixed; margin-top: 1rem;');
configDiv.setAttribute('id','hearEverythingConfig');
let options = '';
for (let i = 0; i < voices.length; i++) {
options += ``;
}
let styleCheckbox = 'style="vertical-align: bottom;"';
let configMute = `
`;
// autoplay, play options, read intro
let configListenComprehension = createConfigOption(LISTEN_COMPREHENSION, 'lc', false, true, false);
let configTranslate = createConfigOption(TRANSLATE, 'ct', true, false, false);
let configGapFill = createConfigOption(GAP_FILL, 'cgf', true, true, true);
let configTapComplete = createConfigOption(TAP_COMPLETE, 'ctc', true, true, true);
let configForm = createConfigOption(FORM, 'cf', true, true, true);
let configDialogue = createConfigOption(DIALOGUE, 'cd', true, true, true);
let configName = createConfigOption(NAME, 'cn', true, false, false);
let configSpeak = createConfigOption(SPEAK, 'cs', true, false, false);
let configListenTap = createConfigOption(LISTEN_TAP, 'clt', true, false, false);
let configReadComprehension = createConfigOption(READ_COMPREHENSION, 'crc', true, true, true);
let configListen = createConfigOption(LISTEN, 'cl', true, false, false);
configDiv.innerHTML = `
${configListenComprehension}
${configTranslate}
${configGapFill}
${configTapComplete}
${configForm}
${configDialogue}
${configName}
${configSpeak}
${configListenTap}
${configReadComprehension}
${configListen}
${configMute}
`;
document.querySelector('[role="progressbar"]').insertAdjacentElement('afterend',configButton);
configButton.insertAdjacentElement('afterend', configDiv);
configButton.addEventListener('click', function () { togglePopout('hearEverythingConfig'); });
let configLanguage = document.getElementById('configLanguage')
configLanguage.querySelector('[value="' + voiceSelect + '"]').setAttribute('selected', true);
configLanguage.addEventListener('change', function() {
voiceSelect = configLanguage.options[configLanguage.selectedIndex].value;
GM_setValue('voiceSelect', voiceSelect);
setVoice();
});
setVisibleConfig();
document.getElementById('hearEverythingConfig').addEventListener('change', function(e) {
GM_setValue(e.target.id, e.target.checked);
config[e.target.id] = e.target.checked;
});
}
if(document.querySelector('#hearEverythingGear') && document.querySelector('[role="progressbar"]')) {
highlightConfig([LISTEN_COMPREHENSION, TRANSLATE, GAP_FILL, TAP_COMPLETE, FORM, DIALOGUE, NAME, SPEAK, LISTEN_TAP, READ_COMPREHENSION, LISTEN]);
}
}
// sets all checkboxes to the current config
function setVisibleConfig() {
(Object.keys(config)).map((key) => {
if (document.getElementById(key)) document.getElementById(key).checked = config[key];
});
}
// highlights the current challenge config
// list = array
function highlightConfig(list) {
list.map((entry) => {
document.querySelector('#config-' + entry).style = 'border: none; padding: 3px;';
});
// document.querySelector('#config-voice').style = 'border: none; padding: 3px;';
let challenge = getChallengeType()[0];
let element = document.querySelector('#config-' + challenge);
if (element !== null) element.style = 'border: 1px solid gray; border-radius: 2px; padding: 3px;';
}
// returns false or the spoken sentence
function isDuoSpeaking() {
}
function start() {
if (document.querySelector('[data-test="challenge-header"]')) {
addConfig();
checkNewPage();
let challenge = getChallengeType()[0];
if(challenge) {
buildDebug();
if (newPage === true) {
if (document.querySelector(ANSWER_QS) !== null) {
renderAnswerSpeakButton();
} else {
renderIntroSpeakButton();
// speak optionen
if (challenge === LISTEN_COMPREHENSION) {
if (addedSpeech === false && document.querySelectorAll(CHALLENGE_JUDGE_INTRO_QS).length !== 0) {
if (config.he_lc_click === true) {
let hint = document.querySelector(HINT_TOKEN_QS).parentNode.innerText.replace('…', '').replace('...', '');
addSpeech(CHALLENGE_JUDGE_QS, hint);
addedSpeech = true;
}
}
}
if (challenge === FORM) {
if (addedSpeech === false && document.querySelectorAll(CHALLENGE_JUDGE_QS).length !== 0) {
if (config.he_cf_click === true) {
addSpeech(CHALLENGE_JUDGE_QS);
addedSpeech = true;
}
}
}
if (challenge === GAP_FILL) {
if (addedSpeech === false && document.querySelectorAll(CHALLENGE_JUDGE_INTRO_QS).length !== 0) {
if (config.he_cgf_click === true) {
addSpeech(CHALLENGE_JUDGE_QS);
addedSpeech = true;
}
}
}
if (challenge === DIALOGUE) {
if (addedSpeech === false && document.querySelectorAll(CHALLENGE_JUDGE_INTRO_QS).length !== 0) {
if (config.he_cd_click === true) {
addSpeech(CHALLENGE_JUDGE_QS);
addedSpeech = true;
}
}
}
if (challenge === TAP_COMPLETE) {
if (addedSpeech === false && document.querySelectorAll(CHALLENGE_TAP_TOKEN_QS).length !== 0) {
if (config.he_ctc_click === true) {
addSpeech(CHALLENGE_TAP_TOKEN_QS);
addedSpeech = true;
}
}
}
if (challenge === READ_COMPREHENSION) {
if (addedSpeech === false && document.querySelectorAll(CHALLENGE_JUDGE_QS).length !== 0) {
if (config.he_crc_click === true) {
let hint = document.querySelector(HINT_TOKEN_QS).parentNode.parentNode.nextSibling.firstChild.innerText.replace('…', '').replace('...', '');
addSpeech(CHALLENGE_JUDGE_QS, hint);
addedSpeech = true;
}
}
}
}
}
} else {
// we detected no content to use, so we are not interested in this page
newPage = false;
}
}
}
function prepareChallengeGapFill() {
let answer;
if (document.querySelector(RIGHT_ANSWER_QS)) {
answer = document.querySelector(RIGHT_OPTION_QS).innerText;
}
if (document.querySelector(WRONG_ANSWER_QS)) {
let answerElement = document.querySelector(ANSWER_CLASS);
if(answerElement.lastElementChild) {
answer = answerElement.lastElementChild.innerText;
} else {
answer = answerElement.innerText;
}
}
// question
let read = document.querySelector(HINT_TOKEN_QS).parentNode.parentNode.innerText;
// new type, which has two blanc places
if (answer.includes('...')) {
let answers = answer.split(' ... ');
debug('answer 1 = ' + answers[0]);
debug('answer 2 = ' + answers[1]);
let reads = read.split('\n');
debug('reads = ' + reads);
if (reads.length === 2) {
read = answers[0] + reads[0] + answers[1] + reads[1];
} else {
read = reads[0] + answers[0] + reads[1] + answers[1] + reads[2];
}
} else {
// if the answer is at the start of the sentence, there's no \n
if (read.includes('\n')) {
read = read.replace('\n', answer);
} else {
read = answer + read;
}
}
document.querySelector(HINT_TOKEN_QS).parentNode.parentNode.innerHTML = `
${read}`;
return read;
}
function introChallengeGapFill() {
if (document.querySelector(HINT_TOKEN_QS)) {
let read = document.querySelector(HINT_TOKEN_QS).parentNode.parentNode.innerText;
let speaker = document.createElement('div');
speaker.innerHTML = speakerButton;
speaker.children[0].id = SPEAK_INTRO;
speaker.children[0].style = 'width:40px; height:40px; background:transparent; margin-left:-16px; margin-right:0px; padding-bottom:5px';
document.querySelector(HINT_TOKEN_QS).insertAdjacentElement('beforeBegin', speaker);
return read;
}
}
function prepareChallengeForm() {
let answer;
if (document.querySelector(RIGHT_ANSWER_QS)) {
answer = document.querySelector(RIGHT_OPTION_QS).innerText;
}
if (document.querySelector(WRONG_ANSWER_QS)) {
let answerElement = document.querySelector(ANSWER_CLASS);
if(answerElement.lastElementChild) {
answer = answerElement.lastElementChild.innerText;
} else {
answer = answerElement.innerText;
}
}
let read = document.querySelector(FORM_PROMPT_QS).getAttribute('data-prompt').replace(/_+/, answer);
document.querySelector(FORM_PROMPT_QS).innerHTML = `
${read}`;
return read;
}
function introChallengeForm() {
if (document.querySelector(FORM_PROMPT_QS)) {
let read = document.querySelector(FORM_PROMPT_QS).getAttribute('data-prompt').replace(/_+/, '\n');
let speaker = document.createElement('div');
speaker.innerHTML = speakerButton;
speaker.children[0].id = SPEAK_INTRO;
speaker.children[0].style = 'width:40px; height:40px; background:transparent; margin-left:-16px; margin-right:0px; padding-bottom:5px';
document.querySelector(FORM_PROMPT_QS).insertAdjacentElement('afterBegin', speaker);
return read;
}
}
function prepareChallengeName() {
let read;
if (document.querySelector(RIGHT_ANSWER_QS)) {
read = document.querySelector(TEXT_INPUT_QS).value;
}
if (document.querySelector(WRONG_ANSWER_QS)) {
read = document.querySelector(ANSWER_CLASS).innerText;
}
return read;
}
function prepareChallengeTranslate() {
let read;
if (document.querySelector(RIGHT_ANSWER_QS)) {
if (document.querySelector(WORD_BANK_QS)) {
// debug('innerText = ' + document.querySelector(CHALLENGE_TAP_TOKEN_QS).parentNode.parentNode.innerText);
read = document.querySelector(CHALLENGE_TAP_TOKEN_QS).parentNode.parentNode.innerText.replace(/\n/g, ' ');
read = read.replace(/' /g, "'");
} else {
let tI = document.querySelector(TRANSLATE_INPUT_QS);
if (tI.lang === config.lang) read = tI.innerHTML;
}
}
if (document.querySelector(WRONG_ANSWER_QS) || document.querySelector(RIGHT_ANSWER_TYPO_QS)) {
let answer = document.querySelector(ANSWER_CLASS);
if(answer.lastElementChild) {
read = answer.lastElementChild.innerText;
} else {
read = answer.innerText;
}
}
if (document.querySelector(SPEAKER_BUTTON_QS)) {
read = document.querySelector(HINT_TOKEN_QS).parentNode.innerText;
}
// console.debug('HearEverything: read = ' + read);
return read;
}
function prepareChallengeTapComplete() {
let read;
if (document.querySelector(RIGHT_ANSWER_QS)) {
read = document.querySelector(HINT_TOKEN_QS).parentNode.parentNode.innerText.replace(/\n/g, '');
}
if (document.querySelector(WRONG_ANSWER_QS)) {
read = document.querySelector(ANSWER_CLASS).innerText;
}
// console.debug('HearEverything: read = ' + read);
return read;
}
function prepareChallengeDialogue() {
let read;
let speaker1 = document.querySelector('[class="' + DIALOGUE_SPEAKER_CLASS + '"]').innerText;
let speaker2;
if(document.querySelector(WRONG_ANSWER_QS)) {
speaker2 = document.querySelector('._1UqAr._1sqiF').innerText;
} else {
speaker2 = document.querySelector('[aria-checked="true"]').querySelector('[data-test="challenge-judge-text"]').innerText;
}
read = speaker1 + '\n' + speaker2;
return read
}
function introChallengeDialogue() {
if (document.querySelector(HINT_SENTENCE_QS)) {
let read = document.querySelector(HINT_SENTENCE_QS).innerText;
let speaker = document.createElement('div');
speaker.innerHTML = speakerButton;
speaker.children[0].id = SPEAK_INTRO;
speaker.children[0].style = 'width:40px; height:40px; background:transparent; margin-left:-16px; margin-right:0px; padding-bottom:5px';
document.querySelector(HINT_SENTENCE_QS).insertAdjacentElement('afterBegin', speaker);
return read;
}
}
function prepareChallengeReadComprehension() {
let read;
let speaker1 = document.querySelector(HINT_TOKEN_QS).parentNode.innerText;
let speaker2;
if(document.querySelector(WRONG_ANSWER_QS)) {
speaker2 = document.querySelector(ANSWER_CLASS).innerText;
} else {
speaker2 = document.querySelector(RIGHT_OPTION_QS).innerText;
}
read = speaker1 + '\n' + document.querySelector(HINT_TOKEN_QS).parentNode.parentNode.nextSibling.firstChild.innerText.replace('...', ' ' +speaker2);
return read
}
function introChallengeReadComprehension() {
if (document.querySelector(HINT_TOKEN_QS)) {
let read = document.querySelector(HINT_TOKEN_QS).parentNode.innerText;
let speaker = document.createElement('div');
speaker.innerHTML = speakerButton;
speaker.children[0].id = SPEAK_INTRO;
speaker.children[0].style = 'width:40px; height:40px; background:transparent; margin-left:-7px; margin-bottom:-10px; margin-right:0px;';
document.querySelector(HINT_TOKEN_QS).parentNode.parentNode.insertAdjacentElement('beforeBegin', speaker);
return read;
}
}
function prepareChallengeSpeak() {
if (document.querySelector(HINT_TOKEN_QS)) {
let read = document.querySelector(HINT_TOKEN_QS).parentNode.innerText;
return read;
}
}
function prepareChallengeListenTap() {
if (document.querySelector(CHALLENGE_TAP_TOKEN_QS)) {
let read = document.querySelector(CHALLENGE_TAP_TOKEN_QS).parentNode.parentNode.innerText;
return read.replace(/\n/g, ' ').replace(/' /g, "'");
}
}
function renderIntroSpeakButton() {
if (document.querySelector(SPEAK_INTRO_QS) === null) {
let read = '';
let challenge = getChallengeType()[0];
if (challenge === DIALOGUE) read = introChallengeDialogue();
if (challenge === READ_COMPREHENSION) read = introChallengeReadComprehension();
if (challenge === FORM) read = introChallengeForm();
if (challenge === GAP_FILL || challenge === TAP_COMPLETE) read = introChallengeGapFill();
if (read !== '') {
if (DEBUG) document.querySelector('#mySentence').innerText = read;
debug('intro = ' + read);
let utter = generateUtter(read);
addSpeakListener(SPEAK_INTRO, utter, read);
if (challenge === DIALOGUE && config.he_cd_autointro) {
document.querySelector(SPEAK_INTRO_QS).click();
}
if (challenge === READ_COMPREHENSION && config.he_crc_autointro) {
document.querySelector(SPEAK_INTRO_QS).click();
}
if (challenge === FORM && config.he_cf_autointro) {
document.querySelector(SPEAK_INTRO_QS).click();
}
if (challenge === GAP_FILL && config.he_cgf_autointro) {
document.querySelector(SPEAK_INTRO_QS).click();
}
if (challenge === TAP_COMPLETE && config.he_ctc_autointro) {
document.querySelector(SPEAK_INTRO_QS).click();
}
}
}
}
function renderAnswerSpeakButton() {
let read = '';
let challenge = getChallengeType()[0];
if (challenge === FORM) {
read = prepareChallengeForm();
}
if (challenge === TRANSLATE || challenge === LISTEN || challenge === COMPLETE_REVERSE_TRANSLATION) {
read = prepareChallengeTranslate();
}
if (challenge === DIALOGUE) {
read = prepareChallengeDialogue();
}
if (challenge === READ_COMPREHENSION) {
read = prepareChallengeReadComprehension();
}
if (challenge === NAME) {
read = prepareChallengeName();
}
if (challenge === GAP_FILL) {
read = prepareChallengeGapFill();
}
/*
if (challenge === COMPLETE_REVERSE_TRANSLATION) {
read = prepareChallengeTranslate();
} */
if (challenge === TAP_COMPLETE) {
read = prepareChallengeTapComplete();
}
if (challenge === SPEAK) {
read = prepareChallengeSpeak();
}
if (challenge === LISTEN_TAP) {
read = prepareChallengeListenTap();
}
debug('read = ' + read);
let utter = generateUtter(read);
// add speaker button to answer and fill in the correct answer in the headline
updateText(read);
// if we have added the speaker button, we find it in the document
addSpeakListener('speak', utter, read);
// do it for every page to trigger the Duo speaker button also
document.removeEventListener('keydown', myShortcutListener);
document.addEventListener('keydown', myShortcutListener);
newPage = false;
// console.debug('Now it\'s an old page');
addedSpeech = false;
// console.debug('Reset: speech isn\'t attached to options any more');
// if you like autoplay, it waits 1 second an plays it
if (((challenge === TRANSLATE) || (challenge === COMPLETE_REVERSE_TRANSLATION)) && config.he_ct_auto === true) {
timeoutAutoplay(challenge, utter);
}
if (challenge === GAP_FILL && config.he_cgf_auto === true) {
timeoutAutoplay(challenge, utter);
}
if (challenge === TAP_COMPLETE && config.he_ctc_auto === true) {
timeoutAutoplay(challenge, utter);
}
if (challenge === FORM && config.he_cf_auto === true) {
timeoutAutoplay(challenge, utter);
}
if (challenge === DIALOGUE && config.he_cd_auto === true) {
timeoutAutoplay(challenge, utter);
}
if (challenge === NAME && config.he_cn_auto === true) {
timeoutAutoplay(challenge, utter);
}
if (challenge === SPEAK && config.he_cs_auto === true) {
timeoutAutoplay(challenge, utter);
}
if (challenge === LISTEN_TAP && config.he_clt_auto === true) {
timeoutAutoplay(challenge, utter);
}
if (challenge === READ_COMPREHENSION && config.he_crc_auto === true) {
timeoutAutoplay(challenge, utter);
}
if (challenge === LISTEN && config.he_cl_auto === true) {
timeoutAutoplay(challenge, utter);
}
}
function timeoutAutoplay(challenge, utter) {
timeout = setTimeout(function() {
debug('auto play ' + challenge);
synth.cancel();
synth.speak(utter);
}, config.ap_timeout);
}
function addSpeakListener(id, utter, read) {
let speak = document.querySelector('#' + id);
if(speak) {
speak.addEventListener('click',function () { synth.cancel(); synth.speak(utter); });
// console.debug('EventListener bound to speak button');
if (DEBUG) document.querySelector('#mySentence').innerText = read;
document.getElementById(id).title = read;
} else {
debug('No speak button found');
}
}
function generateUtter(read) {
let utter = new SpeechSynthesisUtterance(read);
utter.voice = voices[voiceSelect];
utter.volume = 1;
utter.pitch = 1;
utter.rate = 1;
utter.lang = config.lang;
return utter;
}
function myShortcutListener(event) {
let speak = document.querySelector('#speak');
let duoSpeak = document.querySelector(SPEAKER_BUTTON_QS);
// ALT + l combo
if (event.altKey && event.key === 'l') {
if (speak) { speak.click(); }
else if (duoSpeak) duoSpeak.click();
debug('alt = ' + event.altKey + ' + ' + event.key);
}
}
function readTapComplete(tap) {
let read = '';
let words = tap.childNodes;
words.forEach(function(word) {
if (word.nodeName === 'SPAN') read += word.children[0].innerText;
if (word.nodeName === 'DIV') {
read += word.querySelector('[class="_2Z2xv"]').children[0].innerText;
}
});
return read;
}
// gives some debug information directly in the Duo-GUI
function buildDebug() {
/*
const FORM = 'challenge-form';
const TRANSLATE = 'challenge-translate';
const DIALOGUE = 'challenge-dialogue';
const GAP_FILL = 'challenge-gapFill';
const COMPLETE_REVERSE_TRANSLATION = 'challenge-completeReverseTranslation';
const TAP_COMPLETE = 'challenge-tapComplete';
const LISTEN_COMPREHENSION = 'challenge-listenComprehension';
const READ_COMPREHENSION = 'challenge-readComprehension';
const NAME = 'challenge-name';
*/
if(DEBUG) {
if(!document.querySelector('#myChallenge')) {
let challenge = getChallengeType()[0];
let autoPlay = 'disabled';
let speakOption = 'disabled';
let autoIntro = 'disabled';
if (challenge === TRANSLATE && config.he_ct_auto) autoPlay = 'enabled';
// document.getElementById('he_cgf_auto').checked = config.he_cgf_auto;
if (challenge === GAP_FILL && config.he_cgf_auto) autoPlay = 'enabled';
// document.getElementById('he_cgf_click').checked = config.he_cgf_click;
if (challenge === GAP_FILL && config.he_cgf_click) speakOption = 'enabled';
// document.getElementById('he_ctc_auto').checked = config.he_ctc_auto;
if (challenge === TAP_COMPLETE && config.he_ctc_auto) autoPlay = 'enabled';
// document.getElementById('he_ctc_click').checked = config.he_ctc_click;
if (challenge === TAP_COMPLETE && config.he_ctc_click) speakOption = 'enabled';
// document.getElementById('he_cf_auto').checked = config.he_cf_auto;
if (challenge === FORM && config.he_cf_auto) autoPlay = 'enabled';
// document.getElementById('he_cf_click').checked = config.he_cf_click;
if (challenge === FORM && config.he_cf_click) speakOption = 'enabled';
// document.getElementById('he_cd_auto').checked = config.he_cd_auto;
if (challenge === DIALOGUE && config.he_cd_auto) autoPlay = 'enabled';
// document.getElementById('he_cd_click').checked = config.he_cd_click;
if (challenge === DIALOGUE && config.he_cd_click) speakOption = 'enabled';
// document.getElementById('he_cd_autointro').checked = config.he_cd_autointro;
if (challenge === DIALOGUE && config.he_cd_autointro) autoIntro = 'enabled';
// document.getElementById('he_cn_auto').checked = config.he_cn_auto;
if (challenge === NAME && config.he_cn_auto) autoPlay = 'enabled';
// document.getElementById('he_muteduo').checked = config.he_muteduo;
if (challenge === LISTEN_COMPREHENSION && config.he_lc_click) speakOption = 'enabled';
// if (challenge === DIALOGUE && config.he_cd_autointro) autoIntro = 'enabled';
if (challenge === SPEAK && config.he_cs_auto) autoPlay = 'enabled';
let debug = document.createElement('div');
debug.innerHTML = `
Challenge-Name: ${getChallengeType()[0]}
Sentence to speak:
Speak options: ${speakOption}
Auto play: ${autoPlay}
Auto intro: ${autoIntro}
Not found: `;
debug.style = "font-size: small; text-align:left; display:grid;";
document.querySelector('[data-test="challenge-header"]').insertAdjacentElement('afterend', debug);
if (!document.querySelector(HINT_SENTENCE_QS)) document.querySelector('#myNotFound').innerText += ' HINT_SENTENCE: ' + HINT_SENTENCE;
if (!document.querySelector(SPEAKER_BUTTON_QS)) document.querySelector('#myNotFound').innerText += ' SPEAKER_BUTTON: ' + SPEAKER_BUTTON;
}
}
}
function checkNewPage() {
if(!document.querySelector('#myNewPage')) {
let nP = document.createElement('div');
nP.id = 'myNewPage';
document.querySelector('[data-test="challenge-header"]').insertAdjacentElement('afterend', nP);
//console.debug('---- div - newPage ----');
debug('Challenge Type = ' + getChallengeType()[0]);
newPage = true;
if (howlPlay === false) {
synth.cancel();
} else {
howlPlay = false;
}
} else {
//console.debug('---- div - oldPage ----');
}
}
// returns challenge type or false
function getChallengeType() {
let element = document.querySelector('[data-test~="challenge"]');
if (element !== null) {
return [element.getAttribute('data-test').split(' ')[1], element];
} else {
return [false];
}
}
// gets the type of the current challenge
// returns array [type, HTMLElement]
// returns null if no usable type is found
// if returnEverything = true, it returns also noType
function parsedChallengeType(returnEverything = false) {
let type = null;
let noType = ['Unidentified Page Type'];
for (let i = 0; i < TEST.length; i++) {
if (document.querySelector(`[data-test="${TEST[i]}"]`)) {
type = [TEST[i], document.querySelector(`[data-test="${TEST[i]}"]`)];
}
}
if (document.querySelector(`[data-test="${SELECT_TRANSCRIPTION}"]`)) { noType = ['No usable page type (' + SELECT_TRANSCRIPTION + ')','']; }
if (document.querySelector(`[data-test="${READ_COMPREHENSION}"]`)) { noType = ['No usable page type (' + READ_COMPREHENSION + ')','']; }
if (document.querySelector(`[data-test="${LISTEN}"]`)) { noType = ['No usable page type (' + LISTEN + ')','']; }
if (document.querySelector(`[data-test="${LISTEN_TAP}"]`)) { noType = ['No usable page type (' + LISTEN_TAP + ')','']; }
if (document.querySelector(`[data-test="${LISTEN_COMPREHENSION}"]`)) { noType = ['No usable page type (' + LISTEN_COMPREHENSION + ')','']; }
// type ? console.info('HearEverything: Page Type = ' + type[0]) : console.info('HearEverything: ' + noType[0]);
if(returnEverything) {
return type ? type : noType;
} else {
return type;
}
}
function addSpeech(qs, t = '') {
//console.debug('Add speech to the option buttons:');
if (t !== '') t += ' ';
let options = document.querySelectorAll(qs);
for (let i=0; i