WaniKani Lock Script

Allows you to lock a set number of items from your review queue so you keep on top of the rest.

Verze ze dne 30. 08. 2018. Zobrazit nejnovější verzi.

// ==UserScript==
// @name          WaniKani Lock Script
// @namespace     https://www.wanikani.com
// @author        Doncr
// @description   Allows you to lock a set number of items from your review queue so you keep on top of the rest.
// @version       0.0.4
// @include       *://www.wanikani.com/*
// @grant         none
// @run-at        document-body
// ==/UserScript==

(function (xhr) {

    var localStorageConfigKey = "lockScriptCache";
    var localStorageAPIKeyKey = "apikeyv2";

    function getConfig() {

        var config = localStorage.getItem(localStorageConfigKey);

        if (config) {
            return JSON.parse(config);
        } else {
            return {
                availableAt: {},
                lockCount: 0,
            };
        }
    }

    function setConfig(config) {
        localStorage.setItem(localStorageConfigKey, JSON.stringify(config));
    }

    function getAPIKey() {
        return localStorage.getItem(localStorageAPIKeyKey);
    }

    function reloadCache() {

        var apiKey = getAPIKey();

        if (apiKey.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)) {

            // Clear out any availability data from the cache.

            var config = getConfig();

            config.availableAt = {};
            delete config.availableUpdatedAt;

            setConfig(config);

            updateAssignmentsCache();
        }
    }

    function setAPIKey() {

        var apiKey = window.jQuery('#lockScriptAPIKey').val();

        if (apiKey.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)) {

            if (apiKey != getAPIKey()) {
                localStorage.setItem(localStorageAPIKeyKey, apiKey);
            }

            reloadCache();

        } else if (apiKey.match(/^[0-9a-f]{32}$/)) {

            alert("It looks like you entered the API Version 1 key. You need to use the API Version 2 key.");

        } else {

            alert("Invalid API key format. You need to use the API Version 2 key.");
        }
    }

    function updateAssignmentsCache() {

        var cache = getConfig();

        if (getAPIKey()) {

            var uri = "https://www.wanikani.com/api/v2/assignments";

            if (cache.availableUpdatedAt) {
                uri = uri + "?updated_after=" + cache.availableUpdatedAt;
            }

            while (uri) {

                var response = jQuery.ajax({
                    headers: { "Authorization": "Token token=" + getAPIKey() },
                    dataType: "json",
                    async: false,
                    url: uri
                });

                var json = response.responseJSON;

                if (json.data_updated_at) {
                    cache.availableUpdatedAt = json.data_updated_at;
                }

                json.data.forEach(function (datum) {

                    var key = datum.data.subject_id.toString();
                    var avail = datum.data.available_at;
                    var srs_stage = datum.data.srs_stage;
                    var hidden = datum.data.hidden;

                    if ((avail != null) && (srs_stage > 0) && (srs_stage < 9) && (!hidden)) {
                        var value = Date.parse(avail) / 1000;
                        cache.availableAt[key] = { "t": value, "l": srs_stage };
                    } else {
                        delete cache.availableAt[key];
                    }
                });

                uri = json.pages.next_url;
            }

            // Reduce lockCount if there aren't that many items left in the real review queue.

            var now = Date.now() / 1000;
            var realQueueSize = Object.values(cache.availableAt).filter(function (item) { return item.t < now; }).length;

            if (cache.lockCount > realQueueSize) {
                cache.lockCount = realQueueSize;
            }

            window.managedToUpdateLockCache = true;
        }

        setConfig(cache);
    }

    function modifyReviewQueue(queueText) {

        var queue = JSON.parse(queueText);
        var config = getConfig();
        var availableAt = config.availableAt;
        var lockCount = parseInt(config.lockCount);

        if (lockCount > 0) {

            var queueByTime = queue.map(function (item) { return item.id }).sort(function (a, b) {
                var sortMethod1 = availableAt[a].t - availableAt[b].t;

                if (sortMethod1 != 0) {
                    return sortMethod1;
                }

                return a - b;
            });

            queueByTime = queueByTime.slice(lockCount);

            var queueByTimeKeys = {};

            queueByTime.forEach(function (item) {
                queueByTimeKeys[item] = true;
            });

            queue = queue.filter(function (item) {
                return queueByTimeKeys[item.id.toString()];
            });

            window.lockScriptFilter = true;
            window.lockScriptLockCount = config.lockCount;
        }

        return JSON.stringify(queue);
    }

    var originalResponseText = Object.getOwnPropertyDescriptor(xhr, 'responseText');

    Object.defineProperty(xhr, 'responseText', {

        get: function () {

            // Use the original responseText property accessor so we can get the
            // actual response from WK.

            var responseText = originalResponseText.get.apply(this);

            if (this.responseURL == "https://www.wanikani.com/review/queue") {
                updateAssignmentsCache();

                responseText = modifyReviewQueue(responseText);
            }

            return responseText;
        }
    });

    // DOM fiddling

    function graphMarkup() {
        return '' +
            '<div class="pure-g-r">' +
            '  <div class="pure-u-1">' +
            '    <fieldset>' +
            '      <legend>Lock Script options</legend>' +
            '      API Key V2: <input style="font-family: monospace; width: 300px;" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" id="lockScriptAPIKey"> <input type="button" value="Set API key" id="setAPIKey">  <input type="button" value="Reload cache" id="reloadCache"><br>(See <a href="https://www.wanikani.com/settings/account">Account settings</a> to get your API Version *2* key).<br>' +
            '      Locked: <input id="lockScriptLockCount"><br>' +
            '      <input type="button" id="lockScriptSetOptions" value="Save options"><br>' +
            '    </fieldset>' +
            '  </div>' +
            '</div>';
    }

    function lockCountMarkup(numLocked) {
        return '' +
            '<span class="review-lock-count" style="background-color: black; color: white; display: inline-block; line-height: 3em; padding-left: 16px;">' +
            '  <i style="margin-right: 8px" class="icon-lock"></i>' +
            '  <span>' + numLocked + '</span>' +
            '</span>';
    }

    document.body.addEventListener('DOMSubtreeModified', function (event) {

        var attrs = event.target.attributes;

        if (window.location.pathname == "/review") {

            if ((event.target.tagName == 'DIV') &&
                (attrs.class && attrs.class.value == 'review-stats-value')) {

                if (!window.lockMarkupAdded) {

                    window.lockMarkupAdded = true;

                    updateAssignmentsCache();

                    window.jQuery('#review-stats').parent('.pure-g-r').after(graphMarkup);

                    var config = getConfig();

                    window.jQuery('#setAPIKey').click(setAPIKey);
                    window.jQuery('#reloadCache').click(reloadCache);

                    var availableCount = 0;
                    var now = Date.now() / 1000;

                    Object.keys(config.availableAt).forEach(function (key) {
                        if (config.availableAt[key].t < now) {
                            availableCount++;
                        }
                    });

                    window.jQuery('#lockScriptAPIKey').val(getAPIKey());

                    if ( window.managedToUpdateLockCache) {

                        window.jQuery('#lockScriptLockCount').val(config.lockCount);

                        var numLocked = config.lockCount;
                        var numUnlocked = availableCount - config.lockCount;

                        window.jQuery('#review-queue-count').before(lockCountMarkup(numLocked));
                        window.jQuery('#review-queue-count').text(numUnlocked);
                    }

                    window.jQuery('#lockScriptSetOptions').click(function () {

                        var config = getConfig();

                        var lockCount = window.jQuery('#lockScriptLockCount').val();

                        config.lockCount = lockCount;

                        setConfig(config);
                    });
                }
            }
        }

        if (window.location.pathname == "/review/session") {

            if ((event.target.tagName == 'DIV') &&
                (attrs.id && attrs.id.value == 'loading') &&
                (attrs.style && attrs.style.value == "display: none;")) {

                if (window.lockScriptFilter) {
                    window.jQuery('#stats').prepend("<i class='icon-lock'></i><span>" +
                        window.lockScriptLockCount + "</span>");
                }
            }
        }

    }, false);

})(XMLHttpRequest.prototype);