Wanikani Override

Adds an "Ignore Answer" button during reviews that makes WaniKani ignore the current answer (useful if, for example, you made a stupid typo)

  1. // ==UserScript==
  2. // @name Wanikani Override
  3. // @namespace wkoverride
  4. // @description Adds an "Ignore Answer" button during reviews that makes WaniKani ignore the current answer (useful if, for example, you made a stupid typo)
  5. // @include http://www.wanikani.com/review/session*
  6. // @include https://www.wanikani.com/review/session*
  7. // @version 1.1.3
  8. // @author Rui Pinheiro
  9. // @grant GM_addStyle
  10. // @grant unsafeWindow
  11. // @require http://code.jquery.com/jquery-1.11.2.min.js
  12. // @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
  13. // ==/UserScript==
  14. /*
  15. * ==== Wanikani Override ====
  16. * == by Rui Pinheiro ==
  17. *
  18. * One of my biggest peeves with Wanikani is how once you get something wrong,
  19. * you can't change it. If you type too fast and make a typo, or if the spell checker
  20. * doesn't like you, you're out of luck. It's really frustrating to know an answer but
  21. * have Wanikani deny it to you because it wants the '-ing' form of the verb, but you
  22. * typed the infinitive.
  23. * Sometimes you get lucky, and the spell checker is lenient enough to mark the answer
  24. * correctly. Other times, that's not the case.
  25. * Then, there's the other type of mistakes. The ones where you type a reading but were
  26. * supposed to type a meaning, or the opposite. Those are really annoying, since most
  27. * times you actually knew the correct reading or meaning respectively.
  28. *
  29. *
  30. * My problem is that the decision was completely out of my hands. My objective is to
  31. * learn Kanji, and I'm not going to cheat since that wouldn't bring me closer to where
  32. * I want to be, but Wanikani decides that I can't be trusted and does not let me correct it.
  33. *
  34. * But now, with the client-side reviews update, we can finally do something about all this!
  35. *
  36. *
  37. *
  38. * This userscript fixes this problem, by adding an "Ignore Answer" button to the footer
  39. * during reviews. This button can be clicked anytime an answer has been marked as wrong by
  40. * Wanikani.
  41. *
  42. * Once clicked, the current answer will be changed from "incorrect" (red background)
  43. * to "ignored" (yellow background), and Wanikani will forget you even answered it.
  44. * This way, the item will appear again later on during the review cycle, effectively
  45. * giving you a second chance to get it right without stupid typos or the spell checker
  46. * getting in the way.
  47. *
  48. *
  49. * DISCLAIMER:
  50. * I am not responsible for any problems caused by this script.
  51. * This script was developed on Firefox 25.0.1 with Greasemonkey 1.12.
  52. * It was also tested on Chrome 31.0.1650.57 with Tampermonkey 3.5.3630.77.
  53. * Because I'm just one person, I can't guarantee the script works anywhere else.
  54. *
  55. * Also, anyone using this script is responsible for using it correctly.
  56. * This should be used only if you make an honest mistake but actually knew the correct
  57. * answer. Using it in any other way will harm your Kanji learning process,
  58. * cheating will only make learning Japanese harder and you'll end up harming only yourselves!
  59. */
  60. /*
  61. * This program is free software: you can redistribute it and/or modify
  62. * it under the terms of the GNU General Public License as published by
  63. * the Free Software Foundation, either version 3 of the License, or
  64. * (at your option) any later version.
  65. *
  66. * This program is distributed in the hope that it will be useful,
  67. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  68. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  69. * GNU General Public License for more details.
  70. *
  71. * You should have received a copy of the GNU General Public License
  72. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  73. */
  74. /*
  75. * === Changelog ===
  76. *
  77. * 1.1.2 (11 March 2014)
  78. * - Relicensed under the GPLv3.
  79. *
  80. * 1.1.1 (23 January 2014)
  81. * - Now supports the HTTPS protocol.
  82. *
  83. * 1.1.0 (24 November 2013)
  84. * - Previously ignored answers can be unignored by pressing the 'Ignore' button again before skipping to the next question.
  85. * - Code cleaned up, added debug functionality and better error logging (like all my other Wanikani scripts)
  86. *
  87. * 1.0.5 (7 November 2013)
  88. * - Ignores '~' hotkey if pressed while editing an item's notes.
  89. *
  90. * 1.0.4 (14 August 2013)
  91. * - Fixed the '~' hotkey in Chrome.
  92. *
  93. * 1.0.3 (5 August 2013)
  94. * - Wanikani update changed the review URL. While the script worked, it also affected the reviews summary page. This has been fixed
  95. *
  96. * 1.0.2 (29 July 2013)
  97. * - Made '~' a hotkey, as requested by some users of the Wanikani forums.
  98. * - Script now updates the question count and error count correctly.
  99. *
  100. * 1.0.1 (24 July 2013)
  101. * - Fixed a bug that would cause "Null" to appear instead of "Reading" or "Meaning" after ignoring a certain Vocabulary.
  102. * - Tested the script on Google Chrome with Tampermonkey (the script does not work natively).
  103. *
  104. * 1.0.0 (23 July 2013)
  105. * - First release.
  106. *
  107. */
  108. /*
  109. * Debug Settings
  110. */
  111. var debugLogEnabled = true;
  112. var scriptShortName = 'WKO';
  113. scriptLog = debugLogEnabled ? function (msg) {
  114. if (typeof msg === 'string') {
  115. console.log(scriptShortName + ': ' + msg);
  116. } else {
  117. console.log(msg);
  118. }
  119. }
  120. : function () {
  121. };
  122. /*
  123. * Other settings
  124. */
  125. var prefAllowUnignore = true;
  126. /*
  127. * "Ignore Answer" Button Click
  128. */
  129. var ActionEnum = Object.freeze({
  130. ignore: 0,
  131. unignore: 1
  132. });
  133. function WKO_ignoreAnswer()
  134. {
  135. try
  136. {
  137. /* Check if the current item was answered incorrectly */
  138. var elmnts = document.getElementsByClassName('incorrect');
  139. var elmnts2 = document.getElementsByClassName('WKO_ignored');
  140. var curAction;
  141. if (!isEmpty(elmnts[0])) // Current answer is wrong
  142. curAction = ActionEnum.ignore;
  143. else if (prefAllowUnignore && !isEmpty(elmnts2[0])) // Current answer is ignored
  144. curAction = ActionEnum.unignore;
  145. else
  146. // Either there is no current answer, or it's correct
  147. {
  148. alert('WKO: Current item wasn\'t answered incorrectly, nor ignored previously!');
  149. return false;
  150. }
  151. /* Grab information about current question */
  152. var curItem = $.jStorage.get('currentItem');
  153. var questionType = $.jStorage.get('questionType');
  154. /* Build item name */
  155. var itemName;
  156. if (curItem.rad)
  157. itemName = 'r';
  158. else if (curItem.kan)
  159. itemName = 'k';
  160. else
  161. itemName = 'v';
  162. itemName += curItem.id;
  163. scriptLog(itemName);
  164. /* Grab item from jStorage.
  165. *
  166. * item.rc and item.mc => Reading/Meaning Completed (if answered the item correctly)
  167. * item.ri and item.mi => Reading/Meaning Invalid (number of mistakes before answering correctly)
  168. */
  169. var item = $.jStorage.get(itemName) || {
  170. };
  171. /* Update the item data */
  172. if (questionType === 'meaning')
  173. {
  174. if (!('mi' in item) || isEmpty(item.mi))
  175. {
  176. throw Error('item.mi undefined');
  177. return false;
  178. }
  179. else if (item.mi < 0 || (item.mi == 0 && curAction == ActionEnum.ignore))
  180. {
  181. throw Error('item.mi too small');
  182. return false;
  183. }
  184. if (curAction == ActionEnum.ignore)
  185. item.mi -= 1;
  186. else
  187. item.mi += 1;
  188. delete item.mc;
  189. }
  190. else
  191. {
  192. if (!('ri' in item) || isEmpty(item.ri))
  193. {
  194. throw Error('item.ri undefined');
  195. return false;
  196. }
  197. else if (item.ri < 0 || (item.ri == 0 && curAction == ActionEnum.ignore))
  198. {
  199. throw Error('i.ri too small');
  200. return false;
  201. }
  202. if (curAction == ActionEnum.ignore)
  203. item.ri -= 1;
  204. else
  205. item.ri += 1;
  206. delete item.rc;
  207. }
  208. /* Save the new state back into jStorage */
  209. $.jStorage.set(itemName, item);
  210. /* Modify the questions counter and wrong counter and change the style of the answer field */
  211. var wrongCount = $.jStorage.get('wrongCount');
  212. var questionCount = $.jStorage.get('questionCount');
  213. if (curAction == ActionEnum.ignore)
  214. {
  215. $.jStorage.set('wrongCount', wrongCount - 1);
  216. $.jStorage.set('questionCount', questionCount - 1);
  217. $('#answer-form fieldset').removeClass('incorrect');
  218. $('#answer-form fieldset').addClass('WKO_ignored');
  219. }
  220. else
  221. {
  222. $.jStorage.set('wrongCount', wrongCount + 1);
  223. $.jStorage.set('questionCount', questionCount + 1);
  224. $('#answer-form fieldset').removeClass('WKO_ignored');
  225. $('#answer-form fieldset').addClass('incorrect');
  226. }
  227. return true;
  228. }
  229. catch (err) {
  230. logError(err);
  231. }
  232. }
  233. /*
  234. * Bind '~' as a hotkey
  235. */
  236. function bindHotkey()
  237. {
  238. jQuery(document).on('keydown.reviewScreen', function (event)
  239. {
  240. if ($('#reviews').is(':visible') && !$('*:focus').is('textarea, input'))
  241. {
  242. //alert('keycode: ' + event.keyCode);
  243. switch (event.keyCode) {
  244. case 176: //Firefox '~'
  245. case 192: //Chrome '~'
  246. event.stopPropagation();
  247. event.preventDefault();
  248. if ($('#user-response').is(':disabled'))
  249. WKO_ignoreAnswer();
  250. return false;
  251. break;
  252. }
  253. }
  254. });
  255. }
  256. /*
  257. * Inject Ignore Button
  258. */
  259. function addIgnoreAnswerBtn()
  260. {
  261. var footer = document.getElementsByTagName('footer'),
  262. $btn = jQuery('<div id="WKO_button" title="Ignore Answer">Ignore Answer</div>').on('click', WKO_ignoreAnswer);
  263. jQuery(footer[0]).prepend($btn);
  264. }
  265. /*
  266. * Prepares the script
  267. */
  268. function scriptInit()
  269. {
  270. // Add global CSS styles
  271. GM_addStyle('#WKO_button {background-color: #CC0000; color: #FFFFFF; cursor: pointer; display: inline-block; font-size: 0.8125em; padding: 10px; vertical-align: bottom;}');
  272. GM_addStyle('#answer-form fieldset.WKO_ignored input[type="text"]:-moz-placeholder, #answer-form fieldset.WKO_ignored input[type="text"]:-moz-placeholder {color: #FFFFFF; font-family: "Source Sans Pro",sans-serif; font-weight: 300; text-shadow: none; transition: color 0.15s linear 0s; } #answer-form fieldset.WKO_ignored button, #answer-form fieldset.WKO_ignored input[type="text"], #answer-form fieldset.WKO_ignored input[type="text"]:disabled { background-color: #FFCC00 !important; }');
  273. scriptLog('loaded');
  274. // Set up hooks
  275. try
  276. {
  277. addIgnoreAnswerBtn();
  278. bindHotkey();
  279. }
  280. catch (err) {
  281. logError(err);
  282. }
  283. }
  284. /*
  285. * Helper Functions/Variables
  286. */
  287. //use 'jQuery' for greasemonkey's version, $ is WK's jQuery
  288. $ = unsafeWindow.$;
  289. function isEmpty(value) {
  290. return (typeof value === 'undefined' || value === null);
  291. }
  292. /*
  293. * Error handling
  294. * Can use 'error.stack', not cross-browser (though it should work on Firefox and Chrome)
  295. */
  296. function logError(error)
  297. {
  298. var stackMessage = '';
  299. if ('stack' in error)
  300. stackMessage = '\n\tStack: ' + error.stack;
  301. console.error(scriptShortName + ' Error: ' + error.name + '\n\tMessage: ' + error.message + stackMessage);
  302. }
  303. /*
  304. * Start the script
  305. */
  306. scriptInit();