Wanikani Forecast Details

Adds markup & tooltips for item types, critical reviews, burn reviews to the forecast and review buttons

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Wanikani Forecast Details
// @namespace   https://www.wanikani.com
// @author      kernfel
// @description Adds markup & tooltips for item types, critical reviews, burn reviews to the forecast and review buttons
// @version     1.5.5
// @include     /^https://(www|preview).wanikani.com/(dashboard)?$/
// @copyright   2020+, Felix Kern
// @license     MIT; http://opensource.org/licenses/MIT
// @run-at      document-end
// @grant       none
// ==/UserScript==

(function() {

    /* global $, wkof */

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

    //========================================================================
    // Settings
    //-------------------------------------------------------------------
    var settings;
    function load_settings() {
        var defaults = {
            crit_highlight: true,
            crit_highlight_now: true,
            add_crit_icon: true,
            add_crit_icon_now: true,
            crit_icon: '🔺',

            burn_highlight: true,
            burn_highlight_now: true,
            add_burn_icon: true,
            add_burn_icon_now: true,
            burn_icon: '🔥',

            bar_colours: 'type'
        };
        wkof.Settings.load('forecast_details', defaults).then(function(){
            settings = wkof.settings.forecast_details
        });
    }

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

        wkof.ItemData.get_items({
            wk_items:{
                options:{
                    assignments:true
                },
                filters:{
                    srs: '1,2,3,4,5,6,7,8',
                    level: '1..+0'
                }
            }
        })
        .then(process_items);

        wkof.ItemData.get_items({
            wk_items:{
                options:{
                    assignments:true
                },
                filters:{
                    srs: '1,2,3,4',
                    level: '+0',
                    item_type: 'rad,kan'
                }
            }
        })
        .then(process_crits);
    }

    //========================================================================
    // CSS Styling
    //-------------------------------------------------------------------
    var fcr_css =
        // Bar highlights
        '.fcr_apprentice { background: #59c27450; }'+
        '.fcr_burn { background: #fbc04250; }'+
        '.fcr_burn.fcr_apprentice { background: #aac15b50; }'+

        // Review buttons
        '.lessons-and-reviews__button span.fcr_apprentice, .navigation-shortcut a.fcr_apprentice { background: #59c274; text-shadow: none; color: white; }'+
        '.lessons-and-reviews__button span.fcr_burn, .navigation-shortcut a.fcr_burn { background: #fbc042; text-shadow: none; color: white; }'+
        '.lessons-and-reviews__button span.fcr_apprentice.fcr_burn, .navigation-shortcut a.fcr_apprentice.fcr_burn { background: #aac15b; }'+

        // Item type bar split
        '.fcr_radical { background: #00AAFF; }'+
        '.fcr_kanji { background: #FF00AA; }'+
        '.fcr_vocab { background: #AA00FF; }'+

        // SRS level bar split
        '.fcr_bar_apprentice { background: #dd0093; }'+
        '.fcr_bar_guru { background: #882d9e; }'+
        '.fcr_bar_master { background: #294ddb; }'+
        '.fcr_bar_enlightened { background: #0093dd; }'+

        // Crit/burn icons
        '.fcr_icon { float: right; padding-right: 1px; }'+

        // Layout details
        '.fcr_split_bar { display: inline-block; }'+
        '.review-forecast__bar { min-width: 6px; }'+
        '.fcr_lineheight_fix { line-height: 0; }'+
        '.review-forecast__day.mb-3 { padding-bottom: 12px; }'+
        '.review-forecast__day.mb-3.is-collapsed { padding-bottom: 0; }'+
        '.review-forecast__hour.pb-2 { padding-bottom: 0 !important; }'+
        '.review-forecast__hour:last-child>* { padding-bottom: 0 !important; }'
    ;

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

    //========================================================================
    // Add detailed information and burns to forecast bars & review buttons
    //-------------------------------------------------------------------
    function process_items(data) {
        var now = new Date(), rtime, counts = {}, stage;
        // Count typed reviews in each hour
        for ( var idx in data ) {
            if ( Date.parse(data[idx].assignments.available_at) - 7*24*3600*1000 < now ) {
                rtime = data[idx].assignments.available_at.split('.')[0];
                if ( Date.parse(data[idx].assignments.available_at) < now ) {
                    rtime = 'now';
                }
                if ( ! (rtime in counts) ) {
                    counts[rtime] = {
                        'radical':0,
                        'kanji':0,
                        'vocabulary':0,

                        'burn': {
                            'radical':0,
                            'kanji':0,
                            'vocabulary':0
                        },
                        'hasBurn': false,

                        4: 0, // Apprentice
                        5: 0, // Guru
                        7: 0, // Master
                        8: 0 // Enlightened
                    };
                }
                counts[rtime][data[idx].object]++; // counts by type
                stage = data[idx].assignments.srs_stage;
                if ( stage < 5 ) stage = 4;
                if ( stage == 6 ) stage = 5;
                counts[rtime][stage]++; // counts by srs stage
                if ( data[idx].assignments.srs_stage == 8 ) {
                    counts[rtime].burn[data[idx].object]++;
                    counts[rtime].hasBurn = true;
                }
            }
        }

        // Split forecast bars into types, add informative titles, and add burn markup
        var bar, tr, review_btn, w0, n, title;
        for ( rtime in counts ) {
            // Locate the review bar
            bar = $('section.forecast tr.review-forecast__hour time[datetime="' + rtime + 'Z"]')
                .parents('th').first().siblings('td').first().children('span').first();
            if ( !bar.length ) {
                continue;
            }
            tr = bar.parents('tr').first();

            // Split the bar into typed segments
            if ( settings.bar_colours == 'type' ) {
                split_bar(bar, counts[rtime], barsplit_type);
            } else if ( settings.bar_colours == 'srs' ) {
                split_bar(bar, counts[rtime], barsplit_srs);
            }

            // Add tooltips and burn highlights
            tr.attr('title', setTitle(counts[rtime], 'Total reviews'))
                .attr('title', setTitleByLevel(counts[rtime]));
            if ( counts[rtime].hasBurn ) {
                tr.attr('title', setTitle(counts[rtime].burn, 'Burn'));
                if ( settings.burn_highlight ) {
                    tr.addClass('fcr_burn');
                }
                if ( settings.add_burn_icon ) {
                    tr.children('th').append('<span class="fcr_icon fcr_icon_burn">' + settings.burn_icon + '</span>');
                }
            }
        }

        // Add currently available info & burns the review buttons
        if ( 'now' in counts ) {
            review_btn = $('a.lessons-and-reviews__reviews-button, li.navigation-shortcut--reviews')
                .attr('title', setTitle(counts.now, 'Total reviews'))
                .attr('title', setTitleByLevel(counts.now));
            if ( counts.now.hasBurn ) {
                review_btn.attr('title', setTitle(counts.now.burn, 'Burn'));
                if ( settings.burn_highlight_now ) {
                    review_btn.children().addClass('fcr_burn');
                }
                if ( settings.add_burn_icon_now ) {
                    review_btn.children().append(settings.burn_icon);
                }
            }
        }
    }

    //========================================================================
    // Add level-critical information to forecast bars & review buttons
    //-------------------------------------------------------------------
    function process_crits(data) {
        var now = new Date(), rtime, counts = {};
        // Count typed reviews in each hour
        for ( var idx in data ) {
            if ( data[idx].assignments.passed_at === null && Date.parse(data[idx].assignments.available_at) - 7*24*3600*1000 < now ) {
                rtime = data[idx].assignments.available_at.split('.')[0];
                if ( Date.parse(data[idx].assignments.available_at) < now ) {
                    rtime = 'now';
                }
                if ( ! (rtime in counts) ) {
                    counts[rtime] = {
                        'radical':0,
                        'kanji':0
                    };
                }
                counts[rtime][data[idx].object]++; // crit counts by type
            }
        }

        // Add crit markup to forecast
        var tr, review_btn, w0, n, title;
        for ( rtime in counts ) {
            // Locate the review bar
            tr = $('section.forecast tr.review-forecast__hour time[datetime="' + rtime + 'Z"]')
                .parents('tr').first();
            if ( !tr.length ) {
                continue;
            }

            // Add tooltips and markup
            tr.attr('title', setTitle(counts[rtime], 'Critical'));
            if ( settings.crit_highlight ) {
                tr.addClass('fcr_apprentice');
            }
            if ( settings.add_crit_icon ) {
                tr.children('th').append('<span class="fcr_icon fcr_icon_critical">' + settings.crit_icon + '</span>');
            }
        }

        // Add currently available crits to review buttons
        if ( 'now' in counts ) {
            review_btn = $('a.lessons-and-reviews__reviews-button, li.navigation-shortcut--reviews')
                .attr('title', setTitle(counts.now, 'Critical')).children();
            if ( settings.crit_highlight_now ) {
                review_btn.addClass('fcr_apprentice');
            }
            if ( settings.add_crit_icon_now ) {
                review_btn.append(settings.crit_icon);
            }
        }
    }

    //========================================================================
    // Helper functions
    //-------------------------------------------------------------------
    var setTitle = function(countObj, name){
        return function(idx, title){
            title = (title ? title + '\n' : '') + name + ': ' + countObj.radical + ' radicals, '
                + countObj.kanji + ' kanji';
            if ( countObj.vocabulary != undefined ) {
                title += ', ' + countObj.vocabulary + ' vocabulary';
            }
            return title;
        }
    }
    var setTitleByLevel = function(countObj){
        return function(idx, title){
            title = (title ? title + '\n' : '')
                + countObj[4] + ' apprentice, '
                + countObj[5] + ' guru, '
                + countObj[7] + ' master, '
                + countObj[8] + ' enlightened';
            return title;
        }
    }

    function split_bar(bar, counts, config) {
        var n = 0, w0, c;
        for ( c in config ) {
            n += counts[c]
        }
        bar.removeClass('rounded-r-lg').addClass('fcr_split_bar');
        w0 = parseFloat(bar.attr('style').match("width: ([0-9.]+)%")[1]);
        for ( c in config ) {
            if ( counts[c] ) {
                bar.clone().addClass(config[c]).css('width', counts[c] * w0 / n + '%').appendTo(bar.parent());
            }
        }
        bar.siblings().last().addClass('rounded-r-lg').parent().addClass('fcr_lineheight_fix');
        if ( w0 > 95 ) {
            bar.siblings().css('min-width', 0);
        }
        bar.remove();
    }
    var barsplit_type = {
        radical: 'fcr_radical',
        kanji: 'fcr_kanji',
        vocabulary: 'fcr_vocab'
    };
    var barsplit_srs = {
        4: 'fcr_bar_apprentice',
        5: 'fcr_bar_guru',
        7: 'fcr_bar_master',
        8: 'fcr_bar_enlightened'
    };

    //========================================================================
    // Menu
    //-------------------------------------------------------------------
    function install_menu() {
        wkof.Menu.insert_script_link({
            name: 'forecast_details',
            title: 'Forecast Details',
            submenu: 'Settings',
            on_click: open_settings
        });
    }

    function open_settings() {
        var config = {
            script_id: 'forecast_details',
            title: 'Forecast Details settings',
            content: {
                tabset: {
                    type: 'tabset',
                    content: {
                        crits: {
                            type: 'page',
                            label: 'Level-critical reviews',
                            content: {
                                crit_icon: {
                                    type: 'text',
                                    label: 'Icon (Default: 🔺)',
                                },
                                sec_forecast: {
                                    type: 'section',
                                    label: 'Forecast'
                                },
                                crit_highlight: {
                                    type: 'checkbox',
                                    label: 'Highlight bars',
                                },
                                add_crit_icon: {
                                    type: 'checkbox',
                                    label: 'Add icons'
                                },
                                sec_now: {
                                    type: 'section',
                                    label: 'Review buttons (currently available items)'
                                },
                                crit_highlight_now: {
                                    type: 'checkbox',
                                    label: 'Add highlight'
                                },
                                add_crit_icon_now: {
                                    type: 'checkbox',
                                    label: 'Add icon'
                                }
                            }
                        },
                        burns: {
                            type: 'page',
                            label: 'Burn reviews',
                            content: {
                                burn_icon: {
                                    type: 'text',
                                    label: 'Icon (Default: 🔥)',
                                },
                                sec_forecast: {
                                    type: 'section',
                                    label: 'Forecast'
                                },
                                burn_highlight: {
                                    type: 'checkbox',
                                    label: 'Highlight bars',
                                },
                                add_burn_icon: {
                                    type: 'checkbox',
                                    label: 'Add icons'
                                },
                                sec_now: {
                                    type: 'section',
                                    label: 'Review buttons (currently available items)'
                                },
                                burn_highlight_now: {
                                    type: 'checkbox',
                                    label: 'Add highlight'
                                },
                                add_burn_icon_now: {
                                    type: 'checkbox',
                                    label: 'Add icon'
                                }
                            }
                        },
                        misc: {
                            type: 'page',
                            label: 'Other forecast details',
                            content: {
                                bar_colours: {
                                    type: 'dropdown',
                                    label: 'Colour bars by',
                                    content: {
                                        none: 'None',
                                        type: 'Item type',
                                        srs: 'SRS stage',
                                    }
                                }
                            }
                        }
                    }
                },
                divider: {
                    type: 'divider'
                },
                note: {
                    type: 'section',
                    label: 'Note, changes come into effect when the page is reloaded.',
                }
            }
        };
        var dialog = new wkof.Settings(config);
        dialog.open();
    }
})();