WaniKani Resurrect Manager

Utility to mass resurrect and reburn items

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         WaniKani Resurrect Manager
// @namespace    rwesterhof
// @version      1.1
// @description  Utility to mass resurrect and reburn items
// @include      /^https:\/\/(www|preview)\.wanikani\.com\/(radicals|kanji|vocabulary)/
// @run-at       document-end
// @grant        none
// @license      GPL-3.0-or-later
// ==/UserScript==

(function() {
    'use strict';

    /* global $, wkof */

    wkof.include('Settings,ItemData');
    wkof.ready('Settings').then(init_constants).then(add_css).then(installButton);

    //===== CONSTANTS
    var userLevel;
    const actions = [ "-", '/resurrect', '/burn' ];
    const actionLabels = [ "None", 'Resurrect', 'Reburn' ];
    const baseUri = "/assignments/";
    const itemTypes = [ 'Radicals', 'Kanji', 'Vocabulary' ];
    const itemFilterTypes = [ 'radical', 'kanji', 'vocabulary' ];
    const levelGroups = [ 'Pleasant', 'Painful', 'Death', 'Hell', 'Paradise', 'Reality' ];
    const settings = {
        action: '0',
        minSrs: null,
        types: [],
        levels: []
    };

    function init_constants() {
        userLevel = $('li.user-summary__attribute a')[0].href.split('/level/')[1];
    }

    //===== ADD ENTRYPOINT TO PAGE
    function installButton() {
        // install icon
        var rmButton = '<button id="resurrectManager" onclick="manageResurrection()" title="Resurrect Manager" class="iconButton" style="float:right;"><i class="fa fa-fire"></i></button>';
        $('aside.subject-legend').prepend(rmButton);
    }

    //====== CONFIGURATION
    function getSelectedTypes() {
        return settings.types.map((value, index) => { return { type: index, selected: value }; }).filter(item => item.selected).map(item => itemFilterTypes[item.type]);
    }
    function getSelectedLevels() {
        return settings.levels.map((value, index) => { return { level: index, selected: value }; }).filter(item => item.selected).map(item => item.level);
    }
    function getMinSrs() {
        if (settings.action == '2') {
            return settings.minSrs;
        }
        // no meaning for anything other than Reburn
        return null;
    }

    //======== CHECK PRIORS AND PROCESS THE ACTION
    function calculateAndExecute() {
        if (settings.action == '0') {
            console.log("No action selected");
            return;
        }
        var selectedTypes = getSelectedTypes();
        if (selectedTypes.length == 0) {
            console.log("No types selected");
            return;
        }
        var selectedLevels = getSelectedLevels();
        if (selectedLevels.length == 0) {
            console.log("No levels selected");
            return;
        }

        wkof.ready('ItemData').then(fetchItems).then(calculate).then(executeAction);
    }

    //======== PROCESS THE ACTION
    var progressData = [];
    var forceWkOfUpdate = false;
    function executeAction(itemsToProcess) {

        var actionIndex = settings.action;
        var progressIndex = progressData.length;
        var authToken = $('meta[name="csrf-param"]')[0].content;
        var authTokenValue = $('meta[name="csrf-token"]')[0].content;

        progressData[progressIndex] = {
            name: 'resurrect_manager_progress',
            label: actionLabels[actionIndex],
            value: 0,
            max: itemsToProcess.itemIds.length,
            actionIndex: actionIndex,
            authToken: authToken,
            authTokenValue: authTokenValue,
            failed :0
        };
        wkof.Progress.update(progressData[index]);


        const request_delay = 200; //ms
        var index = 0;

        itemsToProcess.itemIds.map(itemId => {
            setTimeout("window.resurrect_manager_actionItem(" + itemId + ", " + progressIndex + ")", index * request_delay);
            index++;
        });
        forceWkOfUpdate = true;
    }

    //======== DELAYED PROCESSING
    function actionItem(itemId, progressIndex) {
        postUpdate(baseUri + itemId + actions[progressData[progressIndex].actionIndex], progressData[progressIndex].authToken, progressData[progressIndex].authTokenValue).then(processActionResult, failActionResult);

        //===================
        function processActionResult(html) {
            progressData[progressIndex].value += 1;
            wkof.Progress.update(progressData[progressIndex]);
            if (progressData[progressIndex].value == progressData[progressIndex].max) {
                console.log(progressData[progressIndex].label + " " + progressData[progressIndex].max + " items complete. Failed " + progressData[progressIndex].failed + " items.");
            }
        }
        function failActionResult(reason) {
            console.log(progressData[progressIndex].label + " of " + itemId + " failed: " + reason);
            progressData[progressIndex].value += 1;
            progressData[progressIndex].failed += 1;
            wkof.Progress.update(progressData[progressIndex]);
            if (progressData[progressIndex].value == progressData[progressIndex].max) {
                console.log(progressData[progressIndex].label + " " + progressData[progressIndex].max + " items complete. Failed " + progressData[progressIndex].failed + " items.");
            }
        }
    }
    window.resurrect_manager_actionItem=actionItem;

    //======== FIRE THE HTML REQUEST
    function promise(){var a,b,c=new Promise(function(d,e){a=d;b=e;});c.resolve=a;c.reject=b;return c;}
	function postUpdate(url, authToken, authTokenValue) {
		var update_promise = promise();

        var request = new XMLHttpRequest();
        request.onreadystatechange = process_result;
        request.open('POST', url, true);
        request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
        request.send("_method=put&"+authToken+"="+ authTokenValue);
        return update_promise;

		function process_result(event){
			if (event.target.readyState !== 4) return;
			if (event.target.status >= 400 || event.target.status === 0) {
                return update_promise.reject(event.target.status);
            }
            update_promise.resolve(event.target.response);
		}
	}

    //======== CALCULATE IMPACT OF CHOICES AND DISPLAY ON BUTTON
    function displayCounts() {
        wkof.ready('ItemData').then(fetchItems).then(calculate).then((itemsToProcess) => {
            $('button.setting[name="calculateButton"]').html("Found " + itemsToProcess.itemIds.length + " items - Recount");
        });
    }

    //======== WKOF ITEM RETRIEVAL
    function fetchItems() {
        var selectedTypes = getSelectedTypes().join(",");
        var selectedLevels = getSelectedLevels().join(",");
//        console.log("Finding " + selectedTypes + " items from levels " + selectedLevels + " for action " + actions[settings.action]);

        var config = {
            wk_items: {
                options: { assignments: true },
			    filters: {
                    have_burned: true,
                    level: selectedLevels,
                    item_type: selectedTypes
                }
			}
		};

        var minSrs = getMinSrs();
        if ((minSrs) && (minSrs != '0')) {
            config.wk_items.filters.srs = minSrs;
        }

        // we force update after an action because the 60s wait can mess with expected counts
		var promise = wkof.ItemData.get_items(config, { force_update: forceWkOfUpdate });
        forceWkOfUpdate = false;
        return promise;
    }

    //======== POST PROCESSING OF ITEM RETRIEVAL - CREATE LIST OF IDS TO ACTION
    function calculate(items) {
        window.tempVar = items;
        var result = {
            itemIds: []
        };
        items.map(item => {
            if (   ((settings.action == '2') && (item.assignments.resurrected_at != null))
                || ((settings.action == '1') && (item.assignments.resurrected_at == null))
               ) {
                result.itemIds.push(item.id)
            }
        });
//        console.log("Found " + result.itemIds.length + " items");
        return result;
    }

    // Load settings and set them to blank
    function blank_settings() {
        settings.action = '0';
        settings.minSrs = null,
        settings.types = [];
        settings.levels = [];
    }

    // Opens the manager
    function manageResurrection() {
        blank_settings();

        var config = {
            script_id: 'resurrect_manager',
            title: 'Resurrect Manager',
            on_save: calculateAndExecute,
            content: {
                resurrectPage: {
                    type: 'page',
                    label: 'Manager',
                    hover_tip: 'Choose action, type and levels and click Calculate',
                    content: {
                        resurrectOptions: {
                            type: 'group',
                            label: 'Options',
                            content: {
                                action: {
                                    type: 'dropdown',
                                    label: 'Action',
                                    hover_tip: 'Choose whether to resurrect or reburn',
                                    default: '0',
                                    no_save: true,
                                    on_change: triggerActionChange,
                                    content: {
                                        0: 'none',
                                        1: 'Resurrect',
                                        2: 'Reburn'
                                    }
                                },
                                minSrs: {
                                    type: 'dropdown',
                                    label: '',
                                    hover_tip: 'Limit reburns to items that have reached at least srs stage',
                                    default: '0',
                                    no_save: true,
                                    on_change: triggerMinSrsChange,
                                    content: {
                                        '0'             : 'Always',
                                        '2,3,4,5,6,7,8' : "When at least Apprentice II",
                                        '5,6,7,8'       : 'When at least Guru',
                                        '7,8'           : 'When at least Master',
                                        '8'             : 'When Enlightened'
                                    }
                                },
                                typeButton: {
                                    type: 'button',
                                    on_change: triggerTypeChange
                                },
                                types: {
                                    type: 'html',
                                    wrapper: 'row',
                                    hover_tip: 'Choose which types to act on'
                                },
                                calculateButton: {
                                    type: 'button',
                                    text: 'Count items',
                                    on_click: displayCounts
                                }
                            }
                        },
                        levels: {
                            type: 'group',
                            label: 'Levels',
                            content: {
                                levelButton: {
                                    type: 'button',
                                    on_change: triggerLevelChange
                                },
                                levelBoxes: {
                                    type: 'html',
                                    wrapper: 'row',
                                    hover_tip: 'Choose which levels to act on'
                                }
                            }
                        }
                    }
                }
            }
        };

        var selectedTypes = config.content.resurrectPage.content.resurrectOptions.content.types;
        selectedTypes.html = '<table width="100%"><tr>';
        for (var index in itemTypes) {
            selectedTypes.html += '<td><input type="checkbox" class="setting" id="resurrect_manager_types_' + index + '" name="typeButton" /><span style="margin-left:5%;">' + itemTypes[index] + '</span></td>';
        }
        selectedTypes.html += '</tr></table>';

        var levelBoxes = config.content.resurrectPage.content.levels.content.levelBoxes;
        levelBoxes.html = '<table width="100%"><thead><tr>';
        for (var levelGroup in levelGroups) {
            levelBoxes.html += '<th style="width:16%;height:2em;text-align:left;">' + levelGroups[levelGroup] + '</th>';
        }
        levelBoxes.html += '</tr></thead><tbody>';
        for (var ones = 0; ones <= 9; ones++) {
            levelBoxes.html += '<tr>';
            for (var tens = 0; tens <= 5; tens++) {
                levelBoxes.html += '<td>';
                var processing = 10*tens + ones + 1;
                if (processing <= userLevel) {
                    levelBoxes.html += '<input type="checkbox" class="setting" id="resurrect_manager_levels_' + processing + '" name="levelButton" /><span style="display:inline-block;width:30%;text-align:right;">' + processing + '</span>';
                }
                levelBoxes.html += '</td>';
            }
            levelBoxes.html += '</tr>';
        }
        levelBoxes.html += "</tbody></table>";

        var dialog = new wkof.Settings(config);
        dialog.refresh();
        dialog.open();
    }
    window.manageResurrection = manageResurrection;

    //======== CSS TO ENFORCE DISPLAY OF OPTIONS DIALOG
    function add_css() {
        $('head').append(
            `<style id="resurrect_manager_css">
                 #wkof_ds .wkof_settings button.setting[name="typeButton"],
                 #wkof_ds .wkof_settings button.setting[name="levelButton"],
                 #wkof_ds .wkof_settings select.setting[name='minSrs'],
                 #wkof_ds .wkof_settings label[for="resurrect_manager_minSrs"] {
                     display:none;
                 }
             </style>`);
    }

    //======== OPTION UPDATES
    function triggerActionChange(name, value, item) {
        settings.action = value;
        $('#wkof_ds .wkof_settings label[for="resurrect_manager_minSrs"').css('display', (settings.action == '2') ? 'inline-block' : 'none');
        $('#wkof_ds .wkof_settings select.setting[name="minSrs"]').css('display', (settings.action == '2') ? 'inline-block' : 'none');
    }

    function triggerMinSrsChange(name, value, item) {
        settings.minSrs = value;
    }

    function triggerTypeChange(name, value, item) {
        var elem = $(event.currentTarget);
        var idComponents = elem.attr('id').split("_");
        settings.types[idComponents[idComponents.length-1]] = elem.is(":checked");
    }

    function triggerLevelChange(name, value, item) {
        var elem = $(event.currentTarget);
        var idComponents = elem.attr('id').split("_");
        settings.levels[idComponents[idComponents.length-1]] = elem.is(":checked");
    }
})();