Wanikani Double-Check

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

  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/*
  6. // @version 3.2.4
  7. // @author Robin Findley
  8. // @copyright 2017-2024, Robin Findley
  9. // @license MIT; http://opensource.org/licenses/MIT
  10. // @run-at document-end
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. // HOTKEYS:
  15. // "+" - Marks answer as 'correct'.
  16. // "-" - Marks answer as 'incorrect'.
  17. // "Escape" or "Backspace" - Resets question, allowing you to retype.
  18.  
  19. // SEE SETTINGS BELOW.
  20.  
  21. window.doublecheck = {};
  22.  
  23. (async function(gobj) {
  24.  
  25. /* global wkof, Stimulus, WaniKani, importShim */
  26.  
  27. let script_name = 'Double-Check';
  28. let wkof_version_needed = '1.2.6';
  29.  
  30. let wkof_check_result = promise();
  31. let wkof_check_retries = 3;
  32. async function check_wkof() {
  33. if (!window.wkof) {
  34. if (--wkof_check_retries >= 0) {
  35. setTimeout(check_wkof, 1000);
  36. return wkof_check_result;
  37. }
  38. if (confirm(script_name+' requires Wanikani Open Framework.\nDo you want to be forwarded to the installation instructions?')) {
  39. window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
  40. }
  41. return wkof_check_result;
  42. }
  43. if (wkof.version.compare_to(wkof_version_needed) === 'older') {
  44. if (confirm(script_name+' requires Wanikani Open Framework version '+wkof_version_needed+'.\nDo you want to be forwarded to the update page?')) {
  45. window.location.href = 'https://greasyfork.org/en/scripts/38582-wanikani-open-framework';
  46. }
  47. return wkof_check_result;
  48. }
  49. wkof_check_result.resolve();
  50. return wkof_check_result;
  51. }
  52. await check_wkof();
  53.  
  54. const delay_before_installing = 500; // milliseconds
  55. wkof.on_pageload([
  56. '/subjects/extra_study',
  57. '/subjects/review',
  58. '/recent-mistakes/*/quiz'
  59. ], () => setTimeout(load_script, delay_before_installing));
  60.  
  61. function load_script() {
  62. wkof.include('Menu,Settings');
  63. wkof.ready('Menu,Settings').then(setup);
  64. }
  65.  
  66. let settings;
  67. let quiz_input, quiz_queue, additional_content, item_info, quiz_audio, quiz_stats, quiz_progress, quiz_header, response_helpers, wanakana;
  68. let answer_checker, answer_check, subject_stats, subject_stats_cache, session_stats;
  69. let old_submit_handler, ignore_submit, state, delay_timer, end_of_session_delay;
  70. let subject, synonyms, accepted_meanings, accepted_readings, srs_mgr;
  71. let qtype, new_answer_check, first_answer_check;
  72.  
  73. function promise(){let a,b,c=new Promise(function(d,e){a=d;b=e;});c.resolve=a;c.reject=b;return c;}
  74.  
  75. //------------------------------------------------------------------------
  76. // setup() - Set up the menu link and default settings.
  77. //------------------------------------------------------------------------
  78. let fresh_load = true;
  79. function setup() {
  80. fresh_load = true;
  81. wkof.Menu.insert_script_link({name:'doublecheck',submenu:'Settings',title:'Double-Check',on_click:open_settings});
  82.  
  83. let defaults = {
  84. allow_retyping: true,
  85. allow_change_correct: false,
  86. show_corrected_answer: false,
  87. allow_change_incorrect: false,
  88. typo_action: 'ignore',
  89. wrong_answer_type_action: 'warn',
  90. wrong_number_n_action: 'warn',
  91. small_kana_action: 'warn',
  92. kanji_reading_for_vocab_action: 'warn',
  93. kanji_meaning_for_vocab_action: 'warn',
  94. delay_wrong: true,
  95. delay_multi_meaning: false,
  96. delay_slightly_off: false,
  97. delay_period: 1.5,
  98. warn_burn: 'never',
  99. burn_delay_period: 1.5,
  100. show_lightning_button: true,
  101. lightning_enabled: false,
  102. srs_msg_period: 1.2,
  103. autoinfo_correct: false,
  104. autoinfo_incorrect: false,
  105. autoinfo_multi_meaning: false,
  106. autoinfo_slightly_off: false,
  107. show_retype_button: true,
  108. show_change_button: true
  109. }
  110. return wkof.Settings.load('doublecheck', defaults)
  111. .then(init_ui);
  112. }
  113.  
  114. //------------------------------------------------------------------------
  115. // open_settings() - Open the Settings dialog.
  116. //------------------------------------------------------------------------
  117. function open_settings() {
  118. let dialog = new wkof.Settings({
  119. script_id: 'doublecheck',
  120. title: 'Double-Check Settings',
  121. on_save: init_ui,
  122. pre_open: settings_preopen,
  123. content: {
  124. tabAnswers: {type:'page',label:'Answers',content:{
  125. grpChangeAnswers: {type:'group',label:'Change Answer',content:{
  126. allow_retyping: {type:'checkbox',label:'Allow retyping answer',default:true,hover_tip:'When enabled, you can retype your answer by pressing Escape or Backspace.',on_change:retype_setting_changed},
  127. 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.',on_change:change_setting_changed},
  128. 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.',on_change:change_setting_changed},
  129. 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.'},
  130. }},
  131. grpAnswerButtons: {type:'group',label:'Button Visibility',content:{
  132. show_retype_button: {type:'checkbox',label:'Show "Retype" button',default:true,hover_tip:'When enabled, the Retype button is visible (when retyping is allowed).'},
  133. show_change_button: {type:'checkbox',label:'Show "Mark Right/Wrong"',default:true,hover_tip:'When enabled, the Mark Right / Mark Wrong button is visible (when changing answer is allowed).'},
  134. }},
  135. }},
  136. tabMistakeDelay: {type:'page',label:'Mistakes',content:{
  137. grpCarelessMistakes: {type:'group',label:'Mistake Handling',content:{
  138. 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.'},
  139. 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.'},
  140. 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.'},
  141. 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 ゅ).'},
  142. 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.'},
  143. 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.'},
  144. }},
  145. grpDelay: {type:'group',label:'Mistake Delay',content:{
  146. 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.'},
  147. 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.'},
  148. 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.'},
  149. 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.'},
  150. }},
  151. }},
  152. tabBurnReviews: {type:'page',label:'Burn Reviews',content:{
  153. grpBurnReviews: {type:'group',label:'Burn Reviews',content:{
  154. 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.'},
  155. 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.'},
  156. }},
  157. }},
  158. tabLightning: {type:'page',label:'Lightning',content:{
  159. grpLightning: {type:'group',label:'Lightning Mode',content:{
  160. show_lightning_button: {type:'checkbox',label:'Show "Lightning Mode" button',default:true,hover_tip:'Show the "Lightning Mode" toggle\nbutton on the review screen.'},
  161. 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.'},
  162. 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)'},
  163. }},
  164. }},
  165. tabAutoInfo: {type:'page',label:'Item Info',content:{
  166. grpAutoInfo: {type:'group',label:'Show Item Info',content:{
  167. autoinfo_correct: {type:'checkbox',label:'After correct answer',default:false,hover_tip:'Automatically show the Item Info after correct answers.', validate:validate_autoinfo_correct},
  168. autoinfo_incorrect: {type:'checkbox',label:'After incorrect answer',default:false,hover_tip:'Automatically show the Item Info after incorrect answers.', validate:validate_autoinfo_incorrect},
  169. 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},
  170. 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},
  171. }},
  172. }},
  173. }
  174. });
  175. dialog.open();
  176. }
  177.  
  178. //------------------------------------------------------------------------
  179. // retype_setting_changed() - Enable/disable "show retype button" based on retype setting.
  180. //------------------------------------------------------------------------
  181. function retype_setting_changed(elem, name, value, item) {
  182. document.querySelector('#doublecheck_show_retype_button').toggleAttribute('disabled', !settings.allow_retyping);
  183. }
  184.  
  185. //------------------------------------------------------------------------
  186. // change_setting_changed() - Enable/disable "show mark right/wrong" based on change setting.
  187. //------------------------------------------------------------------------
  188. function change_setting_changed() {
  189. document.querySelector('#doublecheck_show_change_button').toggleAttribute('disabled', !(settings.allow_change_correct || settings.allow_change_incorrect));
  190. }
  191.  
  192. //------------------------------------------------------------------------
  193. // validate_autoinfo_correct() - Notify user if iteminfo and lightning are both enabled.
  194. //------------------------------------------------------------------------
  195. function validate_autoinfo_correct(enabled) {
  196. if (enabled && settings.lightning_enabled) {
  197. return 'Disable "Lightning Mode"!';
  198. }
  199. }
  200.  
  201. //------------------------------------------------------------------------
  202. // validate_autoinfo_incorrect() - Notify user if iteminfo and lightning are both enabled, and wrong_delay disabled.
  203. //------------------------------------------------------------------------
  204. function validate_autoinfo_incorrect(enabled) {
  205. if (enabled && settings.lightning_enabled && !settings.delay_wrong) {
  206. return 'Disable "Lightning Mode", or<br>enable "Delay when wrong"!';
  207. }
  208. }
  209.  
  210. //------------------------------------------------------------------------
  211. // settings_preopen() - Notify user if iteminfo and lightning are both enabled.
  212. //------------------------------------------------------------------------
  213. function settings_preopen(dialog) {
  214. dialog.dialog({width:525});
  215. dialog.find('#doublecheck_show_retype_button').prop('disabled', !settings.allow_retyping);
  216. dialog.find('#doublecheck_show_change_button').prop('disabled', !(settings.allow_change_incorrect || settings.allow_change_incorrect));
  217. }
  218.  
  219. function insert_icons() {
  220. if (!document.getElementById('wk-icon__lightning')) {
  221. let svg = document.querySelector('svg symbol[id^="wk-icon"]').closest('svg');
  222. svg.insertAdjacentHTML('beforeend','<symbol id="wk-icon__lightning" viewport="0 0 500 500"><path d="M160,12L126,265L272,265L230,488L415,170L270,170L320,12Z"></path></symbol>');
  223. }
  224. }
  225.  
  226. //------------------------------------------------------------------------
  227. // init_ui() - Initialize the user interface.
  228. //------------------------------------------------------------------------
  229. async function init_ui() {
  230. settings = wkof.settings.doublecheck;
  231.  
  232. if (fresh_load) {
  233. fresh_load = false;
  234. await startup();
  235. }
  236.  
  237. // Migrate 'lightning' setting from localStorage.
  238. let lightning = localStorage.getItem('lightning');
  239. if (lightning === 'false' || lightning === 'true') {
  240. localStorage.removeItem('lightning');
  241. settings.lightning_enabled = lightning;
  242. wkof.Settings.save('doublecheck');
  243. }
  244.  
  245. insert_icons();
  246.  
  247. // Initialize the Lightning Mode button.
  248. let lightning_icon = document.querySelector('#lightning-mode');
  249. if (lightning_icon) {
  250. lightning_icon.classList.toggle('doublecheck-active', settings.lightning_enabled);
  251. lightning_icon.hidden = !settings.show_lightning_button;
  252. }
  253.  
  254. let rightwrong_btn = document.querySelector('#option-toggle-rightwrong');
  255. if (rightwrong_btn) rightwrong_btn.classList.toggle('hidden', !((settings.allow_change_correct || settings.allow_change_incorrect) && settings.show_change_button));
  256. let retype_btn = document.querySelector('#option-retype');
  257. if (retype_btn) retype_btn.classList.toggle('hidden', !(settings.allow_retyping && settings.show_retype_button));
  258. resize_buttons();
  259.  
  260. additional_content = get_controller('additional-content');
  261. if (state === 'second_submit') {
  262. if (rightwrong_btn) {
  263. rightwrong_btn.querySelector('a').classList.toggle(additional_content.toggleDisabledClass, !(
  264. (new_answer_check.passed && (settings.allow_change_incorrect || !first_answer_check.passed)) ||
  265. (!new_answer_check.passed && (settings.allow_change_correct || first_answer_check.passed))
  266. ));
  267. }
  268. if (retype_btn) {
  269. retype_btn.querySelector('a').classList.toggle(additional_content.toggleDisabledClass, !settings.allow_retyping);
  270. }
  271. } else {
  272. if (rightwrong_btn) {
  273. rightwrong_btn.querySelector('a').classList.add(additional_content.toggleDisabledClass);
  274. }
  275. }
  276. }
  277.  
  278. //------------------------------------------------------------------------
  279. // lightning_clicked() - Lightning button handler.
  280. //------------------------------------------------------------------------
  281. function lightning_clicked(e) {
  282. e.preventDefault();
  283. settings.lightning_enabled = !settings.lightning_enabled;
  284. wkof.Settings.save('doublecheck');
  285. document.querySelector('#lightning-mode').classList.toggle('doublecheck-active', settings.lightning_enabled);
  286. return false;
  287. }
  288.  
  289. //------------------------------------------------------------------------
  290. // get_correct_answers() - Returns an array of acceptable answers.
  291. //------------------------------------------------------------------------
  292. function get_correct_answers() {
  293. if (qtype === 'reading') {
  294. if (subject.type === 'Kanji') {
  295. return subject.readings.filter((r) => r.type == subject.primary_reading_type).map((r) => r.text);
  296. } else {
  297. return [].concat(
  298. subject.readings.map((r) => r.text),
  299. ).filter((r) => typeof r === 'string');
  300. }
  301. } else {
  302. return [].concat(
  303. synonyms,
  304. subject.meanings.map((m) => m.text),
  305. ).filter((m) => typeof m === 'string');
  306. }
  307. }
  308.  
  309. //------------------------------------------------------------------------
  310. // get_next_correct_answer() - Returns the next acceptable answer from the
  311. // array returned by get_correct_answers().
  312. //------------------------------------------------------------------------
  313. function get_next_correct_answer() {
  314. let result = first_answer_check.correct_answers[first_answer_check.correct_answer_index];
  315. first_answer_check.correct_answer_index = (first_answer_check.correct_answer_index + 1) % first_answer_check.correct_answers.length;
  316. return result;
  317. }
  318.  
  319. //------------------------------------------------------------------------
  320. // toggle_result() - Toggle an answer from right->wrong or wrong->right.
  321. //------------------------------------------------------------------------
  322. function toggle_result(new_state) {
  323. if (new_state === 'toggle') new_state = (new_answer_check.passed ? 'incorrect' : 'correct');
  324. if (state !== 'second_submit') return false;
  325.  
  326. let input = quiz_input.inputTarget;
  327. let current_state = (quiz_input.inputContainerTarget.getAttribute('correct') === 'true' ? 'correct' : 'incorrect');
  328. let answer_to_show, answer_to_grade;
  329. clear_delay();
  330. switch (new_state) {
  331. case 'correct':
  332. if (!settings.allow_change_correct) {
  333. if (!first_answer_check.passed) return;
  334. answer_to_grade = first_answer_check.answer;
  335. answer_to_show = answer_to_grade;
  336. } else if (current_state === 'correct') {
  337. answer_to_grade = get_next_correct_answer();
  338. answer_to_show = answer_to_grade;
  339. } else {
  340. first_answer_check.correct_answer_index = 0;
  341. answer_to_grade = get_next_correct_answer();
  342. answer_to_show = (settings.show_corrected_answer ? answer_to_grade : first_answer_check.answer);
  343. }
  344. input.value = answer_to_grade;
  345. new_answer_check = {
  346. action:'pass',
  347. message:null,
  348. passed:true,
  349. accurate:true,
  350. multipleAnswers:false,
  351. exception:false,
  352. answer:answer_to_grade
  353. };
  354. set_answer_state(new_answer_check);
  355. input.value = answer_to_show;
  356. break;
  357. case 'incorrect':
  358. if (!settings.allow_change_incorrect) {
  359. if (first_answer_check.passed) return;
  360. answer_to_show = first_answer_check.answer;
  361. } else {
  362. answer_to_show = (settings.show_corrected_answer ? 'xxxxxx' : first_answer_check.answer);
  363. }
  364. answer_to_grade = 'xxxxxx';
  365. input.value = answer_to_grade;
  366. new_answer_check = {
  367. action:'fail',
  368. message:{
  369. type:'itemInfoException',
  370. text:`Need help? View the correct ${qtype} and mnemonic.`
  371. },
  372. passed:false,
  373. accurate:false,
  374. multipleAnswers:false,
  375. exception:false,
  376. answer:answer_to_grade
  377. };
  378. set_answer_state(new_answer_check);
  379. input.value = answer_to_show;
  380. break;
  381. case 'retype':
  382. if (!settings.allow_retyping) return false;
  383. set_answer_state({reset:true, retype:true, unanswer:true});
  384. break;
  385. }
  386. }
  387.  
  388. //------------------------------------------------------------------------
  389. // do_delay() - Disable the submit button briefly to prevent clicking past wrong answers.
  390. //------------------------------------------------------------------------
  391. function do_delay(period) {
  392. if (period === undefined) period = settings.delay_period;
  393. ignore_submit = true;
  394. delay_timer = setTimeout(function() {
  395. delay_timer = -1;
  396. ignore_submit = false;
  397. }, period*1000);
  398. }
  399.  
  400. //------------------------------------------------------------------------
  401. // clear_delay() - Clear the delay timer.
  402. //------------------------------------------------------------------------
  403. function clear_delay() {
  404. if (delay_timer) {
  405. ignore_submit = false;
  406. clearTimeout(delay_timer);
  407. delay_timer = undefined;
  408. }
  409. }
  410.  
  411. //------------------------------------------------------------------------
  412. function show_exception(message) {
  413. if (typeof message !== 'string') return;
  414. quiz_input.exceptionTarget.textContent = message;
  415. quiz_input.exceptionContainerTarget.hidden = false;
  416. }
  417.  
  418. //------------------------------------------------------------------------
  419. function hide_exception() {
  420. quiz_input.exceptionContainerTarget.hidden = true;
  421. quiz_input.exceptionTarget.textContent = '';
  422. }
  423.  
  424. //------------------------------------------------------------------------
  425. function set_answer_state(results, final_submit) {
  426. quiz_stats = get_controller('quiz-statistics');
  427. quiz_queue = get_controller('quiz-queue');
  428. additional_content = get_controller('additional-content');
  429. item_info = get_controller('item-info');
  430. quiz_progress = get_controller('quiz-progress');
  431. quiz_audio = get_controller('quiz-audio');
  432. quiz_header = get_controller('quiz-header');
  433. if (!final_submit) {
  434. if (results.exception) {
  435. quiz_input.shakeForm();
  436. show_exception(answer_check.exception);
  437. quiz_input.inputEnabled = true;
  438. quiz_input.inputTarget.focus();
  439. return;
  440. }
  441. let rightwrong = document.querySelector('#option-toggle-rightwrong a');
  442. let rightwrong_text = rightwrong.querySelector('.additional-content__item-text');
  443. let rightwrong_icon = rightwrong.querySelector('svg');
  444. let retype = document.querySelector('#option-retype a');
  445. if (!results.passed || (results.reset === true)) {
  446. rightwrong.classList.toggle(additional_content.toggleDisabledClass, (results.reset === true) || !(settings.allow_change_correct || first_answer_check.passed));
  447. rightwrong_text.innerText = 'Mark Right';
  448. rightwrong_icon.classList.remove('dblchk--invert');
  449. } else {
  450. rightwrong.classList.toggle(additional_content.toggleDisabledClass, (results.reset === true) || !(settings.allow_change_incorrect || !first_answer_check.passed));
  451. rightwrong_text.innerText = 'Mark Wrong';
  452. rightwrong_icon.classList.add('dblchk--invert');
  453. }
  454. retype.classList.toggle(additional_content.toggleDisabledClass, (results.reset === true));
  455.  
  456. if (results.reset) {
  457. additional_content.close();
  458. item_info.disable();
  459. quiz_audio.playButtonTarget.classList.add(quiz_audio.disabledClass)
  460. quiz_input.inputContainerTarget.removeAttribute('correct');
  461. quiz_input.inputTarget.value = '';
  462. quiz_input.inputChars = '';
  463. if (results.unanswer) window.dispatchEvent(new CustomEvent('didUnanswerQuestion'));
  464. quiz_input.inputEnabled = true;
  465. quiz_input.inputTarget.focus();
  466.  
  467. quiz_stats.completeCountTarget.innerText = session_stats.complete.toString();
  468. quiz_stats.remainingCountTarget.innerText = session_stats.remaining.toString();
  469. let percent_complete = Math.round(100*session_stats.complete/(session_stats.complete + session_stats.remaining));
  470. quiz_progress.updateProgress({detail:{percentComplete:percent_complete}});
  471. quiz_stats.percentCorrectTarget.innerText = (session_stats.answered ? Math.round(100 * session_stats.correct / session_stats.answered).toString() + '%' : '100%');
  472. if (quiz_header.hasSrsContainerTarget) quiz_header.srsContainerTarget.dataset.hidden = true;
  473. state = 'first_submit';
  474. return;
  475. }
  476. quiz_input.inputEnabled = false;
  477. quiz_input.inputContainerTarget.setAttribute('correct', results.passed);
  478. }
  479.  
  480. subject_stats = JSON.parse(subject_stats_cache.get(subject.id) || JSON.stringify({
  481. meaning:{
  482. incorrect:0,
  483. complete:false
  484. },
  485. reading:{
  486. incorrect:0,
  487. complete:(['Radical','KanaVocabulary'].indexOf(quiz_input.currentSubject.type) >= 0)
  488. }
  489. }));
  490. if (results.passed) {
  491. subject_stats[quiz_input.currentQuestionType].complete = true;
  492. } else {
  493. subject_stats[quiz_input.currentQuestionType].incorrect++;
  494. }
  495. if (final_submit) {
  496. subject_stats_cache.set(subject.id, JSON.stringify(subject_stats));
  497. }
  498.  
  499. if (session_stats.remaining == null) {
  500. session_stats = {
  501. complete: 0,
  502. remaining: Number(quiz_stats.remainingCountTarget.innerText),
  503. correct: 0,
  504. answered: 0
  505. }
  506. }
  507. let temp_session_stats = Object.assign({}, session_stats);
  508. temp_session_stats.answered++;
  509. if (results.passed) temp_session_stats.correct++;
  510. if (subject_stats.meaning.complete && subject_stats.reading.complete) {
  511. temp_session_stats.complete++;
  512. temp_session_stats.remaining--;
  513. }
  514. end_of_session_delay = false;
  515. if (final_submit) {
  516. Object.assign(session_stats, temp_session_stats);
  517. if (session_stats.remaining === 0) end_of_session_delay = true;
  518. } else {
  519. quiz_stats.completeCountTarget.innerText = temp_session_stats.complete.toString();
  520. quiz_stats.remainingCountTarget.innerText = temp_session_stats.remaining.toString();
  521. let percent_complete = Math.round(100*temp_session_stats.complete/(temp_session_stats.complete + temp_session_stats.remaining));
  522. quiz_progress.updateProgress({detail:{percentComplete:percent_complete}});
  523. quiz_stats.percentCorrectTarget.innerText = Math.round(100 * temp_session_stats.correct / temp_session_stats.answered).toString() + '%';
  524.  
  525. quiz_stats.disconnect();
  526. let event = {detail:{
  527. subjectWithStats:{subject:subject,stats:subject_stats},
  528. questionType:quiz_input.currentQuestionType,
  529. answer:quiz_input.inputTarget.value,
  530. results:results
  531. }};
  532. window.dispatchEvent(new CustomEvent('didAnswerQuestion',event));
  533. quiz_stats.connect();
  534.  
  535. if (subject_stats.meaning.complete && subject_stats.reading.complete) {
  536. if (srs_mgr && !(settings.lightning_enabled && answer_check.passed)) {
  537. srs_mgr.updateSRS({subject:subject,stats:subject_stats});
  538. }
  539. } else {
  540. if (quiz_header.hasSrsContainerTarget) quiz_header.srsContainerTarget.dataset.hidden = true;
  541. }
  542.  
  543. if ((results.passed && settings.autoinfo_correct && !settings.lightning_enabled) ||
  544. (!results.passed && settings.autoinfo_incorrect) ||
  545. (results.passed && results.multipleAnswers && settings.autoinfo_multi_meaning && !settings.lightning_enabled) ||
  546. (results.passed && !results.accurate && settings.autoinfo_slightly_off && !settings.lightning_enabled))
  547. {
  548. item_info.toggleTarget.click();
  549. if (results.passed) item_info.showException(qtype,results)
  550. }
  551. }
  552. }
  553.  
  554. //------------------------------------------------------------------------
  555. // new_submit_handler() - Intercept handler for 'submit' button. Overrides default behavior as needed.
  556. //------------------------------------------------------------------------
  557. function new_submit_handler(e) {
  558. // Don't process 'submit' if we are ignoring temporarily (to prevent double-tapping past important info)
  559. if (ignore_submit) return;
  560.  
  561. hide_exception();
  562.  
  563. let input = quiz_input.inputTarget;
  564. qtype = quiz_input.currentQuestionType;
  565. subject = quiz_input.currentSubject;
  566.  
  567. let submitted_immediately = false;
  568. switch (state) {
  569. case 'first_submit': {
  570. // We intercept the first 'submit' click, and simulate normal Wanikani screen behavior.
  571.  
  572. // Do WK's standard checks for shake.
  573. let answer = quiz_input.inputTarget.value.trim();
  574. if (qtype === 'reading') {
  575. answer = response_helpers.normalizeReadingResponse(answer);
  576. input.value = answer;
  577. }
  578. if (!response_helpers.questionTypeAndResponseMatch(qtype, answer) || (answer.length === 0)) {
  579. quiz_input.shakeForm();
  580. quiz_input.inputEnabled = true;
  581. quiz_input.inputTarget.focus();
  582. return;
  583. }
  584.  
  585. quiz_input.inputEnabled = false;
  586. quiz_input.lastAnswer = answer;
  587.  
  588. // Do WK's standard answer evaluation.
  589. synonyms = quiz_input.quizUserSynonymsOutlet.synonymsForSubjectId(subject.id);
  590. answer_check = answer_checker.evaluate({questionType:qtype, response:answer, item:subject, userSynonyms:synonyms, inputChars:quiz_input.inputChars});
  591. if (answer_check.hasOwnProperty('action')) {
  592. if (answer_check.action === 'retry') {
  593. answer_check.passed = false;
  594. answer_check.accurate = false;
  595. answer_check.multipleAnswers = false;
  596. answer_check.exception = answer_check.message.text;
  597. } else {
  598. answer_check.passed = (answer_check.action === 'pass');
  599. if (answer_check.message === null) {
  600. answer_check.accurate = true;
  601. answer_check.multipleAnswers = false;
  602. answer_check.exception = false;
  603. } else if (/has multiple/.test(answer_check.message.text)) {
  604. answer_check.accurate = true;
  605. answer_check.multipleAnswers = true;
  606. answer_check.exception = false;
  607. } else if (/one of your synonyms/.test(answer_check.message.text)) {
  608. answer_check.accurate = false;
  609. answer_check.multipleAnswers = false;
  610. answer_check.exception = answer_check.message.text;
  611. } else if (/a bit off/.test(answer_check.message.text)) {
  612. answer_check.accurate = false;
  613. answer_check.multipleAnswers = false;
  614. answer_check.exception = false;
  615. }
  616. }
  617. }
  618.  
  619. // Process typos according to settings.
  620. if (answer_check.passed && !answer_check.accurate) {
  621. switch (settings.typo_action) {
  622. case 'warn': answer_check.exception = 'Your answer was close, but not exact'; break;
  623. case 'wrong': answer_check.passed = false; answer_check.custom_msg = 'Your answer was not exact, as required by your settings.'; break;
  624. }
  625. }
  626.  
  627. // Process answer-type errors according to settings.
  628. if (!answer_check.passed) {
  629. if (qtype === 'meaning') {
  630. // Although Wanikani checks for readings entered as meanings, it only
  631. // checks the 'preferred' reading. Here, we check all readings.
  632. if (subject.type === 'KanaVocabulary') {
  633. accepted_readings = [subject.characters];
  634. } else {
  635. accepted_readings = [].concat(
  636. subject.readings?.map((r)=>r.reading),
  637. // subject.auxiliary_readings?.filter((r)=>r.type==='whitelist').map((r)=>r.reading),
  638. subject.onyomi,
  639. subject.kunyomi,
  640. subject.nanori
  641. );
  642. }
  643. let answer_as_kana = to_kana(answer);
  644. if (accepted_readings.indexOf(answer_as_kana) >= 0) {
  645. if (settings.wrong_answer_type_action === 'warn') {
  646. answer_check.exception = answer_check.exception || 'Oops, we want the meaning, not the reading.';
  647. } else {
  648. answer_check.exception = false;
  649. }
  650. }
  651. } else {
  652. accepted_meanings = [].concat(
  653. subject.meanings,
  654. // subject.auxiliary_meanings?.filter((r)=>r.type==='whitelist').map((r)=>r.meaning),
  655. synonyms
  656. ).filter((s) => typeof s === 'string').map((s) => s.trim().toLowerCase().replace(/\s\s+/g,' '));
  657. let meanings_as_hiragana = accepted_meanings.map(m => to_kana(m));
  658. let answer_as_hiragana = Array.from(answer.toLowerCase()).map(c => wanakana.toHiragana(c)).join('');
  659. if (meanings_as_hiragana.indexOf(answer_as_hiragana) >= 0) {
  660. if (settings.wrong_answer_type_action === 'warn') {
  661. answer_check.exception = 'Oops, we want the reading, not the meaning.';
  662. } else {
  663. answer_check.exception = false;
  664. }
  665. }
  666. }
  667. }
  668.  
  669. // Process all other exceptions according to settings.
  670. if (typeof answer_check.exception === 'string') {
  671. if (((settings.kanji_meaning_for_vocab_action === 'wrong') && answer_check.exception.toLowerCase().includes('want the vocabulary meaning, not the kanji meaning')) ||
  672. ((settings.kanji_reading_for_vocab_action === 'wrong') && answer_check.exception.toLowerCase().includes('want the vocabulary reading, not the kanji reading')) ||
  673. ((settings.wrong_number_n_action === 'wrong') && answer_check.exception.toLowerCase().includes('forget that ん')) ||
  674. ((settings.small_kana_action === 'wrong') && answer_check.exception.toLowerCase().includes('watch out for the small')))
  675. {
  676. answer_check.exception = false;
  677. answer_check.passed = false;
  678. }
  679. }
  680.  
  681. // Remain in 'first_submit' if there was an exceptions.
  682. if (answer_check.exception) {
  683. set_answer_state(answer_check);
  684. return false;
  685. }
  686. state = 'second_submit';
  687.  
  688. new_answer_check = Object.assign({answer:answer}, answer_check);
  689. first_answer_check = Object.assign({
  690. answer:answer,
  691. correct_answers:get_correct_answers(),
  692. correct_answer_index: 0,
  693. }, answer_check);
  694.  
  695. // Process "Mistake Delay" according to settings.
  696. if ((!answer_check.passed && settings.delay_wrong) ||
  697. (answer_check.passed &&
  698. ((!answer_check.accurate && settings.delay_slightly_off) ||
  699. (answer_check.multipleAnswers && settings.delay_multi_meaning))
  700. )
  701. )
  702. {
  703. set_answer_state(new_answer_check);
  704. do_delay();
  705. return false;
  706. }
  707.  
  708. set_answer_state(answer_check);
  709.  
  710. // Process lightning mode according to settings.
  711. if (settings.lightning_enabled && answer_check.passed) {
  712. new_submit_handler(e);
  713. return false;
  714. }
  715.  
  716. return false;
  717. }
  718. case 'second_submit': {
  719. // We intercepted the first submit, allowing the user to optionally modify their answer.
  720. // Now, either the user has clicked submit again, or lightning is enabled and we are automatically clicking submit again.
  721.  
  722. let answer = new_answer_check.answer;
  723. input.value = answer;
  724. set_answer_state(new_answer_check, true /* final_submit */);
  725. delete new_answer_check.answer;
  726.  
  727. // Nasty hack to prevent audio from playing twice or stopping upon next question.
  728. let audio = quiz_audio.audioTarget;
  729. audio.setAttribute('data-quiz-audio-target', 'noplay');
  730. audio.insertAdjacentHTML('afterend', '<audio class="quiz-audio__audio dblchk" data-quiz-audio-target="audio"></audio>');
  731. let tmp_audio = document.querySelector('audio.dblchk');
  732. quiz_audio.disconnect();
  733.  
  734. function dispatch_didFinalAnswer(e) {
  735. window.dispatchEvent(new CustomEvent('didFinalAnswer',{detail:e.detail}));
  736. window.removeEventListener('didAnswerQuestion', dispatch_didFinalAnswer);
  737. }
  738. window.addEventListener('didAnswerQuestion', dispatch_didFinalAnswer);
  739. quiz_queue.submitAnswer(answer, new_answer_check);
  740.  
  741. // Nasty audio hack, continued.
  742. setTimeout(() => {
  743. tmp_audio.remove();
  744. audio.setAttribute('data-quiz-audio-target', 'audio');
  745. quiz_audio.connect();
  746. }, 1);
  747.  
  748. if (end_of_session_delay) {
  749. setTimeout(next_item, 500);
  750. } else {
  751. next_item();
  752. }
  753.  
  754. function next_item() {
  755. quiz_queue.nextItem();
  756. set_answer_state({reset:true, unanswer:false});
  757.  
  758. quiz_header = get_controller('quiz-header');
  759. if (quiz_header.hasSrsContainerTarget && settings.lightning_enabled && new_answer_check.passed &&
  760. subject_stats.meaning.complete && subject_stats.reading.complete && srs_mgr) {
  761. setTimeout(() => {
  762. srs_mgr.updateSRS({subject:subject,stats:subject_stats});
  763. setTimeout(()=>{
  764. quiz_header.srsContainerTarget.dataset.hidden = true;
  765. }, 1000 * settings.srs_msg_period);
  766. }, 1);
  767. }
  768.  
  769. state = 'first_submit';
  770. }
  771. return false;
  772. }
  773. default:
  774. return false;
  775. }
  776.  
  777. return false;
  778. }
  779.  
  780. //------------------------------------------------------------------------
  781. // Simulate input character by character and convert with WanaKana to kana
  782. // -- Contributed by user @Sinyaven
  783. //------------------------------------------------------------------------
  784. function to_kana(text) {
  785. return Array.from(text).reduce((total, c) => wanakana.toKana(total + c, {IMEMode: true}), "").replace(/n$/, String.fromCharCode(12435));
  786. }
  787.  
  788. //------------------------------------------------------------------------
  789. // Resize the buttons according to how many are visible.
  790. //------------------------------------------------------------------------
  791. function resize_buttons() {
  792. let buttons = Array.from(document.querySelectorAll('#additional-content .additional-content__menu-item'));
  793. let visible_buttons = buttons.filter((elem)=>!elem.matches('.hidden,[hidden]'));
  794. let btn_count = visible_buttons.length;
  795. for (let btn of visible_buttons) {
  796. let percent = Math.floor(10000/btn_count)/100 + '%';
  797. btn.style.width = `calc(${percent} - 10px)`;
  798. btn.style.flex = `0 0 calc(${percent} - 10px)`;
  799. btn.style.marginRight = '10px';
  800. }
  801. visible_buttons.slice(-1)[0].style.marginRight = '0px';
  802. }
  803.  
  804. //------------------------------------------------------------------------
  805. // External hook for @polv's script, "WaniKani Disable Default Answers"
  806. //------------------------------------------------------------------------
  807. gobj.set_state = function(_state) {
  808. state = _state;
  809. };
  810.  
  811. function get_controller(name) {
  812. return Stimulus.getControllerForElementAndIdentifier(document.querySelector(`[data-controller~="${name}"]`),name);
  813. }
  814.  
  815. //------------------------------------------------------------------------
  816. // startup() - Install our intercept handlers, and add our Double-Check button and hotkey
  817. //------------------------------------------------------------------------
  818. async function startup() {
  819. // Intercept the submit button handler.
  820. let p = promise();
  821. quiz_input = undefined;
  822. quiz_queue = undefined;
  823. additional_content = undefined;
  824. item_info = undefined;
  825. quiz_audio = undefined;
  826. quiz_stats = undefined;
  827. quiz_progress = undefined;
  828. quiz_header = undefined;
  829. answer_checker = undefined;
  830.  
  831. async function get_controllers() {
  832. try {
  833. // Check if all of our hooks into WK are valid, just in case something changed.
  834. if (!quiz_input) {
  835. quiz_input = get_controller('quiz-input');
  836. if (!quiz_input) throw 'Controller "quiz-input" not found.';
  837. }
  838. if (!quiz_queue) {
  839. quiz_queue = get_controller('quiz-queue');
  840. if (!quiz_queue) throw 'Controller "quiz-queue" not found.';
  841. }
  842. if (!additional_content) {
  843. additional_content = get_controller('additional-content');
  844. if (!additional_content) throw 'Controller "additional-content" not found.';
  845. }
  846. if (!item_info) {
  847. item_info = get_controller('item-info');
  848. if (!item_info) throw 'Controller "item-info" not found.';
  849. }
  850. if (!quiz_audio) {
  851. quiz_audio = get_controller('quiz-audio');
  852. if (!quiz_audio) throw 'Controller "quiz-audio" not found.';
  853. }
  854. if (!quiz_stats) {
  855. quiz_stats = get_controller('quiz-statistics');
  856. if (!quiz_stats) throw 'Controller "quiz-statistics" not found.';
  857. }
  858. if (!quiz_progress) {
  859. quiz_progress = get_controller('quiz-progress');
  860. if (!quiz_progress) throw 'Controller "quiz-progress" not found.';
  861. }
  862. if (!quiz_header) {
  863. quiz_header = get_controller('quiz-header');
  864. if (!quiz_header) throw 'Controller "quiz-header" not found.';
  865. }
  866. if (!response_helpers) {
  867. response_helpers = await importShim('lib/answer_checker/utils/response_helpers');
  868. if (!response_helpers) throw 'Import "lib/answer_checker/utils/response_helpers" failed.';
  869. }
  870. if (!wanakana) {
  871. wanakana = await importShim('wanakana');
  872. if (!wanakana) throw 'Import "wanakana" failed.';
  873. }
  874. if (!answer_checker) answer_checker = Stimulus.controllers.find((c)=>c.answerChecker)?.answerChecker;
  875. if (!answer_checker) {
  876. let AnswerChecker = (await importShim('lib/answer_checker/answer_checker')).default;
  877. if (!AnswerChecker) throw 'Import "lib/answer_checker/answer_checker" failed.';
  878. answer_checker = new AnswerChecker;
  879. }
  880. if (quiz_queue.hasSubjectIdsWithSRSTarget) {
  881. srs_mgr = quiz_queue.quizQueue.srsManager;
  882. } else {
  883. srs_mgr = undefined;
  884. }
  885.  
  886. if (quiz_input.submitAnswer !== new_submit_handler) {
  887. old_submit_handler = quiz_input.submitAnswer;
  888. quiz_input.submitAnswer = new_submit_handler;
  889. }
  890.  
  891. p.resolve();
  892. } catch(err) {
  893. console.log('Double-Check:', err, ' Retrying...');
  894. setTimeout(get_controllers, 250);
  895. }
  896. return p;
  897. }
  898.  
  899. await get_controllers();
  900.  
  901. subject_stats_cache = new Map();
  902. session_stats = {};
  903. state = 'first_submit';
  904. ignore_submit = false;
  905.  
  906. // Install the Lightning Mode button.
  907. let scripts_menu = document.getElementById('scripts-menu');
  908.  
  909. // Insert CSS
  910. document.head.insertAdjacentHTML('beforeend',
  911. `<style name="doublecheck">
  912. #lightning-mode.doublecheck-active svg {fill:#ff0; opacity:1.0;}
  913. .wk-icon--thumbs-up.dblchk--invert {transform:scaleY(-1);}
  914. </style>`
  915. );
  916.  
  917. // Insert lightning button
  918. scripts_menu.insertAdjacentHTML('afterend',
  919. `<div id="lightning-mode" class="character-header__menu-navigation-link" hidden>
  920. <a class="lightning-mode summary-button" href="#" title="Lightning Mode - When enabled, auto-\nadvance after answering correctly.">
  921. <svg class="wk-icon wk-icon--lightning" title="Mark Right" viewBox="0 0 500 500" aria-hidden="true">
  922. <use href="#wk-icon__lightning"></use>
  923. </svg>
  924. </a>
  925. </div>`
  926. );
  927. document.querySelector('.lightning-mode').addEventListener('click', lightning_clicked);
  928.  
  929. // Install the Double-Check features.
  930. document.querySelector('#additional-content ul').style.textAlign = 'center';
  931. document.querySelector('#additional-content ul').insertAdjacentHTML('beforeend',
  932. `<li id="option-toggle-rightwrong" class="additional-content__menu-item additional-content__menu-item--5">
  933. <a title="Mark Right" class="additional-content__item ${additional_content.toggleDisabledClass}">
  934. <div class="additional-content__item-text">Mark Right</div>
  935. <div class="additional-content__item-icon-container">
  936. <svg class="wk-icon wk-icon--thumbs-up" title="Mark Right" viewBox="0 0 512 512" aria-hidden="true">
  937. <use href="#wk-icon__thumbs-up"></use>
  938. </svg>
  939. </div>
  940. </a>
  941. </li>
  942. <li id="option-retype" class="additional-content__menu-item additional-content__menu-item--5">
  943. <a title="Retype" class="additional-content__item ${additional_content.toggleDisabledClass}">
  944. <div class="additional-content__item-text">Re-type</div>
  945. <div class="additional-content__item-icon-container">
  946. <svg class="wk-icon wk-icon--reload" title="Re-type Answer" viewBox="0 0 512 512" aria-hidden="true">
  947. <use href="#wk-icon__reload"></use>
  948. </svg>
  949. </div>
  950. </a>
  951. </li>`
  952. );
  953. document.querySelector('#option-toggle-rightwrong').addEventListener('click', toggle_result.bind(null,'toggle'));
  954. document.querySelector('#option-retype').addEventListener('click', toggle_result.bind(null,'retype'));
  955. let input = quiz_input.inputTarget;
  956. document.body.addEventListener('keypress', handle_rightwrong_hotkey);
  957. function handle_rightwrong_hotkey(event){
  958. if (state !== 'first_submit') {
  959. if (!document.querySelector('#wkofs_doublecheck') && (event.target === input || event.target === document.body)) {
  960. if (event.which === 43) {
  961. toggle_result('correct');
  962. event.preventDefault();
  963. event.stopPropagation();
  964. }
  965. if (event.which === 45) {
  966. toggle_result('incorrect');
  967. event.preventDefault();
  968. event.stopPropagation();
  969. }
  970. }
  971. }
  972. };
  973. document.body.addEventListener('keydown', handle_retype_hotkey);
  974. function handle_retype_hotkey(event){
  975. if (state !== 'first_submit') {
  976. if (!document.querySelector('#wkofs_doublecheck') && (event.target === input || event.target === document.body)) {
  977. if ((event.which === 27 || event.which === 8)) {
  978. toggle_result('retype');
  979. event.preventDefault();
  980. event.stopPropagation();
  981. } else if (event.ctrlKey && event.key === 'l') {
  982. event.preventDefault();
  983. event.stopPropagation();
  984. lightning_clicked();
  985. }
  986. }
  987. }
  988. };
  989.  
  990. document.head.insertAdjacentHTML('beforeend',
  991. `<style>
  992. #additional-content>ul>li.hidden {display:none;}
  993. #answer-form fieldset.confburn button, #answer-form fieldset.confburn input[type=text], #answer-form fieldset.confburn input[type=text]:disabled {
  994. background-color: #000 !important;
  995. color: #fff;
  996. text-shadow: 2px 2px 0 rgba(0,0,0,0.2);
  997. transition: background-color 0.1s ease-in;
  998. opacity: 1 !important;
  999. }
  1000. </style>`
  1001. );
  1002. }
  1003.  
  1004. })(window.doublecheck);