Greasy Fork is available in English.

WaniKani Dashboard Progress Plus

Display detailed level progress.

Verzia zo dňa 19.12.2015. Pozri najnovšiu verziu.

// ==UserScript==
// @name        WaniKani Dashboard Progress Plus
// @namespace   rfindley
// @description Display detailed level progress.
// @version     1.2.5
// @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.2.5 - Returned 'full view' as default.
// 1.2.4 - Added option for compact dashboard display.
// 1.2.3 - Added option to hide non-Apprentice items.
// 1.2.2 - Fixed sorting issue.
// 1.2.1 - Fixed disappearing multi-word radicals.
// 1.2.0 - Added option to reverse secondary date sort of items.
// 1.1.9 - Removed leftover code from the last change.
// 1.1.8 - Fixed issue with certain disappearing image-based radicals.
// 1.1.7 - Prevent "onload" race condition at startup.
// 1.1.6 - Fixed "x days" calculation to account for timezone.
// 1.1.5 - Add brackets around 90% level-up. Changed sort order. Fix radical colors.
// 1.1.4 - Refetch API key if user not found.
// 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_radical_colors = '#0af,#999,#999,#999,#999,#0af';
//   localStorage.wkdpp_srs_kanji_colors = '#f0a,#999,#999,#999,#999,#f0a';
//
// Hide the progress ring for everything above Apprentice.
//   localStorage.wkdpp_apprentice_progress_only = 1;
//
// Hide everything above Apprentice.
//   localStorage.wkdpp_apprentice_only = 1;
//
// Hide brackets around 90%.
//   localStorage.wkdpp_hide_90percent = 1;
//
// Reverse the secondary date sort of items.
//   localStorage.wkdpp_reverse_date_sort = 1;
//
// Enable/Disable compact display (0=original, 1=compact)
//   localStorage.wkdpp_compact = 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';

//                        Guru, Master, Enlightened, Burned
window.wkdpp.srs_radical_colors = '#00aaff,#b69acd,#9aa5cf,#a3c3d3,#999999,#00aaff';
window.wkdpp.srs_kanji_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##");}'+

    // Radical colors
    '.radicals-progress .wkdpp-progress[data-srs-lvl="1"] {background-color:##RAD_APPR_COLOR## !important;}'+
    '.radicals-progress .wkdpp-progress[data-srs-lvl="2"] {background-color:##RAD_APPR_COLOR## !important;}'+
    '.radicals-progress .wkdpp-progress[data-srs-lvl="3"] {background-color:##RAD_APPR_COLOR## !important;}'+
    '.radicals-progress .wkdpp-progress[data-srs-lvl="4"] {background-color:##RAD_APPR_COLOR## !important;}'+
    '.radicals-progress .wkdpp-progress[data-srs-lvl="5"] {background-color:##RAD_GURU_COLOR## !important;}'+
    '.radicals-progress .wkdpp-progress[data-srs-lvl="6"] {background-color:##RAD_GURU_COLOR## !important;}'+
    '.radicals-progress .wkdpp-progress[data-srs-lvl="7"] {background-color:##RAD_MAST_COLOR## !important;}'+
    '.radicals-progress .wkdpp-progress[data-srs-lvl="8"] {background-color:##RAD_ENLI_COLOR## !important;}'+
    '.radicals-progress .wkdpp-progress[data-srs-lvl="9"] {background-color:##RAD_BURN_COLOR## !important;}'+
    '.radicals-progress .wkdpp-progress[data-srs-lvl="10"] {background-color:##RAD_LOCK_COLOR## !important;}'+

    // Kanji colors
    '.kanji-progress .wkdpp-progress[data-srs-lvl="1"] {background-color:##KAN_APPR_COLOR## !important;}'+
    '.kanji-progress .wkdpp-progress[data-srs-lvl="2"] {background-color:##KAN_APPR_COLOR## !important;}'+
    '.kanji-progress .wkdpp-progress[data-srs-lvl="3"] {background-color:##KAN_APPR_COLOR## !important;}'+
    '.kanji-progress .wkdpp-progress[data-srs-lvl="4"] {background-color:##KAN_APPR_COLOR## !important;}'+
    '.kanji-progress .wkdpp-progress[data-srs-lvl="5"] {background-color:##KAN_GURU_COLOR## !important;}'+
    '.kanji-progress .wkdpp-progress[data-srs-lvl="6"] {background-color:##KAN_GURU_COLOR## !important;}'+
    '.kanji-progress .wkdpp-progress[data-srs-lvl="7"] {background-color:##KAN_MAST_COLOR## !important;}'+
    '.kanji-progress .wkdpp-progress[data-srs-lvl="8"] {background-color:##KAN_ENLI_COLOR## !important;}'+
    '.kanji-progress .wkdpp-progress[data-srs-lvl="9"] {background-color:##KAN_BURN_COLOR## !important;}'+
    '.kanji-progress .wkdpp-progress[data-srs-lvl="10"] {background-color:##KAN_LOCK_COLOR## !important;}'+

    '.wkdpp-progress[data-srs-lvl="1"] {background-position:39px 0px !important;}'+
    '.wkdpp-progress[data-srs-lvl="2"] {background-position:0px 0px !important;}'+
    '.wkdpp-progress[data-srs-lvl="3"] {background-position:-39px 0px !important;}'+
    '.wkdpp-progress[data-srs-lvl="4"] {background-position:-78px 0px !important;}'+
    '.wkdpp-progress[data-srs-lvl="5"] {background-position:39px 0px !important;}'+
    '.wkdpp-progress[data-srs-lvl="6"] {background-position:##GURU_PROGRESS##39px 0px !important;}'+
    '.wkdpp-progress[data-srs-lvl="7"] {background-position:39px 0px !important;}'+
    '.wkdpp-progress[data-srs-lvl="8"] {background-position:39px 0px !important;}'+
    '.wkdpp-progress[data-srs-lvl="9"] {background-position:39px 0px !important;}'+
    '.wkdpp-progress[data-srs-lvl="10"] {background-position:-117px 0px !important;}'+
    '.wkdpp-noshow {width:0px !important;}';

