您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Provides jump buttons to the next lesson for leveling up (based on minirock / oltodosel autoScroller).
当前为
// ==UserScript== // @name Duolingo LevelJumper // @description Provides jump buttons to the next lesson for leveling up (based on minirock / oltodosel autoScroller). // @version 2.12.2 // @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 // 2.12.1: display disabled lessons at autoscroll selection // 2.12.2: removed bug with checkpoints // 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 row is a checkpoint and so it turns out null if (row === null) { // totally understandable ;) go up, go to last tree section and to the last row there row = lesson.parentNode.parentNode.parentNode.previousSibling.previousSibling.firstChild.lastChild; } // 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>'; options += anchor5 !== null ? '<option value="5">5</option>' : '<option value="5" disabled>5</option>'; options += anchor4 !== null ? '<option value="4">4</option>' : '<option value="4" disabled>4</option>'; options += anchor3 !== null ? '<option value="3">3</option>' : '<option value="3" disabled>3</option>'; options += anchor2 !== null ? '<option value="2">2</option>' : '<option value="2" disabled>2</option>'; options += anchor1 !== null ? '<option value="1">1</option>' : '<option value="1" disabled>1</option>'; options += anchor0 !== null ? '<option value="0">0</option>' : '<option value="0" disabled>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 <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; }