// ==UserScript==
// @name Wanikani Self-Study
// @namespace rfindley
// @description Self-study your items via the Wanikani level pages
// @version 1.2.0
// @include https://www.wanikani.com/level/*
// @exclude https://www.wanikani.com/level/*/*
// @include https://www.wanikani.com/radicals*
// @exclude https://www.wanikani.com/radicals/*
// @include https://www.wanikani.com/kanji*
// @exclude https://www.wanikani.com/kanji/*
// @include https://www.wanikani.com/vocabulary*
// @exclude https://www.wanikani.com/vocabulary/*
// @copyright 2016+, Robin Findley
// @license MIT; http://opensource.org/licenses/MIT
// @run-at document-end
// @grant none
// ==/UserScript==
// Although this script is a 100% my own code, general credits go
// to Axel Örn Sigurðsson (WK user @absalon) for original concept:
// https://www.wanikani.com/chat/api-and-third-party-apps/8575
wkselfstudy = {};
(function(gobj) {
var settings = {
compatible: 2,
// ss_hidelocked - Hide locked items
// ss_hideunlocked - Hide unlocked items (includes ss_hideburn)
// ss_hideburned - Hide burned items
// ss_hideunburned - Hide unburned items (includes ss_hidelock)
// ss_hidechar - Hide the radical/kanji/vocab characters
// ss_hideread - Hide the reading
// ss_hidemean - Hide the meaning
configs: [
['Japanese to English', 'ss_hidelocked ss_hideread ss_hidemean'],
['English to Japanese', 'ss_hidelocked ss_hideread ss_hidechar'],
['[BURNED] Japanese to English', 'ss_hideunburned ss_hideread ss_hidemean'],
['[BURNED] English to Japanese', 'ss_hideunburned ss_hideread ss_hidechar'],
],
selected_config: 0,
enabled: true,
randomize_on_load: true
};
var html =
'<div class="selfstudy">'+
' <label>Self-study:</label>'+
' <div class="btn-group">'+
' <button class="btn enable">OFF</button>'+
' <button class="btn shuffle">Shuffle</button>'+
' <select class="btn config"></select>'+
' <button class="btn config">Config</button>'+
' </div>'+
'</div>';
var config =
'<div id="ss_config" class="hidden">'+
' <div class="btns">'+
' <button class="btn new">New</button>'+
' <button class="btn up">Up</button>'+
' <button class="btn dn">Down</button>'+
' <button class="btn del">Delete</button>'+
' </div>'+
' <div class="list">'+
' <select class="configs" size="7"></select>'+
' </div>'+
' <div class="txtline">'+
' <label>Preset name:</label>'+
' <div class="expand"><input type="text" class="preset"></div>'+
' </div>'+
' <div class="cbbox">'+
' <div><label>Hide Rad/Kan/Voc:</label><input type="checkbox" name="ss_hidechar"></div>'+
' <div><label>Hide Reading:</label><input type="checkbox" name="ss_hideread"></div>'+
' <div><label>Hide Meaning:</label><input type="checkbox" name="ss_hidemean"></div>'+
' </div>'+
' <div class="cbbox">'+
' <div><label>Hide Locked:</label><input type="checkbox" name="ss_hidelocked"></div>'+
' <div><label>Hide Unlocked:</label><input type="checkbox" name="ss_hideunlocked"></div>'+
' <div><label>Hide Unburned:</label><input type="checkbox" name="ss_hideunburned"></div>'+
' <div><label>Hide Burned:</label><input type="checkbox" name="ss_hideburned"></div>'+
' </div>'+
' <div class="dlg_close">'+
' <div class="btn-group">'+
' <button class="btn save">Save</button>'+
' <button class="btn cancel">Cancel</button>'+
' </div>'+
' </div>'+
'</div>';
var css =
'.selfstudy {margin-left:20px; margin-bottom:10px; position:relative;}'+
'.selfstudy label {display:inline; vertical-align:middle; padding-right:4px; color:#999; font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif; text-shadow:0 1px 0 #fff;}'+
'.selfstudy button.enable {width:55px;}'+
'.ss_active .selfstudy button.enable.on {background-color:#b3e6b3; background-image:linear-gradient(to bottom, #ecf9ec, #b3e6b3);}'+
'.selfstudy select.config {width:300px;}'+
'section[id^="level-"].ss_active.ss_hidechar .character-item a span:not(.dummy) {opacity:0; transition:opacity ease-in-out 0.15s}'+
'section[id^="level-"].ss_active.ss_hideread .character-item a li[lang="ja"] {opacity:0; transition:opacity ease-in-out 0.15s}'+
'section[id^="level-"].ss_active.ss_hidemean .character-item a li:not([lang="ja"]) {opacity:0; transition:opacity ease-in-out 0.15s}'+
'section[id^="level-"].ss_active.ss_hideburned .character-item.burned {display:none;}'+
'section[id^="level-"].ss_active.ss_hidelocked .character-item.locked {display:none;}'+
'section[id^="level-"].ss_active.ss_hideunburned .character-item:not(.burned) {display:none;}'+
'section[id^="level-"].ss_active.ss_hideunlocked .character-item:not(.locked) {display:none;}'+
'section.ss_active .character-item:hover a span {opacity: initial !important; transition:opacity ease-in-out 0.05s !important;}'+
'section.ss_active .character-item:hover a li {opacity: initial !important; transition:opacity ease-in-out 0.05s !important;}'+
'#ss_config {position:absolute; z-index:3; width:550px; background-color:rgba(0,0,0,0.8); border-radius:8px; padding:8px;}'+
'#ss_config select.configs {width:475px;}'+
'#ss_config label {color:#ccc; text-shadow:initial; text-align:right; vertical-align:baseline;}'+
'#ss_config .btns {display:inline-block; float:left; vertical-align:top; margin-right:8px;}'+
'#ss_config .btns .btn {display:block; margin-bottom:5px;}'+
'#ss_config .btn {width:70px;}'+
'#ss_config .list {overflow-x:auto;}'+
'#ss_config .list select.configs {width:100%; height:135px;}'+
'#ss_config .txtline label {display:inline-block; float:left; margin-right:8px; width:100px; line-height:30px; clear:both;}'+
'#ss_config .txtline .expand {overflow-x:auto;}'+
'#ss_config .txtline input {box-sizing:border-box; width:100%; height:30px;}'+
'#ss_config .cbbox {display:inline-block; width:49%; vertical-align:top;}'+
'#ss_config .cbbox label {display:inline-block; float:left; margin:0 8px 0 0; width:135px; line-height:30px;}'+
'#ss_config .cbbox input {position:relative; overflow-x:auto; height:30px; margin:0; top:1px;}'+
'#ss_config .dlg_close {text-align:center; margin-top:16px;}'+
'';
var cfg_tmp;
//-------------------------------------------------------------------
// Open the configuration dialog.
//-------------------------------------------------------------------
function configure(e) {
var sel, ssgrp, dialog;
function setup() {
dialog = $(config).appendTo(ssgrp);
sel = $('#ss_config select.configs');
// "New" handler
dialog.find('button.new').on('click', function() {
cfg_tmp.push(['<new>','']);
sel.append('<option value="'+(cfg_tmp.length-1)+'"><new></option>');
select_config(sel.children().length-1);
$('#ss_config .preset').focus().select();
});
// "Delete" handler
dialog.find('button.del').on('click', function() {
var opt = sel.find(':selected');
var idx = opt.index();
opt.remove();
var len = sel.children().length;
if (idx >= len) idx = len-1;
select_config(idx);
});
// "Up" handler
dialog.find('button.up').on('click', function() {
var opt = sel.find(':selected');
if (opt.index() > 0) opt.insertBefore(opt.prev());
});
// "Down" handler
dialog.find('button.dn').on('click', function() {
var opt = sel.find(':selected');
if (opt.index() < sel.children().length-1) opt.insertAfter(opt.next());
});
// "Configs" selection changed
sel.on('change', function() {
select_config(sel.find(':selected').index());
});
// "Preset" name changed
dialog.find('.preset').on('change', function(e) {
var opt = sel.find(':selected');
var text = e.currentTarget.value;
opt.text(text);
var idx = opt.val();
cfg_tmp[idx][0] = text;
});
// "Checkbox" changed
dialog.find('input[type="checkbox"]').on('change', function() {
var opt = sel.find(':selected');
var idx = opt.val();
var props = [];
dialog.find('input[type="checkbox"]:checked').each(function(i,e){props.push(e.name);});
cfg_tmp[idx][1] = props.join(' ');
});
// "Save" handler
dialog.find('button.save').on('click', save_config);
// "Cancel" handler
dialog.find('button.cancel').on('click', cancel_config);
}
function save_config() {
settings.configs = [];
sel.children().each(function(i,v){
var idx = $(v).val();
settings.configs.push(cfg_tmp[idx].slice(0));
});
settings.selected_config = sel.find(':selected').index();
save_settings();
dialog.addClass('hidden');
populate_presets();
set_config(settings.selected_config);
}
function cancel_config() {
cfg_tmp = undefined;
dialog.addClass('hidden');
}
function select_config(idx) {
var opt = sel.children().eq(idx);
opt.prop('selected',true);
$('#ss_config input.preset').val(opt.text());
var props = cfg_tmp[opt.val()][1];
$('#ss_config .cbbox input').prop('checked', false);
props.split(' ').forEach(function(prop,i){
$('#ss_config .cbbox input[name="'+prop+'"]').prop('checked', true);
});
}
ssgrp = $(e.currentTarget).closest('.selfstudy');
window.ssgrp = ssgrp;
dialog = $('#ss_config');
if (dialog.length === 0) {
setup();
} else if (dialog.is(':visible')) {
return cancel_config();
} else {
ssgrp.append(dialog);
sel = $('#ss_config select.configs');
}
// Clone the existing settings.
var options = [];
cfg_tmp = settings.configs.map(function(e,i){
options.push('<option value="'+i+'">'+e[0]+'</option>');
return e.slice(0);
});
window.cfg_tmp = cfg_tmp;
// Populate configs.
sel.html(options.join(''));
select_config(settings.selected_config);
// Unhide the config dialog.
var top = ssgrp.find('.btn-group').height() + 4;
dialog.css('top',top).removeClass('hidden');
}
//-------------------------------------------------------------------
// Save settings.
//-------------------------------------------------------------------
function save_settings() {
localStorage.setItem('selfstudy_settings', JSON.stringify(settings));
}
//-------------------------------------------------------------------
// Button event handler.
//-------------------------------------------------------------------
function toggle_enable() {
settings.enabled = !settings.enabled;
save_settings();
set_enable();
}
//-------------------------------------------------------------------
// Button event handler.
//-------------------------------------------------------------------
function config_change_event(e) {
set_config(Number(e.currentTarget.value));
}
//-------------------------------------------------------------------
// Shuffle items.
//-------------------------------------------------------------------
function shuffle(e) {
if (e === undefined) {
// Shuffle all
$('section[id^="level-"]').each(function(){
var sec = $(this);
sec.find('[class$="-character-grid"]').append(sec.find('.character-item').detach().sort(function(){return Math.round(Math.random())*2-1;}));
});
} else {
// Shuffle specific group
var btn = $(e.currentTarget);
var sec = btn.closest('section[id^="level-"]');
sec.find('[class$="-character-grid"]').append(sec.find('.character-item').detach().sort(function(){return Math.round(Math.random())*2-1;}));
}
}
//-------------------------------------------------------------------
// Enable or disable the plugin.
//-------------------------------------------------------------------
function set_enable() {
var btns = $('.selfstudy button.enable');
var secs = $('section[id^="level-"]');
if (settings.enabled) {
secs.addClass('ss_active');
btns.addClass('on').text('ON');
} else {
secs.removeClass('ss_active');
btns.removeClass('on').text('OFF');
}
}
//-------------------------------------------------------------------
// Select a configuration.
//-------------------------------------------------------------------
function set_config(val) {
var secs = $('section[id^="level-"]');
// Remove all current ss_hide classes
secs.each(function(i,e){
e.className = e.className.split(' ').filter(function(v){return v.match(/^ss_hide/) === null;}).join(' ');
});
settings.selected_config = val;
save_settings();
$('.selfstudy select.config').val(val);
settings.configs[settings.selected_config][1].split(' ').forEach(function(cfgopt,idx){
secs.addClass(cfgopt);
});
}
//-------------------------------------------------------------------
// Populate the presets into the drop-down box.
//-------------------------------------------------------------------
function populate_presets() {
var options = [];
settings.configs.forEach(function(config,idx){
var cfgname = config[0];
var cfgopts = config[1];
options.push('<option value="'+idx+'">'+cfgname+'</option>');
});
$('.selfstudy select.config').html(options.join(''));
}
//-------------------------------------------------------------------
// Startup. Runs at document 'load' event.
//-------------------------------------------------------------------
function startup() {
// Load settings.
var s = localStorage.getItem('selfstudy_settings');
if (s) {
s = JSON.parse(s);
if (s.compatible !== undefined && s.compatible == settings.compatible) {
delete settings.configs;
$.extend(true, settings, s);
}
}
// Insert CSS
$('head').append('<style type="text/css">'+css+'</style>');
// Insert HTML
$('section[id^="level-"]').prepend(html);
populate_presets();
// Install handlers
$('.selfstudy button.enable').on('click', toggle_enable);
$('.selfstudy button.shuffle').on('click', shuffle);
$('.selfstudy select.config').on('change', config_change_event);
$('.selfstudy button.config').on('click', configure);
set_config(settings.selected_config);
if (settings.enabled) {
set_enable();
shuffle();
}
}
// Run startup() after window.onload event.
if (document.readyState === 'complete')
startup();
else
window.addEventListener("load", startup, false);
})(wkselfstudy);