Greasy Fork

Greasy Fork is available in English.

Duolingo LevelJumper

Provides jump buttons to the next lesson for leveling up (based on minirock / oltodosel autoScroller).

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Duolingo LevelJumper
// @description Provides jump buttons to the next lesson for leveling up (based on minirock / oltodosel autoScroller).
// @version     2.12
// @namespace   esh
// @match       https://*.duolingo.com/*
// @grant       GM_setValue
// @grant       GM_getValue
// ==/UserScript==

// 2.3: find a better solution to move to the first lesson of this level
// 2.4: take into concern if there are different levels in one row
// 2.4: better jumpMark more satisfying
// 2.5: set jumpMark to the crown-div, so it moves the old lessons out of view
// 2.5: jumpMark for Crown 3 does not work????
// 2.6: fix if jumpMark is the very first lesson
// 2.7: should work now without plus button
// 2.8: auto-scroll if you want, set it below at const AUTO_SCROLL
// 2.8.1: what happens, if some crowns do not exist, E.g. lessons level 4 and lessons level 2, but no lessons level 3 available
// I suppose it's just set to the lower level, in this example level 2.
// yeah, of course it broke the script ... but not any longer
// 2.9: changed from selecting the last element from the previous level to the first element from the first level and got rid of having maxed out lessons in view
// 2.10: added broken levels
// 2.11: added auto scroll feature
// 2.11.1: autoScroll = false fixed
// 2.11.2: works with different base languages than english
// 2.12: autoscroll is different for different learning languages


// TODO: autoscroll every time you change language or come back from a lesson
// probably tackle checkJumpMark()

// TODO: clean up the code, move selectors to constants

// TODO: parse over url
// Tampermonkey access variables

// TODO: fix bug with anchors in URL ...

// TODO: fix bug with target's in URL
// not worth the time
// great, it happens, when you reload a page with a target in the url, so it's mostly a developer problem nothing more
// can fix it with location.url or something

let autoScroll = false;
let lang;

const CROWN = 'level-crown';
const CROWN_QS = '[data-test="' + CROWN + '"]';
const CROWN_IMG_CLASS = '_18sNN';
const CROWN_IMG_CLASS_QS = '[class~="' + CROWN_IMG_CLASS + '"]';
const SKILL_ICON = 'skill-icon';
const SKILL_ICON_QS = '[data-test="' + SKILL_ICON +'"]';

new MutationObserver(checkJumpMark).observe(document.body, {
	childList: true,
	subtree: true
});

function getConfig() {
    lang = JSON.parse(localStorage.getItem('duo.state')).user.learningLanguage;
    autoScroll = GM_getValue('autoScroll-'+lang, false);
    // console.debug('Duolingo LevelJumper: get autoScroll = ' + autoScroll);
    autoScroll === 'undefined' ? autoScroll = false : '';
    autoScroll === 'false' ? autoScroll = false : '';
}

function setConfig(value) {
    GM_setValue('autoScroll-'+lang, value);
    // console.debug('Duolingo LevelJumper: set autoScroll to ' + value);
}

function checkJumpMark() {
    //setConfig();

    if (document.querySelectorAll('.GkDDe').length!=0) {
        if (document.querySelector('#jumpMark')===null) {
            if (document.querySelector('._3yqw1')!=null) addJumpMarks();
        }
    }
}

function getAnchorElement(elem) {
    // if the element is the first in the tree
    if (elem[0] === document.querySelector('[data-test="skill-tree"]').querySelector(CROWN_IMG_CLASS_QS)) {
        return elem[0];
    } else if (elem[0] === undefined) {
        return null;
    } else {
        // first lesson element
        let lesson = elem[0].parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode;
        let row = lesson.parentNode.previousSibling;
        // sometimes the previous row is a divider
        // has to be CROWN_IMG_CLASS_QS to select possible lessons lvl 0
        if (!row.querySelector(CROWN_IMG_CLASS_QS)) {
            row = row.previousSibling;
        }
        // select the crown image as anchor element
        // has to be CROWN_IMG_CLASS_QS to select possible lessons lvl 0
        return row.querySelector(CROWN_IMG_CLASS_QS);
    }
}

// toggles visibility
function togglePopout(id) {
    let popout = document.getElementById(id);
    popout.style.display === "none" ? popout.style.display = "block" : popout.style.display = "none";
}