var css_compact =
    '.progression {padding-top:4px !important;padding-bottom:4px !important;}'+
    '.progression .chart {display:none;margin:0px !important;}'+
    '.progression h3 {font-size:14px !important;margin:4px !important;}'+
    '.progression hr {margin:0px !important;}'+
    '.progression .lattice-single-character {background-image:none !important;margin:0px 0px 5px 0px !important;}'+
    '.progression .lattice-single-character>div:first-child {display:none;margin:0px !important;}'+
    '.progression .lattice-single-character ul {display:block;margin:0px !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());
    var refetching_apikey = 0;

    // 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')) {
            if (data.error.code=='user_not_found') {
                if (!refetching_apikey) {
                    refetching_apikey = 1;
                    delete localStorage.apiKey;
                    $.Deferred()
                    .resolve()
                    .then(get_api_key)
                    .then(populate_level_info);
                }
            } else {
                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')) {
            if (data.error.code=='user_not_found') {
                if (!refetching_apikey) {
                    refetching_apikey = 1;
                    delete localStorage.apiKey;
                    $.Deferred()
                    .resolve()
                    .then(get_api_key)
                    .then(populate_level_info);
                }
            } else {
                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()-d.getTimezoneOffset()*60*1000)/one_day)-Math.floor((now.getTime()-d.getTimezoneOffset()*60*1000)/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');
        $.each(window.wkdpp.radical_data, function(idx,data){
            arr[data.meaning] = data;
        });
    } else {
        ul = $('.kanji-progress .lattice-single-character>ul');
        $.each(window.wkdpp.kanji_data, function(idx,data){
            arr[data.character] = data;
        });
    }

    // Populate item data.
    ul.children().each(function(idx, li){
        li = $(li);
        var a = li.find('>a');
        var data;
        
        // Populate id, class, href, and text.
        if (type==='radicals') {
            var meaning = a.attr('href').split('/').slice(-1)[0];
            data = arr[meaning];
            if (data === undefined) {
                dlog(1,'wkdpp: Unable to find radical "'+meaning+'"');
                return;
            }
        } else {
            var character = a.text();
            data = arr[character];
            if (data === undefined) {
                dlog(1,'wkdpp: Unable to find kanji "'+character+'"');
                return;
            }
        }
        li.data('api_data', data); // Store API data for use in sort below.
        li.removeAttr('style'); // WK sometimes puts "display:none" here.
        a.addClass('wkdpp-progress');
        
        // Populate 'data-srs-lvl', which is a styling selector.
        var srs = (data.user_specific ? data.user_specific.srs_numeric : 10); // 10 == locked
        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=&quot;ja&quot;>'+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>');
    });

    // Sort items by srs level, then review date, then meaning.
    var li_arr = ul.children();
    li_arr.sort(function(a,b){
        a = $(a).data('api_data');
        b = $(b).data('api_data');
        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;
        var a_avail = (a.user_specific && a.user_specific.available_date ?
            a.user_specific.available_date : Number.MAX_VALUE);
        var b_avail = (b.user_specific && b.user_specific.available_date ?
            b.user_specific.available_date : Number.MAX_VALUE);
        if (window.wkdpp.reverse_date_sort == 1) {
            if (a_avail < b_avail) return 1;
            if (a_avail > b_avail) return -1;
        } else {
            if (a_avail < b_avail) return -1;
            if (a_avail > b_avail) return 1;
        }
        if (a.meaning < b.meaning) return -1;
        if (a.meaning > b.meaning) return 1;
        return 0;
    });
    li_arr.detach().appendTo(ul);

    if (window.wkdpp.apprentice_only == 1) {
        for (var srslvl=5; srslvl<=9; srslvl++) {
            $('.wkdpp-progress[data-srs-lvl="'+srslvl+'"]').parent().addClass('wkdpp-noshow');
        }
    }
    if (window.wkdpp.hide_90percent != 1) {
        // Add marker at 90%, indicating when level will be complete.
        // First, make sure there are at least 10% of items left.
        var idx90 = Math.floor(li_arr.length * 0.1);
        var len = ul.children(':not(.wkdpp-noshow)').length;
        if (idx90 < len) {
            ul.children().eq(idx90).css({'border-left':'2px solid black','border-radius':'7px'});
            ul.children(':not(.wkdpp-noshow)').last().css({'border-right':'2px solid black','border-radius':'7px'});
        }
    }
}


