// ==UserScript== // @name Duolingo HearEverything // @namespace http://tampermonkey.net/ // @version 0.64.1 // @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.64.1 --- 1 ---'; const LOG_STRING = 'Duolingo HearEverything: '; let voiceSelect; const config = {}; const DEBUG = false; // for config mouse hover let hover = true; const synth = window.speechSynthesis; let voices = []; const challengesUrls = []; const challengesReads = []; let timeout; let howlPlay = false; let challenge = ''; const page = {}; page.isNewPage = false; page.isOptionSpeechAdded = false; const speakerButton = ` `; // Element definitions 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 = '._1KXUd._1I13x._2kfEr._1nlVc._2fOC9.UCrz7.t5wFJ'; const SPEAKER_BUTTON_QS = SPEAKER_BUTTON; const HINT_SENTENCE = '._29e-M._39MJv._2Hg6H'; const HINT_SENTENCE_QS = HINT_SENTENCE; 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 = '._2SfAl._2Hg6H'; const FORM_PROMPT_QS = FORM_PROMPT; 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 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'; const TAP_CLOZE_TABLE = 'challenge-tapClozeTable'; // allowed challenge types const ALLOW_LISTEN_BUTTON = [FORM, TRANSLATE, DIALOGUE, GAP_FILL, COMPLETE_REVERSE_TRANSLATION, TAP_COMPLETE, LISTEN_COMPREHENSION, READ_COMPREHENSION, NAME, SPEAK, LISTEN_TAP, LISTEN, TAP_CLOZE_TABLE]; // challenges where duo has to read // const NO_MUTE = [LISTEN, SELECT_TRANSCRIPTION, GAP_FILL, LISTEN_TAP, LISTEN_COMPREHENSION]; // let 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.forEach((rspChallenge) => { if (typeof rspChallenge.tts !== 'undefined') { challengesUrls.push(rspChallenge.tts); challengesReads.push(rspChallenge.prompt); } }); } } }, 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) { // eslint-disable-next-line no-undef Howl.prototype.play = function () { // if we read the options, Duo has to remain silent if (!page.isReadingOptions) { // if Duo is muted, we have to do his job if (config.he_muteduo) { debug('intercepting Duo speaking'); const read = challengesReads[challengesUrls.indexOf(this._src)]; if (DEBUG) document.querySelector('#mySentence').innerText = read; if (typeof read !== 'undefined') { debug('read = ' + read); howlPlay = true; clearTimeout(timeout); const utter = generateUtter(read); synth.cancel(); synth.speak(utter); } else { play.apply(this, arguments); } } else { play.apply(this, arguments); } } else { // We spoke it, so we can let Duo take over page.isReadingOptions = false; debug('Shhh Duo! I am reading the options'); } }; // eslint-disable-next-line no-undef })(Howl.prototype.play); window.onload = function () { 'use strict'; debug(VERSION); voices = window.speechSynthesis.getVoices(); readConfig(); new MutationObserver(start).observe(document.body, { childList: true, subtree: true }); debug('MutationObserver running'); }; // toggles visibility function togglePopout (id) { const popout = document.getElementById(id); 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; } function removeHover () { setTimeout(function () { hover = false; }, '100'); } function closePopout () { if (!hover) { hover = true; togglePopout('hearEverythingConfig'); } } } // gets the stored config function readConfig () { // eslint-disable-next-line no-undef voiceSelect = GM_getValue('voiceSelect', 1000); setVoice(); config.ap_timeout = 1000; // eslint-disable-next-line no-undef config.he_muteduo = GM_getValue('he_muteduo', false); configChallenge(DIALOGUE, 'cd', false, true, true); configChallenge(FORM, 'cf', true, true, false); configChallenge(GAP_FILL, 'cgf', true, true, true); configChallenge(LISTEN, 'cl', true, null, null); configChallenge(LISTEN_COMPREHENSION, 'clc', null, true, null); configChallenge(LISTEN_TAP, 'clt', true, true, null); configChallenge(NAME, 'cn', true, null, null); configChallenge(READ_COMPREHENSION, 'crc', true, false, false); configChallenge(SPEAK, 'cs', true, null, null); configChallenge(TAP_CLOZE_TABLE, 'ctct', false, null, true); configChallenge(TAP_COMPLETE, 'ctc', true, false, true); configChallenge(TRANSLATE, 'ct', true, null, null); // auto/click/autointro default: true/false, if not used: null function configChallenge (_challengeName, shortName, auto, click, autointro) { const keyAuto = 'he_' + shortName + '_auto'; const keyClick = 'he_' + shortName + '_click'; const keyAutointro = 'he_' + shortName + '_autointro'; // eslint-disable-next-line no-undef if (auto !== null) config[keyAuto] = GM_getValue(keyAuto, auto); // eslint-disable-next-line no-undef if (click !== null) config[keyClick] = GM_getValue(keyClick, click); // eslint-disable-next-line no-undef if (autointro !== null) config[keyAutointro] = GM_getValue(keyAutointro, autointro); } function setVoice () { const 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; } } } } } // adds config to the page function addConfig () { if (!document.querySelector('#hearEverythingGear') && document.querySelector('[role="progressbar"]')) { const 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;`); const 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 += ``; } const styleCheckbox = 'style="vertical-align: bottom;"'; const configMute = `
Duo Voice:
`; // autoplay, play options, read intro const configListenComprehension = createConfigOption(LISTEN_COMPREHENSION, 'clc', false, true, false); const configTranslate = createConfigOption(TRANSLATE, 'ct', true, false, false); const configGapFill = createConfigOption(GAP_FILL, 'cgf', true, true, true); const configTapComplete = createConfigOption(TAP_COMPLETE, 'ctc', true, true, true); const configForm = createConfigOption(FORM, 'cf', true, true, true); const configDialogue = createConfigOption(DIALOGUE, 'cd', true, true, true); const configName = createConfigOption(NAME, 'cn', true, false, false); const configSpeak = createConfigOption(SPEAK, 'cs', true, false, false); const configListenTap = createConfigOption(LISTEN_TAP, 'clt', true, true, false); const configReadComprehension = createConfigOption(READ_COMPREHENSION, 'crc', true, true, true); const configListen = createConfigOption(LISTEN, 'cl', true, false, false); const configTapClozeTable = createConfigOption(TAP_CLOZE_TABLE, 'ctct', true, false, true); configDiv.innerHTML = `
Language  
${configDialogue} ${configForm} ${configGapFill} ${configListen} ${configListenComprehension} ${configListenTap} ${configName} ${configReadComprehension} ${configSpeak} ${configTapClozeTable} ${configTapComplete} ${configTranslate} ${configMute}
`; document.querySelector('[role="progressbar"]').insertAdjacentElement('afterend', configButton); configButton.insertAdjacentElement('afterend', configDiv); configButton.addEventListener('click', function () { togglePopout('hearEverythingConfig'); }); const configLanguage = document.getElementById('configLanguage'); configLanguage.querySelector('[value="' + voiceSelect + '"]').setAttribute('selected', true); configLanguage.addEventListener('change', function () { voiceSelect = configLanguage.options[configLanguage.selectedIndex].value; // eslint-disable-next-line no-undef GM_setValue('voiceSelect', voiceSelect); }); setVisibleConfig(); document.getElementById('hearEverythingConfig').addEventListener('change', function (e) { // eslint-disable-next-line no-undef 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, TAP_CLOZE_TABLE]); } // builds a configBlock // auto = autoplay, click = read options, intro = read intro function createConfigOption (challengeName, prefix, auto, click, intro) { let name = challengeName.split('-'); for (let i = 0; i < name.length; i++) { name[i] = name[i][0].toUpperCase() + name[i].substr(1); } name = name.join(' '); const styleCheckbox = 'style="vertical-align: bottom;"'; let clickSpan = ''; let autoSpan = ''; let introSpan = ''; if (auto === true) autoSpan = ``; if (click === true) clickSpan = ``; if (intro === true) introSpan = ``; return `
${name}:
${autoSpan} ${clickSpan} ${introSpan}
`; } // sets all checkboxes to the current config function setVisibleConfig () { (Object.keys(config)).forEach((key) => { if (document.getElementById(key)) document.getElementById(key).checked = config[key]; }); } // highlights the current challenge config // list = array function highlightConfig (list) { list.forEach((entry) => { document.querySelector('#config-' + entry).style = 'border: none; padding: 3px;'; }); const element = document.querySelector('#config-' + challenge); if (element !== null) element.style = 'border: 1px solid gray; border-radius: 2px; padding: 3px;'; } } // start whenever the mutation observer wants you to start function start () { if (window.location.pathname.includes('/skill') && document.querySelector('[data-test="challenge-header"]')) { addConfig(); checkNewPage(); challenge = getChallengeType()[0]; page.challenge = getChallengeType()[0]; if (page.challenge) { buildDebug(); setupNewPage(); } else { page.isNewPage = false; } } function setupNewPage () { if (page.isNewPage) { page.isAnswerVisible = (document.querySelector(ANSWER_QS) !== null); page.hasIntroSpeakerButton = (document.querySelector(SPEAK_INTRO_QS) !== null); page.hasSpeakerButton = (document.querySelector(SPEAKER_BUTTON_QS) !== null); page.isWrongAnswer = (document.querySelector(WRONG_ANSWER_QS) !== null); page.isRightAnswer = (document.querySelector(RIGHT_ANSWER_QS) !== null); page.isRightAnswerTypo = (document.querySelector(RIGHT_ANSWER_TYPO_QS) !== null); if (page.isAnswerVisible) { renderAnswerSpeakButton(); } else { if (page.challenge === LISTEN_COMPREHENSION) setupListenComprehension(); if (page.challenge === FORM) setupForm(); if (page.challenge === GAP_FILL) setupGapFill(); if (page.challenge === DIALOGUE) setupDialogue(); if (page.challenge === TAP_COMPLETE) setupTapComplete(); if (page.challenge === (NAME || SPEAK)) setupIntroSpeakButton(); } if (page.challenge === TAP_CLOZE_TABLE) setupTapClozeTable(); if (page.challenge === READ_COMPREHENSION) setupReadComprehension(); if (page.challenge === (TRANSLATE || LISTEN || COMPLETE_REVERSE_TRANSLATION)) setupTranslate(); if (page.challenge === LISTEN_TAP) setupListenTap(); } } } function setupIntroSpeakButton () { if (!page.hasIntroSpeakerButton) renderIntroSpeakButton(); } function setupTapClozeTable () { if (!page.hasIntroSpeakerButton) renderIntroSpeakButton(introChallengeTapClozeTable(), config.he_ctct_autointro); if (page.isAnswerVisible) renderAnswerSpeakButton(prepareChallengeTapClozeTable(), config.he_ctct_auto); function introChallengeTapClozeTable () { const speaker = document.createElement('div'); speaker.innerHTML = speakerButton; speaker.children[0].id = SPEAK_INTRO; speaker.children[0].style = 'width:40px; height:40px; background:transparent; padding-bottom:15px; margin-bottom:-10px; margin-right:0px;'; document.querySelectorAll(HINT_TOKEN_QS)[1].insertAdjacentElement('beforeBegin', speaker); return document.querySelectorAll(HINT_TOKEN_QS)[1].innerText; } function prepareChallengeTapClozeTable () { if (page.isRightAnswer) { // it reads kind of 'vous\t\ns\navez' and should be 'vous savez' let read = document.querySelectorAll('[data-test="hint-token"]')[2].parentNode.parentNode.parentNode.innerText.replace('\t\n', ' ').replace('\n', ''); read += '\n' + document.querySelectorAll('[data-test="hint-token"]')[3].parentNode.parentNode.parentNode.innerText.replace('\t\n', ' ').replace('\n', ''); return read; } if (page.isWrongAnswer) { const answer = document.querySelector(ANSWER_CLASS); if (answer.lastElementChild) { return answer.lastElementChild.innerText; } else { return answer.innerText; } } } } function setupReadComprehension () { if (!page.hasIntroSpeakerButton) renderIntroSpeakButton(introChallengeReadComprehension(), config.he_crc_autointro); if (page.isAnswerVisible) renderAnswerSpeakButton(prepareChallengeReadComprehension(), config.he_crc_auto); if (!page.isOptionSpeechAdded && config.he_crc_click && document.querySelectorAll(CHALLENGE_JUDGE_QS).length !== 0) { const hint = document.querySelector(HINT_TOKEN_QS).parentNode.parentNode.nextSibling.firstChild.innerText.replace('…', '').replace('...', ''); addSpeech(CHALLENGE_JUDGE_QS, hint); } function prepareChallengeReadComprehension () { const speaker1 = document.querySelector(HINT_TOKEN_QS).parentNode.innerText; let speaker2; if (page.isWrongAnswer) { speaker2 = document.querySelector(ANSWER_CLASS).innerText; } else { speaker2 = document.querySelector(RIGHT_OPTION_QS).innerText; } return speaker1 + '\n' + document.querySelector(HINT_TOKEN_QS).parentNode.parentNode.nextSibling.firstChild.innerText.replace('...', ' ') + speaker2; } function introChallengeReadComprehension () { if (document.querySelector(HINT_TOKEN_QS)) { const read = document.querySelector(HINT_TOKEN_QS).parentNode.innerText; const 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 setupTranslate () { let configValue = config.he_ct_auto; if (page.challenge === LISTEN) configValue = config.he_cl_auto; // complete reverse translation uses the same config as translation, because it looks the same for the user if (page.challenge === COMPLETE_REVERSE_TRANSLATION) configValue = config.he_ct_auto; if (page.isAnswerVisible) renderAnswerSpeakButton(prepareChallengeTranslate(), configValue); function prepareChallengeTranslate () { let read; if (page.isRightAnswer) { if (document.querySelector(WORD_BANK_QS)) { read = document.querySelector(CHALLENGE_TAP_TOKEN_QS).parentNode.parentNode.innerText.replace(/\n/g, ' '); read = read.replace(/' /g, "'"); } else { const tI = document.querySelector(TRANSLATE_INPUT_QS); if (tI.lang === config.lang) read = tI.innerHTML; } } if (page.isWrongAnswer || page.isRightAnswerTypo) { const answer = document.querySelector(ANSWER_CLASS); if (answer.lastElementChild) { read = answer.lastElementChild.innerText; } else { read = answer.innerText; } } if (page.hasSpeakerButton) { read = document.querySelector(HINT_TOKEN_QS).parentNode.innerText; } return read; } } function setupListenTap () { if (page.isAnswerVisible) renderAnswerSpeakButton(prepareChallengeListenTap(), config.he_clt_auto); if (!page.isOptionSpeechAdded && config.he_clt_click && document.querySelectorAll(CHALLENGE_TAP_TOKEN_QS).length !== 0) { addSpeech(CHALLENGE_TAP_TOKEN_QS, '', true); } function prepareChallengeListenTap () { if (page.isWrongAnswer) { const answer = document.querySelector(ANSWER_CLASS); if (answer.lastElementChild) { return answer.lastElementChild.innerText; } else { return answer.innerText; } } if (page.isRightAnswer) { if (document.querySelector(CHALLENGE_TAP_TOKEN_QS)) { const read = document.querySelector(CHALLENGE_TAP_TOKEN_QS).parentNode.parentNode.innerText; return read.replace(/\n/g, ' ').replace(/' /g, "'"); } } } } function setupTapComplete () { renderIntroSpeakButton(); if (page.isOptionSpeechAdded === false && config.he_ctc_click && document.querySelectorAll(CHALLENGE_TAP_TOKEN_QS).length !== 0) { addSpeech(CHALLENGE_TAP_TOKEN_QS); } } function setupDialogue () { renderIntroSpeakButton(); if (page.isOptionSpeechAdded === false && document.querySelectorAll(CHALLENGE_JUDGE_INTRO_QS).length !== 0) { if (config.he_cd_click === true) { addSpeech(CHALLENGE_JUDGE_QS); page.isOptionSpeechAdded = true; } } } function setupGapFill () { renderIntroSpeakButton(); if (page.isOptionSpeechAdded === false && document.querySelectorAll(CHALLENGE_JUDGE_INTRO_QS).length !== 0) { if (config.he_cgf_click === true) { addSpeech(CHALLENGE_JUDGE_QS); page.isOptionSpeechAdded = true; } } } function setupForm () { renderIntroSpeakButton(); if (page.isOptionSpeechAdded === false && document.querySelectorAll(CHALLENGE_JUDGE_QS).length !== 0) { if (config.he_cf_click === true) { addSpeech(CHALLENGE_JUDGE_QS); page.isOptionSpeechAdded = true; } } } function setupListenComprehension () { renderIntroSpeakButton(); if (page.isOptionSpeechAdded === false && document.querySelectorAll(CHALLENGE_JUDGE_INTRO_QS).length !== 0) { if (config.he_clc_click === true) { const hint = document.querySelector(HINT_TOKEN_QS).parentNode.innerText.replace('…', '').replace('...', ''); addSpeech(CHALLENGE_JUDGE_QS, hint); page.isOptionSpeechAdded = true; } } } function prepareChallengeGapFill () { let answer; if (document.querySelector(RIGHT_ANSWER_QS)) { answer = document.querySelector(RIGHT_OPTION_QS).innerText; } if (document.querySelector(WRONG_ANSWER_QS)) { const 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('...')) { const answers = answer.split(' ... '); debug('answer 1 = ' + answers[0]); debug('answer 2 = ' + answers[1]); const 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)) { const read = document.querySelector(HINT_TOKEN_QS).parentNode.parentNode.innerText; const 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)) { const answerElement = document.querySelector(ANSWER_CLASS); if (answerElement.lastElementChild) { answer = answerElement.lastElementChild.innerText; } else { answer = answerElement.innerText; } } const 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)) { const read = document.querySelector(FORM_PROMPT_QS).getAttribute('data-prompt').replace(/_+/, '\n'); const 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 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; } return read; } function prepareChallengeDialogue () { const 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; } return speaker1 + '\n' + speaker2; } function introChallengeDialogue () { if (document.querySelector(HINT_SENTENCE_QS)) { const read = document.querySelector(HINT_SENTENCE_QS).innerText; const 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 prepareChallengeSpeak () { if (document.querySelector(HINT_TOKEN_QS)) { return document.querySelector(HINT_TOKEN_QS).parentNode.innerText; } } function renderIntroSpeakButton (read = '', click = false) { if (document.querySelector(SPEAK_INTRO_QS) === null || challenge === TAP_CLOZE_TABLE) { if (read === '') read = getIntroReadText(); if (read !== '') { if (DEBUG) document.querySelector('#mySentence').innerText = read; debug('intro = ' + read); const utter = generateUtter(read); addSpeakListener(SPEAK_INTRO, utter, read); clickIntroButton(DIALOGUE, 'cd'); // clickIntroButton(READ_COMPREHENSION, 'crc'); clickIntroButton(FORM, 'cf'); clickIntroButton(GAP_FILL, 'cgf'); clickIntroButton(TAP_COMPLETE, 'ctc'); // clickIntroButton(TAP_CLOZE_TABLE, 'ctct'); if (click) document.querySelector(SPEAK_INTRO_QS).click(); } } } function getIntroReadText () { if (challenge === DIALOGUE) return introChallengeDialogue(); // if (challenge === READ_COMPREHENSION) return introChallengeReadComprehension(); if (challenge === FORM) return introChallengeForm(); if (challenge === GAP_FILL || challenge === TAP_COMPLETE) return introChallengeGapFill(); // if (challenge === TAP_CLOZE_TABLE) return introChallengeTapClozeTable(); } function clickIntroButton (challengeName, shortName) { const key = 'he_' + shortName + '_autointro'; if (challenge === challengeName && config[key]) { document.querySelector(SPEAK_INTRO_QS).click(); } } function renderAnswerSpeakButton (read = '', auto = false) { 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 === TAP_COMPLETE) { read = prepareChallengeTapComplete(); } if (challenge === SPEAK) { read = prepareChallengeSpeak(); } /* if (challenge === LISTEN_TAP) { read = prepareChallengeListenTap(); } */ debug('renderAnswerSpeakButton read = ' + read); const 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); page.isNewPage = false; page.isOptionSpeechAdded = false; // if you like autoplay, it waits 1 second an plays it /* if (((challenge === TRANSLATE) || (challenge === COMPLETE_REVERSE_TRANSLATION)) && config.he_ct_auto === true) { timeoutAutoplay(utter); } */ if (challenge === GAP_FILL && config.he_cgf_auto === true) { timeoutAutoplay(utter); } if (challenge === TAP_COMPLETE && config.he_ctc_auto === true) { timeoutAutoplay(utter); } if (challenge === FORM && config.he_cf_auto === true) { timeoutAutoplay(utter); } if (challenge === DIALOGUE && config.he_cd_auto === true) { timeoutAutoplay(utter); } if (challenge === NAME && config.he_cn_auto === true) { timeoutAutoplay(utter); } if (challenge === SPEAK && config.he_cs_auto === true) { timeoutAutoplay(utter); } /* if (challenge === LISTEN_TAP && config.he_clt_auto === true) { timeoutAutoplay(utter); } */ /* if (challenge === READ_COMPREHENSION && config.he_crc_auto === true) { timeoutAutoplay(utter); } */ if (challenge === LISTEN && config.he_cl_auto === true) { timeoutAutoplay(utter); } /* if (challenge === TAP_CLOZE_TABLE && config.he_ctct_auto === true) { timeoutAutoplay(utter); } */ if (auto) timeoutAutoplay(utter); } function timeoutAutoplay (utter) { timeout = setTimeout(function () { debug('auto play ' + page.challenge); synth.cancel(); synth.speak(utter); }, config.ap_timeout); } function addSpeakListener (id, utter, read) { const speak = document.querySelector('#' + id); if (speak) { speak.addEventListener('click', function () { synth.cancel(); synth.speak(utter); }); if (DEBUG) document.querySelector('#mySentence').innerText = read; document.getElementById(id).title = read; } else { debug('No speak button found'); } } function generateUtter (read) { const 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) { const speak = document.querySelector('#speak'); const 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); } } // gives some debug information directly in the Duo-GUI function buildDebug () { if (DEBUG && !document.querySelector('#myChallenge')) { let autoPlay = 'disabled'; let speakOptions = 'disabled'; let autoIntro = 'disabled'; autoPlay = getEnabled(autoPlay, TRANSLATE, config.he_ct_auto); autoPlay = getEnabled(autoPlay, GAP_FILL, config.he_cgf_auto); speakOptions = getEnabled(speakOptions, GAP_FILL, config.he_cgf_click); autoPlay = getEnabled(autoPlay, TAP_COMPLETE, config.he_ctc_auto); speakOptions = getEnabled(speakOptions, TAP_COMPLETE, config.he_ctc_click); autoPlay = getEnabled(autoPlay, FORM, config.he_cf_auto); speakOptions = getEnabled(speakOptions, FORM, config.he_cf_click); autoPlay = getEnabled(autoPlay, DIALOGUE, config.he_cd_auto); speakOptions = getEnabled(speakOptions, DIALOGUE, config.he_cd_click); autoIntro = getEnabled(autoIntro, DIALOGUE, config.he_cd_autointro); autoPlay = getEnabled(autoPlay, NAME, config.he_cn_auto); speakOptions = getEnabled(speakOptions, LISTEN_COMPREHENSION, config.he_clc_click); autoPlay = getEnabled(autoPlay, SPEAK, config.he_cs_auto); buildDebugDiv(speakOptions, autoPlay, autoIntro); } // sets a option to 'enabled' if challengeName and configOption are true for this page function getEnabled (option, challengeName, configOption) { if (page.challenge === challengeName && configOption) option = 'enabled'; return option; } function buildDebugDiv (speakOptions, autoPlay, autoIntro) { const debugDiv = document.createElement('div'); debugDiv.innerHTML = `Challenge-Name: ${getChallengeType()[0]} Sentence to speak: Speak options: ${speakOptions} Auto play: ${autoPlay} Auto intro: ${autoIntro} Not found: `; debugDiv.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')) { const nP = document.createElement('div'); nP.id = 'myNewPage'; document.querySelector('[data-test="challenge-header"]').insertAdjacentElement('afterend', nP); debug('Challenge Type = ' + getChallengeType()[0]); page.isNewPage = true; if (howlPlay === false) { synth.cancel(); } else { howlPlay = false; } } } // returns challenge type or false function getChallengeType () { const element = document.querySelector('[data-test~="challenge"]'); if (element !== null) { return [element.getAttribute('data-test').split(' ')[1], element]; } else { return [false]; } } function addSpeech (qs, t = '', overrideDuo = false) { if (t !== '') t += ' '; const options = document.querySelectorAll(qs); for (const option of options) { const utter = generateUtter(t + option.innerText); option.parentNode.addEventListener('click', function () { if (overrideDuo) page.isReadingOptions = true; debug('Option read = ' + t + option.innerText); synth.cancel(); synth.speak(utter); }); } page.isOptionSpeechAdded = true; } function updateText (t) { // don't add a listen button if there is no text t if (t !== '' && ALLOW_LISTEN_BUTTON.includes(getChallengeType()[0])) { const translateInput = document.querySelector(TRANSLATE_INPUT_QS); const div = document.createElement('div'); div.class = 'np6Tv'; div.style = 'position: absolute; align-self: flex-end; top: 1.8rem;'; div.innerHTML = speakerButton; // if the answer is displayed if (document.querySelector('._3dRS9._3DKa-._1tuLI')) { if (translateInput !== null) { if (translateInput.lang === config.lang) { document.querySelector('._3dRS9._3DKa-._1tuLI').insertAdjacentElement('afterBegin', div); } } else { document.querySelector('._3dRS9._3DKa-._1tuLI').insertAdjacentElement('afterBegin', div); } } } }