Wanikani Double-Check

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

当前为 2018-02-28 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Wanikani Double-Check
// @namespace   wkdoublecheck
// @description Allows retyping typo'd answers, or marking wrong when WK's typo tolerance is too lax.
// @include     https://www.wanikani.com/review/session*
// @version     2.0.9
// @author      Robin Findley
// @copyright   2017+, Robin Findley
// @license     MIT; http://opensource.org/licenses/MIT
// @run-at      document-end
// @grant       none
// ==/UserScript==

// CREDITS: This is a replacement for an original script by Wanikani user @Ethan.
// Ethan's script stopped working due to some Wanikani changes.  The code below is
// 100% my own, but it closely replicates the functionality of Ethan's original script.

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

// SEE SETTINGS BELOW.

window.wkdoublecheck = {};

(function(gobj) {

    //==[ Settings ]=====================================================
    var settings = {

        // Shake when slightly off (i.e. "Close, but no cigar" script)
        shake_when_slightly_off: 0,

        // Delay when answer is wrong.
        delay_wrong: 1,

        // Delay when answer has multiple meanings.
        delay_multi_meaning: 0,

        // Delay when answer is slightly off (e.g. minor typo).
        delay_slightly_off: 0,

        // Amount of time to delay (in milliseconds) before allowing the
        // user to move on after an accepted typo or rejected answer.
        delay_period: 1500,
    };

    // Make the settings accessible from the console via 'wkdoublecheck.settings'
    gobj.settings = settings;

    // For debugging.  Blocks ajax requests.
    settings.block_ajax = 0;

    //===================================================================

    // Theory of operation:
    // ====================
    // Wanikani's normal process:
    //    1) User clicks 'submit'
    //       a) Wanikani checks answer and updates screen with the result.
    //       b) If both reading and meaning have been answered, Wanikani immediately sends the result to the server. (<-- BAD!!)
    //    2) User clicks 'submit' (or enter) again to move to the next question.
    //       a) Wanikani updates the screen to show the next question.
    //
    // Our modified process:
    //    1) User clicks 'submit'
    //       a) We intercept the click, check the answer ourself, and update the screen.
    //          Wanikani's code is unaware of what we're doing.
    //       b) User now has the opportunity to modify their answer.
    //    2) User clicks 'submit' (or enter) again to move to the next question.
    //       a) We intercept the click again.
    //       b) We reset the display back to pre-submitted state, so Wanikani's code won't be confused.
    //       c) We call Wanikani's normal 'submit' function, but we intercept the answer-checker function,
    //          so Wanikani will see whatever result the user requested.
    //          Wanikani's updates the screen with the result.
    //       d) Keep in mind, the user has clicked the 'submit' button twice, but Wanikani has only
    //          seen one click.  So, we have to send a third hidden click so Wanikani will catch up to
    //          where the user thinks we are (i.e. 'next question').
    //    3) We intercept the hidden click, and forward it to Wanikani's code.
    //       a) Wanikani updates the screen to show the next question.

    var old_submit_handler, old_answer_checker, ignore_submit = false, state = 'first_submit', old_audioAutoplay, show_srs;
    var item, itype, item_id, item_status, qtype, valid_answers, wrong_cnt, question_cnt, completed_cnt, answer, new_answer;

    //------------------------------------------------------------------------
    // toggle_result() - Toggle an answer from right->wrong or wrong->right.
    //------------------------------------------------------------------------
    function toggle_result(new_state) {
        if ($('#option-double-check').hasClass('disabled')) return false;
        if (new_state === 'toggle') new_state = (new_answer.passed ? 'incorrect' : 'correct');
        if (new_answer.passed && new_state === 'incorrect') {
            new_answer = {passed:false, accurate:false, multipleAnswers:false, exception:false};
            set_answer_state(new_answer, false /* show_msgs */);
        } else if (!new_answer.passed && new_state === 'correct') {
            new_answer = {passed:true, accurate:true, multipleAnswers:false, exception:false};
            set_answer_state(new_answer, false /* show_msgs */);
        } else if (new_state === 'retype') {
            set_answer_state({reset:true}, false /* show_msgs */);
        }
    }

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

    //------------------------------------------------------------------------
    // return_new_answer() - Alternate answer checker that overrides our results.
    //------------------------------------------------------------------------
    function return_new_answer() {
        return new_answer;
    }

    //------------------------------------------------------------------------
    // set_answer_state() - Update the screen to show results of answer-check.
    //------------------------------------------------------------------------
    function set_answer_state(answer, show_msgs) {
        // If user requested to retype answer, reset the question.
        if (answer.reset) {
            $.jStorage.set('wrongCount', wrong_cnt);
            $.jStorage.set('questionCount', question_cnt);
            $.jStorage.set('completedCount', completed_cnt);
            $.jStorage.set('currentItem', item);
            $("#answer-exception").remove();
            $('#option-double-check').addClass('disabled').find('span').attr('title','Mark Right').find('i').attr('class','icon-thumbs-up');
            $('#option-retype').addClass('disabled');
            Srs.remove();
            state = 'first_submit';
            return;
        }

        // If answer is invalid for some reason, do the shake thing.
        if (answer.exception) {
            if (!$("#answer-form form").is(":animated")) {
                $("#reviews").css("overflow-x", "hidden");
                var xlat = {onyomi:"on'yomi", kunyomi:"kun'yomi", nanori:"nanori"};
                var emph = xlat[item.emph];
                $("#answer-form form").effect("shake", {}, 400, function() {
                    $("#reviews").css("overflow-x", "visible");
                    if (!answer.accurate && settings.shake_when_slightly_off)
                        $("#answer-form form").append($('<div id="answer-exception" class="answer-exception-form"><span>Hmm.. that\'s not quite right.  Typo??</span></div>').addClass("animated fadeInUp"));
                    else
                        $("#answer-form form").append($('<div id="answer-exception" class="answer-exception-form"><span>WaniKani is looking for the '+emph+" reading</span></div>").addClass("animated fadeInUp"));
                }).find("input").focus();
            }
            return;
        }

        // Draw 'correct' or 'incorrect' results, enable Double-Check button, and calculate updated statistics.
        var new_wrong_cnt = wrong_cnt, new_completed_cnt = completed_cnt;
        $("#user-response").blur();
        $('#option-retype').removeClass('disabled');
        var new_status = Object.assign({},item_status);
        if (answer.passed) {
            $("#answer-form fieldset").removeClass('incorrect').addClass("correct");
            $('#option-double-check').removeClass('disabled').find('span').attr('title','Mark Wrong').find('i').attr('class','icon-thumbs-down');
            if (qtype === 'meaning')
                new_status.mc = (new_status.mc || 0) + 1;
            else
                new_status.rc = (new_status.rc || 0) + 1;
        } else {
            $("#answer-form fieldset").removeClass('correct').addClass("incorrect");
            $('#option-double-check').removeClass('disabled').find('span').attr('title','Mark Right').find('i').attr('class','icon-thumbs-up');
            new_wrong_cnt++;
        }
        if ((itype === 'r' || ((new_status.rc || 0) >= 1)) && ((new_status.mc || 0) >= 1)) {
            new_completed_cnt++;
            if (show_srs) Srs.load(new_status,item.srs);
        }
        $.jStorage.set('wrongCount', new_wrong_cnt);
        $.jStorage.set('questionCount', question_cnt + 1);
        $.jStorage.set('completedCount', new_completed_cnt);
        $("#user-response").prop("disabled", !0);
        additionalContent.enableButtons();
        lastItems.disableSessionStats();
        $("#answer-exception").remove();

        // When user is submitting an answer, display the on-screen message that Wanikani normally shows.
        if (show_msgs) {
            var msg;
            if (answer.passed) {
                if (!answer.accurate) {
                    msg = 'Your answer was a bit off. Check the '+qtype+' to make sure you are correct';
                } else if (answer.multipleAnswers) {
                    msg = 'Did you know this item has multiple possible '+qtype+'s?';
                }
            } else {
                msg = 'Need help? View the correct '+qtype+' and mnemonic';
            }
            if (msg)
                $("#additional-content").append($('<div id="answer-exception"><span>'+msg+'</span></div>').addClass("animated fadeInUp"));
        }
    }

    //------------------------------------------------------------------------
    // 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) {
            // If the user presses <enter> during delay period,
            // WK enables the user input field, which makes Item Info not work.
            // Let's make sure the input field is disabled.
            setTimeout(function(){
                $("#user-response").prop('disabled',!0);
            },1);
            return false;
        }

        // For more information about the state machine below,
        // see the "Theory of operation" info at the top of the script.
        switch(state) {
            case 'first_submit':
                // We intercept the first 'submit' click, and simulate normal Wanikani screen behavior.
                state = 'second_submit';

                // Capture the state of the system before submitting the answer.
                item = $.jStorage.get('currentItem');
                itype = (item.rad ? 'r' : (item.kan ? 'k' : 'v'));
                item_id = itype + item.id;
                item_status = $.jStorage.get(item_id) || {};
                qtype = $.jStorage.get('questionType');
                wrong_cnt = $.jStorage.get('wrongCount');
                question_cnt = $.jStorage.get('questionCount');
                completed_cnt = $.jStorage.get('completedCount');
                show_srs = $.jStorage.get('r/srsIndicator');

                // Ask Wanikani if the answer is right (but we don't actually submit the answer).
                answer = old_answer_checker(qtype, $("#user-response").val());

                // Update the screen to reflect the results of our checked answer.
                $("html, body").animate({scrollTop: 0}, 200);
                new_answer = Object.assign({},answer);

                // Close but no cigar
                if  (answer.passed && !answer.accurate && settings.shake_when_slightly_off) {
                    answer.exception = true;
                }
                set_answer_state(answer, true /* show_msgs */);
                if (answer.exception) {
                    state = 'first_submit';
                    return false;
                }

                // Optionally (according to settings), temporarily ignore any additional clicks on the
                // 'submit' button to prevent the user from clicking past important info about the answer.
                if ((!answer.passed && settings.delay_wrong) ||
                    (answer.passed &&
                     ((!answer.accurate && settings.delay_slightly_off) || (answer.multipleAnswers && settings.delay_multi_meaning))
                    )
                   )
                {
                    do_delay();
                }

                return false;

            case 'second_submit':
                // We take the user's second 'submit' click (after they've optionally toggled the answer result),
                // and send it to Wanikani's code as if it were the first click.
                // Then we send a hidden third 'submit', which Wanikani will see as the second 'submit', which moves us to the next question.
                state = 'third_submit';

                old_audioAutoplay = window.audioAutoplay;
                window.audioAutoplay = false;

                // Reset the screen to pre-submitted state, so Wanikani won't get confused when it tries to process the answer.
                // Wanikani code will then update the screen according to our forced answer-check result.
                $('#option-double-check').addClass('disabled').find('span').attr('title','Double-Check').find('i').attr('class','icon-thumbs-up');
                $('#option-retype').addClass('disabled');
                $('#user-response').removeAttr('disabled');
                $('#option-audio audio').remove();
                $.jStorage.set('wrongCount', wrong_cnt);
                $.jStorage.set('questionCount', question_cnt);
                $.jStorage.set('completedCount', completed_cnt);

                // Prepare a hidden third click, which tells Wanikani to move to the next question.
                setTimeout(function(){
                    $("#answer-form button").trigger('click');
                }, 1);

                // We want Wanikani to see our forced answer-check result,
                // so we set up to intercept the answer-checker here.
                return old_submit_handler.apply(this, arguments);

            case 'third_submit':
                // This is hidden third click from above, which Wanikani thinks is the second click.
                // Wanikani will move to the next question.
                state = 'first_submit';

                window.audioAutoplay = old_audioAutoplay;

                // We need to disable the input field, so Wanikani will see this as the second click.
                $('#user-response').attr('disabled','disabled');

                return old_submit_handler.apply(this, arguments);

            default:
                return false;
        }

        return false;
    }

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

    //------------------------------------------------------------------------
    // startup() - Install our intercept handlers, and add our Double-Check button and hotkey ("!")
    //------------------------------------------------------------------------
    function startup() {
        // Check if we can intercept the submit button handler.
        try {
            old_submit_handler = $._data( $('#answer-form button')[0], 'events').click[0].handler;
            old_answer_checker = answerChecker.evaluate;
        } catch(err) {}
        if (typeof old_submit_handler !== 'function' || typeof old_answer_checker !== 'function') {
            alert('Wanikani Mistake Delay script is not working.');
            return;
        }

        // Replace the handler.
        $._data( $('#answer-form button')[0], 'events').click[0].handler = new_submit_handler;

        var btn_count = $('#additional-content ul').children().length + 2;
        $('#additional-content ul').css('text-align','center').append(
            '<li id="option-double-check" class="disabled"><span title="Double Check"><i class="icon-thumbs-up"></i></span></li>'+
            '<li id="option-retype" class="disabled"><span title="Retype"><i class="icon-undo"></i></span></li></ul>'
        );
        $('#additional-content ul > li').css('width',Math.floor(9950/btn_count)/100 + '%');
        $('#option-double-check').on('click', toggle_result.bind(null,'toggle'));
        $('#option-retype').on('click', toggle_result.bind(null,'retype'));
        $('body').on('keypress', function(event){
            if (event.which === 43) toggle_result('correct');
            if (event.which === 45) toggle_result('incorrect');
            return true;
        });
        $('body').on('keydown', function(event){
            if (event.which === 27) toggle_result('retype');
            return true;
        });
        answerChecker.evaluate = return_new_answer;

        // For debugging, block progress submissions.
        if (settings.block_ajax) {
            console.log('======[ "Double-Check" script is in debug mode, and will blocking ajax requests! ]======');
            $.ajax = function(){return $.Deferred().resolve();};
        }
    }

    // Run startup() after window.onload event.
    if (document.readyState === 'complete')
        startup();
    else
        window.addEventListener("load", startup, false);

})(window.wkdoublecheck);