Greasy Fork

Greasy Fork is available in English.

Duolingo HearEverything

Let's you hear phrases and words from single choice questions based on your browsers speech synthesis (right now only phrases).

当前为 2021-06-27 提交的版本,查看 最新版本

// ==UserScript==
// @name         Duolingo HearEverything
// @namespace    http://tampermonkey.net/
// @version      0.39
// @description  Let's you hear phrases and words from single choice questions based on your browsers speech synthesis (right now only phrases).
// @author       Esh
// @match        https://*.duolingo.com/*
// @grant       GM_setValue
// @grant       GM_getValue
// ==/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.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.39 --- 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 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;

// used page types
const FORM = 'challenge challenge-form';
const TRANSLATE = 'challenge challenge-translate';
const DIALOGUE = 'challenge challenge-dialogue';
const GAP_FILL = 'challenge challenge-gapFill';
const COMPLETE_REVERSE_TRANSLATION = 'challenge challenge-completeReverseTranslation';
const TAP_COMPLETE = 'challenge challenge-tapComplete';
const LISTEN_COMPREHENSION = 'challenge challenge-listenComprehension';
const READ_COMPREHENSION = 'challenge challenge-readComprehension';
const NAME = 'challenge challenge-name';

// unused page types
const LISTEN = 'challenge challenge-listen';
// Word bank stuff
// is not read out by Duo all the time (but at "Tap what you hear");
const LISTEN_TAP = 'challenge challenge-listenTap';
// has word bank and some filled up answer to read

// duo reads aloud
const SELECT_TRANSCRIPTION = 'challenge challenge-selectTranscription';

// allowed challenge types
const TEST = [FORM, TRANSLATE, DIALOGUE, GAP_FILL, COMPLETE_REVERSE_TRANSLATION, TAP_COMPLETE, LISTEN_COMPREHENSION, READ_COMPREHENSION, NAME];
var buttonDisabled = true;

function debug(s) {
    console.debug(LOG_STRING + s);
}

window.onload = function() {
    'use strict';
    console.debug(LOG_STRING + VERSION);
    // console.log('---------------------onload-------------------------');
    voices = window.speechSynthesis.getVoices();
    console.debug(voices);
    //setVoice();
    readConfig();
    new MutationObserver(start).observe(document.body, {
        // attributes: true,
        childList: true,
        subtree: true
    });
    console.debug('MutationObserver running');
}

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);
    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>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>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>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>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>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>
        `;

        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}
        </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('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;
        });
    }
}

function start() {
    if (document.querySelector('[data-test="challenge-header"]')) {
        addConfig();
        buildDebug();
        checkNewPage();
        let challenge = getChallengeType();
        if(challenge !== null) {
            if (newPage === true) {
                if (document.querySelector(ANSWER_QS) !== null) {
                    renderAnswerSpeakButton();
                } else if(addedSpeech===false) {
                    if (document.querySelectorAll(CHALLENGE_JUDGE_QS).length!==0) {
                        if (challenge[0] === LISTEN_COMPREHENSION) { // || challenge[0] === READ_COMPREHENSION) {
                            let hint = document.querySelectorAll(HINT_SENTENCE_QS)[1].innerText.replace('...', '');
                            addSpeech(CHALLENGE_JUDGE_QS, hint);
                            addedSpeech = true;
                        }
                        if (challenge[0] === FORM && config.he_cf_click === true) {
                            addSpeech(CHALLENGE_JUDGE_QS);
                            addedSpeech = true;
                        }
                        if (challenge[0] === GAP_FILL && config.he_cgf_click === true) {
                            addSpeech(CHALLENGE_JUDGE_QS);
                            addedSpeech = true;
                        }
                        if (challenge[0] === DIALOGUE && config.he_cd_click === true) {
                            addSpeech(CHALLENGE_JUDGE_QS);
                            addedSpeech = true;
                        }
                    } else if (document.querySelectorAll(CHALLENGE_TAP_TOKEN_QS).length !== 0) {
                        if (challenge[0] === 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)) {
        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);
    }
}

function timeoutAutoplay(challenge, utter) {
    setTimeout(function() {
        console.debug(LOG_STRING + '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(true)[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(true)[0]);
        newPage = true;
        synth.cancel();
    } else {
        //console.debug('---- div - oldPage ----');
    }
}

// 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 getChallengeType(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');
                }
            }
        }
    }
}