Wanikani Double-Check

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

目前為 2022-09-10 提交的版本,檢視 最新版本

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

QingJ © 2025

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