function addJumpMarks() {
    let level0 = document.querySelectorAll('img[src="//d35aaqx5ub95lt.cloudfront.net/images/fafe27c9c1efa486f49f87a3d691a66e.svg"]');
    // has to be CROWN_IMG_CLASS_QS to select possible lessons lvl 0
    let firstLesson = document.querySelector('[data-test="skill-tree"]').querySelector(CROWN_IMG_CLASS_QS);
    let level = document.querySelectorAll(CROWN_QS);
    let level1 = [];
    let level2 = [];
    let level3 = [];
    let level4 = [];
    let level5 = [];
    let levelMissing = [];
    for (let i=0;i<level.length;i++) {
        switch(level[i].innerHTML) {
            case '1':
                isMaxedOut(level[i]) ? isBroken(level[i]) ? level5.push(level[i].previousSibling) : '' : level1.push(level[i].previousSibling);
                break;
            case '2':
                isMaxedOut(level[i]) ? isBroken(level[i]) ? level5.push(level[i].previousSibling) : '' : level2.push(level[i].previousSibling);
                break;
            case '3':
                isMaxedOut(level[i]) ? isBroken(level[i]) ? level5.push(level[i].previousSibling) : '' : level3.push(level[i].previousSibling);
                break;
            case '4':
                isMaxedOut(level[i]) ? isBroken(level[i]) ? level5.push(level[i].previousSibling) : '' : level4.push(level[i].previousSibling);
                break;
            case '5':
                isBroken(level[i]) ? level5.push(level[i].previousSibling) : '';
                break;
        }
    }
/*    levelMissing = [null, level1.length, level2.length, level3.length, level4.length, level5.length];
    // if some level inbetween is missing
    // E.g. level 3 is missing and we need it to anchor the first level 2
    // so we replace level 3 with level 4
    if (level5.length === 0) level5[0] = null;
    if (level4.length === 0) level4 = level5;
    if (level3.length === 0) level3 = level4;
    if (level2.length === 0) level2 = level3;
    if (level1.length === 0) level1 = level2;
    // the last element of the higher level is used to anchor the level below
    // E.g. First level 4 = last level 5
    // if it doesn't exist, we have to the first element of the level below
    if(level5[0] === null && level4[0] !== null) level5[0] = level4[0];
    if(level4[0] === null && level3[0] !== null) level4[0] = level3[0];
    if(level3[0] === null && level2[0] !== null) level3[0] = level2[0];
    if(level2[0] === null && level1[0] !== null) level2[0] = level1[0];
    // freaky selecting the previous row
    */
    // level1 is the anchor element for new lessons
    let anchor0 = null;
    if(level0[0] !== null) anchor0 = getAnchorElement(level0);
    // level2 is the anchor element for level 1 lessons
    let anchor1 = null;
    if(level1[0] !== null) anchor1 = getAnchorElement(level1);
    // level3 is the anchor element for level 2 lessons
    let anchor2 = null;
    if(level2[0] !== null) anchor2 = getAnchorElement(level2);
    // level4 is the anchor element for level 3 lessons
    let anchor3 = null;
    if(level3[0] !== null) anchor3 = getAnchorElement(level3);
    // level5 is the anchor element for level 4 lessons
    let anchor4 = null;
    if(level4[0] !== null) anchor4 = getAnchorElement(level4);
    let anchor5 = null;
    if(level5[0] !== null) anchor5 = getAnchorElement(level5);
    // _3NYLT instead of _3yqw1 (plus button)
    let insertElement = document.querySelector('._3NYLT');
    let jumpMark = document.createElement('div');
    jumpMark.setAttribute('class','_3yqw1 np6Tv');
    jumpMark.setAttribute('style','padding-top: 0.6rem; padding-right: 0.6rem; top: 148px;');
    jumpMark.innerHTML = '<div id="jumpMark"></div>';
    //console.group('Not overriding double id tags');
/*    if(anchor0 !== null) {
        // last level 1 element get the id = level0 for jumping to new lessons
        let id = 'level0';
        anchor0.id = id;
        anchor0 === firstLesson? id = 'javascript:scroll(0,0);' : id = '#'+id;
        jumpMark.innerHTML += // `<a href="#notDone"><img src="//d35aaqx5ub95lt.cloudfront.net/images/fafe27c9c1efa486f49f87a3d691a66e.svg"/></a>
`<div class="_2-dXY _1swBH" style="font-size: 14.84px;">
  <a href="${id}">
    <img alt="crown" class="_18sNN" src="//d35aaqx5ub95lt.cloudfront.net/images/fafe27c9c1efa486f49f87a3d691a66e.svg">
  </a>
</div>`;
    } */
    // jumpMarks only if there are corresponding levels
    if (anchor5 !== null) jumpMark.innerHTML += prepareJumpMark(anchor5, 5, firstLesson);
    if (anchor4 !== null) jumpMark.innerHTML += prepareJumpMark(anchor4, 4, firstLesson);
    if (anchor3 !== null) jumpMark.innerHTML += prepareJumpMark(anchor3, 3, firstLesson);
    if (anchor2 !== null) jumpMark.innerHTML += prepareJumpMark(anchor2, 2, firstLesson);
    if (anchor1 !== null) jumpMark.innerHTML += prepareJumpMark(anchor1, 1, firstLesson);
    if (anchor0 !== null) jumpMark.innerHTML += prepareJumpMark(anchor0, 0, firstLesson);
    jumpMark.innerHTML += `<div class="_2-dXY _1swBH" style="font-size: 14.84px;">
  <a id="ljToggleConfig">
    <img alt="crown" class="_18sNN" style="padding-left: 0.2rem; padding-top: 0.1rem;" src="//d35aaqx5ub95lt.cloudfront.net/images/gear.svg">
  </a>
</div>`;

    //console.groupEnd();
    // beforebegin instead of afterend plus button
    let config = document.createElement('div');
    config.setAttribute('class','_3yqw1 np6Tv');
    config.setAttribute('style','width: 300px; display: none;');
    config.setAttribute('id','ljConfig');
    let options = '<option value="false">--</option>';
    anchor5 !== null ? options += '<option value="5">5</option>' : '';
    anchor4 !== null ? options += '<option value="4">4</option>' : '';
    anchor3 !== null ? options += '<option value="3">3</option>' : '';
    anchor2 !== null ? options += '<option value="2">2</option>' : '';
    anchor1 !== null ? options += '<option value="1">1</option>' : '';
    anchor0 !== null ? options += '<option value="0">0</option>' : '';
    config.innerHTML = `
    <div class="_3uS_y eIZ_c" data-test="skill-popout" style="--margin:20px;">
      <div class="_2O14B _2XlFZ _1v2Gj WCcVn" style="z-index: 1;">
        <div class="_1KUxv _1GJUD _3lagd SSzTP"><div class="_1cv-y"></div>
        <div class="QowCP">
          <div class="_1m77f" style="text-align: center">AutoScroll &nbsp;
            <select style="background-color: #ffc800; color: white;"name="levels" id="configLevels">
            ${options}
            </select>
          </div>
        </div>
      </div>
      <div class="ite_X" style="left: calc(90% - 15px);">
        <div class="_3p5e9 _3lagd SSzTP"></div>
      </div>
    </div>`;

    insertElement.insertAdjacentElement('beforebegin',jumpMark);
    insertElement.insertAdjacentElement('beforebegin',config);
    let top = jumpMark.offsetHeight + 160;
    console.debug('Duolingo LevelJumper: top = ' + top);
    config.style.top = top + 'px';
    getConfig();
    //console.debug('Duolingo LevelJumper: BoundingHeight = ' + jumpMark.offsetHeight);
    console.debug('Duolingo LevelJumper: AutoScroll = ' + autoScroll);
    let configLevels = document.getElementById('configLevels')
    configLevels.querySelector('[value="' + autoScroll + '"]').setAttribute('selected', true);
    configLevels.addEventListener('change', function() {
        setConfig(configLevels.options[configLevels.selectedIndex].value);
    });
    document.getElementById('ljToggleConfig').addEventListener('click', function () { togglePopout('ljConfig'); });
    if (autoScroll) document.getElementById('myJumpTo' + autoScroll).click();
}

