Wanikani Dashboard Progress Plus

Display detailed level progress.

目前為 2021-08-13 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 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     3.0.5
// @include     /^https://(www|preview).wanikani.com/(dashboard)?$/
// @copyright   2018+, Robin Findley
// @license     MIT; http://opensource.org/licenses/MIT
// @run-at      document-end
// @grant       none
// ==/UserScript==

window.dpp = {};

(function(gobj) {

    /* global $, wkof */

    //===================================================================
    // Initialization of the Wanikani Open Framework.
    //-------------------------------------------------------------------
    var script_name = 'Dashboard Progress Plus';
    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,Menu,Settings').then(load_settings).then(startup);

    //========================================================================
    // Global variables
    //-------------------------------------------------------------------
    var settings, settings_dialog;

    //========================================================================
    // Load the script settings.
    //-------------------------------------------------------------------
    function load_settings() {
        var defaults = {
            visible_items: 'all',
            locked_position: 'first',
            show_90percent: true,
            show_char: true,
            show_lock_icon: true,
            show_lesson_icon: true,
            enable_popup: true,
            show_meaning: true,
            show_reading: true,
            show_srs: true,
            show_next_review: true,
            show_passed: true,
            time_format: '12hour',
        };
        return wkof.Settings.load('dpp', defaults).then(function(data){
            settings = wkof.settings.dpp;
        });
    }

    //========================================================================
    // Open the settings dialog
    //-------------------------------------------------------------------
    function open_settings() {
        var config = {
            script_id: 'dpp',
            title: 'Dashboard Progress Plus',
            on_save: settings_saved,
            on_refresh: refresh_settings,
            content: {
                tabs: {type:'tabset', content: {
                    pgLayout: {type:'page', label:'Main View', hover_tip:'Settings for the main view.', content: {
                        visible_items: {type:'dropdown', label:'Visible Items', default:'all', content:{all:'All Items',appr_only:'Apprentice Only',guru_only:'Guru+ Only'}, hover_tip:'Choose which items to show.'},
                        show_90percent: {type:'checkbox', label:'Show 90% Bracket', default:true, hover_tip:'Show the bracket around 90% of items.'},
                        show_char: {type:'checkbox', label:'Show Kanji/Radical', default:true, hover_tip:'Show the kanji or radical inside each tile.'},
                        show_lock_icon: {type:'checkbox', label:'Show Lock Icon', default:true, hover_tip:'Show a lock icon on locked items.'},
                        show_lesson_icon: {type:'checkbox', label:'Show Lesson Icon', default:true, hover_tip:'Show an "L" icon on pending lessons.'},
                        locked_position: {type:'dropdown', label:'Locked Item Position', default:'first', content:{first:'First',last:'Last'}, hover_tip:'Choose where locked items are placed.'},
                    }},
                    pgPopupInfo: {type:'page', label:'Pop-up Info', hover_tip:'Information shown in the popup box.', content: {
                        enable_popup: {type:'checkbox', label:'Enable Pop-up Info Box', default:true, refresh_on_change:true, hover_tip:'Choose whether to show pop-up info box when hovering over an item.'},
                        grpPopupInfo: {type:'group', label:'Pop-up Info', hover_tip:'Information to display in the pop-up box.', content:{
                            show_meaning: {type:'checkbox', label:'Show Meaning', default:true, hover_tip:'Choose whether to show the item\'s meaning in the pop-up info.'},
                            show_reading: {type:'checkbox', label:'Show Reading', default:true, hover_tip:'Choose whether to show the item\'s reading in the pop-up info.'},
                            show_srs: {type:'checkbox', label:'Show SRS Level', default:true, hover_tip:'Choose whether to show the item\'s SRS level in the pop-up info.'},
                            show_next_review: {type:'checkbox', label:'Show Next Review Date', default:true, hover_tip:'Choose whether to show the item\'s next review date in the pop-up info.'},
                            show_passed: {type:'checkbox', label:'Show Passed Date', default:true, hover_tip:'Choose whether to show the date that the item passed in the pop-up info.'},
                            time_format: {type:'dropdown', label:'Time Format', default:'12hour', content:{'12hour':'12-hour','24hour':'24-hour'}, hover_tip:'Display time in 12 or 24-hour format.'},
                        }}
                    }}
                }}
            }
        };
        var settings_dialog = new wkof.Settings(config);
        settings_dialog.open();
    }

    //========================================================================
    // Refresh settings dialog
    //------------------------------------------------------------------------
    function refresh_settings(settings) {
        if (settings.enable_popup) {
            $('#dpp_show_meaning').prop('disabled', false).closest('.row').removeClass('disabled');
            $('#dpp_show_reading').prop('disabled', false).closest('.row').removeClass('disabled');
            $('#dpp_show_srs').prop('disabled', false).closest('.row').removeClass('disabled');
            $('#dpp_show_next_review').prop('disabled', false).closest('.row').removeClass('disabled');
            $('#dpp_show_passed').prop('disabled', false).closest('.row').removeClass('disabled');
            $('#dpp_time_format').prop('disabled', false).closest('.row').removeClass('disabled');
        } else {
            $('#dpp_show_meaning').prop('disabled', true).closest('.row').addClass('disabled');
            $('#dpp_show_reading').prop('disabled', true).closest('.row').addClass('disabled');
            $('#dpp_show_srs').prop('disabled', true).closest('.row').addClass('disabled');
            $('#dpp_show_next_review').prop('disabled', true).closest('.row').addClass('disabled');
            $('#dpp_show_passed').prop('disabled', true).closest('.row').addClass('disabled');
            $('#dpp_time_format').prop('disabled', true).closest('.row').addClass('disabled');
        }
    }

    //========================================================================
    // Startup
    //-------------------------------------------------------------------
    function startup() {
        install_css();
        install_menu();
        init_ui();

        wkof.ItemData.get_items({
            wk_items:{
                options:{
                    assignments:true,
                    review_statistics:true
                },
                filters:{
                    level:'+0',
                    item_type:'radical,kanji',
                }
            }
        })
        .then(process_items);
    }

    //========================================================================
    // CSS Styling
    //-------------------------------------------------------------------
    var progress_css =
        '#wkofs_dpp .row.disabled label {opacity:0.5;}'+

        'div.progress-entries {grid-gap:12px 0px;}'+
        'div.progress-entry {padding:4px;}'+
        '.progress-entry.pct90 {background:#fff; border-radius:0; border-color:#777; border-style:solid; border-width:1px 0; padding-top:3px; padding-bottom:3px;}'+
        '.progress-entry.pct90.pct90_left {border-left-width:1px; border-top-left-radius:7px; border-bottom-left-radius:7px; padding-left:3px;}'+
        '.progress-entry.pct90.pct90_right {border-right-width:1px; border-top-right-radius:7px; border-bottom-right-radius:7px; padding-right:3px;}'+
        '.progression[data-hide-char="true"] .progress-entry a {color:transparent; text-shadow:unset;}'+
        '.progress-entry.dpp-noshow {display:none;}'+

        // Radical colors
        '.progress-entry[data-srs-lvl="-1"] .radical-icon, .progress-entry[data-srs-lvl="-1"] .kanji-icon {background-repeat:no-repeat;background-image: url("'+
        'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACcAAAAnCAYAAACMo1E1AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccll'+
        'PAAAAF5JREFUeNrs07ENwCAMBEDwspkp0zpBQiJN+i+OAqSvXjY3u/se51z7jcgqtdi6KrXYyua71pFY7Du5yH9XqcWAAAIIIIAAAggggAACCCCAAA'+
        'IIIIAAAgggfrNHgAEAXq5IabsNBOwAAAAASUVORK5CYII='+
        '");}'+
        '.progress-entries[data-lock-icon="y"] .progress-entry[data-srs-lvl="-1"] .radical-icon::before, .progress-entries[data-lock-icon="y"] .progress-entry[data-srs-lvl="-1"] .kanji-icon::before {'+
        '  content:"\\f023";font-family:"FontAwesome";font-size:13pt;color:#ff8;position:relative;left:-14px;top:-18px;-webkit-text-stroke:1px black;}'+
        '.progress-entries[data-lesson-icon="y"] .progress-entry[data-srs-lvl="0"] .radical-icon::before, .progress-entries[data-lesson-icon="y"] .progress-entry[data-srs-lvl="0"] .kanji-icon::before {'+
        '  content:"L";font-family:monospace;font-size:8px;font-weight:bold;color:black;position:relative;left:-14px;top:-22px;border-radius:50%;border:1px solid black; width:12px; height:12px; display:inline-block; padding:0; margin:0; line-height:12px;background-color:#ff8;}'+
        '.progress-entry[data-srs-lvl="-1"] .radical-icon {background-color:#00aaff;}'+
        '.progress-entry[data-srs-lvl="0"] .radical-icon {background-color:#00aaff;}'+
        '.progress-entry[data-srs-lvl="1"] .radical-icon {background-color:#00aaff;}'+
        '.progress-entry[data-srs-lvl="2"] .radical-icon {background-color:#00aaff; background-image:linear-gradient(0deg,#00aaff,#00aaff);}'+
        '.progress-entry[data-srs-lvl="3"] .radical-icon {background-color:#00aaff; background-image:linear-gradient(0deg,#00aaff,#00aaff);}'+
        '.progress-entry[data-srs-lvl="4"] .radical-icon {background-color:#00aaff; background-image:linear-gradient(0deg,#00aaff,#00aaff);}'+
        '.progress-entry[data-srs-lvl="5"] .radical-icon {background-color:#b69acd; background-image:linear-gradient(0deg,#9065b3,#b69acd);}'+
        '.progress-entry[data-srs-lvl="6"] .radical-icon {background-color:#b69acd; background-image:linear-gradient(0deg,#9065b3,#b69acd);}'+
        '.progress-entry[data-srs-lvl="7"] .radical-icon {background-color:#9aa5cf; background-image:linear-gradient(0deg,#7483be,#9aa5cf);}'+
        '.progress-entry[data-srs-lvl="8"] .radical-icon {background-color:#a3c3d3; background-image:linear-gradient(0deg,#75a5bd,#a3c3d3);}'+
        '.progress-entry[data-srs-lvl="9"] .radical-icon {background-color:#999999; background-image:linear-gradient(0deg,#737373,#999999);}'+

        // Kanji colors
        '.progress-entry[data-srs-lvl="-1"] .radical-icon {background-color:#00aaff;}'+
        '.progress-entry[data-srs-lvl="0"] .kanji-icon {background-color:#ff00aa;}'+
        '.progress-entry[data-srs-lvl="1"] .kanji-icon {background-color:#ff00aa; background-image:linear-gradient(0deg,#cc0088,#ff00aa);}'+
        '.progress-entry[data-srs-lvl="2"] .kanji-icon {background-color:#ff00aa; background-image:linear-gradient(0deg,#cc0088,#ff00aa);}'+
        '.progress-entry[data-srs-lvl="3"] .kanji-icon {background-color:#ff00aa; background-image:linear-gradient(0deg,#cc0088,#ff00aa);}'+
        '.progress-entry[data-srs-lvl="4"] .kanji-icon {background-color:#ff00aa; background-image:linear-gradient(0deg,#cc0088,#ff00aa);}'+
        '.progress-entry[data-srs-lvl="5"] .kanji-icon {background-color:#b69acd; background-image:linear-gradient(0deg,#9065b3,#b69acd);}'+
        '.progress-entry[data-srs-lvl="6"] .kanji-icon {background-color:#b69acd; background-image:linear-gradient(0deg,#9065b3,#b69acd);}'+
        '.progress-entry[data-srs-lvl="7"] .kanji-icon {background-color:#9aa5cf; background-image:linear-gradient(0deg,#7483be,#9aa5cf);}'+
        '.progress-entry[data-srs-lvl="8"] .kanji-icon {background-color:#a3c3d3; background-image:linear-gradient(0deg,#75a5bd,#a3c3d3);}'+
        '.progress-entry[data-srs-lvl="9"] .kanji-icon {background-color:#999999; background-image:linear-gradient(0deg,#737373,#999999);}'+

        '.progress-entries .popover {border-radius:5px; border:5px solid rgba(75,75,75,0.8); box-shadow:none;}'+
        '.progression .popover.right .arrow {border-right-color:rgba(75,75,75,0.8); left:-16px;}'+
        '.progression .popover.right .arrow:after {border-color:transparent;}'+
        '.progression .popover.left .arrow {border-left-color:rgba(75,75,75,0.55);}'+
        '.progression .popover .popover-content {text-shadow: 0 1px 0 #fff;}'+
        '.progression .popover .srs {font-size:75%; font-weight:bold;}'+
        '.progression .popover .next {font-size:75%; font-weight:bold;}'+

        '.progression[data-show-item-name="false"] .lattice-single-character li>a {color:rgba(0,0,0,0);text-shadow:0 0 0 rgba(0,0,0,0);}';

    //========================================================================
    // Install stylesheet.
    //-------------------------------------------------------------------
    function install_css() {
        $('head').append('<style>'+progress_css+'</style>');
    }

    //========================================================================
    // Install menu link
    //-------------------------------------------------------------------
    function install_menu() {
		// Set up menu item to open script.
		wkof.Menu.insert_script_link({name:'dpp',submenu:'Settings',title:'Dashboard Progress Plus',on_click:open_settings});
    }

    //========================================================================
    // Initialize the user interface.
    //-------------------------------------------------------------------
    function init_ui() {
        $('.progression').attr('data-hide-char', !settings.show_char);
        if (settings.enable_popup) {
            $('.progress-entries').popover({
                selector:'.progress-entry',
                trigger:'hover',
                animation: false,
                html:true,
                content:generate_item_info,
                placement:place_item_info,
            });
        } else {
            $('.progress-entries').popover('destroy');
        }
        $('.progress-entries').attr('data-lock-icon', (settings.show_lock_icon ? 'y' : 'n'));
        $('.progress-entries').attr('data-lesson-icon', (settings.show_lesson_icon ? 'y' : 'n'));
    }

    //========================================================================
    // Handler for when user clicks 'Save' in the settings window.
    //-------------------------------------------------------------------
    function settings_saved(new_settings) {
        init_ui();
        populate_item_info('radical');
        populate_item_info('kanji');
    }

    //========================================================================
    // Populate level info from API.
    //-------------------------------------------------------------------
    function process_items(data) {
        gobj.items = wkof.ItemData.get_index(data, 'item_type');

        populate_item_info('radical');
        populate_item_info('kanji');
    }

    //========================================================================
    // Generate content for popover.
    //-------------------------------------------------------------------
    function generate_item_info() {
        // Populate the next review date.
        var elem = $(this)
        var item = $(this).data('item');
        var html = [];

        // Functions for filtering and sorting information.
        function accepted_first(a, b) {
            if (a.accepted_answer === b.accepted_answer) return 0;
            if (a.accepted_answer) return -1;
            return 1;
        }
        function primary(a) {return a.primary;}
        function to_meaning(a) {return a.meaning;}
        function to_reading(a) {return a.reading;}

        // Meaning
        if (settings.show_meaning) {
            var meaning = item.data.meanings.filter(primary).sort(accepted_first).map(to_meaning).join(', ');
            html.push('<span class="meaning">'+meaning+'</span>');
        }

        // Reading
        if (settings.show_reading && item.object === 'kanji') {
            var reading = item.data.readings.filter(primary).sort(accepted_first).map(to_reading).join(', ');
            html.push('<span class="reading" lang="ja">'+reading+'</span>');
        }

        // SRS Stage
        if (settings.show_srs && item.assignments && item.assignments.srs_stage_name) {
            html.push('<span class="srs">SRS: '+item.assignments.srs_stage_name+'</span>');
        }

        // Pass Date and Next Review
        var next = [];
        var date;
        if (item.assignments && item.assignments.available_at) {
            if (item.assignments.passed_at) {
                if (settings.show_passed) {
                    if (item.assignments.passed_at) {
                        date = formatDate(new Date(item.assignments.passed_at), false /* is_next_date */);
                    } else {
                        date = 'A long time ago...';
                    }
                    next.push('Passed: '+date);
                }
            }
            if (item.assignments.srs_stage == 9) {
                if (settings.show_passed) {
                    date = formatDate(new Date(item.assignments.burned_at), false /* is_next_date */);
                    next.push('Burned: '+date);
                } else {
                    next.push('Burned!');
                }
            } else if (settings.show_next_review) {
                date = formatDate(new Date(item.assignments.available_at), true /* is_next_date */);
                next.push('Next: '+date);
            }
        } else if (item.assignments && item.assignments.unlocked_at) {
            next.push('Lesson: Available Now');
        } else {
            next.push('Locked!');
        }

        // Populate remaining data for popup window.
        if (next.length !== 0) {
            html.push('<span class="next">'+next.join('<br>')+'</span>');
        }

        return html.join('<br>');
    }

    //========================================================================
    // Determine whether the popover should be to the left or right of the element.
    //-------------------------------------------------------------------
    function place_item_info() {
        var elem = this.$element.eq(0);
        var parent = elem.parent();
        return ((elem.position().left + elem.width() - parent.position().left) <= (parent.width()/2) ? 'right' : 'left');
    }

    //========================================================================
    // Determine whether the item is "Initiate" stage (i.e. unlocked but lesson not done).
    //-------------------------------------------------------------------
    function is_initiate(item) {
        return (item.assignments && item.assignments.unlocked_at ? true : false);
    }

    //========================================================================
    // Populate level info from API.
    //-------------------------------------------------------------------
    function populate_item_info(itype) {
        var group,elems;
        if (itype === 'radical') {
            group = $('.progress-entries').eq(0);
            group.attr('data-type','radical');
        } else {
            group = $('.progress-entries').eq(1);
            group.attr('data-type','kanji');
        }
        elems = group.find('.progress-entry');
        var items = wkof.ItemData.get_index(gobj.items[itype], 'slug');

        if (itype === 'kanji') {
            var xx = items['中'];
            xx.assignments.unlocked_at = null;
        }

        // Populate item data.
        elems.each(function(idx, elem){
            elem = $(elem);
            elem.removeAttr('title');
            var a = elem.find('a');
            var slug;
            if (itype === 'radical') {
                slug = a.attr('href').split('/')[2];
            } else {
                slug = a.text();
            }
            var item = items[slug];
            elem.data('item', item);

            elem.addClass('dpp-progress');

            // Populate 'data-srs-lvl', which is a styling selector.
            var srs = (item.assignments && item.assignments.srs_stage ? item.assignments.srs_stage : (is_initiate(item) ? 0 : -1)); // -1 == locked
            elem.attr('data-srs-lvl', srs);
        });

        // Sort items by srs level, then review date, then meaning.
        var srs_locked = (settings.locked_position === 'first' ? -1 : 10);
        elems.sort(function(a,b){
            if (itype === 'radical') {
                a = items[$(a).find('.radical-icon').attr('href').split('/')[2]];
                b = items[$(b).find('.radical-icon').attr('href').split('/')[2]];
            } else {
                a = items[$(a).text()];
                b = items[$(b).text()];
            }

            var a_passed = (a && a.assignments && a.assignments.passed_at);
            var b_passed = (b && b.assignments && b.assignments.passed_at);
            if (!a_passed && b_passed) return -1;
            if (a_passed && !b_passed) return 1;
            var a_srs = (a && a.assignments && a.assignments.srs_stage ? a.assignments.srs_stage : (is_initiate(a) ? 0 : srs_locked));
            var b_srs = (b && b.assignments && b.assignments.srs_stage ? b.assignments.srs_stage : (is_initiate(b) ? 0 : srs_locked));
            if (a_srs < b_srs) return -1;
            if (a_srs > b_srs) return 1;
            if (a_srs != 0) {
                var a_avail = (a && a.assignments && a.assignments.available_at ?
                               new Date(a.assignments.available_at).getTime() : Number.MAX_SAFE_INTEGER);
                var b_avail = (b && b.assignments && b.assignments.available_at ?
                               new Date(b.assignments.available_at).getTime() : Number.MAX_SAFE_INTEGER);
                if (a_avail < b_avail) return 1;
                if (a_avail > b_avail) return -1;
            }
            if (a.data.slug < b.data.slug) return -1;
            if (a.data.slug > b.data.slug) return 1;
            return 0;
        });
        elems.detach().appendTo(group);

        elems.removeClass('dpp-noshow pct90_left pct90 pct90_right');
        var srslvl;
        switch (settings.visible_items) {
            case 'appr_only':
                for (srslvl=5; srslvl<=9; srslvl++) {
                    $('.progress-entry[data-srs-lvl="'+srslvl+'"]').addClass('dpp-noshow');
                }
                break;
            case 'guru_only':
                for (srslvl=0; srslvl<=4; srslvl++) {
                    $('.progress-entry[data-srs-lvl="'+srslvl+'"]').addClass('dpp-noshow');
                }
                break;
        }
        if (settings.show_90percent && itype === 'kanji') {
            // 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(elems.length * 0.1);
            var len = elems.children(':not(.dpp-noshow)').length;
            if (idx90 < len) {
                var visible_elems = elems.filter(':not(.dpp-noshow)');
                visible_elems.eq(idx90).addClass('pct90_left');
                visible_elems.slice(idx90).addClass('pct90');
                visible_elems.last().addClass('pct90_right');
            }
        }
    }

    //========================================================================
    // Print date in pretty format.
    //-------------------------------------------------------------------
    function formatDate(d, is_next_date){
        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 (is_next_date && 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+', ';
        }
        if (settings.time_format === '24hour') {
            s += ('0'+hh).slice(-2)+':'+('0'+mm).slice(-2);
        } else {
            s += (((hh+11)%12)+1)+':'+('0'+mm).slice(-2)+['am','pm'][Math.floor(d.getHours()/12)];
        }

        // Append "(X days)".
        if (is_next_date && !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;
    }

})(window.dpp);