Wanikani Burn Manager

Mass Resurrect/Retire of Burn items on WaniKani

目前为 2017-04-05 提交的版本。查看 最新版本

// ==UserScript==
// @name        Wanikani Burn Manager
// @namespace   rfindley
// @description Mass Resurrect/Retire of Burn items on WaniKani
// @version     1.0.2
// @include     https://www.wanikani.com/*
// @exclude     https://www.wanikani.com/lesson*
// @exclude     https://www.wanikani.com/review*
// @copyright   2016+, Robin Findley
// @license     MIT; http://opensource.org/licenses/MIT
// @run-at      document-end
// @grant       none
// ==/UserScript==

wkburnmgr = {};

(function(gobj) {

    var settings = {
        fetch: {
            levels_per: 5,
            retries: 3,
            timeout: 10000,
            simultaneous: 5
        },
        retire: {
            simultaneous: 5
        }
    };

    var mgr_added = false;
    var busy = false;
    var fetched = {rad:[], kan:[], voc:[]}, items = {rad:[], kan:[], voc:[]}, item_info={rad:{}, kan:{}, voc:{}};
    var srslvls = ['Apprentice 1','Apprentice 2','Apprentice 3','Apprentice 4','Guru 1','Guru 2','Master','Enlightened','Burned'];
    var user_level = 0, apikey='';

    gobj.fetched = fetched;
    gobj.items = items;
    gobj.item_info = item_info;

    //============================================================================
    // Asynchronous serialization and parallelization. (c) 2016 Robin Findley
    //----------------------------------------------------------------------------
    // Functions:
    //   do_parallel_n(n, [func], list);  // Do N items at a time.
    //   do_parallel([func], list);       // Do all items at same time.
    //   do_serial([func], list);         // Do 1 item one at a time.
    //
    // Parameters:
    //   n - Number of items to do at a time.
    //   func - If present, single function to run once for each item in list.
    //   list - If func is present, each element is args passed to func.
    //          If func is absent, each element is a function to call.
    //            You can use .bind() on functions to pass parameters (see bind()).
    //
    // Example:
    //   do_serial([
    //     initialize,
    //     do_parallel_n(2, fetch, [
    //       'http://www.example.com/page1',
    //       'http://www.example.com/page2',
    //       'http://www.example.com/page3'
    //     ]),
    //     parse_pages,
    //     do_parallel([
    //       setTimeout(console.log.bind(console,'Delay 1'), 500),
    //       setTimeout(console.log.bind(console,'Delay 2'),1000),
    //       setTimeout(console.log.bind(console,'Delay 3'),1500)
    //     ])
    //   ])
    //   .then(console.log.bind(console,'Done!'));
    //   .fail(console.log.bind(console,'Fail!'));
    //----------------------------------------------------------------------------
    function do_parallel_n(_n, _func, _list) {
        var self = this,
            promise = $.Deferred(),
            next = 0,
            simul = _n,
            completed = 0,
            mode = 0,
            result = [],
            list, func;

        if (typeof _func === 'function') {
            mode = 1;
            func = _func;
            list = _list;
        } else {
            mode = 0;
            list = _func;
        }
        if (simul <= 0) simul = list.length;
        if (list.length === 0) return promise.resolve();

        function do_one(value) {
            if (promise.state() === 'rejected') return;
            var n = next++;
            var p;

            if (mode === 0) {
                p = list[n](value);
            } else {
                var args = list[n];
                if (!Array.isArray(args))
                    args = [args];
                if (simul===1)
                    args.push(value);
                p = func.apply(self,args);
            }

            if ((typeof p !== 'object') || (typeof p.then !== 'function'))
                finish(p);
            else
                p.then(finish, fail);

            function finish(value) {
                result[n] = value;
                if (value === false) return promise.reject(result);
                if (next < list.length) do_one(value);
                if (++completed === list.length) promise.resolve(result);
            }

            function fail(value) {
                result[n] = value;
                promise.reject(result);
            }
        }

        for (var i = 0; i < simul && next < list.length; i++)
            do_one();

        return promise;
    }

    //----------------------------------------------------------------------------
    function do_parallel(_func, _list) {return do_parallel_n.call(this, -1, _func, _list);}
    function do_serial(_func, _list) {return do_parallel_n.call(this, 1, _func, _list);}

    //============================================================================
    // Fetch a document from the server.
    //----------------------------------------------------------------------------
    function ajax_retry(url, retries, timeout, callback, _options) {
        var promise = $.Deferred();
        retries = retries || 1;
        timeout = timeout || 120000;

        var options = {
            url: url,
            timeout: timeout
        };

        if (typeof callback === 'function') {
//            if (debug.dev_server)
                options.dataType = 'json';
//            else
//                options.dataType = 'jsonp';
        }
        $.extend(options, _options);

        function action() {
            $.ajax(options)
                .done(function(data, status){
                // Call optional callback.
                if (typeof callback === 'function')
                    callback(url, status, data);

                // Resolve or reject
                if (status === 'success')
                    promise.resolve(data);
                else
                    promise.reject();
            })
                .fail(function(xhr, status, error){
                // Try again?
                if (status === 'error' && --retries > 0)
                    return action();

                // Call optional callback, then reject.
                if (typeof callback === 'function')
                    callback(url, status, error, xhr);
                promise.reject();
            });
        }

        action();

        return promise;
    }

    //-------------------------------------------------------------------
    // Convert level-selection string to array.
    //-------------------------------------------------------------------
    function levels_from_string(str) {
      var levels = [];
      str = str.replace(' ','');
      $.each(str.split(','), function(idx, val){
        var dash = val.split('-');
        if (dash.length < 1 || dash.length > 2) return;
        var n1 = Number(dash[0]);
        var n2 = n1;
        if (dash.length === 2) n2 = Number(dash[1]);
        if (isNaN(n1) || isNaN(n2) || n1 < 1 || n2 < 1 || n1 > n2 || n1 > 200 || n2 > 200) return [];
        for (var n = n1; n <= n2; n++)
          levels[n] = 1;
      });
      return levels;
    }

    //-------------------------------------------------------------------
    // Convert level-selection array to string.
    //-------------------------------------------------------------------
    function levels_to_string(arr) {
      var levels = [];
      var n1 = 1, n2, cnt = arr.length;
      while (n1 <= cnt) {
        if (arr[n1] === 1) {
          n2 = n1+1;
          while (n2 <= cnt && arr[n2] ===1) n2++;
          n2--;
          if (n1 == n2)
            levels.push(n1);
          else
            levels.push(n1+'-'+n2);
          n1 = n2;
        }
        n1++;
      }
      return levels.join(',');
    }

    //-------------------------------------------------------------------
    // Add a <style> section to the document.
    //-------------------------------------------------------------------
    function addStyle(aCss) {
        var head, style;
        head = document.getElementsByTagName('head')[0];
        if (head) {
            style = document.createElement('style');
            style.setAttribute('type', 'text/css');
            style.textContent = aCss;
            head.appendChild(style);
            return style;
        }
        return null;
    }

    //-------------------------------------------------------------------
    // Display the Burn Manager object.
    //-------------------------------------------------------------------
    function add_mgr() {
        var html =
            '<div id="burn_mgr"><div id="burn_mgr_box" class="container">'+
            '<h3 class="small-caps invert">Burn Manager <span id="burn_mgr_instr" href="#">[ Instructions ]</span></h3>'+

            '<form accept-charset="UTF-8" action="#" class="form-horizontal"><fieldset class="additional-info">'+

            // Instructions
            '  <div class="instructions">'+
            '    <div class="header small-caps invert">Instructions</div>'+
            '    <div class="content">'+
            '      <p>Enter your Resurrect/Retire criteria below, then click <span class="btn">Preview</span>.<br>A preview window will open, showing burn items matching the Level and Type criteria.<br>'+
                     'You can change your criteria at any time, then click <span class="btn">Preview</span> again to update your settings... but any <b>manually toggled changes will be lost</b>.</p>'+
            '      <p class="nogap">In the preview window:</p>'+
            '      <ul>'+
            '        <li><b>Hover</b> over an item to see <b>item details</b>.</li>'+
            '        <li><b>Click</b> an item to <b>toggle</b> its desired state between <b>Resurrect</b> and <b>Retired</b>.</li>'+
            '      </ul>'+
            '      <p>After you have adjusted all items to their desired state, click <span class="btn">Execute</span> to begin changing you item statuses<br>'+
                     'While executing, please allow the progress bar to reach 100% before navigating to another page, otherwise some items will not be Resurrected or Retired.</p>'+
            '      <span class="rad">十</span><span class="kan">本</span><span class="voc">本当</span> = Will be Resurrected<br>'+
            '      <span class="rad inactive">十</span><span class="kan inactive">本</span><span class="voc inactive">本当</span> = Will be Retired'+
            '    </div>'+
            '  </div>'+

            // Settings
            '  <div class="control-group">'+
            '    <label class="control-label" for="burn_mgr_levels">Level Selection:</label>'+
            '    <div class="controls">'+
            '      <input id="burn_mgr_levels" type="text" autocomplete="off" class="span6" max_length=255 name="burn_mgr[levels]" placeholder="Levels to resurrect or retire (e.g. &quot;1-3,5&quot;)" value>'+
            '    </div>'+
            '  </div>'+
            '  <div class="control-group">'+
            '    <label class="control-label">Item types:</label>'+
            '    <div id="burn_mgr_types" class="controls">'+
            '      <label class="checkbox inline"><input id="burn_mgr_rad" name="burn_mgr[rad]" type="checkbox" value="1" checked="checked">Radicals</label>'+
            '      <label class="checkbox inline"><input id="burn_mgr_kan" name="burn_mgr[kan]" type="checkbox" value="1" checked="checked">Kanji</label>'+
            '      <label class="checkbox inline"><input id="burn_mgr_voc" name="burn_mgr[voc]" type="checkbox" value="1" checked="checked">Vocab</label>'+
            '    </div>'+
            '  </div>'+
            '  <div class="control-group">'+
            '    <label class="control-label" for="burn_mgr_initial">Action / Initial State:</label>'+
            '    <div id="burn_mgr_initial" class="controls">'+
            '      <label class="radio inline"><input id="burn_mgr_initial_current" name="burn_mgr[initial]" type="radio" value="0" checked="checked">No change / Current state</label>'+
            '      <label class="radio inline"><input id="burn_mgr_initial_resurrect" name="burn_mgr[initial]" type="radio" value="1">Resurrect All</label>'+
            '      <label class="radio inline"><input id="burn_mgr_initial_retire" name="burn_mgr[initial]" type="radio" value="2">Retire All</label>'+
            '    </div>'+
            '  </div>'+
            '  <div class="control-group">'+
            '    <div id="burn_mgr_btns" class="controls">'+
            '      <a id="burn_mgr_preview" href="#burn_mgr_preview" class="btn btn-mini">Preview</a>'+
            '      <a id="burn_mgr_execute" href="#burn_mgr_execute" class="btn btn-mini">Execute</a>'+
            '      <a id="burn_mgr_close" href="#burn_mgr_close" class="btn btn-mini">Close</a>'+
            '    </div>'+
            '  </div>'+

            // Preview
            '  <div class="status"><div class="message controls"></div></div>'+
            '  <div class="preview"></div>'+
            '  <div id="burn_mgr_item_info" class="hidden"></div>'+

            '</fieldset>'+
            '</form>'+
            '<hr>'+
            '</div></div>';

        var css =
            '#burn_mgr {display:none;}'+

            '#burn_mgr_instr {margin-left:20px; font-size:0.8em; opacity:0.8; cursor:pointer;}'+
            '#burn_mgr .instructions {display:none;}'+
            '#burn_mgr .instructions .content {padding:5px;}'+
            '#burn_mgr .instructions p {font-size:13px; line-height:17px; margin-bottom:1.2em;}'+
            '#burn_mgr .instructions p.nogap {margin-bottom:0;}'+
            '#burn_mgr .instructions ul {margin-left:16px; margin-bottom:1.2em;}'+
            '#burn_mgr .instructions li {font-size:13px; line-height:17px;}'+
            '#burn_mgr .instructions span {cursor:default;}'+
            '#burn_mgr .instructions .btn {color:#000; padding:0px 3px 2px 3px;}'+
            '#burn_mgr .noselect {-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;}'+

            '#burn_mgr h3 {'+
            '  margin-top:10px; margin-bottom:0px; padding:0 30px; border-radius: 5px 5px 0 0;'+
            '  background-color: #fbc042;'+
            '  background-image: -moz-linear-gradient(-45deg, #fbc550, #faac05);'+
            '  background-image: -webkit-linear-gradient(-45deg, #fbc550, #faac05);'+
            '  background-image: -o-linear-gradient(-45deg, #fbc550, #faac05);'+
            '  background-image: linear-gradient(-45deg, #fbc550, #faac05);'+
            '}'+

            '#burn_mgr form {border-radius:0 0 5px 5px; margin-bottom:10px;}'+
            '#burn_mgr #burn_mgr_box fieldset {border-radius:0 0 5px 5px; margin-bottom:0px; padding:10px;}'+
            '#burn_mgr .control-group {margin-bottom:10px;}'+
            '#burn_mgr .controls .inline {padding-right:10px;}'+
            '#burn_mgr .controls .inline input {margin-left:-15px;}'+
            '#burn_mgr_btns .btn {width:50px; margin-right:10px;}'+

            '#burn_mgr .status {display:none;}'+
            '#burn_mgr .status .message {display:inline-block; background-color:#ffc; padding:2px 10px; font-weight:bold; border:1px solid #999; min-width:196px;}'+

            '#burn_mgr .preview {display:none;}'+
            '#burn_mgr .header {padding:0px 3px; line-height:1.2em; margin:0px;}'+
            '#burn_mgr .preview .header .count {text-transform:none; margin-left:10px;}'+
            '#burn_mgr .content {padding:0px 2px 2px 2px; border:1px solid #999; border-top:0px; background-color:#fff; margin-bottom:10px; position:relative;}'+
            '#burn_mgr .content span {'+
            '  color:#fff;'+
            '  font-size:13px;'+
            '  line-height:13px;'+
            '  margin:0px 1px;'+
            '  padding:2px 3px 3px 2px;'+
            '  border-radius:4px;'+
            '  box-shadow:0 -2px 0 rgba(0,0,0,0.2) inset;'+
            '  display:inline-block;'+
            '}'+
            '#burn_mgr .rad {background-color:#0096e7; background-image:linear-gradient(to bottom, #0af, #0093dd);}'+
            '#burn_mgr .kan {background-color:#ee00a1; background-image:linear-gradient(to bottom, #f0a, #dd0093);}'+
            '#burn_mgr .voc {background-color:#9800e8; background-image:linear-gradient(to bottom, #a0f, #9300dd);}'+
            '#burn_mgr .rad.inactive {background-color:#c3e3f3; background-image:linear-gradient(to bottom, #d4ebf7, #c3e3f3);}'+
            '#burn_mgr .kan.inactive {background-color:#f3c3e3; background-image:linear-gradient(to bottom, #f7d4eb, #f3c3e3);}'+
            '#burn_mgr .voc.inactive {background-color:#e3c3f3; background-image:linear-gradient(to bottom, #ebd4f7, #e3c3f3);}'+

            '#burn_mgr .preview .content span {cursor:pointer;}'+

            '#burn_mgr_item_info {'+
            '  position: absolute;'+
            '  padding:8px;'+
            '  color: #eeeeee;'+
            '  background-color:rgba(0,0,0,0.8);'+
            '  border-radius:8px;'+
            '  font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;'+
            '  font-weight: bold;'+
            '  z-index:3;'+
            '}'+
            '#burn_mgr_item_info .item {font-size:2em; line-height:1.2em;}'+
            '#burn_mgr_item_info .item img {height:1em; width:1em; vertical-align:bottom;}'+
            '#burn_mgr_item_info>div {padding:0 8px; background-color:#333333;}'+

            '#burn_mgr hr {border-top-color:#bbb; margin-top:0px; margin-bottom:0px;}';

        addStyle(css);
        $(html).insertAfter($('.navbar'));

        // Add event handlers
        $('#burn_mgr_preview').on('click', on_preview);
        $('#burn_mgr_execute').on('click', on_execute);
        $('#burn_mgr_close').on('click', on_close);
        $('#burn_mgr_instr').on('click', on_instructions);

        mgr_added = true;
    }

    //-------------------------------------------------------------------
    // Toggle the Burn Manager open/closed.
    //-------------------------------------------------------------------
    function toggle_mgr(e) {
        if (e !== undefined) e.preventDefault();

        // Add the manager if not already.
        if (!mgr_added) add_mgr();

        $('#burn_mgr').slideToggle();
        $('html, body').animate({scrollTop:0},800);
    }

    //-------------------------------------------------------------------
    // Fetch API key from account page.
    //-------------------------------------------------------------------
    function get_apikey() {
        var promise = $.Deferred();

        apikey = localStorage.getItem('apiKey');
        if (typeof apikey === 'string' && apikey.length == 32)
            return promise.resolve();

        $('#burn_mgr .status .message').html('Fetching API key...');
        $('#burn_mgr .status').slideDown();
        ajax_retry('/settings/account')
        .then(function(page) {

            // --[ SUCCESS ]----------------------
            // Make sure what we got is a web page.
            if (typeof page !== 'string') {return reject();}

            // Extract the user name.
            page = $(page);

            // Extract the API key.
            apikey = page.find('#user_api_key').attr('value');
            if (typeof apikey !== 'string' || apikey.length !== 32)  {return reject();}

            localStorage.setItem('apiKey', apikey);
            promise.resolve();

        }).fail(function(result) {

            // --[ FAIL ]-------------------------
            $('#burn_mgr .status .message').html('Fetching API key...  FAILED!');
            promise.reject(new Error('Failed to fetch API key!'));

        });

        return promise;
    }

    //-------------------------------------------------------------------
    // Event handler for item click.
    //-------------------------------------------------------------------
    function item_click_event(e) {
        $(e.currentTarget).toggleClass('inactive');
    }

    //-------------------------------------------------------------------
    // Event handler for item hover info.
    //-------------------------------------------------------------------
    function item_info_event(e) {
        var hinfo = $('#burn_mgr_item_info');
        var target = $(e.currentTarget);
        switch (e.type) {
            //-----------------------------
            case 'mouseenter':
                var itype = target.data('type');
                var ref = target.data('ref');
                var item = item_info[itype][ref];
                var status = (can_resurrect(item)===true ? 'Retired' : 'Resurrected');
                var str = '<div class="'+itype+'">';
                switch (itype) {
                    case 'rad':
                        str += '<span class="item">Item: <span lang="ja">';
                        if (item.character !== null)
                            str += item.character+'</span></span><br />';
                        else
                            str += '<img src="'+item.image+'" /></span></span><br />';
                        str += 'Meaning: '+toTitleCase(item.meaning)+'<br />';
                        break;
                    case 'kan':
                        str += '<span class="item">Item: <span lang="ja">'+item.character+'</span></span><br />';
                        str += toTitleCase(item.important_reading)+': <span lang="ja">'+item[item.important_reading]+'</span><br />';
                        str += 'Meaning: '+toTitleCase(item.meaning)+'<br />';
                        if (item.user_specific.user_synonyms !== null && item.user_specific.user_synonyms.length > 0)
                            str += 'Synonyms: '+toTitleCase(item.user_specific.user_synonyms.join(', '))+'<br />';
                        break;
                    case 'voc':
                        str += '<span class="item">Item: <span lang="ja">'+item.character+'</span></span><br />';
                        str += 'Reading: <span lang="ja">'+item.kana+'</span><br />';
                        str += 'Meaning: '+toTitleCase(item.meaning)+'<br />';
                        if (item.user_specific.user_synonyms !== null && item.user_specific.user_synonyms.length > 0)
                            str += 'Synonyms: '+toTitleCase(item.user_specific.user_synonyms.join(', '))+'<br />';
                        break;
                }
                str += 'Level: '+item.level+'<br />';
                str += 'SRS Level: '+srslvls[item.user_specific.srs_numeric-1]+'<br />';
                str += 'Currently: '+status+'<br />';
                str += '</div>';
                hinfo.html(str);
                hinfo.css('left', target.offset().left - target.position().left);
                hinfo.css('top', target.offset().top + target.outerHeight() + 3);
                hinfo.removeClass('hidden');
                break;

            //-----------------------------
            case 'mouseleave':
                hinfo.addClass('hidden');
                break;
        }
    }

    //-------------------------------------------------------------------
    // Make first letter of each word upper-case.
    //-------------------------------------------------------------------
    function toTitleCase(str) {
        return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
    }

    //-------------------------------------------------------------------
    // Read the user's "initial state" setting.
    //-------------------------------------------------------------------
    function read_initial_state() {
        return Number($('#burn_mgr_initial input:checked').val());
    }

    //-------------------------------------------------------------------
    // Run when user clicks 'Preview' button
    //-------------------------------------------------------------------
    function on_preview(e, refresh) {
        if (refresh !== true) e.preventDefault();
        if (busy) return;
        busy = true;

        // Read the user's level selection criteria
        var levels = levels_from_string($('#burn_mgr_levels').val());

        // Convert the input field to normalized form
        $('#burn_mgr_levels').val(levels_to_string(levels));

        // Fetch data
        load_data(levels)
        .then(function(){
            // Hide the "Loading" message.
            busy = false;
            $('#burn_mgr .status').slideUp();

            var html = '';
            var itypes = ['rad', 'kan', 'voc'];
            var state = read_initial_state();
            if (refresh === true) state = 0;
            var get_initial = [
                /* 0 */ function(item) {return (can_retire(item)===true);}, // Show current item state.
                /* 1 */ function(item) {return true;},                      // Mark all items for resurrection.
                /* 2 */ function(item) {return false;},                     // Mark all items for retirement.
            ][state];

            for (var level = 1; level <= user_level; level++) {
                if (levels[level] !== 1) continue;
                var active = {rad:0, kan:0, voc:0};
                var total = {rad:0, kan:0, voc:0};
                var item_html = '';
                $.each(itypes, function(idx, itype){
                    // Skip item types that aren't checked.
                    var subact=0, subtot=0;
                    var list = items[itype][level];
                    if (!$('#burn_mgr_'+itype).is(':checked')) return;
                    if (list === undefined) return;
                    $.each(list,function(idx,item){
                        var text, ref, state;
                        if (itype === 'rad') {
                            if (item.image !== null)
                                text = '<i class="radical-'+item.meaning+'"></i>';
                            else
                                text = item.character;
                        } else {
                            text = item.character;
                        }
                        ref = item.ref;
                        if (get_initial(item)) {
                            state = '';
                            subact++;
                        } else {
                            state = ' inactive';
                        }
                        subtot++;
                        item_html += '<span class="'+itype+state+'" data-type="'+itype+'" data-ref="'+ref+'">'+text+'</span>';
                    });
                    active[itype] += subact;
                    total[itype] += subtot;
                });
                html +=
                    '<div class="header small-caps invert">Level '+level+
//                    '  <span class="count">Resurrected: [ R: '+active.rad+'/'+total.rad+', K: '+active.kan+'/'+total.kan+', V: '+active.voc+'/'+total.voc+' ]</span>'+
                    '</div>'+
                    '<div class="content level noselect">'+
                    item_html+
                    '</div>';
            }
            $('#burn_mgr .preview').html(html).slideDown();
            $('#burn_mgr .preview .content.level')
                .on('mouseenter', 'span', item_info_event)
                .on('mouseleave', item_info_event)
                .on('click', 'span', item_click_event);
        });
    }

    //-------------------------------------------------------------------
    // Run when user clicks 'Execute' button
    //-------------------------------------------------------------------
    function on_execute(e) {
        e.preventDefault();
        if (busy) return;
        busy = true;

        var status = $('#burn_mgr .status'), message = $('#burn_mgr .status .message');

        // Read the user's level selection criteria
        var levels = levels_from_string($('#burn_mgr_levels').val());

        // Convert the input field to normalized form
        $('#burn_mgr_levels').val(levels_to_string(levels));

        // Fetch data
        load_data(levels)
        .then(function(){
            var task_list = [];
            var xlat = {rad:'radical',kan:'kanji',voc:'vocabulary'};
            var use_preview = $('#burn_mgr .preview').is(':visible');
            if (use_preview) {
                $('#burn_mgr .preview .content span').each(function(idx,elem){
                    elem = $(elem);
                    var ref = elem.data('ref');
                    var itype = elem.data('type');
                    var item = item_info[itype][ref];
                    var current = can_resurrect(item);
                    var want = elem.hasClass('inactive');
                    if (current != want)
                        task_list.push({url:'/retired/'+xlat[itype]+'/'+ref+'?'+(want?'retire':'resurrect')+'=true',item:item});
                });
            } else {
                // Don't use Preview information.
                var state = read_initial_state();
                if (state !== 0) {
                    $.each(['rad','kan','voc'], function(idx, itype){
                        if (!$('#burn_mgr_'+itype).is(':checked')) return;
                        $.each(item_info[itype], function(idx, item){
                            var ref = item.ref;
                            var current = can_resurrect(item);
                            var want = (state===2);
                            if (current != want)
                                task_list.push({url:'/retired/'+xlat[itype]+'/'+ref+'?'+(want?'retire':'resurrect')+'=true',item:item});
                        });
                    });
                }
            }

            var tot = task_list.length;
            var cnt = 0;

            function task_done(item, url, status, text) {
                if (status !== 'success') return;

                message.html('Working... ('+(++cnt)+' of '+tot+')');
                var lines = text.split('\n');
                var idx;
                for (idx = lines.length-1; idx >= 0; idx--) {
                    var line = lines[idx];
                    if (line.match(/var progression/) === null) continue;
                    var json = JSON.parse(line.match(/(\{.*\})/g)).requested_information;
                    item.user_specific.burned = json.burned;
                    item.user_specific.burned_date = json.burned_date;
                    item.user_specific.available_date = json.available_date;
                    item.user_specific.srs = json.srs_level;
                    break;
                }
            }

            function retire(task) {
                return ajax_retry(task.url, settings.retire.retries, settings.retire.timeout, task_done.bind(null,task.item), {
                    type:'POST',
                    data:'_method=put',
                    dataType:'text'
                }
                );
            }

            function finish() {
                message.html('Done! ('+cnt+' of '+tot+')');
                busy = false;
                on_preview(null, true /* refresh */);
            }

            message.html('Executing 0 / '+tot);
            status.slideDown();
            do_serial([
                do_parallel_n.bind(null,settings.retire.simultaneous, retire, task_list),
                finish
            ]);
        });
    }

    //-------------------------------------------------------------------
    // Run when user clicks 'Close' button
    //-------------------------------------------------------------------
    function on_close(e) {
        e.preventDefault();
        $('#burn_mgr').slideUp();
    }

    //-------------------------------------------------------------------
    // Run when user clicks 'Instructions'
    //-------------------------------------------------------------------
    function on_instructions(e) {
        e.preventDefault();
        $('#burn_mgr .instructions').slideToggle();
    }

    //-------------------------------------------------------------------
    // Return 'true' if item can be retired.
    //-------------------------------------------------------------------
    function can_retire(item){
        if (item.user_specific === null) return false;
        if (item.user_specific.srs !== 'burned' && item.user_specific.burned_date !== 0) return true;
        return false;
    }

    //-------------------------------------------------------------------
    // Return 'true' if item can be resurrected.
    //-------------------------------------------------------------------
    function can_resurrect(item){
        if (item.user_specific === null) return false;
        if (item.user_specific.srs === 'burned') return true;
        return false;
    }

    //-------------------------------------------------------------------
    // Return 'true' if item has been burned.
    //-------------------------------------------------------------------
    function has_burned(item){
        if (item.user_specific === null) return false;
        if (item.user_specific.srs === 'burned') return true;
        if (item.user_specific.burned_date !== 0) return true;
        return false;
    }

    //-------------------------------------------------------------------
    // Fetch API data for any requested levels that aren't yet fetched
    //-------------------------------------------------------------------
    function load_data(levels) {
        var criteria = [];
        var status = $('#burn_mgr .status'), message = $('#burn_mgr .status .message');
        var percent, total, done;

        var itypes = ['radicals', 'kanji', 'vocabulary'];
        $.each(itypes, function(idx, itype){
            // Skip item types that aren't checked.
            var itype3 = itype.slice(0,3);
            if (!$('#burn_mgr_'+itype3).is(':checked')) return;
            var needed = {itype:itype, levels:[]};
            for (level = 1; level <= user_level; level++) {
                if (levels[level] !== 1 || fetched[itype3][level] === 1) continue;
                needed.levels.push(level);
                if (itype3 !== 'rad' && needed.levels.length === settings.fetch.levels_per) {
                    criteria.push([needed]);
                    needed = {itype:itype, levels:[]};
                }
            }
            if (needed.levels.length > 0)
                criteria.push([needed]);
        });

        function received(criteria, url, status, json) {
            if (status !== 'success') return;

            done++;
            percent.html(Math.floor(done/total*100)+'%');

            // Mark which levels we've fetched, so we don't re-fetch on later queries.
            var itype = criteria.itype.slice(0,3);
            $.each(criteria.levels, function(idx, level){
                fetched[itype][level] = 1;
            });

            $.each(json.requested_information, function(idx, item){
                if (!has_burned(item)) return;
                if (itype === 'rad')
                    item.ref = item.meaning;
                else
                    item.ref = item.character;
                var level_items = items[itype][item.level];
                if (level_items === undefined) {
                    level_items = [];
                    items[itype][item.level] = level_items;
                }
                level_items.push(item);
                item_info[itype][item.ref] = item;
            });
        }

        function fetch(criteria) {
            var url = 'https://www.wanikani.com/api/user/'+apikey+'/'+criteria.itype+'/'+criteria.levels.join(',');
            return ajax_retry(url, settings.fetch.retries, settings.fetch.timeout, received.bind(null,criteria));
        }

        total = criteria.length;
        done = 0;
        if (total === 0) return $.Deferred().resolve();

        message.html('Loading item data: <span class="percent">0%</span>');
        status.slideDown();
        percent = $('#burn_mgr .status .percent');

        return do_serial([
            get_apikey,
            do_parallel_n.bind(null,settings.fetch.simultaneous, fetch, criteria)
        ]);
    }

    //-------------------------------------------------------------------
    // Startup. Runs at document 'load' event.
    //-------------------------------------------------------------------
    function startup() {
        // Make sure we have a top-menu, req'd for insertion.
        if ($('.navbar').length === 0) return;

        user_level = Number($('.levels span:nth(0)').text());

        // Insert a menu link in the drop-down menu.
        $('<li><a href="#burnmgr">Burn Manager</a></li>')
        .insertBefore($('.account .dropdown-menu .nav-header:eq(1)'))
        .on('click', toggle_mgr);
    }

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

})(wkburnmgr);

QingJ © 2025

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