Duolingo Unlocker

ABANDONED Allows you to practice any skill and adds a few niceties to the UI.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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