// ==UserScript==
// @name Wanikani Forecast Details
// @namespace https://www.wanikani.com
// @author kernfel
// @description Adds markup & tooltips for item types, critical reviews, burn reviews to the forecast and review buttons
// @version 1.5.5
// @include /^https://(www|preview).wanikani.com/(dashboard)?$/
// @copyright 2020+, Felix Kern
// @license MIT; http://opensource.org/licenses/MIT
// @run-at document-end
// @grant none
// ==/UserScript==
(function() {
/* global $, wkof */
//===================================================================
// Initialization of the Wanikani Open Framework.
//-------------------------------------------------------------------
var script_name = 'Forecast Details';
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,Settings');
wkof.ready('document,ItemData,Settings')
.then(install_menu)
.then(load_settings)
.then(startup);
//========================================================================
// Settings
//-------------------------------------------------------------------
var settings;
function load_settings() {
var defaults = {
crit_highlight: true,
crit_highlight_now: true,
add_crit_icon: true,
add_crit_icon_now: true,
crit_icon: '🔺',
burn_highlight: true,
burn_highlight_now: true,
add_burn_icon: true,
add_burn_icon_now: true,
burn_icon: '🔥',
bar_colours: 'type'
};
wkof.Settings.load('forecast_details', defaults).then(function(){
settings = wkof.settings.forecast_details
});
}
//========================================================================
// Startup
//-------------------------------------------------------------------
function startup() {
install_css();
wkof.ItemData.get_items({
wk_items:{
options:{
assignments:true
},
filters:{
srs: '1,2,3,4,5,6,7,8',
level: '1..+0'
}
}
})
.then(process_items);
wkof.ItemData.get_items({
wk_items:{
options:{
assignments:true
},
filters:{
srs: '1,2,3,4',
level: '+0',
item_type: 'rad,kan'
}
}
})
.then(process_crits);
}
//========================================================================
// CSS Styling
//-------------------------------------------------------------------
var fcr_css =
// Bar highlights
'.fcr_apprentice { background: #59c27450; }'+
'.fcr_burn { background: #fbc04250; }'+
'.fcr_burn.fcr_apprentice { background: #aac15b50; }'+
// Review buttons
'.lessons-and-reviews__button span.fcr_apprentice, .navigation-shortcut a.fcr_apprentice { background: #59c274; text-shadow: none; color: white; }'+
'.lessons-and-reviews__button span.fcr_burn, .navigation-shortcut a.fcr_burn { background: #fbc042; text-shadow: none; color: white; }'+
'.lessons-and-reviews__button span.fcr_apprentice.fcr_burn, .navigation-shortcut a.fcr_apprentice.fcr_burn { background: #aac15b; }'+
// Item type bar split
'.fcr_radical { background: #00AAFF; }'+
'.fcr_kanji { background: #FF00AA; }'+
'.fcr_vocab { background: #AA00FF; }'+
// SRS level bar split
'.fcr_bar_apprentice { background: #dd0093; }'+
'.fcr_bar_guru { background: #882d9e; }'+
'.fcr_bar_master { background: #294ddb; }'+
'.fcr_bar_enlightened { background: #0093dd; }'+
// Crit/burn icons
'.fcr_icon { float: right; padding-right: 1px; }'+
// Layout details
'.fcr_split_bar { display: inline-block; }'+
'.review-forecast__bar { min-width: 6px; }'+
'.fcr_lineheight_fix { line-height: 0; }'+
'.review-forecast__day.mb-3 { padding-bottom: 12px; }'+
'.review-forecast__day.mb-3.is-collapsed { padding-bottom: 0; }'+
'.review-forecast__hour.pb-2 { padding-bottom: 0 !important; }'+
'.review-forecast__hour:last-child>* { padding-bottom: 0 !important; }'
;
//========================================================================
// Install stylesheet.
//-------------------------------------------------------------------
function install_css() {
$('head').append('<style>'+fcr_css+'</style>');
}
//========================================================================
// Add detailed information and burns to forecast bars & review buttons
//-------------------------------------------------------------------
function process_items(data) {
var now = new Date(), rtime, counts = {}, stage;
// Count typed reviews in each hour
for ( var idx in data ) {
if ( Date.parse(data[idx].assignments.available_at) - 7*24*3600*1000 < now ) {
rtime = data[idx].assignments.available_at.split('.')[0];
if ( Date.parse(data[idx].assignments.available_at) < now ) {
rtime = 'now';
}
if ( ! (rtime in counts) ) {
counts[rtime] = {
'radical':0,
'kanji':0,
'vocabulary':0,
'burn': {
'radical':0,
'kanji':0,
'vocabulary':0
},
'hasBurn': false,
4: 0, // Apprentice
5: 0, // Guru
7: 0, // Master
8: 0 // Enlightened
};
}
counts[rtime][data[idx].object]++; // counts by type
stage = data[idx].assignments.srs_stage;
if ( stage < 5 ) stage = 4;
if ( stage == 6 ) stage = 5;
counts[rtime][stage]++; // counts by srs stage
if ( data[idx].assignments.srs_stage == 8 ) {
counts[rtime].burn[data[idx].object]++;
counts[rtime].hasBurn = true;
}
}
}
// Split forecast bars into types, add informative titles, and add burn markup
var bar, tr, review_btn, w0, n, title;
for ( rtime in counts ) {
// Locate the review bar
bar = $('section.forecast tr.review-forecast__hour time[datetime="' + rtime + 'Z"]')
.parents('th').first().siblings('td').first().children('span').first();
if ( !bar.length ) {
continue;
}
tr = bar.parents('tr').first();
// Split the bar into typed segments
if ( settings.bar_colours == 'type' ) {
split_bar(bar, counts[rtime], barsplit_type);
} else if ( settings.bar_colours == 'srs' ) {
split_bar(bar, counts[rtime], barsplit_srs);
}
// Add tooltips and burn highlights
tr.attr('title', setTitle(counts[rtime], 'Total reviews'))
.attr('title', setTitleByLevel(counts[rtime]));
if ( counts[rtime].hasBurn ) {
tr.attr('title', setTitle(counts[rtime].burn, 'Burn'));
if ( settings.burn_highlight ) {
tr.addClass('fcr_burn');
}
if ( settings.add_burn_icon ) {
tr.children('th').append('<span class="fcr_icon fcr_icon_burn">' + settings.burn_icon + '</span>');
}
}
}
// Add currently available info & burns the review buttons
if ( 'now' in counts ) {
review_btn = $('a.lessons-and-reviews__reviews-button, li.navigation-shortcut--reviews')
.attr('title', setTitle(counts.now, 'Total reviews'))
.attr('title', setTitleByLevel(counts.now));
if ( counts.now.hasBurn ) {
review_btn.attr('title', setTitle(counts.now.burn, 'Burn'));
if ( settings.burn_highlight_now ) {
review_btn.children().addClass('fcr_burn');
}
if ( settings.add_burn_icon_now ) {
review_btn.children().append(settings.burn_icon);
}
}
}
}
//========================================================================
// Add level-critical information to forecast bars & review buttons
//-------------------------------------------------------------------
function process_crits(data) {
var now = new Date(), rtime, counts = {};
// Count typed reviews in each hour
for ( var idx in data ) {
if ( data[idx].assignments.passed_at === null && Date.parse(data[idx].assignments.available_at) - 7*24*3600*1000 < now ) {
rtime = data[idx].assignments.available_at.split('.')[0];
if ( Date.parse(data[idx].assignments.available_at) < now ) {
rtime = 'now';
}
if ( ! (rtime in counts) ) {
counts[rtime] = {
'radical':0,
'kanji':0
};
}
counts[rtime][data[idx].object]++; // crit counts by type
}
}
// Add crit markup to forecast
var tr, review_btn, w0, n, title;
for ( rtime in counts ) {
// Locate the review bar
tr = $('section.forecast tr.review-forecast__hour time[datetime="' + rtime + 'Z"]')
.parents('tr').first();
if ( !tr.length ) {
continue;
}
// Add tooltips and markup
tr.attr('title', setTitle(counts[rtime], 'Critical'));
if ( settings.crit_highlight ) {
tr.addClass('fcr_apprentice');
}
if ( settings.add_crit_icon ) {
tr.children('th').append('<span class="fcr_icon fcr_icon_critical">' + settings.crit_icon + '</span>');
}
}
// Add currently available crits to review buttons
if ( 'now' in counts ) {
review_btn = $('a.lessons-and-reviews__reviews-button, li.navigation-shortcut--reviews')
.attr('title', setTitle(counts.now, 'Critical')).children();
if ( settings.crit_highlight_now ) {
review_btn.addClass('fcr_apprentice');
}
if ( settings.add_crit_icon_now ) {
review_btn.append(settings.crit_icon);
}
}
}
//========================================================================
// Helper functions
//-------------------------------------------------------------------
var setTitle = function(countObj, name){
return function(idx, title){
title = (title ? title + '\n' : '') + name + ': ' + countObj.radical + ' radicals, '
+ countObj.kanji + ' kanji';
if ( countObj.vocabulary != undefined ) {
title += ', ' + countObj.vocabulary + ' vocabulary';
}
return title;
}
}
var setTitleByLevel = function(countObj){
return function(idx, title){
title = (title ? title + '\n' : '')
+ countObj[4] + ' apprentice, '
+ countObj[5] + ' guru, '
+ countObj[7] + ' master, '
+ countObj[8] + ' enlightened';
return title;
}
}
function split_bar(bar, counts, config) {
var n = 0, w0, c;
for ( c in config ) {
n += counts[c]
}
bar.removeClass('rounded-r-lg').addClass('fcr_split_bar');
w0 = parseFloat(bar.attr('style').match("width: ([0-9.]+)%")[1]);
for ( c in config ) {
if ( counts[c] ) {
bar.clone().addClass(config[c]).css('width', counts[c] * w0 / n + '%').appendTo(bar.parent());
}
}
bar.siblings().last().addClass('rounded-r-lg').parent().addClass('fcr_lineheight_fix');
if ( w0 > 95 ) {
bar.siblings().css('min-width', 0);
}
bar.remove();
}
var barsplit_type = {
radical: 'fcr_radical',
kanji: 'fcr_kanji',
vocabulary: 'fcr_vocab'
};
var barsplit_srs = {
4: 'fcr_bar_apprentice',
5: 'fcr_bar_guru',
7: 'fcr_bar_master',
8: 'fcr_bar_enlightened'
};
//========================================================================
// Menu
//-------------------------------------------------------------------
function install_menu() {
wkof.Menu.insert_script_link({
name: 'forecast_details',
title: 'Forecast Details',
submenu: 'Settings',
on_click: open_settings
});
}
function open_settings() {
var config = {
script_id: 'forecast_details',
title: 'Forecast Details settings',
content: {
tabset: {
type: 'tabset',
content: {
crits: {
type: 'page',
label: 'Level-critical reviews',
content: {
crit_icon: {
type: 'text',
label: 'Icon (Default: 🔺)',
},
sec_forecast: {
type: 'section',
label: 'Forecast'
},
crit_highlight: {
type: 'checkbox',
label: 'Highlight bars',
},
add_crit_icon: {
type: 'checkbox',
label: 'Add icons'
},
sec_now: {
type: 'section',
label: 'Review buttons (currently available items)'
},
crit_highlight_now: {
type: 'checkbox',
label: 'Add highlight'
},
add_crit_icon_now: {
type: 'checkbox',
label: 'Add icon'
}
}
},
burns: {
type: 'page',
label: 'Burn reviews',
content: {
burn_icon: {
type: 'text',
label: 'Icon (Default: 🔥)',
},
sec_forecast: {
type: 'section',
label: 'Forecast'
},
burn_highlight: {
type: 'checkbox',
label: 'Highlight bars',
},
add_burn_icon: {
type: 'checkbox',
label: 'Add icons'
},
sec_now: {
type: 'section',
label: 'Review buttons (currently available items)'
},
burn_highlight_now: {
type: 'checkbox',
label: 'Add highlight'
},
add_burn_icon_now: {
type: 'checkbox',
label: 'Add icon'
}
}
},
misc: {
type: 'page',
label: 'Other forecast details',
content: {
bar_colours: {
type: 'dropdown',
label: 'Colour bars by',
content: {
none: 'None',
type: 'Item type',
srs: 'SRS stage',
}
}
}
}
}
},
divider: {
type: 'divider'
},
note: {
type: 'section',
label: 'Note, changes come into effect when the page is reloaded.',
}
}
};
var dialog = new wkof.Settings(config);
dialog.open();
}
})();