//-------------------------------------------------------------------
// 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
    );

    // Upgrade colors from previous script versions.
    if (localStorage.wkdpp_srs_colors !== undefined) {
        var colors = localStorage.wkdpp_srs_colors;
        localStorage.wkdpp_srs_kanji_colors = colors;
        colors = colors.split(',');
        colors[0] = '#00aaff';
        colors[5] = '#00aaff';
        colors = colors.join();
        localStorage.wkdpp_srs_radical_colors = colors;
        delete localStorage.wkdpp_srs_colors;
    }

    // Alternate color scheme for SRS levels.  One color each for
    // apprentice, guru, master, enlightened, burned, and locked.
    window.wkdpp.srs_radical_colors = value_or_default(
        localStorage.wkdpp_srs_radical_colors,
        window.wkdpp.srs_radical_colors
    );
    window.wkdpp.srs_kanji_colors = value_or_default(
        localStorage.wkdpp_srs_kanji_colors,
        window.wkdpp.srs_kanji_colors
    );

    // Hide the progress ring for everything above Apprentice.
    window.wkdpp.apprentice_progress_only = value_or_default(
        localStorage.wkdpp_apprentice_progress_only,
        0
    );

    // Hide everything above Apprentice.
    window.wkdpp.apprentice_only = value_or_default(
        localStorage.wkdpp_apprentice_only,
        0
    );

    // Hide brackets around 90%.
    window.wkdpp.hide_90percent = value_or_default(
        localStorage.wkdpp_hide_90percent,
        0
    );

    // Reverse the secondary date sort of items.
    window.wkdpp.reverse_date_sort = value_or_default(
        localStorage.wkdpp_reverse_date_sort,
        0
    );
    
    // Enable compact display.
    window.wkdpp.compact = value_or_default(
        localStorage.wkdpp_compact,
        0
    );
}

function setup_styles() {
    var css = window.wkdpp.progress_css;
    var rad_colors = window.wkdpp.srs_radical_colors.split(',');
    var kan_colors = window.wkdpp.srs_kanji_colors.split(',');
    
    css = css.replace(/##PROGRESS_IMG##/g  , window.wkdpp.progress_img);
    css = css.replace(/##GURU_PROGRESS##/g , (window.wkdpp.apprentice_progress_only=='1' ? '' : '-'));

    css = css.replace(/##RAD_APPR_COLOR##/g, rad_colors[0]);
    css = css.replace(/##RAD_GURU_COLOR##/g, rad_colors[1]);
    css = css.replace(/##RAD_MAST_COLOR##/g, rad_colors[2]);
    css = css.replace(/##RAD_ENLI_COLOR##/g, rad_colors[3]);
    css = css.replace(/##RAD_BURN_COLOR##/g, rad_colors[4]);
    css = css.replace(/##RAD_LOCK_COLOR##/g, rad_colors[5]);

    css = css.replace(/##KAN_APPR_COLOR##/g, kan_colors[0]);
    css = css.replace(/##KAN_GURU_COLOR##/g, kan_colors[1]);
    css = css.replace(/##KAN_MAST_COLOR##/g, kan_colors[2]);
    css = css.replace(/##KAN_ENLI_COLOR##/g, kan_colors[3]);
    css = css.replace(/##KAN_BURN_COLOR##/g, kan_colors[4]);
    css = css.replace(/##KAN_LOCK_COLOR##/g, kan_colors[5]);

    addStyle(css);
    if (window.wkdpp.compact==1)
        addStyle(css_compact);
}

//-------------------------------------------------------------------
// 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.
//-------------------------------------------------------------------
if (document.readyState === 'complete')
    main();
else
    window.addEventListener("load", main, false);