Duolingo Unlocker

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

  1. // ==UserScript==
  2. // @name Duolingo Unlocker
  3. // @namespace noplanman
  4. // @description ABANDONED Allows you to practice any skill and adds a few niceties to the UI.
  5. // @include https://www.duolingo.com/
  6. // @version 1.1
  7. // @author Armando Lüscher
  8. // @oujs:author noplanman
  9. // @copyright 2016 Armando Lüscher
  10. // @grant GM_addStyle
  11. // @grant window
  12. // @require https://code.jquery.com/jquery-1.12.4.min.js
  13. // @homepageURL https://github.com/noplanman/Duolingo-Unlocker
  14. // @supportURL https://github.com/noplanman/Duolingo-Unlocker/issues
  15. // ==/UserScript==
  16.  
  17. /**
  18. * Main Duolingo Unlocker object.
  19. *
  20. * @type {Object}
  21. */
  22. var DU = {};
  23.  
  24. /**
  25. * Debugging level. (disabled,[l]og,[i]nfo,[w]arning,[e]rror)
  26. *
  27. * @type {Boolean}
  28. */
  29. DU.debugLevel = 'l';
  30.  
  31. /**
  32. * Load the necessary data variables.
  33. */
  34. DU.loadVariables = function() {
  35. DU.user = unsafeWindow.duo.user.attributes;
  36. DU.lang = DU.user.language_data[DU.user.learning_language];
  37. DU.skills = {};
  38. jQuery.each(DU.lang.skills.models, function(i, skill) {
  39. DU.skills[skill.attributes.short] = skill.attributes;
  40. });
  41. DU.log('Variables loaded');
  42. };
  43.  
  44. /**
  45. * Unlock all the locked items and convert them to links.
  46. */
  47. DU.unlockTree = function() {
  48. var unlockedSkills = [];
  49. jQuery('.skill-tree-row:not(.bonus-row, .row-shortcut) .skill-badge-small.locked').each(function() {
  50. var $skillItemOld = jQuery(this).removeClass('locked').addClass('skill-item');
  51.  
  52. // Get just the text of the skill (without the number of excercises)
  53. var skillNameShort = $skillItemOld.find('.skill-badge-name')
  54. .clone().children().remove()
  55. .end().text().trim();
  56. var skill = DU.skills[skillNameShort];
  57.  
  58. $skillItem = jQuery('<a/>', {
  59. 'html' : $skillItemOld.html(),
  60. 'class' : $skillItemOld.attr('class'),
  61. 'data-skill' : skill.name,
  62. 'href' : '/skill/' + DU.lang.language + '/' + skill.url_title,
  63. });
  64.  
  65. $skillItem.find('.skill-icon')
  66. .removeClass('locked')
  67. .addClass('unlocked')
  68. .addClass(skill.icon_color);
  69.  
  70. // Replace the <span/> with the new <a/> element
  71. $skillItemOld.replaceWith($skillItem);
  72.  
  73. unlockedSkills.push(skill);
  74. });
  75.  
  76. DU.log('Skill tree unlocked: ' + unlockedSkills.length + ' new skills unlocked');
  77. };
  78.  
  79. /**
  80. * Add the progress bar for the level, showing how many points are needed to level up.
  81. *
  82. * @todo What happens when a tree is finished? It should just be a full bar.
  83. */
  84. DU.progressBar = function() {
  85. var progressText = DU.lang.level_percent + '% ( ' + DU.lang.level_progress + ' / ' + DU.lang.level_points + ' )';
  86. var $levelTextLeft = jQuery('.level-text');
  87. var $levelTextRight = $levelTextLeft
  88. .clone(true)
  89. .addClass('right')
  90. .text(
  91. (DU.lang.level_percent < 100)
  92. ? $levelTextLeft.text().replace(/(\d+)+/g, function(match, number) {
  93. // Increase the level number.
  94. return parseInt(number) + 1;
  95. })
  96. : 'MAX'
  97. )
  98. .insertAfter($levelTextLeft);
  99.  
  100. // Add the progress bar after the level text fields.
  101. $levelTextRight.after(
  102. '<div class="progress-bar-dynamic strength-bar DU-strength-bar">' +
  103. ' <div class="DU-meter-text">' + progressText + '</div>' +
  104. ' <div style="opacity: 1; width: ' + DU.lang.level_percent + '%;" class="DU-meter-bar bar gold"></div>' +
  105. '</div>'
  106. );
  107.  
  108. DU.log('Progress bar updated');
  109. };
  110.  
  111. /**
  112. * Start the party.
  113. */
  114. DU.init = function() {
  115. // Add the global CSS rules.
  116. GM_addStyle(
  117. '.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; }' +
  118. '.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; }' +
  119. '.DU-meter-text { width: 100%; position: absolute; z-index: 1; color: #000; opacity: .5; text-align: center; font-size: .8em; }' +
  120. '.DU-strength-bar { width: 100% !important; left: 0 !important; margin-top: 10px }' +
  121. '.DU-meter-bar { height: 100% !important; margin: 0 !important; }'
  122. );
  123.  
  124. // Initial execution.
  125. DU.loadVariables();
  126. DU.unlockTree();
  127. DU.progressBar();
  128.  
  129. // Observe main page for changes.
  130. DU.Observer.add('#app', [DU.loadVariables, DU.unlockTree, DU.progressBar]);
  131. };
  132.  
  133. // source: https://muffinresearch.co.uk/does-settimeout-solve-the-domcontentloaded-problem/
  134. if (/(?!.*?compatible|.*?webkit)^mozilla|opera/i.test(navigator.userAgent)) { // Feeling dirty yet?
  135. document.addEventListener('DOMContentLoaded', DU.init, false);
  136. } else {
  137. window.setTimeout(DU.init, 0);
  138. }
  139.  
  140. /**
  141. * Make a log entry if debug mode is active.
  142. * @param {string} logMessage Message to write to the log console.
  143. * @param {string} level Level to log ([l]og,[i]nfo,[w]arning,[e]rror).
  144. * @param {boolean} alsoAlert Also echo the message in an alert box.
  145. */
  146. DU.log = function(logMessage, level, alsoAlert) {
  147. if (!DU.debugLevel) {
  148. return;
  149. }
  150.  
  151. var logLevels = { l : 0, i : 1, w : 2, e : 3 };
  152.  
  153. // Default to "log" if nothing is provided.
  154. level = level || 'l';
  155.  
  156. if ('disabled' !== DU.debugLevel && logLevels[DU.debugLevel] <= logLevels[level]) {
  157. switch(level) {
  158. case 'l' : console.log( logMessage); break;
  159. case 'i' : console.info( logMessage); break;
  160. case 'w' : console.warn( logMessage); break;
  161. case 'e' : console.error(logMessage); break;
  162. }
  163. alsoAlert && alert(logMessage);
  164. }
  165. };
  166.  
  167. /**
  168. * The MutationObserver to detect page changes.
  169. *
  170. * @type {Object}
  171. */
  172. DU.Observer = {
  173. /**
  174. * The mutation observer objects.
  175. *
  176. * @type {Array}
  177. */
  178. observers : [],
  179.  
  180. /**
  181. * Add an observer to observe for DOM changes.
  182. *
  183. * @param {String} queryToObserve Query string of elements to observe.
  184. * @param {Array|Function} cbs Callback function(s) for the observer.
  185. */
  186. add : function(queryToObserve, cbs) {
  187. // Check if we can use the MutationObserver.
  188. if ('MutationObserver' in window) {
  189. var toObserve = document.querySelector(queryToObserve);
  190. if (toObserve) {
  191. if (!jQuery.isArray(cbs)) {
  192. cbs = [cbs];
  193. }
  194. cbs.forEach(function(cb) {
  195. var mo = new MutationObserver(cb);
  196.  
  197. // No need to observe subtree changes!
  198. mo.observe(toObserve, {
  199. childList: true
  200. });
  201.  
  202. DU.Observer.observers.push(mo);
  203. });
  204. }
  205. }
  206. }
  207. };

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址