Greasy Fork is available in English.
Reads aloud most sentences in Duo's challenges.
当前为
// ==UserScript==
// @name Duolingo HearEverything
// @namespace http://tampermonkey.net/
// @version 0.44
// @description Reads aloud most sentences in Duo's challenges.
// @author Esh
// @match https://*.duolingo.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// ==/UserScript==
/*
// 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.30.2: TODO: move autoplay and config check/uncheck checkbox GM_setValue to own function
// 0.30.3: TODO: move let utter to own function
// TODO: close button / close with outside click on config bubble
// TODO: config for challenges
// TODO: replace gaps with correct answers FORM,
// TODO: DIALOGUE should start with reading speaker 1
// TODO: maybe add listening to speaking experience?
// TODO: clean up the script even more
// TODO: Tipp-Page
// TODO: Hoots
// TODO: selector where the speak button should be
// TODO: translateInput speaker position top - where to place?
// TODO: Voice selector (automagic) - 1/4 done
// TODO: Add autoplay selector
// more examples on the bottom
// TODO: popup could be toggled by mouseover/mouseout
*/
const VERSION = '0.44 --- 1 ---';
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 speakerButton = `
<a class="_3UpNo _3EXrQ _2VrUB" data-test="speaker-button" title="Listen" id="speak">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 94 73" width="94" height="73" preserveAspectRatio="xMidYMid meet" style="padding-left: 20%; width: 80%; height: 100%; transform: translate3d(0px, 0px, 0px);">
<defs>
<clipPath id="__lottie_element_402"><rect width="94" height="73" x="0" y="0"></rect></clipPath>
<clipPath id="__lottie_element_404">
<path d="M0,0 L1000,0 L1000,1038 L0,1038z"></path>
</clipPath>
<clipPath id="__lottie_element_409">
<path d="M0,0 L1338,0 L1338,738 L0,738z"></path>
</clipPath>
</defs>
<g clip-path="url(#__lottie_element_402)">
<g clip-path="url(#__lottie_element_404)" transform="matrix(0.26499998569488525,0,0,0.26499998569488525,-84.5,-101.53498840332031)" opacity="1" style="display: block;">
<g transform="matrix(1.3600000143051147,0,0,1.3600000143051147,516.219970703125,522.4000244140625)" opacity="0.9069389991639046" style="display: block;">
<path stroke-linecap="round" stroke-linejoin="miter" fill-opacity="0" stroke-miterlimit="4" stroke="rgb(28,176,246)" stroke-opacity="1" stroke-width="22.485592375331898" d=" M48.88100051879883,-88.13400268554688 C79.822998046875,-70.9219970703125 100.77899932861328,-37.88800048828125 100.77899932861328,0 C100.77899932861328,37.9109992980957 79.7979965209961,70.96199798583984 48.82500076293945,88.16500091552734"></path>
</g>
<g style="display: block;" transform="matrix(1.3600000143051147,0,0,1.3600000143051147,516.219970703125,522.4000244140625)" opacity="1">
<path stroke-linecap="round" stroke-linejoin="miter" fill-opacity="0" stroke-miterlimit="4" stroke="rgb(28,176,246)" stroke-opacity="1" stroke-width="20.500305987715482" d=" M24.131000518798828,-42.808998107910156 C39.055999755859375,-34.37099838256836 49.14099884033203,-18.354000091552734 49.14099884033203,0 C49.14099884033203,18.386999130249023 39.02000045776367,34.42900085449219 24.049999237060547,42.854000091552734"></path>
</g>
<g clip-path="url(#__lottie_element_409)" transform="matrix(1.0370399951934814,0,0,0.9629600048065186,136.53640747070312,163.66775512695312)" opacity="1" style="display: block;">
<g transform="matrix(1,0,0,1,260.93701171875,373.6780090332031)" opacity="1" style="display: block;">
<g opacity="1" transform="matrix(6,0,0,6,0,0)">
<path fill="rgb(28,176,246)" fill-opacity="1" d=" M-8.293000221252441,-11.675000190734863 C-8.293000221252441,-11.675000190734863 -0.12300000339746475,-11.675000190734863 -0.12300000339746475,-11.675000190734863 C2.9070000648498535,-11.675000190734863 5.367000102996826,-9.21500015258789 5.367000102996826,-6.184999942779541 C5.367000102996826,-6.184999942779541 5.367000102996826,6.425000190734863 5.367000102996826,6.425000190734863 C5.367000102996826,9.454999923706055 2.9070000648498535,11.914999961853027 -0.12300000339746475,11.914999961853027 C-0.12300000339746475,11.914999961853027 -8.293000221252441,11.914999961853027 -8.293000221252441,11.914999961853027 C-11.322999954223633,11.914999961853027 -13.782999992370605,9.454999923706055 -13.782999992370605,6.425000190734863 C-13.782999992370605,6.425000190734863 -13.782999992370605,-6.184999942779541 -13.782999992370605,-6.184999942779541 C-13.782999992370605,-9.21500015258789 -11.322999954223633,-11.675000190734863 -8.293000221252441,-11.675000190734863z M-4.980999946594238,-11.656999588012695 C-4.980999946594238,-11.656999588012695 10.218999862670898,-22.32699966430664 10.218999862670898,-22.32699966430664 C11.24899959564209,-23.047000885009766 12.659000396728516,-22.797000885009766 13.369000434875488,-21.777000427246094 C13.638999938964844,-21.39699935913086 13.779000282287598,-20.937000274658203 13.779000282287598,-20.476999282836914 C13.779000282287598,-20.476999282836914 13.779000282287598,20.472999572753906 13.779000282287598,20.472999572753906 C13.779000282287598,21.722999572753906 12.769000053405762,22.732999801635742 11.519000053405762,22.732999801635742 C11.059000015258789,22.732999801635742 10.609000205993652,22.593000411987305 10.218999862670898,22.322999954223633 C10.218999862670898,22.322999954223633 -4.980999946594238,11.652999877929688 -4.980999946594238,11.652999877929688 C-5.580999851226807,11.232999801635742 -5.940999984741211,10.543000221252441 -5.940999984741211,9.803000450134277 C-5.940999984741211,9.803000450134277 -5.940999984741211,-9.807000160217285 -5.940999984741211,-9.807000160217285 C-5.940999984741211,-10.536999702453613 -5.580999851226807,-11.22700023651123 -4.980999946594238,-11.656999588012695z"></path>
<g opacity="1" transform="matrix(1,0,0,1,0,0)"></g>
</g>
</g>
</g>
</g>
</g>
</svg>
</a>
`;
// 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 HINT_SENTENCE = 'hint-sentence';
const HINT_SENTENCE_QS = '[data-test="' + HINT_SENTENCE + '"]';
const CHALLENGE_JUDGE = 'challenge-judge-text';
const CHALLENGE_JUDGE_QS = '[data-test="' + CHALLENGE_JUDGE + '"]';
const FORM_PROMPT = 'challenge-form-prompt';
const FORM_PROMPT_QS = '[data-test="' + 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 NEXT_BUTTON = 'player-next';
const NEXT_BUTTON_QS = '[data-test="' + NEXT_BUTTON + '"]';
// 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';
// unused page types
const LISTEN = 'challenge-listen';
// Word bank stuff
// is not read out by Duo all the time (but at "Tap what you hear");
const LISTEN_TAP = 'challenge-listenTap';
// has word bank and some filled up answer to read
// 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];
// 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);
}
// intercept xmlhttprequest to get session json
(function(open) {
XMLHttpRequest.prototype.open = function() {
this.addEventListener("readystatechange", function() {
if (this.readyState === 4 && this.responseURL.includes('sessions')) {
debug(this.responseURL);
let sessions = JSON.parse(this.response);
console.log(sessions);
}
//console.log("readyState: "+this.readyState);
}, false);
open.apply(this, arguments);
};
})(XMLHttpRequest.prototype.open);
// intercept htmlmediaelement to get playing source ... not working
(function(play) {
HTMLMediaElement.prototype.play = function() {
//this.addEventListener('canplaythrough', function() {
debug('play');
console.debug(this);
//}, false);
play.apply(this.arguments);
};
})(HTMLMediaElement.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');
// does not work
window.addEventListener('canplaythrough', function() {
debug('Duo is speaking');
}, false);
}
function setVoice() {
voiceSelect = GM_getValue('voiceSelect', 1000);
console.debug('HearEverything: 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;
console.debug('HearEverything: auto set voice');
}
}
}
console.debug(`HearEverything: 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');
// debug('hover = false');
}
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_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_ctc_auto = GM_getValue('he_ctc_auto', true);
config.he_ctc_click = GM_getValue('he_ctc_click', false);
config.he_cn_auto = GM_getValue('he_cn_auto', true);
config.he_muteduo = GM_getValue('he_muteduo', false);
setVoice();
console.debug(config);
}
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 = '<option value="1000">Auto</option>';
for (let i = 0; i < voices.length; i++) {
options += `<option value="${i}">${voices[i].name}</option>`;
}
let styleCheckbox = 'style="vertical-align: bottom;"';
let configTranslate = `
<div class="QowCP">
<div id="config-${TRANSLATE}">Challenge Translate:
<div class="myOptions">
<span><input type="checkbox" id="he_ct_auto" value="autoplay" ${styleCheckbox}></input><label for="he_ct_auto"> auto play</label></span>
<span></span>
<span></span>
</div>
</div>
</div>
`;
let configGapFill = `
<div class="QowCP">
<div id="config-${GAP_FILL}">Challenge Gap Fill:
<div class="myOptions"><span><input type="checkbox" id="he_cgf_auto" value="autoplay" ${styleCheckbox}></input><label for="he_cgf_auto"> auto play</label></span>
<span><input type="checkbox" id="he_cgf_click" value="readoptions" ${styleCheckbox}></input><label for="he_cgf_click"> read options</label></span>
<span></span></div>
</div>
</div>
`;
let configTapComplete = `
<div class="QowCP">
<div id="config-${TAP_COMPLETE}">Challenge Tap Complete:
<div class="myOptions"><span><input type="checkbox" id="he_ctc_auto" value="autoplay" ${styleCheckbox}></input><label for="he_ctc_auto"> auto play</label></span>
<span><input type="checkbox" id="he_ctc_click" value="readoptions" ${styleCheckbox}></input><label for="he_ctc_click"> read options</label></span>
<span></span></div>
</div>
</div>
`;
let configForm = `
<div class="QowCP">
<div id="config-${FORM}">Challenge Form:
<div class="myOptions"><span><input type="checkbox" id="he_cf_auto" value="autoplay" ${styleCheckbox}></input><label for="he_cf_auto"> auto play</label></span>
<span><input type="checkbox" id="he_cf_click" value="readoptions" ${styleCheckbox}></input><label for="he_cf_click"> read options</label></span>
<span></span></div>
</div>
</div>
`;
let configDialogue = `
<div class="QowCP">
<div id="config-${DIALOGUE}">Challenge Dialogue:
<div class="myOptions"><span><input type="checkbox" id="he_cd_auto" value="autoplay" ${styleCheckbox}></input><label for="he_cd_auto"> auto play</label></span>
<span><input type="checkbox" id="he_cd_click" value="readoptions" ${styleCheckbox}></input><label for="he_cd_click"> read options</label></span>
<span><input type="checkbox" id="he_cd_autointro" value="autointro" ${styleCheckbox}></input><label for="he_cd_autointro"> auto intro</label></span></div>
</div>
</div>
`;
let configName = `
<div class="QowCP">
<div id="config-${NAME}">Challenge Name:
<div class="myOptions">
<span><input type="checkbox" id="he_cn_auto" value="autoplay" ${styleCheckbox}></input><label for="he_cn_auto"> auto play</label></span>
<span></span>
<span></span>
</div>
</div>
</div>
`;
let configMute = `
<div class="QowCP">
<div id="config-voice">Duo Voice:
<div class="myOptions">
<span><input type="checkbox" id="he_muteduo" value="muteduo" ${styleCheckbox}></input><label for="he_muteduo"> mute</label></span>
<span></span>
<span></span>
</div>
</div>
</div>
`;
configDiv.innerHTML = `
<div class="_3uS_y eIZ_c" data-test="config-popout" style="--margin:20px;">
<div class="_2O14B _2XlFZ _1v2Gj WCcVn" style="z-index: 1;">
<div class="_1KUxv _1GJUD _3lagd SSzTP" style="width: auto;"><div class="_1cv-y"></div>
<div class="QowCP">
<div class="_1m77f" style="text-align: center">Language
<select style="background-color: #ffc800; color: white;" id="configLanguage">
${options}
</select>
</div>
</div>
<div class="QowCP" id="he_configChallenges">
<style>
.myOptions {
display: flex;
justify-content: space-around;
}
.myOptions span {
width: 15ch;
}
</style>
${configTranslate}
${configGapFill}
${configTapComplete}
${configForm}
${configDialogue}
${configName}
${configMute}
</div>
</div>
</div>`;
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();
});
document.getElementById('he_ct_auto').checked = config.he_ct_auto;
document.getElementById('he_cgf_auto').checked = config.he_cgf_auto;
document.getElementById('he_cgf_click').checked = config.he_cgf_click;
document.getElementById('he_ctc_auto').checked = config.he_ctc_auto;
document.getElementById('he_ctc_click').checked = config.he_ctc_click;
document.getElementById('he_cf_auto').checked = config.he_cf_auto;
document.getElementById('he_cf_click').checked = config.he_cf_click;
document.getElementById('he_cd_auto').checked = config.he_cd_auto;
document.getElementById('he_cd_click').checked = config.he_cd_click;
document.getElementById('he_cd_autointro').checked = config.he_cd_autointro;
document.getElementById('he_cn_auto').checked = config.he_cn_auto;
document.getElementById('he_muteduo').checked = config.he_muteduo;
document.getElementById('hearEverythingConfig').addEventListener('change', function(e) {
// console.debug(LOG_STRING + 'Target ID = ' + e.target.id);
GM_setValue(e.target.id, e.target.checked);
config[e.target.id] = e.target.checked;
});
}
if(document.querySelector('#hearEverythingGear') && document.querySelector('[role="progressbar"]')) {
document.querySelector('#config-'+TRANSLATE).style = 'border: none';
document.querySelector('#config-'+GAP_FILL).style = 'border: none';
document.querySelector('#config-'+TAP_COMPLETE).style = 'border: none';
document.querySelector('#config-'+FORM).style = 'border: none';
document.querySelector('#config-'+DIALOGUE).style = 'border: none';
document.querySelector('#config-'+NAME).style = 'border: none';
document.querySelector('#config-voice').style = 'border: none';
let challenge = getChallengeType()[0];
let element = document.querySelector('#config-'+challenge)
if (element !== null) element.style = 'border: 1px solid white';
if(!NO_MUTE.includes(challenge)) document.querySelector('#config-voice').style = 'border: 1px solid white';
/* .${challenge} {
border: 1px solid white;
} */
}
}
function muteDuo(mute) {
// mute web audio
// interact with website variables
window.eval(`window.Howler._muted = ${mute};`);
//debug('Howler muted: ' + window.eval('window.Howler._muted'));
}
function start() {
if (document.querySelector('[data-test="challenge-header"]')) {
addConfig();
buildDebug();
checkNewPage();
let challenge = getChallengeType()[0];
//if(TEST.includes(challenge)) {
if(challenge) {
if (NO_MUTE.includes(challenge)) {
muteDuo(false);
debug(challenge + ' not muted');
} else if (document.querySelector(NEXT_BUTTON_QS).disabled !== true) {
muteDuo(false);
} else {
muteDuo(config.he_muteduo);
}
if (newPage === true) {
if (document.querySelector(ANSWER_QS) !== null) {
renderAnswerSpeakButton();
} else if(addedSpeech===false) {
if (document.querySelectorAll(CHALLENGE_JUDGE_QS).length!==0) {
if (challenge === LISTEN_COMPREHENSION) {
let hint = document.querySelector(HINT_SENTENCE_QS).innerText.replace('...', '');
addSpeech(CHALLENGE_JUDGE_QS, hint);
addedSpeech = true;
}
if (challenge === FORM && config.he_cf_click === true) {
addSpeech(CHALLENGE_JUDGE_QS);
addedSpeech = true;
}
if (challenge === GAP_FILL && config.he_cgf_click === true) {
addSpeech(CHALLENGE_JUDGE_QS);
addedSpeech = true;
}
if (challenge === DIALOGUE && config.he_cd_click === true) {
addSpeech(CHALLENGE_JUDGE_QS);
addedSpeech = true;
}
} else if (document.querySelectorAll(CHALLENGE_TAP_TOKEN_QS).length !== 0) {
if (challenge === TAP_COMPLETE && config.he_ctc_click === true) {
addSpeech(CHALLENGE_TAP_TOKEN_QS);
addedSpeech = true;
}
}
}
renderIntroSpeakButton();
}
} else {
// we detected no content to use, so we are not interested in this page
newPage = false;
}
} // end challenge-header detection
}
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_SENTENCE_QS).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_SENTENCE_QS).parentNode.innerHTML = `<span>${read}</span>`;
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 = `<span>${read}</span>`;
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 = '';
// console.debug('HearEverything: read = ' + read);
return read;
}
function prepareChallengeTapComplete() {
let read;
if (document.querySelector(RIGHT_ANSWER_QS)) {
read = document.querySelector(HINT_SENTENCE_QS).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() {
let read = document.querySelector(HINT_SENTENCE_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:-16px; margin-right:0px;padding-bottom:5px';
document.querySelector(HINT_SENTENCE_QS).insertAdjacentElement('beforeBegin', speaker);
return read;
}
function prepareChallengeReadComprehension() {
let read;
let speaker1 = document.querySelector(HINT_SENTENCE_QS).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.querySelectorAll(HINT_SENTENCE_QS)[1].innerText.replace('...', ' ' +speaker2);
return read
}
function renderIntroSpeakButton() {
if (document.querySelector(SPEAK_INTRO_QS) === null) {
let read = '';
let challenge = getChallengeType()[0];
if (challenge === DIALOGUE) {
read = introChallengeDialogue();
}
if (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();
}
}
}
function renderAnswerSpeakButton() {
let read = '';
let challenge = getChallengeType()[0];
if (challenge === FORM) {
read = prepareChallengeForm();
}
if (challenge === TRANSLATE) {
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();
}
console.debug('HearEverything: 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 (DEBUG) document.querySelector('#myOptions').innerText = 'disabled';
// 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);
}
}
function timeoutAutoplay(challenge, utter) {
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 {
console.debug('HearEverything: 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();
console.debug(LOG_STRING + '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() {
if(DEBUG) {
if(!document.querySelector('#myChallenge')) {
let debug = document.createElement('div');
debug.innerHTML = `<span>Challenge-Name: <span id="myChallenge">${getChallengeType()[0]}</span></span>
<span>Sentence to speak: <span id="mySentence"></span></span>
<span>Speak options: <span id="myOptions">disabled</span></span>`;
debug.style = "font-size: small; text-align:left; display:grid;";
document.querySelector('[data-test="challenge-header"]').insertAdjacentElement('afterend', debug);
}
}
}
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 ----');
console.debug('HearEverything: Challenge Type = ' + getChallengeType()[0]);
newPage = true;
synth.cancel();
} 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<options.length; i++) {
let utter = generateUtter(t + options[i].innerText);
options[i].parentNode.addEventListener('click',function () { synth.cancel(); synth.speak(utter); });
// console.debug('EventListener bound: ' + options[i].innerHTML);
console.debug('HearEverything: Option = ' + t + options[i].innerText);
// options[i].id = 'eventListener' + i;
}
if (DEBUG) document.querySelector('#myOptions').innerText = 'enabled';
}
function updateText(t) {
// don't add a listen button if there is no text t
if (t !== '') {
// console.debug('We should now add a speak button');
let formPrompt = document.querySelector('[data-test="challenge-form-prompt"]');
let translateInput = document.querySelector('[data-test="challenge-translate-input"]');
if(TEST.includes(getChallengeType()[0])) {
let 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);
// console.debug('Speaker Button Bottom added');
}
} else {
document.querySelector('._3dRS9._3DKa-._1tuLI').insertAdjacentElement('afterBegin',div);
// console.debug('Speaker Button Bottom added');
}
}
}
}
}