// ==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. "1-3,5")" 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);