WaniKani Dashboard Progress Plus

Display detailed level progress.

目前为 2015-04-28 提交的版本。查看 最新版本

// ==UserScript==
// @name        WaniKani Dashboard Progress Plus
// @namespace   rfindley
// @description Display detailed level progress.
// @version     1.0.6
// @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.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.
//===================================================================

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 = [];

var progress_png =
    '.wkdpp-progress {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJwAAAAnCAYAAAD6tSH7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoyNjY0MTY4NzY1RUFFNDExOUE4OERFMDQ5OThDNEVFNiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo4RDU0MjUxQUVENzAxMUU0QTFFMDhGNEI3QzRCMUM2NSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo4RDU0MjUxOUVENzAxMUU0QTFFMDhGNEI3QzRCMUM2NSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkVEQkNDNzgxNzBFREU0MTE5QjJFQzRERDc3QUZGN0I5IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjI2NjQxNjg3NjVFQUU0MTE5QTg4REUwNDk5OEM0RUU2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+NKKUkQAAAjtJREFUeNrsXLFOAkEU3CPGHilpiMSEji8wgYrGa/X3bKXF/kz8AjoTIqGxVHub8714JIbIAedmd97tTDIUl91k8D3OG/YNWVmWziMK5xdTavOms0C41nFEmwHVbPrChmOzBb3GhmOzBb125vmNzoRdYV84FI6EY2EPoAipaSsQGzDzbBr+wqXwWjgRDiI+mKekDfZuF6LhtrgS3ghzbXSQorZVG+y/1pANt8Wt8E54AVTUtmmjafiFufBe+AH4sN0WbbCmIZZLXQgfhCVgYduiDdI0dCJ/YhegXytY1wbZbA7gpOFRuAEtbBu08aRhByvhE2hRrWujadiDZ+E7aGGtaqNpqMFauAQtqmVtNA01eHG4sKiNpuEAXoGLalkbTcMevAEX1ao2moYafAIX1aK2ZMaTmuLLhT+b/C92x8Jn1ftA1EfTYAhdz+tCaKNpMIy+53UhtdE0GMTQ87pQ2mgajGLkeV0IbTxpMAod8x4fuXZcrUfRRtNgEJopODbI0qvWI2ijaTAIzRJMTtwzqfahaKNpMAQNrgxO3DOo9iFoo2kwBA2s5A335tX+mNpoGgxBi6npqKzh/qzan0fWlmwQ2tqd7ZQoXh00XaWBl3lkbckGodENQtOwcR30j6uBF80grCJoYxAaDL5+TuEQNu4ng6Bj4etA2iCf41JquHMX/8dsNIOgY+E6qavDkzrPth0x8qkN1jSk1HCFSwdTx5MGItKHiycNRNA7HVQDsuHYbDxpINrZbPryLcAAR3upHUQO964AAAAASUVORK5CYII=");background-position:39px 0px;background-repeat:no-repeat;}'+
    '.wkdpp-progress[data-srs-lvl="10"] {background-position:-117px 0px !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;background-color:#a035ba !important;}'+
    '.wkdpp-progress[data-srs-lvl="6"] {background-position:-39px 0px !important;background-color:#a035ba !important;}'+
    '.wkdpp-progress[data-srs-lvl="7"] {background-position:39px 0px !important;background-color:#4866e0 !important;}'+
    '.wkdpp-progress[data-srs-lvl="8"] {background-position:39px 0px !important;background-color:#00a3f5 !important;}'+
    '.wkdpp-progress[data-srs-lvl="9"] {background-position:39px 0px !important;background-color:#505050 !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;
    
    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)
        s += ' ('+(Math.floor(d.getTime()/one_day)-Math.floor(now.getTime()/one_day))+' days)';
    
	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();
        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 = '';
        if (data.user_specific && data.user_specific.available_date) {
            var date = formatDate(new Date(data.user_specific.available_date*1000));
            next = '<br><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') {
            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 {
            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>'
    });
}

//-------------------------------------------------------------------
// main() - Runs after page is done loading.
//-------------------------------------------------------------------
function main() {
    addStyle(progress_png);
    
    // 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);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址