WaniKani Dashboard Progress Plus

Display detailed level progress.

目前為 2015-04-30 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        WaniKani Dashboard Progress Plus
// @namespace   rfindley
// @description Display detailed level progress.
// @version     1.1.4
// @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.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_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());
    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()/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=&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>');

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