Greasy Fork

Greasy Fork is available in English.

Duolingo HearEverything

Reads aloud most sentences in Duo's challenges.

当前为 2021-07-16 提交的版本,查看 最新版本

// ==UserScript==
// @name         Duolingo HearEverything
// @namespace    http://tampermonkey.net/
// @version      0.44.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
// ==/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.44.1: eventHandler for unmute - everything beta
*/

/*
// 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 --- 2 ---';
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 (beta)</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 &nbsp;
            <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 unmuteDuo() {
    muteDuo(false);
    debug('unmute');
}

function start() {
    if (document.querySelector('[data-test="challenge-header"]')) {
        document.querySelector(NEXT_BUTTON_QS).removeEventListener('click', unmuteDuo, true);
        document.querySelector(NEXT_BUTTON_QS).addEventListener('click', unmuteDuo, true);
        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');
                }
            }
        }
    }
}