Wanikani Double-Check

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

目前为 2024-03-13 提交的版本。查看 最新版本

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

QingJ © 2025

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