[Userscript]: Double-Check (Version 2.x)

Just ctrl+r should work. Or ctrl+f5. No need for the shift. (The shift makes it also clear cached files, which is not necessary in this case and just makes the reload take longer)

1 Like

Although I donā€™t know if this applies to the Reviews page, Iā€™ve seen Turbo capture refreshes before and do only a pseudo-refresh. So, if the hotkeys donā€™t solve the issue, make sure you try the physical refresh button in the browser.

4 Likes

Works great with the refresh. What a relief to have Double-Check working. Thank you for the awesome script!

2 Likes

Is there a way to make the workaround work on Kiwi Browser on Android? Unless I did something wrong, it isnā€™t workingā€¦

The reload trick had been working fine for me, but today, it randomly stopped working. It may be another script update interfering - I deactivated everything except for WaniKani Open Framework, Double-Check, and a few scripts I use for other sites (Kanji Highlighter 2, NHK Easy Practice), and that got it working again. Unfortunately, I donā€™t have time to check against all other scripts right now, but it could be one of the recently updated ones.

Hereā€™s a fix thatā€™s been working for me (including getting things to work with turbo loads).

Essentially, the problem is CSS inserted by the Menu module[1], but since thatā€™s imported[2], we have to work around that by including this CSS first.

It doesnā€™t need to be necessarily this script that does this, but it needs to happen before any other script runs wkof.Menu.insert_script_link(), so I just put it in this one, to be called once on script load and again on any turbo load (to inject the script menu on the dashboard).

The functions are as follows:

function insertCss() {
    if (!document.getElementById('wkof_scripts_submenu')) {
        document.head.insertAdjacentHTML('beforeend',
            `<style id="wkof_scripts_submenu" name="scripts_submenu">
            #sitemap #scripts-menu.sitemap__section.scripts-noposition {position:initial;}
            #sitemap #scripts-menu .scripts-submenu>.dropdown-menu {display:none;}
            #sitemap #scripts-menu .scripts-submenu.open>.dropdown-menu {display:block;position:absolute;top:0px;margin-top:0;left:-8px;transform:scale(1) translateX(-100%);min-width:200px}
            #sitemap #scripts-menu .scripts-submenu .dropdown-menu:before {left:100%;top:12px;z-index:-1;}
            #sitemap #scripts-menu .scripts-submenu .dropdown-menu .sitemap__pages {padding:5px 15px 0px 15px;}
            #sitemap #scripts-menu .scripts-submenu .dropdown-menu .sitemap__page:last-child {margin-bottom:0;}
            #sitemap #scripts-menu .scripts-submenu>a:before {content:"< ";}
            @media (max-width: 979px) {
              #sitemap #scripts-menu .scripts-submenu>a:before {content:"";}
              #sitemap #scripts-menu .scripts-submenu>.dropdown-menu {display:contents;position:initial;top:initial;margin-top:initial;left:initial;transform:none;min-width:initial}
            }

            .character-header {z-index:1;}
            .character-header .character-header__menu-navigation a {text-decoration:none;}
            .character-header .character-header__menu-navigation-link {margin-right: 8px;}
            .character-header #scripts-menu {text-shadow:none;}
            .character-header #scripts-menu:not(.open) > .dropdown-menu {display:none;}
            .character-header #scripts-menu .scripts-submenu:not(.open) > .dropdown-menu {display:none;}
            .character-header #scripts-menu ul.dropdown-menu {position:absolute; background-color:#eee; margin:0; padding:5px 0; list-style-type:none; border:1px solid #333; display:block;}

            .character-header #scripts-menu ul.dropdown-menu > li {text-align:left; color:#333; white-space:nowrap; line-height:20px; padding:3px 0; display:list-item; margin:0;}
            .character-header #scripts-menu ul.dropdown-menu > li.scripts-header {text-transform:uppercase; font-size:11px; font-weight:bold; padding:3px 20px; display:list-item;}
            .character-header #scripts-menu ul.dropdown-menu > li:hover:not(.scripts-header) {background-color:rgba(0,0,0,0.15)}

            .character-header #scripts-menu ul.dropdown-menu a {padding:3px 20px; color:#333; opacity:1; margin:0; border:0; display:inline;}

            .character-header #scripts-menu .scripts-submenu {position:relative;}
            .character-header #scripts-menu .scripts-submenu > a:after {content:">"; position:absolute; top:0; right:0; padding:3px 4px 3px 0;}
            .character-header #scripts-menu .scripts-submenu .dropdown-menu {left:100%; top:-7px;}
            </style>`
        );
    }
}
function addDashboardScriptsMenu() {
    document.querySelector('.user-summary')?.insertAdjacentHTML('afterend',
        `<li id="scripts-menu" class="sitemap__section sitemap__section--subsection scripts-noposition">
          <h3 class="sitemap__section-header--subsection">Scripts</h3>
          <ul class="sitemap__pages scripts-header"></ul>
         </li>`
    );
}

Which I then have being called at the begin of setup(). I.e.,

function setup() {
    addDashboardScriptsMenu();

and before the if (is_turbo_page()) around line 41. I.e.,

    insertCss();
    if (is_turbo_page()) {

Edit: I had to change my .quiz selector to .character-header (as appropriate) in order for it to work properly on lessons pages as well.


  1. And what is inserted is based on the current page, so Iā€™ve included all of it and updated the selectors to only work on the right pages. ā†©ļøŽ

  2. And so we canā€™t simply make a local override to that script. ā†©ļøŽ

1 Like

Alternatively, if any of you donā€™t want to mess around with Javascript just for CSS changes, you can just make a simple Userstyle with Stylus containing the CSS.

Edit for clarity: inserioā€™s post has more than just CSS, Iā€™m commenting if you know that what you want only involves the CSS.


Well for me, there more problems to fix such as the submenus opening to the left off screen, so I needed to do more doctoring to it.

If youā€™re already using Breeze Dark 2, here are the edits Iā€™ve made to that in the scripts section (CTRL+F for the commented heading /* Scripts Menu */ to save time hunting)

    #scripts-menu ul.dropdown-menu {
        background-color: var(--color-menu);
        position: absolute;
        padding: 5px;
        border-radius: 6px;
        
        & > li {
            line-height: 20px;
            padding: 3px 0;
            display: list-item;
        }
    }
    
    .quiz #scripts-menu:not(.open) > ul.dropdown-menu {
        display: none;
    }

    #scripts-menu ul.dropdown-menu > li,
    #scripts-menu ul.dropdown-menu > li a {
        color: var(--color-text);
    }
    
    #scripts-menu ul.dropdown-menu > li.scripts-header {
        text-transform: uppercase;
        font-size: 11px;
        font-weight: bold;
        padding: 3px 20px;
        display: list-item;
    }

    .quiz #scripts-menu ul.dropdown-menu > li:hover:not(.scripts-header) a:hover {
        background-color: transparent;
        filter: brightness(0.8);
    }
    
    .quiz #scripts-menu ul.dropdown-menu a {
        padding: 3px 20px;
        opacity: 1;
        border: 0;
        display: inline;
        text-decoration: none;
    }
    
    .quiz #scripts-menu .scripts-submenu > a::before {
        content: "";
    }
    
    .quiz #scripts-menu .scripts-submenu > a::after {
        content: " >";
    }
    
    .quiz #scripts-menu .scripts-submenu.open > .dropdown-menu {
        display: block;
        position: absolute;
        top: 0px;
        margin-left: 25px;
        transform: scale(1) translateX(50%);
        min-width: 200px;
    }
    
    .character-header__menu-navigation-link:not(:first-of-type) {
        margin-left: 0.5rem;
    }

