Mass R###rrect/Retire of Burn items on WaniKani
// ==UserScript== // @name Wanikani Burn Manager // @namespace rfindley // @description Mass R###rrect/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 R###rrect/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>R###rrect</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 R###rrected or Retired.</p>'+ ' <span class="rad">十</span><span class="kan">本</span><span class="voc">本当</span> = Will be R###rrected<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 r###rrect 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_r###rrect" name="burn_mgr[initial]" type="radio" value="1">R###rrect 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_r###rrect(item)===true ? 'Retired' : 'R###rrected'); 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 r###rrection. /* 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_r###rrect(item); var want = elem.hasClass('inactive'); if (current != want) { task_list.push({url:'/assignments/'+ref+'/'+(want?'burn':'r###rrect'),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_r###rrect(item); if (current != want) { task_list.push({url:'/assignments/'+ref+'/'+(want?'burn':'r###rrect'),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(r###lt) { 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 r###rrected. //------------------------------------------------------------------- function can_r###rrect(item){ return (item.assignments.srs_stage === 9); } })(window.burnmgr);