// returns true if element has the highest level
function isMaxedOut(element) {
    // TODO move to constant
    if (element.parentNode.parentNode.parentNode.querySelector('[href="//d35aaqx5ub95lt.cloudfront.net/images/9dc5f133240809ea530649bca27c7eca.svg"]')) {
        //console.debug(element);
        return true;
    } else { return false; }
}

// returns true if element is broken
function isBroken(element) {
    // TODO move to constant
    if (element.parentNode.parentNode.previousSibling.querySelector('._1m7gz')) { return true; } else { return false; }
}

function prepareJumpMark(anchor, number, firstLesson) {
    let innerHTML = '';
    if(anchor !== null) {
        // show the first level 'number' lesson
        let id = 'level' + number;
        // if there have two level the same jump mark, it uses the given one instead of overriding it
        if(anchor.id !== '') {
            console.debug('Duolingo LevelJumper: Id ' + anchor.id + ' already exists');
            id = anchor.id;
        } else {
            anchor.id = id;
        }
        anchor === firstLesson? id = 'javascript:scroll(0,0);' : id = '#'+id;
        let crownImage;
        let crownClass;
        let crownNumber = number;
        if (number === 0) {
            // grey crown
            crownImage = '//d35aaqx5ub95lt.cloudfront.net/images/fafe27c9c1efa486f49f87a3d691a66e.svg';
            crownNumber = '';
        } else {
            // golden crown
            crownImage = '//d35aaqx5ub95lt.cloudfront.net/images/b3ede3d53c932ee30d981064671c8032.svg';
        }
        if (number !== 5) {
            crownClass = "GkDDe";
        } else {
            crownClass = "GkDDe _1m7gz";
            crownNumber = '';
        }
        innerHTML =
            `<div class="_2-dXY _1swBH" style="font-size: 14.84px;">
  <a id="myJumpTo${number}" href="${id}">
    <img alt="crown" class="_18sNN" src="${crownImage}">
    <div class="${crownClass}" data-test="level-crown">${crownNumber}</div>
  </a>
</div>`;
    }
    return innerHTML;
}