// ==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 // @downloadURL https://update.greasyfork.icu/scripts/18822/Wanikani%20Burn%20Manager.user.js // @updateURL https://update.greasyfork.icu/scripts/18822/Wanikani%20Burn%20Manager.meta.js // ==/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 = '
'+ '

Burn Manager [ Instructions ]

'+ '
'+ // Instructions '
'+ '
Instructions
'+ '
'+ '

Enter your Resurrect/Retire criteria below, then click Preview.
A preview window will open, showing burn items matching the Level and Type criteria.
'+ 'You can change your criteria at any time, then click Preview again to update your settings... but any manually toggled changes will be lost.

'+ '

In the preview window:

'+ '
    '+ '
  • Hover over an item to see item details.
  • '+ '
  • Click an item to toggle its desired state between Resurrect and Retired.
  • '+ '
'+ '

After you have adjusted all items to their desired state, click Execute to begin changing you item statuses
'+ 'While executing, please allow the progress bar to reach 100% before navigating to another page, otherwise some items will not be Resurrected or Retired.

'+ ' 本当 = Will be Resurrected
'+ ' 本当 = Will be Retired'+ '
'+ '
'+ // Settings '
'+ ' '+ '
'+ ' '+ '
'+ '
'+ '
'+ ' '+ '
'+ ' '+ ' '+ ' '+ '
'+ '
'+ '
'+ ' '+ '
'+ ' '+ ' '+ ' '+ '
'+ '
'+ '
'+ '
'+ ' Preview'+ ' Execute'+ ' Close'+ '
'+ '
'+ // Preview '
'+ '
'+ ' '+ '
'+ '
'+ '
'+ '
'; 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(''); $(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 = '
'; 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 += 'Item: '; if (item.data.characters !== null) { str += item.data.characters+'
'; } else { str += '
'; } str += 'Meaning: '+toTitleCase(meaning_str)+'
'; if (item.study_materials && item.study_materials.meaning_synonyms.length > 0) { str += 'Synonyms: '+toTitleCase(item.study_materials.meaning_synonyms.join(', '))+'
'; } 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 += 'Item: '+item.data.characters+'
'; str += toTitleCase(important_reading)+': '+reading_str+'
'; str += 'Meaning: '+toTitleCase(meaning_str)+'
'; if (item.study_materials && item.study_materials.meaning_synonyms.length > 0) { str += 'Synonyms: '+toTitleCase(item.study_materials.meaning_synonyms.join(', '))+'
'; } 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 += 'Item: '+item.data.characters+'
'; str += 'Reading: '+reading_str+'
'; str += 'Meaning: '+toTitleCase(meaning_str)+'
'; if (item.study_materials && item.study_materials.meaning_synonyms.length > 0) { str += 'Synonyms: '+toTitleCase(item.study_materials.meaning_synonyms.join(', '))+'
'; } break; } str += 'Level: '+item.data.level+'
'; str += 'SRS Level: '+srslvls[item.assignments.srs_stage-1]+'
'; str += 'Currently: '+status+'
'; str += '
'; 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 += '
Level '+level+ '
'+ '
'+ item_html+ '
'; } 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 = ''; } else { text = item.data.characters; } } else { text = item.data.characters; } if (get_initial(item)) { state = ''; } else { state = ' inactive'; } item_html += ''+text+''; } $('#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