Greasy Fork

Duolingo LevelJumper

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

目前为 2021-05-15 提交的版本。查看 最新版本

// ==UserScript==
// @name        Duolingo LevelJumper
// @description Provides jump buttons to the next lesson for leveling up (based on minirock / oltodosel autoScroller).
// @version     2.9.1
// @namespace   esh
// @match       https://*.duolingo.com/*
// ==/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

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

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

// TODO: fix bug clickable before jumpMark ready
// 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

// TODO: highlight lesson by mouseover / mousein / pointerin / focus event
// elem.dispatchEvent(event)
// this is the 20 % which take me 80 % of the time
// could be silly thinking as I got a proper positioning in 2.5

// false or level to go to
// E.g. if you want to bring every lesson to level 2
// AUTO_SCROLL = 2
// options: false, 0, 1, 2, 3, 4
const AUTO_SCROLL = false;

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

window.onload = function () {
    //let lessons = document.querySelectorAll('img[src="//d35aaqx5ub95lt.cloudfront.net/images/b3ede3d53c932ee30d981064671c8032.svg"]');
    //lessons[lessons.length-1].scrollIntoView();
    //document.body.addEventListener('pageshow', addJumpMarks());
    //addJumpMarks();
};

function checkJumpMark() {
     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('[alt="crown"]')) {
        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
        if (!row.querySelector('[alt="crown"]')) {
            row = row.previousSibling;
        }
        // select the crown image as anchor element
        return row.querySelector('[alt="crown"]');
    }
}

function addJumpMarks() {
    let level0 = document.querySelectorAll('img[src="//d35aaqx5ub95lt.cloudfront.net/images/fafe27c9c1efa486f49f87a3d691a66e.svg"]');
    let firstLesson = document.querySelector('[data-test="skill-tree"]').querySelector('[alt="crown"]');
    let level = document.querySelectorAll('[data-test="level-crown"]');
    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]) ? '' : level1.push(level[i].previousSibling);
                break;
            case '2':
                isMaxedOut(level[i]) ? '' : level2.push(level[i].previousSibling);
                break;
            case '3':
                isMaxedOut(level[i]) ? '' : level3.push(level[i].previousSibling);
                break;
            case '4':
                isMaxedOut(level[i]) ? '' : level4.push(level[i].previousSibling);
                break;
            case '5':
                // for what
                //level5.push(level[i]);
                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);
    // _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"/>';
    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 (anchor1 !== null) jumpMark.innerHTML += prepareJumpMark(anchor1, 1, firstLesson);
    if (anchor2 !== null) jumpMark.innerHTML += prepareJumpMark(anchor2, 2, firstLesson);
    if (anchor3 !== null) jumpMark.innerHTML += prepareJumpMark(anchor3, 3, firstLesson);
    if (anchor4 !== null) jumpMark.innerHTML += prepareJumpMark(anchor4, 4, firstLesson);
    console.groupEnd();
    // beforebegin instead of afterend plus button
    insertElement.insertAdjacentElement('beforebegin',jumpMark);
    if (AUTO_SCROLL) document.getElementById('level' + AUTO_SCROLL).scrollIntoView();
}

// 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; }
}

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.info('Id already exists');
            console.info(anchor.id);
            id = anchor.id;
        } else {
            anchor.id = id;
        }
        anchor === firstLesson? id = 'javascript:scroll(0,0);' : id = '#'+id;
        innerHTML =
            `<div class="_2-dXY _1swBH" style="font-size: 14.84px;">
  <a href="${id}">
    <img alt="crown" class="_18sNN" src="//d35aaqx5ub95lt.cloudfront.net/images/b3ede3d53c932ee30d981064671c8032.svg">
    <div class="GkDDe" data-test="level-crown">${number}</div>
  </a>
</div>`;
    }
    return innerHTML;
}