Edit as you see fit.

I would just note that the modification of the selectors is a big part of it. Essentially I added a .quiz selector to all of the ones that should run on the reviews pages and a #sitemap selector to all of the ones that run on the dashboard page.

1 Like

Thanks, I wasnā€™t having any issues with the Dashboard ones so Iā€™ll be sure to edit that with the Reviews selector.

Fair enough.
What made me notice the overlap was the

#scripts-menu .scripts-submenu > a:after {content:">"; position:absolute; top:0; right:0; padding:3px 4px 3px 0;}

and

.scripts-submenu>a:before {content:"< ";}

selectors in the Menu module. If I included all of the css in the same include and didnā€™t differentiate them, youā€™d see a < to the left and a > to the right of the settings texts.

1 Like

Yeah I accidentally made that happen on the Reviews while I messing around. But I havenā€™t yet seen that happen with the Dashboard #sitemap menus, so I didnā€™t catch that we would need to differentiate them.

Double check wasnā€™t working this morning (I tried implementing the fix mentioned here but it didnā€™t work, probably because Iā€™m computer-dumb), but now itā€™s working again! Whoever fixed it, thank you so much!!


But seriously. You probably fixed it yourself by refreshing your tab or opening a new one or something, lol.

I will add that my fix is still just a patch at best.
It doesnā€™t fix the scenario of going from a review session to the dashboard and then to a new review session.

The full fix for that is more involved, since thereā€™s other logic in the script thatā€™s set to only run once. Though Iā€™ll paste it here for those who are interested.

Click Me
// ==UserScript==
// @name        Wanikani Double-Check
// @namespace   wkdoublecheck
// @description Allows retyping typo'd answers, or marking wrong when WK's typo tolerance is too lax.
// @match       https://www.wanikani.com/*
// @match       https://preview.wanikani.com/*
// @version     3.1.15
// @author      Robin Findley
// @copyright   2017-2024, Robin Findley
// @license     MIT; http://opensource.org/licenses/MIT
// @run-at      document-end
// @grant       none
// @downloadURL https://update.greasyfork.org/scripts/35063/Wanikani%20Double-Check.user.js
// @updateURL https://update.greasyfork.org/scripts/35063/Wanikani%20Double-Check.meta.js
// ==/UserScript==

// HOTKEYS:
//   "+"      - Marks answer as 'correct'.
//   "-"      - Marks answer as 'incorrect'.
//   "Escape" or "Backspace" - Resets question, allowing you to retype.

// SEE SETTINGS BELOW.

window.doublecheck = {};

