NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Duolingo Unlocker // @namespace noplanman // @description Allows you to practice any skill and adds a few niceties to the UI. // @include https://www.duolingo.com/ // @version 1.1 // @author Armando Lüscher // @oujs:author noplanman // @copyright 2016 Armando Lüscher // @grant GM_addStyle // @grant window // @require https://code.jquery.com/jquery-1.12.4.min.js // @homepageURL https://github.com/noplanman/Duolingo-Unlocker // @supportURL https://github.com/noplanman/Duolingo-Unlocker/issues // ==/UserScript== /** * Main Duolingo Unlocker object. * * @type {Object} */ var DU = {}; /** * Debugging level. (disabled,[l]og,[i]nfo,[w]arning,[e]rror) * * @type {Boolean} */ DU.debugLevel = 'l'; /** * Load the necessary data variables. */ DU.loadVariables = function() { DU.user = unsafeWindow.duo.user.attributes; DU.lang = DU.user.language_data[DU.user.learning_language]; DU.skills = {}; jQuery.each(DU.lang.skills.models, function(i, skill) { DU.skills[skill.attributes.short] = skill.attributes; }); DU.log('Variables loaded'); }; /** * Unlock all the locked items and convert them to links. */ DU.unlockTree = function() { var unlockedSkills = []; jQuery('.skill-tree-row:not(.bonus-row, .row-shortcut) .skill-badge-small.locked').each(function() { var $skillItemOld = jQuery(this).removeClass('locked').addClass('skill-item'); // Get just the text of the skill (without the number of excercises) var skillNameShort = $skillItemOld.find('.skill-badge-name') .clone().children().remove() .end().text().trim(); var skill = DU.skills[skillNameShort]; $skillItem = jQuery('<a/>', { 'html' : $skillItemOld.html(), 'class' : $skillItemOld.attr('class'), 'data-skill' : skill.name, 'href' : '/skill/' + DU.lang.language + '/' + skill.url_title, }); $skillItem.find('.skill-icon') .removeClass('locked') .addClass('unlocked') .addClass(skill.icon_color); // Replace the <span/> with the new <a/> element $skillItemOld.replaceWith($skillItem); unlockedSkills.push(skill); }); DU.log('Skill tree unlocked: ' + unlockedSkills.length + ' new skills unlocked'); }; /** * Add the progress bar for the level, showing how many points are needed to level up. * * @todo What happens when a tree is finished? It should just be a full bar. */ DU.progressBar = function() { var progressText = DU.lang.level_percent + '% ( ' + DU.lang.level_progress + ' / ' + DU.lang.level_points + ' )'; var $levelTextLeft = jQuery('.level-text'); var $levelTextRight = $levelTextLeft .clone(true) .addClass('right') .text( (DU.lang.level_percent < 100) ? $levelTextLeft.text().replace(/(\d+)+/g, function(match, number) { // Increase the level number. return parseInt(number) + 1; }) : 'MAX' ) .insertAfter($levelTextLeft); // Add the progress bar after the level text fields. $levelTextRight.after( '<div class="progress-bar-dynamic strength-bar DU-strength-bar">' + ' <div class="DU-meter-text">' + progressText + '</div>' + ' <div style="opacity: 1; width: ' + DU.lang.level_percent + '%;" class="DU-meter-bar bar gold"></div>' + '</div>' ); DU.log('Progress bar updated'); }; /** * Start the party. */ DU.init = function() { // Add the global CSS rules. GM_addStyle( '.meter { -moz-border-radius: 25px; -webkit-border-radius: 25px; background: #555; border-radius: 25px; box-shadow: inset 0 -1px 1px rgba(255,255,255,0.3); height: 20px; padding: 2px; position: relative; display: block; }' + '.meter-level { display: block; height: 100%; border-top-right-radius: 8px; border-bottom-right-radius: 8px; border-top-left-radius: 20px; border-bottom-left-radius: 20px; background-color: #ffa200; background-image: linear-gradient( center bottom, #ffa200 37%, rgb(84,240,84) 69% ); box-shadow: inset 0 2px 9px rgba(255,255,255,0.3),inset 0 -2px 6px rgba(0,0,0,0.4); position: relative; overflow: hidden; }' + '.DU-meter-text { width: 100%; position: absolute; z-index: 1; color: #000; opacity: .5; text-align: center; font-size: .8em; }' + '.DU-strength-bar { width: 100% !important; left: 0 !important; margin-top: 10px }' + '.DU-meter-bar { height: 100% !important; margin: 0 !important; }' ); // Initial execution. DU.loadVariables(); DU.unlockTree(); DU.progressBar(); // Observe main page for changes. DU.Observer.add('#app', [DU.loadVariables, DU.unlockTree, DU.progressBar]); }; // source: https://muffinresearch.co.uk/does-settimeout-solve-the-domcontentloaded-problem/ if (/(?!.*?compatible|.*?webkit)^mozilla|opera/i.test(navigator.userAgent)) { // Feeling dirty yet? document.addEventListener('DOMContentLoaded', DU.init, false); } else { window.setTimeout(DU.init, 0); } /** * Make a log entry if debug mode is active. * @param {string} logMessage Message to write to the log console. * @param {string} level Level to log ([l]og,[i]nfo,[w]arning,[e]rror). * @param {boolean} alsoAlert Also echo the message in an alert box. */ DU.log = function(logMessage, level, alsoAlert) { if (!DU.debugLevel) { return; } var logLevels = { l : 0, i : 1, w : 2, e : 3 }; // Default to "log" if nothing is provided. level = level || 'l'; if ('disabled' !== DU.debugLevel && logLevels[DU.debugLevel] <= logLevels[level]) { switch(level) { case 'l' : console.log( logMessage); break; case 'i' : console.info( logMessage); break; case 'w' : console.warn( logMessage); break; case 'e' : console.error(logMessage); break; } alsoAlert && alert(logMessage); } }; /** * The MutationObserver to detect page changes. * * @type {Object} */ DU.Observer = { /** * The mutation observer objects. * * @type {Array} */ observers : [], /** * Add an observer to observe for DOM changes. * * @param {String} queryToObserve Query string of elements to observe. * @param {Array|Function} cbs Callback function(s) for the observer. */ add : function(queryToObserve, cbs) { // Check if we can use the MutationObserver. if ('MutationObserver' in window) { var toObserve = document.querySelector(queryToObserve); if (toObserve) { if (!jQuery.isArray(cbs)) { cbs = [cbs]; } cbs.forEach(function(cb) { var mo = new MutationObserver(cb); // No need to observe subtree changes! mo.observe(toObserve, { childList: true }); DU.Observer.observers.push(mo); }); } } } };