Greasy Fork镜像 支持简体中文。

Wanikani Double-Check

Allows retyping typo'd answers, or marking wrong when WK's typo tolerance is too lax.

目前為 2021-11-18 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Wanikani Double-Check
  3. // @namespace wkdoublecheck
  4. // @description Allows retyping typo'd answers, or marking wrong when WK's typo tolerance is too lax.
  5. // @include /^https://(www|preview).wanikani.com/review/session/
  6. // @version 2.2.38
  7. // @author Robin Findley
  8. // @copyright 2017+, Robin Findley
  9. // @license MIT; http://opensource.org/licenses/MIT
  10. // @run-at document-end
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. // CREDITS: This is a replacement for an original script by Wanikani user @Ethan.
  15. // Ethan's script stopped working due to some Wanikani changes. The code below is
  16. // 100% my own, but it closely replicates the functionality of Ethan's original script.
  17.  
  18. // HOTKEYS:
  19. // "+" - Marks answer as 'correct'.
  20. // "-" - Marks answer as 'incorrect'.
  21. // "Escape" or "Backspace" - Resets question, allowing you to retype.
  22.  
  23. // SEE SETTINGS BELOW.
  24.  
  25. window.doublecheck = {};
  26.  
  27. (function(gobj) {
  28.  
  29. /* global $, wkof, additionalContent, lastItems, Srs, wanakana, WaniKani */
  30.  
  31. var settings;
  32.  
  33. wkof.include('Menu,Settings');
  34. wkof.ready('document,Menu,Settings').then(setup);
  35.  
  36. //------------------------------------------------------------------------
  37. // setup() - Set up the menu link and default settings.
  38. //------------------------------------------------------------------------
  39. function setup() {
  40. wkof.Menu.insert_script_link({name:'doublecheck',submenu:'Settings',title:'Double-Check',on_click:open_settings});
  41.  
  42. var defaults = {
  43. allow_retyping: true,
  44. allow_change_correct: false,
  45. show_corrected_answer: false,
  46. allow_change_incorrect: false,
  47. typo_action: 'ignore',
  48. wrong_answer_type_action: 'warn',
  49. wrong_number_n_action: 'warn',
  50. small_kana_action: 'warn',
  51. kanji_reading_for_vocab_action: 'warn',
  52. kanji_meaning_for_vocab_action: 'warn',
  53. delay_wrong: true,
  54. delay_multi_meaning: false,
  55. delay_slightly_off: false,
  56. delay_period: 1.5,
  57. warn_burn: 'never',
  58. burn_delay_period: 1.5,
  59. show_lightning_button: true,
  60. lightning_enabled: false,
  61. srs_msg_period: 1.2,
  62. autoinfo_correct: false,
  63. autoinfo_incorrect: false,
  64. autoinfo_multi_meaning: false,
  65. autoinfo_slightly_off: false
  66. }
  67. return wkof.Settings.load('doublecheck', defaults)
  68. .then(init_ui.bind(null, true /* first_time */));
  69. }
  70.  
  71. //------------------------------------------------------------------------
  72. // open_settings() - Open the Settings dialog.
  73. //------------------------------------------------------------------------
  74. function open_settings() {
  75. var dialog = new wkof.Settings({
  76. script_id: 'doublecheck',
  77. title: 'Double-Check Settings',
  78. on_save: init_ui,
  79. pre_open: settings_preopen,
  80. content: {
  81. tabAnswers: {type:'page',label:'Answers',content:{
  82. grpChangeAnswers: {type:'group',label:'Change Answer',content:{
  83. allow_retyping: {type:'checkbox',label:'Allow retyping answer',default:true,hover_tip:'When enabled, you can retype your answer by pressing Escape or Backspace.'},
  84. allow_change_incorrect: {type:'checkbox',label:'Allow changing to "incorrect"',default:true,hover_tip:'When enabled, you can change your answer\nto "incorrect" by pressing the "-" key.'},
  85. allow_change_correct: {type:'checkbox',label:'Allow changing to "correct"',default:true,hover_tip:'When enabled, you can change your answer\nto "correct" by pressing the "+" key.'},
  86. show_corrected_answer: {type:'checkbox',label:'Show corrected answer',default:false,hover_tip:'When enabled, pressing \'+\' to correct your answer puts the\ncorrected answer in the input field. Pressing \'+\' multiple\ntimes cycles through all acceptable answers.'},
  87. }},
  88. grpCarelessMistakes: {type:'group',label:'Careless Mistakes',content:{
  89. typo_action: {type:'dropdown',label:'Typos in meaning',default:'ignore',content:{ignore:'Ignore',warn:'Warn/shake',wrong:'Mark wrong'},hover_tip:'Choose an action to take when meaning contains typos.'},
  90. wrong_answer_type_action: {type:'dropdown',label:'Wrong answer type',default:'warn',content:{warn:'Warn/shake',wrong:'Mark wrong'},hover_tip:'Choose an action to take when reading was entered instead of meaning, or vice versa.'},
  91. wrong_number_n_action: {type:'dropdown',label:'Wrong number of n\'s',default:'warn',content:{warn:'Warn/shake',wrong:'Mark wrong'},hover_tip:'Choose an action to take when you type the wrong number of n\'s in certain reading questions.'},
  92. small_kana_action: {type:'dropdown',label:'Big kana instead of small',default:'warn',content:{warn:'Warn/shake',wrong:'Mark wrong'},hover_tip:'Choose an action to take when you type a big kana instead of small (e.g. ゆ instead of ゅ).'},
  93. kanji_reading_for_vocab_action: {type:'dropdown',label:'Kanji reading instead of vocab',default:'warn',content:{warn:'Warn/shake',wrong:'Mark wrong'},hover_tip:'Choose an action to take when the reading of a kanji is entered for a single character vocab word instead of the correct vocab reading.'},
  94. kanji_meaning_for_vocab_action: {type:'dropdown',label:'Kanji meaning instead of vocab',default:'warn',content:{warn:'Warn/shake',wrong:'Mark wrong'},hover_tip:'Choose an action to take when the meaning of a kanji is entered for a single character vocab word instead of the correct vocab meaning.'},
  95. }},
  96. }},
  97. tabMistakeDelay: {type:'page',label:'Mistake Delay',content:{
  98. grpDelay: {type:'group',label:'Delay Next Question',content:{
  99. delay_wrong: {type:'checkbox',label:'Delay when wrong',default:true,refresh_on_change:true,hover_tip:'If your answer is wrong, you cannot advance\nto the next question for at least N seconds.'},
  100. delay_multi_meaning: {type:'checkbox',label:'Delay when multiple meanings',default:false,hover_tip:'If the item has multiple meanings, you cannot advance\nto the next question for at least N seconds.'},
  101. delay_slightly_off: {type:'checkbox',label:'Delay when answer has typos',default:false,hover_tip:'If your answer contains typos, you cannot advance\nto the next question for at least N seconds.'},
  102. delay_period: {type:'number',label:'Delay period (in seconds)',default:1.5,hover_tip:'Number of seconds to delay before allowing\nyou to advance to the next question.'},
  103. }},
  104. }},
  105. tabBurnReviews: {type:'page',label:'Burn Reviews',content:{
  106. grpBurnReviews: {type:'group',label:'Burn Reviews',content:{
  107. warn_burn: {type:'dropdown',label:'Warn before burning',default:'never',content:{never:'Never',cheated:'If you changed answer',always:'Always'},hover_tip:'Choose when to warn before burning an item.'},
  108. burn_delay_period: {type:'number',label:'Delay after warning (in seconds)',default:1.5,hover_tip:'Number of seconds to delay before allowing\nyou to advance to the next question after seeing a burn warning.'},
  109. }},
  110. }},
  111. tabLightning: {type:'page',label:'Lightning',content:{
  112. grpLightning: {type:'group',label:'Lightning Mode',content:{
  113. show_lightning_button: {type:'checkbox',label:'Show "Lightning Mode" button',default:true,hover_tip:'Show the "Lightning Mode" toggle\nbutton on the review screen.'},
  114. lightning_enabled: {type:'checkbox',label:'Enable "Lightning Mode"',default:true,refresh_on_change:true,hover_tip:'Enable "Lightning Mode", which automatically advances to\nthe next question if you answer correctly.'},
  115. srs_msg_period: {type:'number',label:'SRS popup time (in seconds)',default:1.2,min:0,hover_tip:'How long to show SRS up/down popup when in lightning mode. (0 = don\'t show)'},
  116. }},
  117. }},
  118. tabAutoInfo: {type:'page',label:'Item Info',content:{
  119. grpAutoInfo: {type:'group',label:'Show Item Info',content:{
  120. autoinfo_correct: {type:'checkbox',label:'After correct answer',default:false,hover_tip:'Automatically show the Item Info after correct answers.', validate:validate_autoinfo_correct},
  121. autoinfo_incorrect: {type:'checkbox',label:'After incorrect answer',default:false,hover_tip:'Automatically show the Item Info after incorrect answers.', validate:validate_autoinfo_incorrect},
  122. autoinfo_multi_meaning: {type:'checkbox',label:'When multiple meanings',default:false,hover_tip:'Automatically show the Item Info when an item has multiple meanings.', validate:validate_autoinfo_correct},
  123. autoinfo_slightly_off: {type:'checkbox',label:'When answer has typos',default:false,hover_tip:'Automatically show the Item Info when your answer has typos.', validate:validate_autoinfo_correct},
  124. }},
  125. }},
  126. }
  127. });
  128. dialog.open();
  129. }
  130.  
  131. //------------------------------------------------------------------------
  132. // validate_autoinfo_correct() - Notify user if iteminfo and lightning are both enabled.
  133. //------------------------------------------------------------------------
  134. function validate_autoinfo_correct(enabled) {
  135. if (enabled && settings.lightning_enabled) {
  136. return 'Disable "Lightning Mode"!';
  137. }
  138. }
  139.  
  140. //------------------------------------------------------------------------
  141. // validate_autoinfo_incorrect() - Notify user if iteminfo and lightning are both enabled, and wrong_delay disabled.
  142. //------------------------------------------------------------------------
  143. function validate_autoinfo_incorrect(enabled) {
  144. if (enabled && settings.lightning_enabled && !settings.delay_wrong) {
  145. return 'Disable "Lightning Mode", or<br>enable "Delay when wrong"!';
  146. }
  147. }
  148.  
  149. //------------------------------------------------------------------------
  150. // settings_preopen() - Notify user if iteminfo and lightning are both enabled.
  151. //------------------------------------------------------------------------
  152. function settings_preopen(dialog) {
  153. dialog.dialog({width:525});
  154. }
  155.  
  156. //------------------------------------------------------------------------
  157. // init_ui() - Initialize the user interface.
  158. //------------------------------------------------------------------------
  159. var first_time = true;
  160. function init_ui() {
  161. settings = wkof.settings.doublecheck;
  162.  
  163. if (first_time) {
  164. first_time = false;
  165. startup();
  166. }
  167.  
  168. // Migrate 'lightning' setting from localStorage.
  169. var lightning = localStorage.getItem('lightning');
  170. if (lightning === 'false' || lightning === 'true') {
  171. localStorage.removeItem('lightning');
  172. settings.lightning_enabled = lightning;
  173. wkof.Settings.save('doublecheck');
  174. }
  175.  
  176. // Initialize the Lightning Mode button.
  177. if (settings.lightning_enabled) {
  178. $('#lightning-mode').addClass('doublecheck-active');
  179. } else {
  180. $('#lightning-mode').removeClass('doublecheck-active');
  181. }
  182. $('#lightning-mode').prop('hidden', !settings.show_lightning_button);
  183.  
  184. setClass('#option-double-check', 'hidden', !(settings.allow_change_correct || settings.allow_change_incorrect));
  185. setClass('#option-retype', 'hidden', !settings.allow_retyping);
  186. resize_buttons();
  187.  
  188. if (state === 'second_submit') {
  189. setClass('#option-double-check', 'disabled', !(
  190. (new_answer.passed && (settings.allow_change_incorrect || !first_answer.passed)) ||
  191. (!new_answer.passed && (settings.allow_change_correct || first_answer.passed))
  192. ));
  193. setClass('#option-retype', 'disabled', !settings.allow_retyping);
  194. } else {
  195. setClass('#option-double-check', 'disabled', true);
  196. }
  197. }
  198.  
  199. var old_submit_handler, old_answer_checker, ignore_submit = false, state = 'first_submit', show_srs, srs_load, delay_timer;
  200. var item, itype, item_id, item_status, qtype, valid_answers, wrong_cnt, question_cnt, completed_cnt, answer, new_answer, active_queue;
  201. var last_item_id, last_qtype, first_answer;
  202.  
  203. function promise(){var a,b,c=new Promise(function(d,e){a=d;b=e;});c.resolve=a;c.reject=b;return c;}
  204.  
  205. //------------------------------------------------------------------------
  206. // lightning_clicked() - Lightning button handler.
  207. //------------------------------------------------------------------------
  208. function lightning_clicked() {
  209. settings.lightning_enabled = !settings.lightning_enabled;
  210. wkof.Settings.save('doublecheck');
  211. $('#lightning-mode').toggleClass('doublecheck-active', settings.lightning_enabled);
  212. return false;
  213. }
  214.  
  215. //------------------------------------------------------------------------
  216. // get_correct_answers() - Returns an array of acceptable answers.
  217. //------------------------------------------------------------------------
  218. function get_correct_answers() {
  219. if (qtype === 'reading') {
  220. if (itype === 'k') {
  221. switch (item.emph) {
  222. case "onyomi": return item.on;
  223. case "kunyomi": return item.kun;
  224. case "nanori": return item.nanori;
  225. }
  226. } else {
  227. return item.kana;
  228. }
  229. } else {
  230. return [].concat(item.syn,item.en);
  231. }
  232. }
  233.  
  234. //------------------------------------------------------------------------
  235. // get_next_correct_answer() - Returns the next acceptable answer from the
  236. // array returned by get_correct_answers().
  237. //------------------------------------------------------------------------
  238. function get_next_correct_answer() {
  239. var result = first_answer.correct_answers[first_answer.correct_answer_index];
  240. first_answer.correct_answer_index = (first_answer.correct_answer_index + 1) % first_answer.correct_answers.length;
  241. return result;
  242. }
  243.  
  244. //------------------------------------------------------------------------
  245. // toggle_result() - Toggle an answer from right->wrong or wrong->right.
  246. //------------------------------------------------------------------------
  247. function toggle_result(new_state) {
  248. if (new_state === 'toggle') new_state = (new_answer.passed ? 'incorrect' : 'correct');
  249. if (state !== 'second_submit') return false;
  250.  
  251. var input = $('#answer-form fieldset input');
  252. var current_response = input.val();
  253. clear_delay();
  254. switch (new_state) {
  255. case 'correct':
  256. if (!(settings.allow_change_correct || first_answer.passed)) return false;
  257. if (first_answer.passed) {
  258. input.val(first_answer.response);
  259. } else {
  260. input.val(get_next_correct_answer());
  261. }
  262. new_answer = {passed:true, accurate:true, multipleAnswers:false, exception:false};
  263. set_answer_state(new_answer, false /* show_msgs */);
  264. if (!settings.show_corrected_answer) input.val(current_response);
  265. break;
  266. case 'incorrect':
  267. if (!(new_answer.passed && (settings.allow_change_incorrect || !first_answer.passed))) return false;
  268. if (first_answer.passed) {
  269. input.val('xxxxxx');
  270. } else {
  271. input.val(first_answer.response);
  272. }
  273. new_answer = {passed:false, accurate:false, multipleAnswers:false, exception:false};
  274. set_answer_state(new_answer, false /* show_msgs */);
  275. if (!settings.show_corrected_answer) input.val(current_response);
  276. break;
  277. case 'retype':
  278. if (!settings.allow_retyping) return false;
  279. set_answer_state({reset:true, due_to_retype:true});
  280. break;
  281. }
  282. }
  283.  
  284. //------------------------------------------------------------------------
  285. // do_delay() - Disable the submit button briefly to prevent clicking past wrong answers.
  286. //------------------------------------------------------------------------
  287. function do_delay(period) {
  288. if (period === undefined) period = settings.delay_period;
  289. ignore_submit = true;
  290. delay_timer = setTimeout(function() {
  291. delay_timer = -1;
  292. ignore_submit = false;
  293. }, period*1000);
  294. }
  295.  
  296. //------------------------------------------------------------------------
  297. // clear_delay() - Clear the delay timer.
  298. //------------------------------------------------------------------------
  299. function clear_delay() {
  300. if (delay_timer) {
  301. ignore_submit = false;
  302. clearTimeout(delay_timer);
  303. delay_timer = undefined;
  304. }
  305. }
  306.  
  307. //------------------------------------------------------------------------
  308. // return_new_answer() - Alternate answer checker that overrides our results.
  309. //------------------------------------------------------------------------
  310. function return_new_answer() {
  311. return new_answer;
  312. }
  313.  
  314. //------------------------------------------------------------------------
  315. // set_answer_state() - Update the screen to show results of answer-check.
  316. //------------------------------------------------------------------------
  317. function set_answer_state(answer, show_msgs) {
  318. // If user requested to retype answer, reset the question.
  319. if (answer.reset) {
  320. clear_delay();
  321. if (state === 'second_submit') {
  322. $.jStorage.set('wrongCount', wrong_cnt);
  323. $.jStorage.set('questionCount', question_cnt);
  324. $.jStorage.set('completedCount', completed_cnt);
  325. $.jStorage.set('activeQueue', active_queue);
  326. }
  327. state = 'first_submit';
  328.  
  329. // If we are resetting due to the user clicking 'retype', then we need to trigger
  330. // a refresh the input field and stats by updating 'currentItem' in jStorage.
  331. if (answer.due_to_retype) {
  332. $.jStorage.set('currentItem', $.jStorage.get('currentItem'));
  333. return
  334. }
  335.  
  336. window.wkRefreshAudio();
  337. $("#answer-exception").remove();
  338. $('#option-double-check').addClass('disabled').find('span').attr('title','Mark Right').find('i').attr('class','fa fa-thumbs-up');
  339. $('#option-retype').addClass('disabled');
  340. Srs.remove();
  341. return;
  342. }
  343.  
  344. // If answer is invalid for some reason, do the shake thing.
  345. var dblchk = $('#option-double-check');
  346. var input = $('#user-response');
  347. if (answer.exception) {
  348. $("#answer-exception").remove();
  349. if (answer.confirming_burn) {
  350. // NOTE: We can only reach this branch if the current answer is correct, otherwise we wouldn't be burning it.
  351. dblchk.find('span').attr('title','Mark Wrong').find('i').attr('class','fa fa-thumbs-down');
  352. setClass(dblchk, 'disabled', !(settings.allow_change_incorrect || !first_answer.passed));
  353. $("#answer-form fieldset").removeClass('incorrect').removeClass('correct').addClass('confburn');
  354. $("#additional-content").append($('<div id="answer-exception"><span>'+answer.exception+'</span></div>').addClass("animated fadeInUp"));
  355. return;
  356. }
  357. if (!$("#answer-form form").is(":animated")) {
  358. $("#reviews").css("overflow-x", "hidden");
  359. $("#answer-form form").effect("shake", {}, 300, function() {
  360. $("#reviews").css("overflow-x", "visible");
  361. if (!answer.accurate && input.val() !== '') {
  362. if (typeof answer.exception === "string") {
  363. $("#answer-form form").append($('<div id="answer-exception" class="answer-exception-form"><span>' + answer.exception + '</span></div>').addClass("animated fadeInUp"));
  364. }
  365. }
  366. }).find("input").focus();
  367. }
  368. return;
  369. }
  370.  
  371. // Draw 'correct' or 'incorrect' results, enable Double-Check button, and calculate updated statistics.
  372. $("#user-response").blur();
  373. var new_status = Object.assign({},item_status);
  374. var retype = $('#option-retype');
  375. setClass(retype, 'disabled', !settings.allow_retyping);
  376. if (answer.passed) {
  377. $("#answer-form fieldset").removeClass('incorrect').removeClass('confburn').addClass('correct');
  378. dblchk.find('span').attr('title','Mark Wrong').find('i').attr('class','fa fa-thumbs-down');
  379. setClass(dblchk, 'disabled', !(settings.allow_change_incorrect || !first_answer.passed));
  380. if (qtype === 'meaning') {
  381. new_status.mc = (new_status.mc || 0) + 1;
  382. } else {
  383. new_status.rc = (new_status.rc || 0) + 1;
  384. if (input.val().slice(-1) === 'n') input.val(input.val().slice(0,-1)+'ん');
  385. }
  386. } else {
  387. $("#answer-form fieldset").removeClass('correct').removeClass('confburn').addClass('incorrect');
  388. dblchk.find('span').attr('title','Mark Right').find('i').attr('class','fa fa-thumbs-up');
  389. setClass(dblchk, 'disabled', !(settings.allow_change_correct || first_answer.passed));
  390. $.jStorage.set('wrongCount', wrong_cnt + 1);
  391. }
  392. $.jStorage.set('questionCount', question_cnt + 1);
  393.  
  394. if (((itype === 'r') || ((new_status.rc || 0) >= 1)) && ((new_status.mc || 0) >= 1)) {
  395. if (show_srs) {
  396. if (settings.lightning_enabled) {
  397. if (settings.srs_msg_period > 0) {
  398. var status = Object.assign({},new_status);
  399. var srs = item.srs;
  400. setTimeout(Srs.load.bind(Srs, status, srs), 100);
  401. setTimeout(Srs.remove, settings.srs_msg_period * 1000);
  402. }
  403. } else {
  404. Srs.remove();
  405. Srs.load(new_status,item.srs);
  406. }
  407. }
  408. $.jStorage.set('completedCount', completed_cnt + 1);
  409. $.jStorage.set('activeQueue', active_queue.slice(1));
  410. }
  411.  
  412. $("#user-response").prop("disabled", true);
  413.  
  414. window.wkRefreshAudio();
  415. additionalContent.enableButtons();
  416. lastItems.disableSessionStats();
  417. $("#answer-exception").remove();
  418.  
  419. // Open item info, depending on settings.
  420. var showing_info = false;
  421. if (answer.passed && !settings.lightning_enabled &&
  422. (settings.autoinfo_correct ||
  423. (settings.autoinfo_slightly_off && !answer.accurate) ||
  424. (settings.autoinfo_multi_meaning && answer.multipleAnswers)
  425. )) {
  426. showing_info = true;
  427. $('#option-item-info').click();
  428. } else if (!answer.passed && !(settings.lightning_enabled && !settings.delay_wrong) && settings.autoinfo_incorrect) {
  429. showing_info = true;
  430. $('#option-item-info').click();
  431. }
  432.  
  433. // When user is submitting an answer, display the on-screen message that Wanikani normally shows.
  434. if (show_msgs) {
  435. var msg;
  436. if (answer.passed) {
  437. if (!answer.accurate) {
  438. msg = 'Your answer was a bit off. Check the '+qtype+' to make sure you are correct';
  439. } else if (answer.multipleAnswers) {
  440. msg = 'Did you know this item has multiple possible '+qtype+'s?';
  441. }
  442. } else if (answer.custom_msg) {
  443. msg = answer.custom_msg;
  444. } else {
  445. msg = 'Need help? View the correct '+qtype+' and mnemonic';
  446. }
  447. if (msg) {
  448. if (showing_info) {
  449. $("#information").prepend($('<div id="answer-exception" style="top:0;"><span>'+msg+'</span></div>').addClass("animated fadeInUp"));
  450. } else {
  451. $("#additional-content").append($('<div id="answer-exception"><span>'+msg+'</span></div>').addClass("animated fadeInUp"));
  452. }
  453. }
  454. }
  455. }
  456.  
  457. //------------------------------------------------------------------------
  458. // setClass() - Add or remove a class based on the 'enabled' state.
  459. //------------------------------------------------------------------------
  460. function setClass(elem, classname, enabled) {
  461. if (typeof elem === 'string') elem = $(elem);
  462. if (enabled) {
  463. elem.addClass(classname)
  464. } else {
  465. elem.removeClass(classname);
  466. }
  467. }
  468.  
  469. //------------------------------------------------------------------------
  470. // new_submit_handler() - Intercept handler for 'submit' button. Overrides default behavior as needed.
  471. //------------------------------------------------------------------------
  472. function new_submit_handler(e) {
  473. // Don't process 'submit' if we are ignoring temporarily (to prevent double-tapping past important info)
  474.  
  475. if (ignore_submit) {
  476. // If the user presses <enter> during delay period,
  477. // WK enables the user input field, which makes Item Info not work.
  478. // Let's make sure the input field is disabled.
  479. setTimeout(function(){
  480. $("#user-response").prop('disabled',true);
  481. },1);
  482. return false;
  483. }
  484.  
  485. var submitted_immediately = false;
  486. switch(state) {
  487. case 'first_submit':
  488. // We intercept the first 'submit' click, and simulate normal Wanikani screen behavior.
  489. state = 'second_submit';
  490.  
  491. // Capture the state of the system before submitting the answer.
  492. item = $.jStorage.get('currentItem');
  493. itype = (item.rad ? 'r' : (item.kan ? 'k' : 'v'));
  494. item_id = itype + item.id;
  495. item_status = $.jStorage.get(item_id) || {};
  496. qtype = $.jStorage.get('questionType');
  497. wrong_cnt = $.jStorage.get('wrongCount') || 0;
  498. question_cnt = $.jStorage.get('questionCount') || 0;
  499. completed_cnt = $.jStorage.get('completedCount') || 0;
  500. active_queue = $.jStorage.get('activeQueue') || [];
  501. show_srs = $.jStorage.get('r/srsIndicator');
  502.  
  503. // Ask Wanikani if the answer is right (but we don't actually submit the answer).
  504. answer = old_answer_checker(qtype, $("#user-response").val());
  505.  
  506. // Update the screen to reflect the results of our checked answer.
  507. $("html, body").animate({scrollTop: 0}, 200);
  508.  
  509. // Check if [meaning has kana] or [reading has latin]
  510. var text = $('#user-response').val();
  511. if ((qtype === 'reading' && window.answerChecker.isNonKanaPresent(text)) ||
  512. (qtype === 'meaning' && window.answerChecker.isKanaPresent(text)) ||
  513. (text === '')) {
  514. answer.exception = answer.exception || true;
  515. }
  516.  
  517. // Non-exact answer (i.e. "Close but no cigar" script)
  518. if (answer.passed && !answer.accurate) {
  519. switch (settings.typo_action) {
  520. case 'warn': answer.exception = 'Your answer was close, but not exact'; break;
  521. case 'wrong': answer.passed = false; answer.custom_msg = 'Your answer was not exact, as required by your settings.'; break;
  522. }
  523. }
  524.  
  525. // Check for reading/meaning mixups
  526. if (!answer.passed) {
  527. if (qtype === 'meaning') {
  528. var accepted_readings = [].concat(item.kana, item.on, item.kun, item.nanori);
  529. var answer_as_kana = to_kana($('#user-response').val());
  530. if (accepted_readings.indexOf(answer_as_kana) >= 0) {
  531. if (settings.wrong_answer_type_action === 'warn') {
  532. answer.exception = 'Oops, we want the meaning, not the reading.';
  533. } else {
  534. answer.exception = false;
  535. }
  536. }
  537. } else {
  538. // Although Wanikani now checks for readings entered as meanings, it only
  539. // checks the 'preferred' reading. Here, we check all readings.
  540. var accepted_meanings = item.en;
  541. try {
  542. accepted_meanings = accepted_meanings.concat(item.syn, item.auxiliary_meanings
  543. .filter((meaning) => meaning.type === 'whitelist')
  544. .map((meaning) => meaning.meaning));
  545. } catch(e) {}
  546. var meanings_as_hiragana = accepted_meanings.map(m => to_kana(m.toLowerCase()).replace(/\s/g,''));
  547. var answer_as_hiragana = Array.from($('#user-response').val().toLowerCase()).map(c => wanakana.toHiragana(c)).join('');
  548. if (meanings_as_hiragana.indexOf(answer_as_hiragana) >= 0) {
  549. if (settings.wrong_answer_type_action === 'warn') {
  550. answer.exception = 'Oops, we want the reading, not the meaning.';
  551. } else {
  552. answer.exception = false;
  553. }
  554. }
  555. }
  556. }
  557.  
  558. // Check for Wanikani warnings that should be changed to 'wrong', based on settings.
  559. if (typeof answer.exception === 'string') {
  560. if (((settings.kanji_meaning_for_vocab_action === 'wrong') && answer.exception.toLowerCase().includes('want the vocabulary meaning, not the kanji meaning')) ||
  561. ((settings.kanji_reading_for_vocab_action === 'wrong') && answer.exception.toLowerCase().includes('want the vocabulary reading, not the kanji reading')) ||
  562. ((settings.wrong_number_n_action === 'wrong') && answer.exception.toLowerCase().includes('forget that ん')) ||
  563. ((settings.small_kana_action === 'wrong') && answer.exception.toLowerCase().includes('watch out for the small')))
  564. {
  565. answer.exception = false;
  566. answer.passed = false;
  567. }
  568. }
  569.  
  570. // Copy the modified answer to new_answer, which is what will be submitted to Wanikani.
  571. new_answer = Object.assign({}, answer);
  572.  
  573. // Check for exceptions that are preventing the answer from being submitted.
  574. if (answer.exception) {
  575. set_answer_state(answer, true /* show_msgs */);
  576. state = 'first_submit';
  577. return false;
  578. }
  579.  
  580. // At this point, the answer is ready for submission (i.e. no exceptions).
  581. // If this is the user's first attempt at this question, remember the result so
  582. // we can determine whether they altered their answer later.
  583. if (!((item_id === last_item_id) && (qtype === last_qtype))) {
  584. first_answer = Object.assign({
  585. response:$("#user-response").val(),
  586. correct_answers:get_correct_answers(),
  587. correct_answer_index: 0,
  588. }, answer);
  589. }
  590. last_item_id = item_id;
  591. last_qtype = qtype;
  592.  
  593. // Optionally (according to settings), temporarily ignore any additional clicks on the
  594. // 'submit' button to prevent the user from clicking past important info about the answer.
  595. if ((!answer.passed && settings.delay_wrong) ||
  596. (answer.passed &&
  597. ((!answer.accurate && settings.delay_slightly_off) ||
  598. (answer.multipleAnswers && settings.delay_multi_meaning))
  599. )
  600. )
  601. {
  602. set_answer_state(answer, true /* show_msgs */);
  603. do_delay();
  604. return false;
  605. }
  606.  
  607. set_answer_state(answer, true /* show_msgs */);
  608. if (settings.lightning_enabled) {
  609. new_submit_handler(e);
  610. }
  611.  
  612. return false;
  613.  
  614. case 'second_submit':
  615.  
  616. // If the user changed their answer to 'correct', mark the item
  617. // in storage, so we can warn the user if it comes up for burn.
  618. // The mark is kept for 10 days in case the user doesn't complete
  619. // the item (reading and meaning) within one session.
  620. if (!first_answer.passed && new_answer.passed) {
  621. $.jStorage.set('confburn/' + item.id, true, {TTL:1000*3600*24*10});
  622. }
  623.  
  624. // Before accepting a final submit, notify the user if item will burn (depending on settings).
  625. new_answer.exception = false;
  626. if (!new_answer.confirming_burn) {
  627. // Check if we need to warn the user that this is a 'burn' review.
  628. // NOTE: "item_status.ni" seems to be used by other scripts.
  629. var will_burn = (item.srs === 8) && new_answer.passed &&
  630. !(item_status.mi || item_status.ri || item_status.ni) &&
  631. ((itype === 'r') ||
  632. (((item_status.rc || 0) + (qtype === 'reading' ? 1 : 0) > 0) &&
  633. ((item_status.mc || 0) + (qtype === 'meaning' ? 1 : 0) > 0)));
  634. var cheated = $.jStorage.get('confburn/' + item.id) ? true : false;
  635. if (will_burn && (settings.warn_burn !== 'never')) {
  636. // Prompt before burning, and suppress proceeding for a moment.
  637. if (cheated) {
  638. new_answer.exception = 'You modified an answer on this item. It will be burned if you continue.';
  639. } else if (settings.warn_burn === 'always') {
  640. new_answer.exception = 'This item will be burned if you continue.'
  641. }
  642. if (new_answer.exception) {
  643. new_answer.confirming_burn = true;
  644. set_answer_state(new_answer, true /* show_msgs */);
  645. // Not sure what's causing the input field to be re-enabled, but we have to disable it:
  646. setTimeout(function () {
  647. $("#user-response").prop('disabled',true);
  648. }, 1);
  649. if (settings.burn_delay_period > 0) {
  650. do_delay(settings.burn_delay_period);
  651. }
  652. return false;
  653. }
  654. }
  655. } else {
  656. // We are burning the item now, so we can remove the marker.
  657. $.jStorage.deleteKey('confburn/' + item.id);
  658. delete new_answer.confirming_burn;
  659. }
  660.  
  661. // We intercepted the first submit, allowing the user to optionally modify their answer.
  662. // Now, either the user has clicked submit again, or lightning is enabled and we are automatically clicking submit again.
  663. // Since Wanikani didn't see the first submit (because we intercepted it), now we need to simulate two submits for Wanikani:
  664. // 1. One for Wanikani to check the (possibly corrected) result, and
  665. // 2. One for Wanikani to move on to the next question.
  666.  
  667. // Reset the screen to pre-submitted state, so Wanikani won't get confused when it tries to process the answer.
  668. // Wanikani code will then update the screen according to our forced answer-check result.
  669. $('#option-double-check').addClass('disabled').find('span').attr('title','Double-Check').find('i').attr('class','fa fa-thumbs-up');
  670. $('#option-retype').addClass('disabled');
  671. $('#user-response').prop('disabled', false);
  672. $.jStorage.set('wrongCount', wrong_cnt);
  673. $.jStorage.set('questionCount', question_cnt);
  674. $.jStorage.set('completedCount', completed_cnt);
  675. $.jStorage.set('activeQueue', active_queue);
  676.  
  677. // Prevent WK from posting a second SRS notice.
  678. srs_load = Srs.load;
  679. Srs.load = function(){};
  680.  
  681. // This is the first submit actually forwarded to Wanikani.
  682. // It will check our (possibly corrected) answer.
  683. var old_audioAutoplay = window.audioAutoplay;
  684. window.audioAutoplay = false;
  685.  
  686. var result = click_submit.apply(this, arguments);
  687. result.await_submit
  688. .then(() => {
  689. // This is hidden third click from above, which Wanikani thinks is the second click.
  690. // Wanikani will move to the next question.
  691. state = 'first_submit';
  692.  
  693. // We need to disable the input field, so Wanikani will see this as the second click.
  694. $('#user-response').prop('disabled', true);
  695.  
  696. // Restore the SRS message function, which we disabled in second_submit above.
  697. Srs.load = srs_load;
  698.  
  699. // This is the second submit actually forwarded to Wanikani.
  700. // It will move on to the next question.
  701. click_submit.apply(this, arguments)
  702. .await_submit
  703. .then(() => {
  704. window.audioAutoplay = old_audioAutoplay;
  705. window.wkRefreshAudio();
  706. });
  707. });
  708. return result.status;
  709.  
  710. default:
  711. return false;
  712. }
  713.  
  714. return false;
  715. }
  716.  
  717. //------------------------------------------------------------------------
  718. // Simulate input character by character and convert with WanaKana to kana
  719. // -- Contributed by user @Sinyaven
  720. //------------------------------------------------------------------------
  721. function to_kana(text) {
  722. return Array.from(text).reduce((total, c) => wanakana.toKana(total + c, {IMEMode: true}), "").replace(/n$/, String.fromCharCode(12435));
  723. }
  724.  
  725. //------------------------------------------------------------------------
  726. // Resize the buttons according to how many are visible.
  727. //------------------------------------------------------------------------
  728. function resize_buttons() {
  729. var buttons = $('#additional-content ul>li');
  730. var btn_count = buttons.length - buttons.filter('.hidden,[hidden]').length;
  731. $('#additional-content ul > li').css('width',Math.floor(9900/btn_count)/100 + '%');
  732. }
  733.  
  734. //------------------------------------------------------------------------
  735. // External hook for @polv's script, "WaniKani Disable Default Answers"
  736. //------------------------------------------------------------------------
  737. gobj.set_state = function(_state) {
  738. state = _state;
  739. };
  740.  
  741. //------------------------------------------------------------------------
  742. // startup() - Install our intercept handlers, and add our Double-Check button and hotkey
  743. //------------------------------------------------------------------------
  744. function startup() {
  745. // Intercept the submit button handler.
  746. try {
  747. var intercepted = false;
  748. try {
  749. old_submit_handler = $._data( $('#answer-form form')[0], 'events').submit[0].handler;
  750. $._data( $('#answer-form form')[0], 'events').submit[0].handler = new_submit_handler;
  751. intercepted = true;
  752. } catch(err) {}
  753. if (!intercepted) {
  754. try {
  755. old_submit_handler = $._data( $('#answer-form button')[0], 'events').click[0].handler;
  756. $._data( $('#answer-form button')[0], 'events').click[0].handler = new_submit_handler;
  757. intercepted = true;
  758. } catch(err) {}
  759. }
  760. if (intercepted) {
  761. old_answer_checker = window.enhanceAnswerChecker({evaluate:window.answerChecker.evaluate}).evaluate;
  762. }
  763. } catch(err) {}
  764. if (typeof old_submit_handler !== 'function' || typeof old_answer_checker !== 'function') {
  765. alert('Wanikani Double-Check script is not working.');
  766. return;
  767. }
  768.  
  769. // Clear warning popups if question changes due to reasons outside of this script
  770. $.jStorage.listenKeyChange("currentItem", function(key, action){
  771. set_answer_state({reset:true});
  772. });
  773.  
  774. // Install the Lightning Mode button.
  775. $('head').append('<style>#lightning-mode.doublecheck-active {color:#ff0; opacity:1.0;}</style>');
  776. $('#summary-button').append('<a id="lightning-mode" href="#" hidden ><i class="fa fa-bolt" title="Lightning Mode - When enabled, auto-\nadvance after answering correctly."></i></a>');
  777. $('#lightning-mode').on('click', lightning_clicked);
  778.  
  779. // Install the Double-Check features.
  780. $('#additional-content ul').css('text-align','center').append(
  781. '<li id="option-double-check" class="disabled"><span title="Double Check"><i class="fa fa-thumbs-up"></i></span></li>'+
  782. '<li id="option-retype" class="disabled"><span title="Retype"><i class="fa fa-undo"></i></span></li></ul>'
  783. );
  784. $('#option-double-check').on('click', toggle_result.bind(null,'toggle'));
  785. $('#option-retype').on('click', toggle_result.bind(null,'retype'));
  786. $('body').on('keypress', function(event){
  787. if (event.which === 43) toggle_result('correct');
  788. if (event.which === 45) toggle_result('incorrect');
  789. return true;
  790. });
  791. $('body').on('keydown', function(event){
  792. if ((event.which === 27 || event.which === 8) &&
  793. (state !== 'first_submit') &&
  794. (event.target.nodeName === 'BODY') &&
  795. (!document.querySelector('#wkofs_doublecheck')))
  796. {
  797. toggle_result('retype');
  798. return false;
  799. } else if (event.ctrlKey && event.key === 'l') {
  800. lightning_clicked();
  801. return false;
  802. }
  803. return true;
  804. });
  805. $('head').append(
  806. '<style>'+
  807. '#additional-content>ul>li.hidden {display:none;}'+
  808. '#answer-form fieldset.confburn button, #answer-form fieldset.confburn input[type=text], #answer-form fieldset.confburn input[type=text]:disabled {'+
  809. ' background-color: #000 !important;'+
  810. ' color: #fff;'+
  811. ' text-shadow: 2px 2px 0 rgba(0,0,0,0.2);'+
  812. ' transition: background-color 0.1s ease-in;'+
  813. ' opacity: 1 !important;'+
  814. '}'+
  815. '</style>');
  816.  
  817. // Override the answer checker.
  818. window.answerChecker.evaluate = return_new_answer;
  819. window.enhanceAnswerChecker = function(answerChecker) {return answerChecker;};
  820.  
  821. // To prevent Wanikani from cutting the audio off in lightning mode,
  822. // We instruct any currently playing audio to unload when it's done,
  823. // rather than unloading it immediately.
  824. window.Howler.unload = function(){
  825. for (var i = window.Howler._howls.length-1; i >= 0; i--) {
  826. var howl = window.Howler._howls[i];
  827. if (howl.playing() || howl._queue.length > 0) {
  828. howl.on('end', howl.unload.bind(howl));
  829. } else {
  830. howl.unload();
  831. }
  832. }
  833. };
  834. }
  835.  
  836. function click_submit() {
  837. var p = promise();
  838.  
  839. if (!WaniKani.wanikani_compatibility_mode) {
  840. // Set up callback for when 'submit' button is re-enabled after being clicked.
  841. var mo = new MutationObserver((mutation) => {
  842. if (mutation.pop().target.disabled) return;
  843. mo.disconnect();
  844. mo = undefined;
  845. p.resolve();
  846. });
  847. mo.observe(document.querySelector('#answer-form button'), {attributeFilter: ['disabled']});
  848. } else {
  849. p.resolve();
  850. }
  851.  
  852. var result = old_submit_handler.apply(this, arguments);
  853.  
  854. return {status: result, await_submit: p};
  855. }
  856.  
  857. })(window.doublecheck);

QingJ © 2025

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