WaniKanify

Firefox version of chedkid's excellent Chrome app

目前为 2014-08-31 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name WaniKanify
  3. // @namespace wanikani
  4. // @description Firefox version of chedkid's excellent Chrome app
  5. // @include *
  6. // @version 1.4.0
  7. // @grant GM_xmlhttpRequest
  8. // @grant GM_getValue
  9. // @grant GM_setValue
  10. // @grant GM_registerMenuCommand
  11. // @require http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js
  12. // ==/UserScript==
  13.  
  14. // Current format version of the vocab database
  15. var FORMAT_VER = 2;
  16.  
  17. // Lock to ensure only one download at once
  18. var downloading = false;
  19.  
  20. /* Main */
  21. window.addEventListener ("load", function () {
  22.  
  23. GM_registerMenuCommand("Wanikanify: Run", tryRun);
  24. GM_registerMenuCommand("Wanikanify: Refresh vocabulary", tryRefreshVocabulary);
  25. GM_registerMenuCommand("Wanikanify: Enable auto-run", promptAutoRun);
  26. GM_registerMenuCommand("Wanikanify: Set API key", promptApiKey);
  27.  
  28. if (GM_getValue("autoRun") == 1)
  29. run(false);
  30.  
  31. }, false);
  32.  
  33. /**
  34. * User interface
  35. */
  36.  
  37. /* Run script */
  38. function tryRun() {
  39. var apiKey = getApiKey();
  40. if (apiKey != undefined)
  41. run(false);
  42. }
  43.  
  44. /* Refresh vocabulary */
  45. function tryRefreshVocabulary() {
  46. var apiKey = getApiKey();
  47. if (apiKey != undefined)
  48. downloadVocab();
  49. }
  50.  
  51. /* Specifiy whether to run automatically */
  52. function promptAutoRun() {
  53. var autoRun = parseInt(window.prompt("Enter 1 to enable auto-run and 0 to disable", GM_getValue("autoRun") ? 1 : 0));
  54. GM_setValue("autoRun", autoRun);
  55. }
  56.  
  57. /* Specify the WaniKani API key */
  58. function promptApiKey() {
  59. var apiKey;
  60.  
  61. while (true) {
  62. apiKey = GM_getValue("apiKey");
  63. apiKey = window.prompt("Please enter your API key", apiKey ? apiKey : "");
  64.  
  65. if (apiKey && !/^[a-fA-F0-9]{32}$/.test(apiKey))
  66. alert("That was not a valid API key, please try again");
  67. else
  68. break;
  69. }
  70.  
  71. GM_setValue("apiKey", apiKey);
  72. GM_setValue("mustRefresh", 1);
  73. }
  74.  
  75. /**
  76. * Runtime code
  77. */
  78.  
  79. function getApiKey() {
  80. var apiKey = GM_getValue("apiKey");
  81.  
  82. if (apiKey == undefined)
  83. promptApiKey();
  84.  
  85. return apiKey;
  86. }
  87.  
  88. function run(refreshFirst) {
  89. // Ignore current vocab download
  90. downloading = false;
  91.  
  92. // Get the current timestamp (in minutes)
  93. var currentTime = Math.floor(new Date().getTime() / 60000);
  94.  
  95. // Get the time and the vocab format from when the vocab list was last refreshed
  96. var refreshTime = GM_getValue("refreshTime");
  97. var refreshFormat = GM_getValue("formatVer");
  98.  
  99. // Hotfix: Allow script to enforce refresh
  100. var mustRefresh = GM_getValue("mustRefresh") == 1;
  101. GM_setValue("mustRefresh", 0);
  102.  
  103. // See if we should to refresh. If a week has passed, it's probably a good idea anyway
  104. var shouldRefresh = mustRefresh || refreshFirst || (GM_getValue("vocab", "") == "") || (refreshFormat != FORMAT_VER)
  105. || (refreshTime == undefined) || (currentTime - refreshTime >= 10080);
  106.  
  107. // No refresh needed, simply replace vocab...
  108. if (!shouldRefresh)
  109. replaceVocab();
  110. // Update vocab list if last refresh was too long ago (and replace afterwards), or old version
  111. else
  112. downloadVocab(true);
  113. }
  114.  
  115. function downloadVocab(runAfter, wasManual) {
  116. if (downloading) {
  117. console.log("Attempted to download WaniKani data while already downloading");
  118. return;
  119. }
  120.  
  121. var apiKey = GM_getValue("apiKey");
  122. if (apiKey == undefined)
  123. return;
  124.  
  125. console.log("Downloading new vocab data...");
  126. downloading = true;
  127.  
  128. GM_xmlhttpRequest({
  129. method: "GET",
  130. url: "http://www.wanikani.com/api/v1.2/user/" + apiKey + "/vocabulary/",
  131. onerror: function (response) {
  132. alert("Error while downloading WaniKani data. Please try again later.");
  133. downloading = false;
  134. },
  135. onload: function (response) {
  136. var json;
  137.  
  138. try {
  139. json = JSON.parse(response.responseText);
  140. } catch (e) {
  141. alert("Unable to process WaniKani data. Please try again later.", e);
  142. }
  143.  
  144. if (json) {
  145. if ("error" in json) {
  146. alert("Error from WaniKani: " + json.error.message);
  147. downloading = false;
  148. return;
  149. }
  150.  
  151. GM_setValue("refreshTime", Math.floor(new Date().getTime() / 60000));
  152.  
  153. // Create the vocab map that will be stored
  154. var vocabMap = {};
  155. // Loop through all received words
  156. var vocabList = json.requested_information.general;
  157.  
  158. for (var i in vocabList) {
  159. var vocab = vocabList[i];
  160.  
  161. // Skip words not yet learned
  162. if (!vocab.user_specific)
  163. continue;
  164.  
  165. // Split multiple spellings
  166. var meanings = vocab.meaning.split(", ");
  167.  
  168. // Conjugate verbs
  169. var conjugations = [];
  170. for (var m in meanings) {
  171. var word = meanings[m];
  172.  
  173. // If a verb...
  174. if (/^to /.test(word))
  175. {
  176. // Remove leading 'to'
  177. meanings[m] = word.substr(3);
  178.  
  179. // Remove 'e' suffix for conjugations
  180. if (word.slice(-1) == "e")
  181. word = word.slice(0, -1);
  182.  
  183. if (!/ /.test(word))
  184. conjugations.push(word + "ed", word + "es", word + "en", word + "es", word + "s", word + "ing");
  185. }
  186.  
  187. // Not a verb, try plural
  188. else if (word.length >= 3 && word.slice(-1) != "s")
  189. conjugations.push(word + "s");
  190. }
  191. meanings.push.apply(meanings, conjugations);
  192.  
  193. // After updating the meanings,
  194. for (var m in meanings) {
  195. vocabMap[meanings[m]] = vocab.character;
  196. }
  197. }
  198.  
  199. String.prototype.hashCode = function() {
  200. var hash = 0, i, char;
  201. if (this.length == 0) return hash;
  202. for (i = 0, l = this.length; i < l; i++) {
  203. char = this.charCodeAt(i);
  204. hash = ((hash<<5)-hash)+char;
  205. hash |= 0; // Convert to 32bit integer
  206. }
  207. return hash;
  208. };
  209.  
  210. // Update the new vocab list in the storage
  211. vocabJson = JSON.stringify(vocabMap);
  212. GM_setValue("vocab", vocabJson);
  213. if(!runAfter) {
  214. if (vocabJson.hashCode() != GM_getValue("vocabHash")) {
  215. alert("Successfully updated vocab!");
  216. GM_setValue("vocabHash", vocabJson.hashCode());
  217. }
  218. // Only inform the user that the vocab is up to date if he manually requested to update it
  219. else {
  220. alert("Vocab already up to date");
  221. }
  222. }
  223. GM_setValue("formatVer", FORMAT_VER);
  224.  
  225. // Unlock download lock
  226. downloading = false;
  227.  
  228. // If wanted, immediately run the script
  229. if (runAfter)
  230. replaceVocab(vocabMap);
  231. }
  232. }
  233. });
  234. }
  235.  
  236. function replaceVocab(vocabMap) {
  237. // No vocab map given, try to parse from stored JSON
  238. if (!vocabMap) {
  239. try {
  240. vocabMap = JSON.parse(GM_getValue("vocab"), {});
  241. if (!vocabMap || (vocabMap && jQuery.isEmptyObject(vocabMap)))
  242. throw 1;
  243. } catch (e) {
  244. alert("Error while parsing the vocab list; deleting it now. Please try again.");
  245. GM_setValue("vocab", "");
  246. return;
  247. }
  248. }
  249.  
  250. console.log(vocabMap);
  251. console.log("Replacing vocab...");
  252.  
  253. var replaceCallback = function(str) {
  254. if (vocabMap.hasOwnProperty(str.toLowerCase())) {
  255. var translation = vocabMap[str.toLowerCase()];
  256. return '<span class="wanikanified" title="' + str + '" data-en="' + str + '" data-jp="' + translation +
  257. '" onClick="var t = this.getAttribute(\'title\'); this.setAttribute(\'title\', this.innerHTML); this.innerHTML = t;">' + translation + '<\/span>';
  258. }
  259.  
  260. // Couldn't replace anything, leave as is
  261. return str;
  262. };
  263.  
  264. var nodes = $("body *:not(noscript):not(script):not(style)");
  265.  
  266. // Very naive attempt at replacing vocab consisting of multiple words first
  267. nodes.replaceText(/\b(\S+?\s+\S+?\s+\S+?\s+\S+?)\b/g, replaceCallback);
  268. nodes.replaceText(/\b(\S+?\s+\S+?\s+\S+?)\b/g, replaceCallback);
  269. nodes.replaceText(/\b(\S+?\s+\S+?)\b/g, replaceCallback);
  270. nodes.replaceText(/\b(\S+?)\b/g, replaceCallback);
  271.  
  272. console.log("Vocab replaced!");
  273. }
  274.  
  275. /*
  276. * jQuery replaceText - v1.1 - 11/21/2009
  277. * http://benalman.com/projects/jquery-replacetext-plugin/
  278. *
  279. * Copyright (c) 2009 "Cowboy" Ben Alman
  280. * Dual licensed under the MIT and GPL licenses.
  281. * http://benalman.com/about/license/
  282. */
  283. (function($){$.fn.replaceText=function(b,a,c){return this.each(function(){var f=this.firstChild,g,e,d=[];if(f){do{if(f.nodeType===3){g=f.nodeValue;e=g.replace(b,a);if(e!==g){if(!c&&/</.test(e)){$(f).before(e);d.push(f)}else{f.nodeValue=e}}}}while(f=f.nextSibling)}d.length&&$(d).remove()})}})(jQuery);