(function(gobj) {

    /* global wkof, Stimulus, WaniKani, importShim, $, app_load, before_page_render, before_frame_render */

    let script_name = 'Double-Check';
    let wkof_version_needed = '1.1.10';

    const match_patterns = [
        '/subjects/extra_study',
        '/subjects/review',
        '/recent-mistakes/*/quiz',
    ];
    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);}
    function is_turbo_page() {return (document.querySelector('script[type="importmap"]')?.innerHTML.match('@hotwired/turbo') != null);}

    insertCss();
    if (is_turbo_page()) {
        try {app_load();} catch(e){}
        try {document.documentElement.addEventListener('turbo:load', page_load);} catch(e){}
        //try {document.documentElement.addEventListener('turbo:before-render', before_page_render);} catch(e){}
        //try {document.documentElement.addEventListener('turbo:frame-load', frame_load);} catch(e){}
        //try {document.documentElement.addEventListener('turbo:before-frame-render', before_frame_render);} catch(e){}
    } else {
        try {app_load();} catch(e){}
        try {page_load({detail:{url:window.location.href},target:document.documentElement});} catch(e){}
        try {frame_load({target:document.documentElement});} catch(e){}
    }

    function page_load(e) { // e = {detail: {url: '...'}, target: <elem> }
        if (!url_matches(undefined, e.detail.url)) return;
        setTimeout(load_script,0);
    }

    function frame_load(e) {
        if (!url_matches()) return;
        setTimeout(load_script,0);
    }

    function insertCss() {
        if (!document.getElementById('wkof_scripts_submenu')) {
            document.head.insertAdjacentHTML('beforeend',
                `<style id="wkof_scripts_submenu" name="scripts_submenu">
                #sitemap #scripts-menu.sitemap__section.scripts-noposition {position:initial;}
                #sitemap #scripts-menu .scripts-submenu>.dropdown-menu {display:none;}
                #sitemap #scripts-menu .scripts-submenu.open>.dropdown-menu {display:block;position:absolute;top:0px;margin-top:0;left:-8px;transform:scale(1) translateX(-100%);min-width:200px}
                #sitemap #scripts-menu .scripts-submenu .dropdown-menu:before {left:100%;top:12px;z-index:-1;}
                #sitemap #scripts-menu .scripts-submenu .dropdown-menu .sitemap__pages {padding:5px 15px 0px 15px;}
                #sitemap #scripts-menu .scripts-submenu .dropdown-menu .sitemap__page:last-child {margin-bottom:0;}
                #sitemap #scripts-menu .scripts-submenu>a:before {content:"< ";}
                @media (max-width: 979px) {
                  #sitemap #scripts-menu .scripts-submenu>a:before {content:"";}
                  #sitemap #scripts-menu .scripts-submenu>.dropdown-menu {display:contents;position:initial;top:initial;margin-top:initial;left:initial;transform:none;min-width:initial}
                }

                .character-header {z-index:1;}
                .character-header .character-header__menu-navigation a {text-decoration:none;}
                .character-header .character-header__menu-navigation-link {margin-right: 8px;}
                .character-header #scripts-menu {text-shadow:none;}
                .character-header #scripts-menu:not(.open) > .dropdown-menu {display:none;}
                .character-header #scripts-menu .scripts-submenu:not(.open) > .dropdown-menu {display:none;}
                .character-header #scripts-menu ul.dropdown-menu {position:absolute; background-color:#eee; margin:0; padding:5px 0; list-style-type:none; border:1px solid #333; display:block;}

                .character-header #scripts-menu ul.dropdown-menu > li {text-align:left; color:#333; white-space:nowrap; line-height:20px; padding:3px 0; display:list-item; margin:0;}
                .character-header #scripts-menu ul.dropdown-menu > li.scripts-header {text-transform:uppercase; font-size:11px; font-weight:bold; padding:3px 20px; display:list-item;}
                .character-header #scripts-menu ul.dropdown-menu > li:hover:not(.scripts-header) {background-color:rgba(0,0,0,0.15)}

                .character-header #scripts-menu ul.dropdown-menu a {padding:3px 20px; color:#333; opacity:1; margin:0; border:0; display:inline;}

                .character-header #scripts-menu .scripts-submenu {position:relative;}
                .character-header #scripts-menu .scripts-submenu > a:after {content:">"; position:absolute; top:0; right:0; padding:3px 4px 3px 0;}
                .character-header #scripts-menu .scripts-submenu .dropdown-menu {left:100%; top:-7px;}
                </style>`
            );
        }
    }
    function addDashboardScriptsMenu() {
        document.querySelector('.user-summary')?.insertAdjacentHTML('afterend',
            `<li id="scripts-menu" class="sitemap__section sitemap__section--subsection scripts-noposition">
              <h3 class="sitemap__section-header--subsection">Scripts</h3>
              <ul class="sitemap__pages scripts-header"></ul>
             </li>`
        );
    }

    let settings;
    let quiz_input, quiz_queue, additional_content, item_info, quiz_audio, quiz_stats, quiz_progress, quiz_header, response_helpers, wanakana;
    let answer_checker, answer_check, subject_stats, subject_stats_cache, session_stats;
    let old_submit_handler, ignore_submit, state, delay_timer, end_of_session_delay;
    let subject, synonyms, accepted_meanings, accepted_readings, srs_mgr;
    let qtype, new_answer_check, first_answer_check;

    function promise(){let a,b,c=new Promise(function(d,e){a=d;b=e;});c.resolve=a;c.reject=b;return c;}

    function load_script() {
        if (!window.wkof) {
            if (confirm(script_name+' requires Wanikani Open Framework.\nDo you want to be forwarded to the installation instructions?')) {
                window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
            }
            return;
        }
        if (wkof.version.compare_to(wkof_version_needed) === 'older') {
            if (confirm(script_name+' requires Wanikani Open Framework version '+wkof_version_needed+'.\nDo you want to be forwarded to the update page?')) {
                window.location.href = 'https://greasyfork.org/en/scripts/38582-wanikani-open-framework';
            }
            return;
        }
        wkof.include('Menu,Settings');
        wkof.ready('Menu,Settings').then(setup);
    }

    //------------------------------------------------------------------------
    // setup() - Set up the menu link and default settings.
    //------------------------------------------------------------------------
    function setup() {
        addDashboardScriptsMenu();
        wkof.Menu.insert_script_link({name:'doublecheck',submenu:'Settings',title:'Double-Check',on_click:open_settings});

        let defaults = {
            allow_retyping: true,
            allow_change_correct: false,
            show_corrected_answer: false,
            allow_change_incorrect: false,
            typo_action: 'ignore',
            wrong_answer_type_action: 'warn',
            wrong_number_n_action: 'warn',
            small_kana_action: 'warn',
            kanji_reading_for_vocab_action: 'warn',
            kanji_meaning_for_vocab_action: 'warn',
            delay_wrong: true,
            delay_multi_meaning: false,
            delay_slightly_off: false,
            delay_period: 1.5,
            warn_burn: 'never',
            burn_delay_period: 1.5,
            show_lightning_button: true,
            lightning_enabled: false,
            srs_msg_period: 1.2,
            autoinfo_correct: false,
            autoinfo_incorrect: false,
            autoinfo_multi_meaning: false,
            autoinfo_slightly_off: false,
            show_retype_button: true,
            show_change_button: true
        };
        return wkof.Settings.load('doublecheck', defaults)
            .then(init_ui.bind(null, true /* first_time */));
    }

    //------------------------------------------------------------------------
    // open_settings() - Open the Settings dialog.
    //------------------------------------------------------------------------
    function open_settings() {
        let dialog = new wkof.Settings({
            script_id: 'doublecheck',
            title: 'Double-Check Settings',
            on_save: init_ui,
            pre_open: settings_preopen,
            content: {
                tabAnswers: {type:'page',label:'Answers',content:{
                    grpChangeAnswers: {type:'group',label:'Change Answer',content:{
                        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},
                        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},
                        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},
                        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.'},
                    }},
                    grpAnswerButtons: {type:'group',label:'Button Visibility',content:{
                        show_retype_button: {type:'checkbox',label:'Show "Retype" button',default:true,hover_tip:'When enabled, the Retype button is visible (when retyping is allowed).'},
                        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).'},
                    }},
                }},
                tabMistakeDelay: {type:'page',label:'Mistakes',content:{
                    grpCarelessMistakes: {type:'group',label:'Mistake Handling',content:{
                        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.'},
                        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.'},
                        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.'},
                        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 悅).'},
                        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.'},
                        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.'},
                    }},
                    grpDelay: {type:'group',label:'Mistake Delay',content:{
                        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.'},
                        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.'},
                        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.'},
                        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.'},
                    }},
                }},
                tabBurnReviews: {type:'page',label:'Burn Reviews',content:{
                    grpBurnReviews: {type:'group',label:'Burn Reviews',content:{
                        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.'},
                        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.'},
                    }},
                }},
                tabLightning: {type:'page',label:'Lightning',content:{
                    grpLightning: {type:'group',label:'Lightning Mode',content:{
                        show_lightning_button: {type:'checkbox',label:'Show "Lightning Mode" button',default:true,hover_tip:'Show the "Lightning Mode" toggle\nbutton on the review screen.'},
                        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.'},
                        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)'},
                    }},
                }},
                tabAutoInfo: {type:'page',label:'Item Info',content:{
                    grpAutoInfo: {type:'group',label:'Show Item Info',content:{
                        autoinfo_correct: {type:'checkbox',label:'After correct answer',default:false,hover_tip:'Automatically show the Item Info after correct answers.', validate:validate_autoinfo_correct},
                        autoinfo_incorrect: {type:'checkbox',label:'After incorrect answer',default:false,hover_tip:'Automatically show the Item Info after incorrect answers.', validate:validate_autoinfo_incorrect},
                        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},
                        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},
                    }},
                }},
            }
        });
        dialog.open();
    }

    //------------------------------------------------------------------------
    // retype_setting_changed() - Enable/disable "show retype button" based on retype setting.
    //------------------------------------------------------------------------
    function retype_setting_changed(elem, name, value, item) {
        document.getElementById('doublecheck_show_retype_button')?.toggleAttribute('disabled', !settings.allow_retyping);
    }

    //------------------------------------------------------------------------
    // change_setting_changed() - Enable/disable "show mark right/wrong" based on change setting.
    //------------------------------------------------------------------------
    function change_setting_changed() {
        document.getElementById('doublecheck_show_change_button')?.toggleAttribute('disabled', !(settings.allow_change_correct || settings.allow_change_incorrect));
    }

    //------------------------------------------------------------------------
    // validate_autoinfo_correct() - Notify user if iteminfo and lightning are both enabled.
    //------------------------------------------------------------------------
    function validate_autoinfo_correct(enabled) {
        if (enabled && settings.lightning_enabled) {
            return 'Disable "Lightning Mode"!';
        }
    }

    //------------------------------------------------------------------------
    // validate_autoinfo_incorrect() - Notify user if iteminfo and lightning are both enabled, and wrong_delay disabled.
    //------------------------------------------------------------------------
    function validate_autoinfo_incorrect(enabled) {
        if (enabled && settings.lightning_enabled && !settings.delay_wrong) {
            return 'Disable "Lightning Mode", or<br>enable "Delay when wrong"!';
        }
    }

    //------------------------------------------------------------------------
    // settings_preopen() - Notify user if iteminfo and lightning are both enabled.
    //------------------------------------------------------------------------
    function settings_preopen(dialog) {
        dialog.dialog({width:525});
        dialog.find('#doublecheck_show_retype_button').prop('disabled', !settings.allow_retyping);
        dialog.find('#doublecheck_show_change_button').prop('disabled', !(settings.allow_change_incorrect || settings.allow_change_incorrect));
    }

    function insert_icons() {
		if (!document.getElementById('wk-icon__lightning')) {
			let svg = document.querySelector('svg symbol[id^="wk-icon"]')?.closest('svg');
			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>');
		}
	}

    //------------------------------------------------------------------------
    // init_ui() - Initialize the user interface.
    //------------------------------------------------------------------------
    async function init_ui() {
        settings = wkof.settings.doublecheck;

        await startup();

        // Migrate 'lightning' setting from localStorage.
        let lightning = localStorage.getItem('lightning');
        if (lightning === 'false' || lightning === 'true') {
            localStorage.removeItem('lightning');
            settings.lightning_enabled = lightning;
            wkof.Settings.save('doublecheck');
        }

        insert_icons();

        // Initialize the Lightning Mode button.
        let lightning_mode_el = document.getElementById('lightning-mode');
        if (lightning_mode_el) {
            lightning_mode_el.classList.toggle('doublecheck-active', settings.lightning_enabled);
            lightning_mode_el.hidden = !settings.show_lightning_button;
        }

        document.getElementById('option-toggle-rightwrong')?.classList?.toggle('hidden', !((settings.allow_change_correct || settings.allow_change_incorrect) && settings.show_change_button));
        document.getElementById('option-retype')?.classList?.toggle('hidden', !(settings.allow_retyping && settings.show_retype_button));
        resize_buttons();

        additional_content = get_controller('additional-content');
        if (state === 'second_submit') {
            document.getElementById('option-toggle-rightwrong')?.querySelector('a')?.classList?.toggle(additional_content.toggleDisabledClass, !(
                (new_answer_check.passed && (settings.allow_change_incorrect || !first_answer_check.passed)) ||
                (!new_answer_check.passed && (settings.allow_change_correct || first_answer_check.passed))
            ));
            document.getElementById('option-retype')?.querySelector('a')?.classList?.toggle(additional_content.toggleDisabledClass, !settings.allow_retyping);
        } else {
            document.getElementById('option-toggle-rightwrong')?.querySelector('a').classList?.add(additional_content.toggleDisabledClass);
        }
    }

    //------------------------------------------------------------------------
    // lightning_clicked() - Lightning button handler.
    //------------------------------------------------------------------------
    function lightning_clicked(e) {
        e.preventDefault();
        settings.lightning_enabled = !settings.lightning_enabled;
        wkof.Settings.save('doublecheck');
        document.getElementById('lightning-mode')?.classList?.toggle('doublecheck-active', settings.lightning_enabled);
        return false;
    }

    //------------------------------------------------------------------------
    // get_correct_answers() - Returns an array of acceptable answers.
    //------------------------------------------------------------------------
    function get_correct_answers() {
        if (qtype === 'reading') {
            if (subject.type === 'Kanji') {
                return subject[subject.primary_reading_type];
            } else {
                return [].concat(
                    subject.readings.map((r) => r.reading),
                    subject.auxiliary_readings.filter((r) => r.type === 'whitelist').map((r) => r.reading)
                ).filter((r) => typeof r === 'string');
            }
        } else {
            return [].concat(
                synonyms,
                subject.meanings,
                subject.auxiliary_meanings.filter((m) => m.type === 'whitelist').map((m) => m.meaning),
            );
        }
    }

    //------------------------------------------------------------------------
    // get_next_correct_answer() - Returns the next acceptable answer from the
    //    array returned by get_correct_answers().
    //------------------------------------------------------------------------
    function get_next_correct_answer() {
        let result = first_answer_check.correct_answers[first_answer_check.correct_answer_index];
        first_answer_check.correct_answer_index = (first_answer_check.correct_answer_index + 1) % first_answer_check.correct_answers.length;
        return result;
    }

    //------------------------------------------------------------------------
    // toggle_result() - Toggle an answer from right->wrong or wrong->right.
    //------------------------------------------------------------------------
    function toggle_result(new_state) {
        if (new_state === 'toggle') new_state = (new_answer_check.passed ? 'incorrect' : 'correct');
        if (state !== 'second_submit') return false;

        let input = quiz_input.inputTarget;
        let current_state = (quiz_input.inputContainerTarget.getAttribute('correct') === 'true' ? 'correct' : 'incorrect');
        let answer_to_show, answer_to_grade;
        clear_delay();
        switch (new_state) {
            case 'correct':
                if (!settings.allow_change_correct) {
                    if (!first_answer_check.passed) return;
                    answer_to_grade = first_answer_check.answer;
                    answer_to_show = answer_to_grade;
                } else if (current_state === 'correct') {
                    answer_to_grade = get_next_correct_answer();
                    answer_to_show = answer_to_grade;
                } else {
                    first_answer_check.correct_answer_index = 0;
                    answer_to_grade = get_next_correct_answer();
                    answer_to_show = (settings.show_corrected_answer ? answer_to_grade : first_answer_check.answer);
                }
                input.value = answer_to_grade;
                new_answer_check = {
                    action:'pass',
                    message:null,
                    passed:true,
                    accurate:true,
                    multipleAnswers:false,
                    exception:false,
                    answer:answer_to_grade
                };
                set_answer_state(new_answer_check);
                input.value = answer_to_show;
                break;
            case 'incorrect':
                if (!settings.allow_change_incorrect) {
                    if (first_answer_check.passed) return;
                    answer_to_show = first_answer_check.answer;
                } else {
                    answer_to_show = (settings.show_corrected_answer ? 'xxxxxx' : first_answer_check.answer);
                }
                answer_to_grade = 'xxxxxx';
                input.value = answer_to_grade;
                new_answer_check = {
                    action:'fail',
                    message:{
                        type:'itemInfoException',
                        text:`Need help? View the correct ${qtype} and mnemonic.`
                    },
                    passed:false,
                    accurate:false,
                    multipleAnswers:false,
                    exception:false,
                    answer:answer_to_grade
                };
                set_answer_state(new_answer_check);
                input.value = answer_to_show;
                break;
            case 'retype':
                if (!settings.allow_retyping) return false;
                set_answer_state({reset:true, retype:true});
                break;
        }
    }

    //------------------------------------------------------------------------
    // do_delay() - Disable the submit button briefly to prevent clicking past wrong answers.
    //------------------------------------------------------------------------
    function do_delay(period) {
        if (period === undefined) period = settings.delay_period;
        ignore_submit = true;
        delay_timer = setTimeout(function() {
            delay_timer = -1;
            ignore_submit = false;
        }, period*1000);
    }

    //------------------------------------------------------------------------
    // clear_delay() - Clear the delay timer.
    //------------------------------------------------------------------------
    function clear_delay() {
        if (delay_timer) {
            ignore_submit = false;
            clearTimeout(delay_timer);
            delay_timer = undefined;
        }
    }

    //------------------------------------------------------------------------
    function show_exception(message) {
        if (typeof message !== 'string') return;
        quiz_input.exceptionTarget.textContent = message;
        quiz_input.exceptionContainerTarget.hidden = false;
    }

    //------------------------------------------------------------------------
    function hide_exception() {
        quiz_input.exceptionContainerTarget.hidden = true;
        quiz_input.exceptionTarget.textContent = '';
    }

    //------------------------------------------------------------------------
    function set_answer_state(results, final_submit) {
        quiz_stats = get_controller('quiz-statistics');
        quiz_queue = get_controller('quiz-queue');
        additional_content = get_controller('additional-content');
        item_info = get_controller('item-info');
        quiz_progress = get_controller('quiz-progress');
        quiz_audio = get_controller('quiz-audio');
        quiz_header = get_controller('quiz-header');
        if (!final_submit) {
            if (results.exception) {
                quiz_input.shakeForm();
                show_exception(answer_check.exception);
                quiz_input.inputEnabled = true;
                quiz_input.inputTarget.focus();
                return;
            }
            let rightwrong = document.getElementById('option-toggle-rightwrong')?.querySelector('a');
            let rightwrong_text = rightwrong?.querySelector('.additional-content__item-text');
            let rightwrong_icon = rightwrong?.querySelector('svg');
            let retype = document?.getElementById('option-retype')?.querySelector('a');
            if (rightwrong && rightwrong_text && rightwrong_icon) {
                if (!results.passed || (results.reset === true)) {
                    rightwrong.classList.toggle(additional_content.toggleDisabledClass, (results.reset === true) || !(settings.allow_change_correct || first_answer_check.passed));
                    rightwrong_text.innerText = 'Mark Right';
                    rightwrong_icon.classList.remove('dblchk--invert');
                } else {
                    rightwrong.classList.toggle(additional_content.toggleDisabledClass, (results.reset === true) || !(settings.allow_change_incorrect || !first_answer_check.passed));
                    rightwrong_text.innerText = 'Mark Wrong';
                    rightwrong_icon.classList.add('dblchk--invert');
                }
            }
            retype?.classList?.toggle(additional_content.toggleDisabledClass, (results.reset === true));

            if (results.reset) {
                additional_content.close();
                item_info.disable();
                quiz_audio.playButtonTarget.classList.add(quiz_audio.disabledClass);
                quiz_input.inputContainerTarget.removeAttribute('correct');
                quiz_input.inputTarget.value = '';
                quiz_input.inputChars = '';
                window.dispatchEvent(new CustomEvent('didUnanswerQuestion'));
                quiz_input.inputEnabled = true;
                quiz_input.inputTarget.focus();

                quiz_stats.completeCountTarget.innerText = session_stats.complete.toString();
                quiz_stats.remainingCountTarget.innerText = session_stats.remaining.toString();
                let percent_complete = Math.round(100*session_stats.complete/(session_stats.complete + session_stats.remaining));
                quiz_progress.updateProgress({detail:{percentComplete:percent_complete}});
                quiz_stats.percentCorrectTarget.innerText = (session_stats.answered ? Math.round(100 * session_stats.correct / session_stats.answered).toString() + '%' : '100%');
                if (quiz_header.hasSrsContainerTarget) quiz_header.srsContainerTarget.dataset.hidden = true;
                state = 'first_submit';
                return;
            }
            quiz_input.inputEnabled = false;
            quiz_input.inputContainerTarget.setAttribute('correct', results.passed);
        }

        subject_stats = JSON.parse(subject_stats_cache.get(subject.id) || JSON.stringify({
            meaning:{
                incorrect:0,
                complete:false
            },
            reading:{
                incorrect:0,
                complete:(['Radical','KanaVocabulary'].indexOf(quiz_input.currentSubject.type) >= 0)
            }
        }));
        if (results.passed) {
            subject_stats[quiz_input.currentQuestionType].complete = true;
        } else {
            subject_stats[quiz_input.currentQuestionType].incorrect++;
        }
        if (final_submit) {
            subject_stats_cache.set(subject.id, JSON.stringify(subject_stats));
        }

        if (session_stats.remaining == null) {
            session_stats = {
                complete: 0,
                remaining: Number(quiz_stats.remainingCountTarget.innerText),
                correct: 0,
                answered: 0
            }
        }
        let temp_session_stats = Object.assign({}, session_stats);
        temp_session_stats.answered++;
        if (results.passed) temp_session_stats.correct++;
        if (subject_stats.meaning.complete && subject_stats.reading.complete) {
            temp_session_stats.complete++;
            temp_session_stats.remaining--;
        }
        end_of_session_delay = false;
        if (final_submit) {
            Object.assign(session_stats, temp_session_stats);
            if (session_stats.remaining === 0) end_of_session_delay = true;
        } else {
            quiz_stats.completeCountTarget.innerText = temp_session_stats.complete.toString();
            quiz_stats.remainingCountTarget.innerText = temp_session_stats.remaining.toString();
            let percent_complete = Math.round(100*temp_session_stats.complete/(temp_session_stats.complete + temp_session_stats.remaining));
            quiz_progress.updateProgress({detail:{percentComplete:percent_complete}});
            quiz_stats.percentCorrectTarget.innerText = Math.round(100 * temp_session_stats.correct / temp_session_stats.answered).toString() + '%';

            quiz_stats.disconnect();
            let event = {detail:{
                subjectWithStats:{subject:subject,stats:subject_stats},
                questionType:quiz_input.currentQuestionType,
                answer:quiz_input.inputTarget.value,
                results:results
            }};
            window.dispatchEvent(new CustomEvent('didAnswerQuestion',event));
            quiz_stats.connect();

            if (subject_stats.meaning.complete && subject_stats.reading.complete) {
                if (srs_mgr && !(settings.lightning_enabled && answer_check.passed)) {
                    srs_mgr.updateSRS({subject:subject,stats:subject_stats});
                }
            } else {
                if (quiz_header.hasSrsContainerTarget) quiz_header.srsContainerTarget.dataset.hidden = true;
            }

            if ((results.passed && settings.autoinfo_correct && !settings.lightning_enabled) ||
                (!results.passed && settings.autoinfo_incorrect) ||
                (results.passed && results.multipleAnswers && settings.autoinfo_multi_meaning && !settings.lightning_enabled) ||
                (results.passed && !results.accurate && settings.autoinfo_slightly_off && !settings.lightning_enabled))
            {
                item_info.toggleTarget.click();
                if (results.passed) item_info.showException(qtype,results)
            }
        }
    }

    //------------------------------------------------------------------------
    // new_submit_handler() - Intercept handler for 'submit' button.  Overrides default behavior as needed.
    //------------------------------------------------------------------------
    function new_submit_handler(e) {
        // Don't process 'submit' if we are ignoring temporarily (to prevent double-tapping past important info)
        if (ignore_submit) return;

        hide_exception();

        let input = quiz_input.inputTarget;
        qtype = quiz_input.currentQuestionType;
        subject = quiz_input.currentSubject;

        let submitted_immediately = false;
        switch (state) {
            case 'first_submit': {
                // We intercept the first 'submit' click, and simulate normal Wanikani screen behavior.

                // Do WK's standard checks for shake.
                let answer = quiz_input.inputTarget.value.trim();
                if (qtype === 'reading') {
                    answer = response_helpers.normalizeReadingResponse(answer);
                    input.value = answer;
                }
                if (!response_helpers.questionTypeAndResponseMatch(qtype, answer) || (answer.length === 0)) {
                    quiz_input.shakeForm();
                    quiz_input.inputEnabled = true;
                    quiz_input.inputTarget.focus();
                    return;
                }

                quiz_input.inputEnabled = false;
                quiz_input.lastAnswer = answer;

                // Do WK's standard answer evaluation.
                synonyms = quiz_input.quizUserSynonymsOutlet.synonymsForSubjectId(subject.id);
                answer_check = answer_checker.evaluate({questionType:qtype, response:answer, item:subject, userSynonyms:synonyms, inputChars:quiz_input.inputChars});
                if (answer_check.hasOwnProperty('action')) {
                    if (answer_check.action === 'retry') {
                        answer_check.passed = false;
                        answer_check.accurate = false;
                        answer_check.multipleAnswers = false;
                        answer_check.exception = answer_check.message.text;
                    } else {
                        answer_check.passed = (answer_check.action === 'pass');
                        if (answer_check.message === null) {
                            answer_check.accurate = true;
                            answer_check.multipleAnswers = false;
                            answer_check.exception = false;
                        } else if (/has multiple/.test(answer_check.message.text)) {
                            answer_check.accurate = true;
                            answer_check.multipleAnswers = true;
                            answer_check.exception = false;
                        } else if (/one of your synonyms/.test(answer_check.message.text)) {
                            answer_check.accurate = false;
                            answer_check.multipleAnswers = false;
                            answer_check.exception = answer_check.message.text;
                        } else if (/a bit off/.test(answer_check.message.text)) {
                            answer_check.accurate = false;
                            answer_check.multipleAnswers = false;
                            answer_check.exception = false;
                        }
                    }
                }

                // Process typos according to settings.
                if (answer_check.passed && !answer_check.accurate) {
                    switch (settings.typo_action) {
                        case 'warn': answer_check.exception = 'Your answer was close, but not exact'; break;
                        case 'wrong': answer_check.passed = false; answer_check.custom_msg = 'Your answer was not exact, as required by your settings.'; break;
                    }
                }

                // Process answer-type errors according to settings.
                if (!answer_check.passed) {
                    if (qtype === 'meaning') {
                        // Although Wanikani checks for readings entered as meanings, it only
                        // checks the 'preferred' reading.  Here, we check all readings.
                        if (subject.type === 'KanaVocabulary') {
                            accepted_readings = [subject.characters];
                        } else {
                            accepted_readings = [].concat(
                                subject.readings?.map((r)=>r.reading),
                                subject.auxiliary_readings?.filter((r)=>r.type==='whitelist').map((r)=>r.reading),
                                subject.onyomi,
                                subject.kunyomi,
                                subject.nanori
                            );
                        }
                        let answer_as_kana = to_kana(answer);
                        if (accepted_readings.indexOf(answer_as_kana) >= 0) {
                            if (settings.wrong_answer_type_action === 'warn') {
                                answer_check.exception = answer_check.exception || 'Oops, we want the meaning, not the reading.';
                            } else {
                                answer_check.exception = false;
                            }
                        }
                    } else {
                        accepted_meanings = [].concat(
                            subject.meanings,
                            subject.auxiliary_meanings?.filter((r)=>r.type==='whitelist').map((r)=>r.meaning),
                            synonyms
                        ).filter((s) => typeof s === 'string').map((s) => s.trim().toLowerCase().replace(/\s\s+/g,' '));
                        let meanings_as_hiragana = accepted_meanings.map(m => to_kana(m));
                        let answer_as_hiragana = Array.from(answer.toLowerCase()).map(c => wanakana.toHiragana(c)).join('');
                        if (meanings_as_hiragana.indexOf(answer_as_hiragana) >= 0) {
                            if (settings.wrong_answer_type_action === 'warn') {
                                answer_check.exception = 'Oops, we want the reading, not the meaning.';
                            } else {
                                answer_check.exception = false;
                            }
                        }
                    }
                }

                // Process all other exceptions according to settings.
                if (typeof answer_check.exception === 'string') {
                    if (((settings.kanji_meaning_for_vocab_action === 'wrong') && answer_check.exception.toLowerCase().includes('want the vocabulary meaning, not the kanji meaning')) ||
                        ((settings.kanji_reading_for_vocab_action === 'wrong') && answer_check.exception.toLowerCase().includes('want the vocabulary reading, not the kanji reading')) ||
                        ((settings.wrong_number_n_action === 'wrong') && answer_check.exception.toLowerCase().includes('forget that 悓')) ||
                        ((settings.small_kana_action === 'wrong') && answer_check.exception.toLowerCase().includes('watch out for the small')))
                    {
                        answer_check.exception = false;
                        answer_check.passed = false;
                    }
                }

                // Remain in 'first_submit' if there was an exceptions.
                if (answer_check.exception) {
                    set_answer_state(answer_check);
                    return false;
                }
                state = 'second_submit';

                new_answer_check = Object.assign({answer:answer}, answer_check);
                first_answer_check = Object.assign({
                    answer:answer,
                    correct_answers:get_correct_answers(),
                    correct_answer_index: 0,
                }, answer_check);

                // Process "Mistake Delay" according to settings.
                if ((!answer_check.passed && settings.delay_wrong) ||
                    (answer_check.passed &&
                     ((!answer_check.accurate && settings.delay_slightly_off) ||
                      (answer_check.multipleAnswers && settings.delay_multi_meaning))
                    )
                   )
                {
                    set_answer_state(new_answer_check);
                    do_delay();
                    return false;
                }

                set_answer_state(answer_check);

                // Process lightning mode according to settings.
                if (settings.lightning_enabled && answer_check.passed) {
                    new_submit_handler(e);
                    return false;
                }

                return false;
            }
            case 'second_submit': {
                // We intercepted the first submit, allowing the user to optionally modify their answer.
                // Now, either the user has clicked submit again, or lightning is enabled and we are automatically clicking submit again.

                let answer = new_answer_check.answer;
                input.value = answer;
                set_answer_state(new_answer_check, true /* final_submit */);
                delete new_answer_check.answer;

                // Nasty hack to prevent audio from playing twice or stopping upon next question.
                let audio = quiz_audio.audioTarget;
                audio.setAttribute('data-quiz-audio-target', 'noplay');
                audio.insertAdjacentHTML('afterend', '<audio class="quiz-audio__audio dblchk" data-quiz-audio-target="audio"></audio>');
                let tmp_audio = document.querySelector('audio.dblchk');
                quiz_audio.disconnect();

                function dispatch_didFinalAnswer(e) {
                    window.dispatchEvent(new CustomEvent('didFinalAnswer',{detail:e.detail}));
                    window.removeEventListener('didAnswerQuestion', dispatch_didFinalAnswer);
                }
                window.addEventListener('didAnswerQuestion', dispatch_didFinalAnswer);
                quiz_queue.submitAnswer(answer, new_answer_check);

                // Nasty audio hack, continued.
                setTimeout(() => {
                    tmp_audio.remove();
                    audio.setAttribute('data-quiz-audio-target', 'audio');
                    quiz_audio.connect();
                }, 1);

                if (end_of_session_delay) {
                    setTimeout(next_item, 500);
                } else {
                    next_item();
                }

                function next_item() {
                    quiz_queue.nextItem();
                    set_answer_state({reset:true});

                    quiz_header = get_controller('quiz-header');
                    if (quiz_header.hasSrsContainerTarget && settings.lightning_enabled && new_answer_check.passed &&
                        subject_stats.meaning.complete && subject_stats.reading.complete && srs_mgr) {
                        setTimeout(() => {
                            srs_mgr.updateSRS({subject:subject,stats:subject_stats});
                            setTimeout(()=>{
                                quiz_header.srsContainerTarget.dataset.hidden = true;
                            }, 1000 * settings.srs_msg_period);
                        }, 1);
                    }

                    state = 'first_submit';
                }
                return false;
            }
            default:
                return false;
        }

        return false;
    }

    //------------------------------------------------------------------------
    // Simulate input character by character and convert with WanaKana to kana
    //  -- Contributed by user @Sinyaven
    //------------------------------------------------------------------------
    function to_kana(text) {
        return Array.from(text).reduce((total, c) => wanakana.toKana(total + c, {IMEMode: true}), "").replace(/n$/, String.fromCharCode(12435));
    }

    //------------------------------------------------------------------------
    // Resize the buttons according to how many are visible.
    //------------------------------------------------------------------------
    function resize_buttons() {
        const additional_content = document.getElementById('additional-content');
        if (!additional_content) return;
        const buttons = Array.from(additional_content.querySelectorAll('.additional-content__menu-item'));
        const visible_buttons = buttons.filter((elem)=>!elem.matches('.hidden,[hidden]'));
        const btn_count = visible_buttons.length;
        for (const btn of visible_buttons) {
            const percent = Math.floor(10000/btn_count)/100 + '%';
            btn.style.width = `calc(${percent} - 10px)`;
            btn.style.flex = `0 0 calc(${percent} - 10px)`;
            btn.style.marginRight = '10px';
        }
        visible_buttons.slice(-1)[0].style.marginRight = '0px';
    }

    //------------------------------------------------------------------------
    // External hook for @polv's script, "WaniKani Disable Default Answers"
    //------------------------------------------------------------------------
    gobj.set_state = function(_state) {
        state = _state;
    };

    function get_controller(name) {
        return Stimulus.getControllerForElementAndIdentifier(document.querySelector(`[data-controller~="${name}"]`),name);
    }

    //------------------------------------------------------------------------
    // startup() - Install our intercept handlers, and add our Double-Check button and hotkey
    //------------------------------------------------------------------------
    async function startup() {
        if (document.getElementById('lightning-mode')) return; // startup has already run
        // Intercept the submit button handler.
        let p = promise();
        quiz_input = undefined;
        quiz_queue = undefined;
        additional_content = undefined;
        item_info = undefined;
        quiz_audio = undefined;
        quiz_stats = undefined;
        quiz_progress = undefined;
        quiz_header = undefined;
        answer_checker = undefined;

        async function get_controllers() {
            try {
                // Check if all of our hooks into WK are valid, just in case something changed.
                if (!quiz_input) {
                    quiz_input = get_controller('quiz-input');
                    if (!quiz_input) throw 'Controller "quiz-input" not found.';
                }
                if (!quiz_queue) {
                    quiz_queue = get_controller('quiz-queue');
                    if (!quiz_queue) throw 'Controller "quiz-queue" not found.';
                }
                if (!additional_content) {
                    additional_content = get_controller('additional-content');
                    if (!additional_content) throw 'Controller "additional-content" not found.';
                }
                if (!item_info) {
                    item_info = get_controller('item-info');
                    if (!item_info) throw 'Controller "item-info" not found.';
                }
                if (!quiz_audio) {
                    quiz_audio = get_controller('quiz-audio');
                    if (!quiz_audio) throw 'Controller "quiz-audio" not found.';
                }
                if (!quiz_stats) {
                    quiz_stats = get_controller('quiz-statistics');
                    if (!quiz_stats) throw 'Controller "quiz-statistics" not found.';
                }
                if (!quiz_progress) {
                    quiz_progress = get_controller('quiz-progress');
                    if (!quiz_progress) throw 'Controller "quiz-progress" not found.';
                }
                if (!quiz_header) {
                    quiz_header = get_controller('quiz-header');
                    if (!quiz_header) throw 'Controller "quiz-header" not found.';
                }
                if (!response_helpers) {
                    response_helpers = await importShim('lib/answer_checker/utils/response_helpers');
                    if (!response_helpers) throw 'Import "lib/answer_checker/utils/response_helpers" failed.';
                }
                if (!wanakana) {
                    wanakana = await importShim('wanakana');
                    if (!wanakana) throw 'Import "wanakana" failed.';
                }
                if (!answer_checker) answer_checker = Stimulus.controllers.find((c)=>c.answerChecker)?.answerChecker;
                if (!answer_checker) {
                    let AnswerChecker = (await importShim('lib/answer_checker/answer_checker')).default;
                    if (!AnswerChecker) throw 'Import "lib/answer_checker/answer_checker" failed.';
                    answer_checker = new AnswerChecker;
                }
                if (quiz_queue.hasSubjectIdsWithSRSTarget) {
                    srs_mgr = quiz_queue.quizQueue.srsManager;
                } else {
                    srs_mgr = undefined;
                }

                if (quiz_input.submitAnswer !== new_submit_handler) {
                    old_submit_handler = quiz_input.submitAnswer;
                    quiz_input.submitAnswer = new_submit_handler;
                }

                p.resolve();
            } catch(err) {
                console.log('Double-Check:', err, ' Retrying...');
                setTimeout(get_controllers, 250);
            }
            return p;
        }

        await get_controllers();

        subject_stats_cache = new Map();
        session_stats = {};
        state = 'first_submit';
        ignore_submit = false;

        // Install the Lightning Mode button.
        let scripts_menu = document.getElementById('scripts-menu');

        // Insert CSS
        if (!document.getElementById('doublecheck-style'))
            document.head.insertAdjacentHTML('beforeend',
                `<style id="doublecheck-style" name="doublecheck">
                #lightning-mode.doublecheck-active svg {fill:#ff0; opacity:1.0;}
                .wk-icon--thumbs-up.dblchk--invert {transform:scaleY(-1);}
                </style>`
            );

        // Insert lightning button
        scripts_menu.insertAdjacentHTML('afterend',
            `<div id="lightning-mode" class="character-header__menu-navigation-link" hidden>
                <a class="lightning-mode summary-button" href="#" title="Lightning Mode - When enabled, auto-\nadvance after answering correctly.">
                    <svg class="wk-icon wk-icon--lightning" title="Mark Right" viewBox="0 0 500 500" aria-hidden="true">
                        <use href="#wk-icon__lightning"></use>
                    </svg>
                </a>
            </div>`
        );
        document.querySelector('.lightning-mode').addEventListener('click', lightning_clicked);

        // Install the Double-Check features.
        let additional_content_el = document.getElementById('additional-content')?.querySelector('ul');
        if (additional_content_el) {
            additional_content_el.style.textAlign = 'center';
            additional_content_el.insertAdjacentHTML('beforeend',
                `<li id="option-toggle-rightwrong" class="additional-content__menu-item additional-content__menu-item--5">
                    <a title="Mark Right" class="additional-content__item ${additional_content.toggleDisabledClass}">
                        <div class="additional-content__item-text">Mark Right</div>
                        <div class="additional-content__item-icon-container">
                            <svg class="wk-icon wk-icon--thumbs-up" title="Mark Right" viewBox="0 0 512 512" aria-hidden="true">
                                <use href="#wk-icon__thumbs-up"></use>
                            </svg>
                        </div>
                    </a>
                </li>
                <li id="option-retype" class="additional-content__menu-item additional-content__menu-item--5">
                    <a title="Retype" class="additional-content__item ${additional_content.toggleDisabledClass}">
                        <div class="additional-content__item-text">Re-type</div>
                        <div class="additional-content__item-icon-container">
                            <svg class="wk-icon wk-icon--reload" title="Re-type Answer" viewBox="0 0 512 512" aria-hidden="true">
                                <use href="#wk-icon__reload"></use>
                            </svg>
                        </div>
                    </a>
                </li>`
            );
        }
        document.getElementById('option-toggle-rightwrong')?.addEventListener('click', toggle_result.bind(null,'toggle'));
        document.getElementById('option-retype')?.addEventListener('click', toggle_result.bind(null,'retype'));
        let input = quiz_input.inputTarget;
        document.body.addEventListener('keypress', handle_rightwrong_hotkey);
        function handle_rightwrong_hotkey(event){
            if (state !== 'first_submit') {
                if (!document.getElementById('wkofs_doublecheck') && (event.target === input || event.target === document.body)) {
                    if (event.which === 43) {
                        toggle_result('correct');
                        event.preventDefault();
                        event.stopPropagation();
                    }
                    if (event.which === 45) {
                        toggle_result('incorrect');
                        event.preventDefault();
                        event.stopPropagation();
                    }
                }
            }
        }
        document.body.addEventListener('keydown', handle_retype_hotkey);
        function handle_retype_hotkey(event){
            if (state !== 'first_submit') {
                if (!document.getElementById('wkofs_doublecheck') && (event.target === input || event.target === document.body)) {
                    if ((event.which === 27 || event.which === 8)) {
                        toggle_result('retype');
                        event.preventDefault();
                        event.stopPropagation();
                    } else if (event.ctrlKey && event.key === 'l') {
                        event.preventDefault();
                        event.stopPropagation();
                        lightning_clicked();
                    }
                }
            }
        }

        if (!document.getElementById('doublecheck-style-2')) {
            document.head.insertAdjacentHTML('beforeend',
                `<style id="doublecheck-style-2">
                #additional-content>ul>li.hidden {display:none;}
                #answer-form fieldset.confburn button, #answer-form fieldset.confburn input[type=text], #answer-form fieldset.confburn input[type=text]:disabled {
                  background-color: #000 !important;
                  color: #fff;
                  text-shadow: 2px 2px 0 rgba(0,0,0,0.2);
                  transition: background-color 0.1s ease-in;
                  opacity: 1 !important;
                }
                </style>`
            );
        }
    }

})(window.doublecheck);

