// ==UserScript==
// @name Wanikani Burn Manager
// @namespace rfindley
// @description Mass Resurrect/Retire of Burn items on WaniKani
// @version 2.0.5
// @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==
window.burnmgr = {};
(function(gobj) {
/* globals $, wkof */
/* eslint no-multi-spaces: "off" */
//===================================================================
// Initialization of the Wanikani Open Framework.
//-------------------------------------------------------------------
var script_name = 'Burn Manager';
if (!window.wkof) {
if (confirm(script_name+' requires Wanikani Open Framework.\nDo you want to be forwarded to the installation instructions?')) {
window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
}
return;
}
wkof.include('ItemData,Menu');
wkof.ready('ItemData,Menu').then(startup);
var mgr_added = false, busy = false, items, items_by_id;
function startup() {
wkof.Menu.insert_script_link({
name: 'burnmgr',
submenu: 'Open',
title: 'Burn Manager',
on_click: open_burnmgr
});
}
function open_burnmgr() {
// Add the manager if not already.
if (!mgr_added) add_mgr();
$('#burn_mgr').slideDown();
$('html, body').animate({scrollTop:0},800);
}
var srslvls = ['Apprentice 1','Apprentice 2','Apprentice 3','Apprentice 4','Guru 1','Guru 2','Master','Enlightened','Burned'];
//-------------------------------------------------------------------
// 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 > img {height:0.9em;}'+
'#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;}';
$('head').append('<style type="text/css">'+css+'</style>');
$(html).insertAfter($('.global-header'));
// 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;
}
//-------------------------------------------------------------------
// 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 = items_by_id[ref];
var status = (can_resurrect(item)===true ? 'Retired' : 'Resurrected');
var str = '<div class="'+itype+'">';
var readings, reading_str, important_reading, meanings, meaning_str, synonyms, synonym_str;
switch (itype) {
case 'rad':
meanings = item.data.meanings.filter(primary);
meaning_str = meanings.map(meaning).join(', ');
str += '<span class="item">Item: <span lang="ja">';
if (item.data.characters !== null) {
str += item.data.characters+'</span></span><br />';
} else {
str += '<img src="'+item.data.character_images[0].url+'" /></span></span><br />';
}
str += 'Meaning: '+toTitleCase(meaning_str)+'<br />';
if (item.study_materials && item.study_materials.meaning_synonyms.length > 0) {
str += 'Synonyms: '+toTitleCase(item.study_materials.meaning_synonyms.join(', '))+'<br />';
}
break;
case 'kan':
readings = item.data.readings.filter(primary);
important_reading = readings[0].type;
reading_str = readings.map(reading).join(', ');
meanings = item.data.meanings.filter(primary);
meaning_str = meanings.map(meaning).join(', ');
str += '<span class="item">Item: <span lang="ja">'+item.data.characters+'</span></span><br />';
str += toTitleCase(important_reading)+': <span lang="ja">'+reading_str+'</span><br />';
str += 'Meaning: '+toTitleCase(meaning_str)+'<br />';
if (item.study_materials && item.study_materials.meaning_synonyms.length > 0) {
str += 'Synonyms: '+toTitleCase(item.study_materials.meaning_synonyms.join(', '))+'<br />';
}
break;
case 'voc':
readings = item.data.readings.filter(primary);
reading_str = readings.map(reading).join(', ');
meanings = item.data.meanings.filter(primary);
meaning_str = meanings.map(meaning).join(', ');
str += '<span class="item">Item: <span lang="ja">'+item.data.characters+'</span></span><br />';
str += 'Reading: <span lang="ja">'+reading_str+'</span><br />';
str += 'Meaning: '+toTitleCase(meaning_str)+'<br />';
if (item.study_materials && item.study_materials.meaning_synonyms.length > 0) {
str += 'Synonyms: '+toTitleCase(item.study_materials.meaning_synonyms.join(', '))+'<br />';
}
break;
}
str += 'Level: '+item.data.level+'<br />';
str += 'SRS Level: '+srslvls[item.assignments.srs_stage-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;
}
}
//-------------------------------------------------------------------
// Filters and maps
//-------------------------------------------------------------------
function primary(info) {return info.primary;}
function meaning(info) {return info.meaning;}
function reading(info) {return info.reading;}
//-------------------------------------------------------------------
// 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;
var preview_is_open = $('#burn_mgr .preview').is(':visible');
if (preview_is_open) {
$('#burn_mgr .preview').html('').slideUp();
busy = true;
fetch_items(true /* force_update */).then(populate_data.bind(null, refresh));
} else {
busy = true;
fetch_items(true /* force_update */).then(populate_data.bind(null, refresh));
}
}
//-------------------------------------------------------------------
// Fetch the requested items
//-------------------------------------------------------------------
function fetch_items(force_update) {
var levels = $('#burn_mgr_levels').val();
if (levels === '') levels = '*';
var item_type = [];
if ($('#burn_mgr_rad').attr('checked') === 'checked') item_type.push('rad');
if ($('#burn_mgr_kan').attr('checked') === 'checked') item_type.push('kan');
if ($('#burn_mgr_voc').attr('checked') === 'checked') item_type.push('voc');
$('#burn_mgr .status .message').html('Fetching data...');
$('#burn_mgr .status').slideDown();
return wkof.ItemData.get_items({
wk_items: {
options: {subjects: true, assignments: true, study_materials: true},
filters: {
have_burned: true,
level: levels,
item_type: item_type
}
}
}, {force_update: force_update});
}
//-------------------------------------------------------------------
// Populate the item data on-screen.
//-------------------------------------------------------------------
function populate_data(refresh, data) {
// Hide the "Loading" message.
busy = false;
$('#burn_mgr .status').slideUp();
items = data;
items_by_id = wkof.ItemData.get_index(items, 'subject_id');
window.items = items;
var html = '';
var itypes = ['radical', 'kanji', 'vocabulary'];
var state = read_initial_state();
if (refresh === true) state = 0;
var get_initial = [
/* 0 */ function(item) {return can_retire(item);}, // 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];
var items_by_level = wkof.ItemData.get_index(items, 'level');
var item_html, items_by_type, level_items, itype3, list;
for (var level = 1; level <= wkof.user.level; level++) {
level_items = items_by_level[level];
if (!level_items) continue;
items_by_type = wkof.ItemData.get_index(level_items, 'item_type');
item_html = '';
$.each(itypes, populate_by_type);
html +=
'<div class="header small-caps invert">Level '+level+
'</div>'+
'<div class="content level noselect">'+
item_html+
'</div>';
}
function populate_by_type(idx, itype) {
// Skip item types that aren't checked.
itype3 = itype.slice(0,3);
list = items_by_type[itype];
if (!$('#burn_mgr_'+itype3).is(':checked')) return;
if (list === undefined) return;
$.each(list, populate_individual_items);
}
function populate_individual_items(idx,item){
var text, ref, state;
text = item.data.slug;
if (itype3 === 'rad') {
if (item.data.character_images.length > 0) {
text = '<img src="'+item.data.character_images[0].url+'">';
} else {
text = item.data.characters;
}
} else {
text = item.data.characters;
}
if (get_initial(item)) {
state = '';
} else {
state = ' inactive';
}
item_html += '<span class="'+itype3+state+'" data-type="'+itype3+'" data-ref="'+item.id+'">'+text+'</span>';
}
$('#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');
var use_preview = $('#burn_mgr .preview').is(':visible');
var task_list = [];
var auth_token = encodeURIComponent($('[name="csrf-token"]').attr('content'));
if (use_preview) {
$('#burn_mgr .preview .content span').each(function(idx,elem){
elem = $(elem);
var ref = elem.data('ref');
var item = items_by_id[ref];
var current = can_resurrect(item);
var want = elem.hasClass('inactive');
if (current != want) {
task_list.push({url:'/assignments/'+ref+'/'+(want?'burn':'resurrect'),item:item});
}
});
start_execute();
} else {
// Don't use Preview information.
fetch_items(true /* force_update */).then(function(items){
var state = read_initial_state();
if (state === 0) return;
var want = (state===2);
$.each(items, function(idx, item){
var ref = item.id;
var current = can_resurrect(item);
if (current != want) {
task_list.push({url:'/assignments/'+ref+'/'+(want?'burn':'resurrect'),item:item});
}
});
start_execute();
});
}
var cnt, tot;
function start_execute() {
tot = task_list.length;
cnt = 0;
message.html('Executing 0 / '+tot);
status.slideDown();
var simultaneous = Math.min(5, tot);
for (cnt=0; cnt<simultaneous; cnt++) {
retire(task_list[cnt]).then(next, next);
}
function next(result) {
if (cnt < tot) {
message.html('Working... ('+cnt+' of '+tot+')');
retire(task_list[cnt++]).then(next, next);
} else {
message.html('Done! ('+cnt+' of '+tot+')');
busy = false;
on_preview(null, true /* refresh */);
}
}
}
function retire(task) {
return new Promise(function(resolve, reject){
$.ajax(task.url, {
type:'POST',
data:'_method=put&authenticity_token='+auth_token,
dataType:'text'
}).done(function(){
resolve({status:'success', task:task});
}).fail(function(){
reject({status:'fail', task:task});
});
});
}
}
//-------------------------------------------------------------------
// Run when user clicks 'Close' button
//-------------------------------------------------------------------
function on_close(e) {
e.preventDefault();
var preview_is_open = $('#burn_mgr .preview').is(':visible');
if (preview_is_open) $('#burn_mgr .preview').html('').slideUp();
$('#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){
return (item.assignments.srs_stage !== 9);
}
//-------------------------------------------------------------------
// Return 'true' if item can be resurrected.
//-------------------------------------------------------------------
function can_resurrect(item){
return (item.assignments.srs_stage === 9);
}
})(window.burnmgr);