// ==UserScript==
// @name Duolingo Unlocker
// @namespace noplanman
// @description ABANDONED 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);
});
}
}
}
};