If youā€™re comfortable trusting my changes, you can use the version Iā€™m running locally by overwriting the whole script with the one above. Or you could use it to see what Iā€™ve done.

Edit:
Changed my .quiz selector to just .character-header (as appropriate) so that it works on lessons pages as well. Also added some additional null checks.

Edit2:
Added an early exit to startup() for if itā€™s already run (hopefully that doesnā€™t break something elseā€¦)

5 Likes

Does the script sometimes stop working partway through a review for anyone else?

2 Likes

@rfindley, could you please update again? The script is stopped working for me. :pleading_face:

1 Like

Iā€™ve updated Open Framework and Double-Check to properly handle the latest changes on WaniKani. Youā€™ll no longer need to refresh the Reviews page to get Double-Check to work.

For script authors: Iā€™ve posted some details on the Open Framework Developer thread, if you decide you want to make use of the new wkof.on_pageload() feature.

12 Likes

The updated version doesnā€™t work. Upd: @Inserioā€™s script does work.

1 Like

Blind guess: do you also have the latest update of wanikani open framework (click the check for updates button in your user script manager to be sure)

2 Likes

I updated OF and DC last night but it still doesnā€™t work. In fact, now even when I hit the reload button, the redo icon doesnā€™t come up, so it doesnā€™t work at all. Did I need to do something else?

Iā€™ve posted a new update for OF this morning, though Iā€™m not sure it would fix what you are seeing (or not seeing).

Could you post any error messages in the Javascript console? (press F12, then click on the Console tab, and post a snapshot of any errors.)

Edit: By the way, Iā€™m assuming you arenā€™t seeing the gear icon in the upper-left corner of the screen either?

1 Like