// ==UserScript==
// @name WaniKani Dashboard Progress Plus
// @namespace rfindley
// @description Display detailed level progress.
// @version 1.1.3
// @author Robin Findley
// @copyright 2015+, Robin Findley
// @license MIT; http://opensource.org/licenses/MIT
// @include http://www.wanikani.com/
// @include https://www.wanikani.com/
// @include http://www.wanikani.com/dashboard
// @include https://www.wanikani.com/dashboard
// @run-at document-end
// @grant none
// ==/UserScript==
//==[ History ]======================================================
// 1.1.3 - Fix commented sample setting typo.
// 1.1.2 - Modified date-only setting to match other users' scripts.
// 1.1.1 - Added configurable settings (see 'Settings' below).
// 1.1.0 - Color tweaks to help distinguish Guru from Vocab.
// 1.0.9 - Third attempt at modified v1.0.7 fix.
// 1.0.8 - Fix next review date for '1 day', and undefined. Modified v1.0.7 fix.
// 1.0.7 - Force remove display attr to attempt inconsistent Firefox fix.
// 1.0.6 - Firefox CSS compatibility background-position-x.
// 1.0.5 - Round percentage displays.
// 1.0.4 - Insert icon-radicals with html instead of text.
// 1.0.3 - Removed references to wkdata script.
// 1.0.2 - Enable on http (in addition to https).
// 1.0.1 - Changed 'apiKey' reference to 'api_key'.
// 1.0.0 - Initial release.
//===================================================================
//==[ Settings ]=====================================================
// The following script configuration variables are available. You
// can enable them by pasting the corresponding line in the javascript
// console (ctrl-shift-j on Chrome, ctrl-shift-k on Firefox), or by
// uncommenting the line below. The setting will be saved in storage.
// To remove a setting from storage, enter the following line in the
// javascript console, with corresponding setting name replaced:
// delete localStorage.wkdpp_setting_name;
//-------------------------------------------------------------------
//
// Hide WK's standard popup info for apprentice items in the review
// queue, so user doesn't won't accidentally see it before review.
// localStorage.wkdpp_popup_date_only = 1;
//
// Alternate progress ring image (URL or Base64).
// localStorage.wkdpp_progress_img = "http://example.com/progress.png";
// localStorage.wkdpp_progress_img = "base64-encoded string here";
//
// Alternate color scheme for SRS levels. One color each for
// apprentice, guru, master, enlightened, burned, and locked.
// localStorage.wkdpp_srs_colors = '#f0a,#999,#999,#999,#999,#f0a';
//
// Hide the progress ring for everything above Apprentice.
// localStorage.wkdpp_apprentice_progress_only = 1;
//===================================================================
var dlog_level = 1;
function dlog(level) {
if (level > dlog_level) return;
if (!console || typeof console.log !== 'function') return;
var args = Array.prototype.slice.call(arguments);
args.shift();
console.log.apply(console,args);
}
//===================================================================
window.wkdpp = {};
window.wkdpp.radical_data = [];
window.wkdpp.kanji_data = [];
window.wkdpp.progress_img =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJwAAAAnCAYAAAD6tSH7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccll'+
'PAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+ID'+
'x4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8w'+
'Mi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbn'+
'MjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5z'+
'OnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb2'+
'0veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoyNjY0MTY4NzY1RUFFNDExOUE4OERFMDQ5OThDNEVFNiIgeG1wTU06'+
'RG9jdW1lbnRJRD0ieG1wLmRpZDo5RTk5NUIzQ0VFRTQxMUU0QUZFNzgxMEQwMDQwMzgwMCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo5RTk5NU'+
'IzQkVFRTQxMUU0QUZFNzgxMEQwMDQwMzgwMCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKSI+IDx4bXBNTTpE'+
'ZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkVEQkNDNzgxNzBFREU0MTE5QjJFQzRERDc3QUZGN0I5IiBzdFJlZjpkb2N1bWVudE'+
'lEPSJ4bXAuZGlkOjI2NjQxNjg3NjVFQUU0MTE5QTg4REUwNDk5OEM0RUU2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBt'+
'ZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+U3W7wQAAAkVJREFUeNrsXLFOwzAUdCpYqy5dqgp16Y7UmW7wA3wECz+Uj2BiKxudkdhZqqpi6YK6MoT3VF'+
'egiqZNMPa95E66DJEtXf1enFzt56woChcQuQuLO2oLpjNHuNdxRJMBlWx6YcIx2aLeY8Ix2aLeywJ/w50Lu8K+cCgcCcfCHsB3Utu05YgJGDrhfoMO'+
'4KVwIhwkDGqbtMHOdjESbocL4ZVnBhLUpmqDfbXGTLgdroU3/hWCEtSmaaNp+IEn4aNwA/ix3RRtsKYhlUt9Fs6EBWBgm6IN0jR0Ej+xc9C/Faxrg0'+
'w2B7DSoAP3DhrYJmjjSsMelsIX0KBa10bTcACvwg/QwFrVRtNQgpXwDTSolrXRNJRg4XBhURtNwwlPqwOeSaxqo2k4gDVwUK1qo2kowQY4qBa1wZqG'+
'M5CB+3Tx1yb/iv1t4ff+dyDqo2kwhG7gdjG00TQYRj9wu5jaaBoMYhi4XSxtNA1GMQrcLoY2rjQYnt3GJ7YdR57ljmmjaTAIrSk4tZCl59sjaKNpMA'+
'itJZhU7DPx/VC00TQYghatVK2WGvh+CNogv+NSFNFYgBas3LrqVVIKHdAHt92Zm0obTYMhTN22Oiqr2T/z/aeJtbW2ENrazFalFK8Mus45CzjT1dXW'+
'2kJodINQt9j42Ot17rlMoI2F0GAIdZzCMWjBi9Yg6LbwVSRtNA2JEfrAmDrQGgTdFr7wibd231uM/uOgncafnoSM3LUHrT49iQmXPvloGphw7Us2zn'+
'BMNpoGJlxzk00vXwIMAERrvuh7OTAxAAAAAElFTkSuQmCC';
// Apprentice, Guru, Master, Enlightened, Burned, Locked
window.wkdpp.srs_colors = '#ff00aa,#b69acd,#9aa5cf,#a3c3d3,#999999,#ff00aa';
window.wkdpp.progress_css =
'.wkdpp-progress {background-position:39px 0px;background-repeat:no-repeat;background-image: url("##PROGRESS_IMG##");}'+
'.wkdpp-progress[data-srs-lvl="10"] {background-position:-117px 0px !important;background-color:##LOCKED_COLOR## !important;}'+
'.wkdpp-progress[data-srs-lvl="1"] {background-position:39px 0px !important;background-color:##APPRENTICE_COLOR## !important;}'+
'.wkdpp-progress[data-srs-lvl="2"] {background-position:0px 0px !important;background-color:##APPRENTICE_COLOR## !important;}'+
'.wkdpp-progress[data-srs-lvl="3"] {background-position:-39px 0px !important;background-color:##APPRENTICE_COLOR## !important;}'+
'.wkdpp-progress[data-srs-lvl="4"] {background-position:-78px 0px !important;background-color:##APPRENTICE_COLOR## !important;}'+
'.wkdpp-progress[data-srs-lvl="5"] {background-position:39px 0px !important;background-color:##GURU_COLOR## !important;}'+
'.wkdpp-progress[data-srs-lvl="6"] {background-position:##GURU_PROGRESS##39px 0px !important;background-color:##GURU_COLOR## !important;}'+
'.wkdpp-progress[data-srs-lvl="7"] {background-position:39px 0px !important;background-color:##MASTER_COLOR## !important;}'+
'.wkdpp-progress[data-srs-lvl="8"] {background-position:39px 0px !important;background-color:##ENLIGHTENED_COLOR## !important;}'+
'.wkdpp-progress[data-srs-lvl="9"] {background-position:39px 0px !important;background-color:##BURNED_COLOR## !important;}';
//-------------------------------------------------------------------
// Add a <style> section to the document.
//-------------------------------------------------------------------
function addStyle(aCss) {
var head, style;
head = document.getElementsByTagName('head')[0];
if (head) {
style = document.createElement('style');
style.setAttribute('type', 'text/css');
style.textContent = aCss;
head.appendChild(style);
return style;
}
return null;
}
//-------------------------------------------------------------------
// Get the user's API Key.
//-------------------------------------------------------------------
function get_api_key() {
var done = $.Deferred();
// First check if the API key is in local storage.
var api_key = localStorage.getItem('apiKey');
if (api_key && api_key.length == 32) return done.resolve();
// We don't have the API key. Fetch it from the /account page.
dlog(1,'wkdpp: Fetching api_key');
$.get('/account')
.done(function(page){
// Make sure what we got is a web page.
if (typeof page !== 'string') {return done.reject()}
// Extract the API key.
var api_key = $(page).find('#api-button').parent().find('input').attr('value');
if (typeof api_key !== 'string' || api_key.length !== 32) {return done.reject()}
// Store the updated user info.
localStorage.setItem('apiKey', api_key);
// Return success.
done.resolve();
})
.fail(function(){
// Failed to get web page.
done.reject();
});
return done.promise();
}
//-------------------------------------------------------------------
// Populate level info from API.
//-------------------------------------------------------------------
function populate_level_info() {
// Grab the user's current level.
var api_key = localStorage.getItem('apiKey');
var user_level = parseInt($('.dropdown.levels>a>span').text());
// Request kanji information.
$.getJSON('/api/user/'+api_key+'/kanji/'+user_level)
.done(function(data){
// Check if we got an API error.
if (data.hasOwnProperty('error')) {
dlog(1,'wkdpp: API Error - '+data.error.message);
return;
}
$.each(data.requested_information, function(i, e) {
window.wkdpp.kanji_data.push(e);
});
update_progress('kanji');
});
// Request radicals information.
$.getJSON('/api/user/'+api_key+'/radicals/'+user_level)
.done(function(data){
// Check if we got an API error.
if (data.hasOwnProperty('error')) {
dlog(1,'wkdpp: API Error - '+data.error.message);
return;
}
$.each(data.requested_information, function(i, e) {
window.wkdpp.radical_data.push(e);
});
update_progress('radicals');
});
}
//-------------------------------------------------------------------
// Print date in pretty format.
//-------------------------------------------------------------------
function formatDate(d){
var s = '';
var now = new Date();
var YY = d.getFullYear(),
MM = d.getMonth(),
DD = d.getDate(),
hh = d.getHours(),
mm = d.getMinutes(),
one_day = 24*60*60*1000;
if (d < now) return "Available Now";
var same_day = ((YY == now.getFullYear()) && (MM == now.getMonth()) && (DD == now.getDate()) ? 1 : 0);
// If today: "Today 8:15pm"
// otherwise: "Wed, Apr 15, 8:15pm"
if (same_day) {
s += 'Today ';
} else {
s += ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][d.getDay()]+', '+
['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][MM]+' '+DD+', ';
}
s += (((hh+11)%12)+1)+':'+('0'+mm).slice(-2)+['am','pm'][Math.floor(d.getHours()/12)];
// Append "(X days)".
if (!same_day) {
var days = (Math.floor(d.getTime()/one_day)-Math.floor(now.getTime()/one_day));
if (days) s += ' ('+days+' day'+(days>1?'s':'')+')';
}
return s;
}
//-------------------------------------------------------------------
// Capitalize all words in a string.
//-------------------------------------------------------------------
function capitalize_words(string) {
return string.replace(/\b\w+\b/g,function(w){return w.charAt(0).toUpperCase()+w.slice(1);});
}
//-------------------------------------------------------------------
// Update the dashboard info.
//-------------------------------------------------------------------
function update_progress(type) {
var ul;
var arr;
// Fetch container element, remove existing elements, and select data source.
if (type==='radicals') {
ul = $('.radicals-progress .lattice-single-character>ul');
arr = window.wkdpp.radical_data;
} else {
ul = $('.kanji-progress .lattice-single-character>ul');
arr = window.wkdpp.kanji_data;
}
var li_proto = ul.children().first().clone();
ul.children().remove();
// Sort items by srs level, then by character or meaning.
arr.sort(function(a,b){
var a_srs = (a.user_specific ? a.user_specific.srs_numeric : 10);
var b_srs = (b.user_specific ? b.user_specific.srs_numeric : 10);
if (a_srs < b_srs) return -1;
if (a_srs > b_srs) return 1;
if (a.meaning < b.meaning) return -1;
if (a.meaning > b.meaning) return 1;
return 0;
});
// Populate item data.
var renum = 0;
$.each(arr, function(idx, data){
var li;
var a;
var span;
// Populate id, class, href, and text.
li = li_proto.clone();
li.removeAttr('style'); // WK sometimes puts "display:none" here.
a = li.find('>a');
a.addClass('wkdpp-progress');
if (type==='radicals') {
li.attr('id', 'radical-x'+(renum++));
a.attr('href','/radicals/'+data.meaning);
if (data.character) {
a.text(data.character);
} else {
a.html('<i class="radical-'+data.meaning.replace(' ','-')+'"></i>');
}
} else {
li.attr('id', 'kanji-x'+(renum++));
a.attr('href','/kanji/'+encodeURIComponent(data.character));
a.text(data.character);
}
// Populate 'data-srs-lvl', which is a styling selector.
var srs = (data.user_specific ? data.user_specific.srs_numeric : 10);
a.attr('data-srs-lvl', srs);
// Populate the next review date.
var next = (window.wkdpp.popup_date_only==1?'':'<br>');
if (data.user_specific && data.user_specific.available_date) {
var date = formatDate(new Date(data.user_specific.available_date*1000));
next += '<span style="font-size:75%;font-weight:bold;">Next: '+date+'</span>';
}
// Populate remaining data for popup window.
var percent = 0;
var correct;
var total;
if (type==='radicals') {
if (window.wkdpp.popup_date_only==1)
a.attr('data-original-title', next);
else
a.attr('data-original-title', capitalize_words(data.meaning)+next);
if (data.user_specific) {
correct = data.user_specific.meaning_correct;
total = correct+data.user_specific.meaning_incorrect;
if (total > 0) percent = Math.floor(100.0*correct/total);
}
} else {
if (window.wkdpp.popup_date_only==1)
a.attr('data-original-title', next);
else
a.attr('data-original-title', capitalize_words(data.meaning)+'<br><span lang="ja">'+data[data.important_reading]+'</span>'+next);
if (data.user_specific) {
correct = data.user_specific.meaning_correct+data.user_specific.reading_correct;
total = correct+data.user_specific.meaning_incorrect+data.user_specific.reading_incorrect;
if (total > 0) percent = Math.floor(100.0*correct/total);
}
}
a.attr('data-content', '<div class="progress"><div class="bar full" style="width: '+Math.max(percent,15)+'%;">'+percent+'%</div></div>');
ul.append(li);
});
// WaniKani function to add popover.
// InfoTip.popoverLattice();
if (type==='radicals') {
arr = $(".radicals-progress [rel=auto-popover]");
} else {
arr = $(".kanji-progress [rel=auto-popover]");
}
arr.popover({
html:!0,
animation:!1,
trigger:"hover",
placement:function(e,t){var n,r;return r=window.innerWidth,n=$(t).offset().left,r<500?"bottom":r-n>400?"right":"left"},
template:'<div class="popover lattice"><div class="arrow"></div><div class="popover-inner"><h3 class="popover-title"></h3><div class="popover-content"><p></p></div></div></div>'
});
}
//-------------------------------------------------------------------
// Process stored configuration settings.
//-------------------------------------------------------------------
function process_settings() {
function value_or_default(value, dflt) {
return (value===undefined ? dflt : value);
}
// Hide WK's standard popup info for apprentice items in the review
// queue, so user doesn't won't accidentally see it before review.
window.wkdpp.popup_date_only = value_or_default(
localStorage.wkdpp_popup_date_only,
0
);
// Alternate progress ring image (URL or Base64).
window.wkdpp.progress_img = value_or_default(
localStorage.wkdpp_progress_img,
window.wkdpp.progress_img
);
// Alternate color scheme for SRS levels. One color each for
// apprentice, guru, master, enlightened, burned, and locked.
window.wkdpp.srs_colors = value_or_default(
localStorage.wkdpp_srs_colors,
window.wkdpp.srs_colors
);
// Hide the progress ring for everything above Apprentice.
window.wkdpp.apprentice_progress_only = value_or_default(
localStorage.wkdpp_apprentice_progress_only,
0
);
}
function setup_styles() {
var css = window.wkdpp.progress_css;
var colors = window.wkdpp.srs_colors.split(',');
css = css.replace(/##PROGRESS_IMG##/g , window.wkdpp.progress_img);
css = css.replace(/##APPRENTICE_COLOR##/g , colors[0]);
css = css.replace(/##GURU_COLOR##/g , colors[1]);
css = css.replace(/##MASTER_COLOR##/g , colors[2]);
css = css.replace(/##ENLIGHTENED_COLOR##/g, colors[3]);
css = css.replace(/##BURNED_COLOR##/g , colors[4]);
css = css.replace(/##LOCKED_COLOR##/g , colors[5]);
css = css.replace(/##GURU_PROGRESS##/g , (window.wkdpp.apprentice_progress_only=='1' ? '' : '-'));
addStyle(css);
}
//-------------------------------------------------------------------
// main() - Runs after page is done loading.
//-------------------------------------------------------------------
function main() {
process_settings();
setup_styles();
// Set up a sequence of deferred actions, so we can
// control asynchronous flow in a more readable manner.
$.Deferred()
.resolve()
.then(get_api_key)
.then(populate_level_info);
}
//-------------------------------------------------------------------
// Run main() upon load.
//-------------------------------------------------------------------
window.addEventListener("load", main, false);