Wanikani Dashboard Progress Plus

Display detailed level progress.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        Wanikani Dashboard Progress Plus
// @namespace   rfindley
// @description Display detailed level progress.
// @version     3.1.8
// @match       https://www.wanikani.com/
// @match       https://www.wanikani.com/dashboard
// @match       https://preview.wanikani.com/
// @match       https://preview.wanikani.com/dashboard
// @copyright   2015-2023, 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.
    //-------------------------------------------------------------------
    let 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
    //-------------------------------------------------------------------
    let settings, settings_dialog;

    //========================================================================
    // Load the script settings.
    //-------------------------------------------------------------------
    function load_settings() {
        let 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,
            lesson_order: 'wanikani',
            time_format: '12hour',
        };
        return wkof.Settings.load('dpp', defaults).then(function(data){
            settings = wkof.settings.dpp;
        });
    }

    //========================================================================
    // Open the settings dialog
    //-------------------------------------------------------------------
    function open_settings() {
        let 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',non_passed:'Non-passed (Apprentice)',passed:'Passed Items (Guru+)'}, 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.'},
                        lesson_order: {type:'dropdown', label:'Lesson Sort Order', default:'wanikani', content:{wanikani:'WaniKani Order',subject:'Subject'}, hover_tip:'Choose the sort order of lesson items.'},
                    }},
                    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.'},
                        }}
                    }}
                }}
            }
        };
        let 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
                },
                filters:{
                    level:'+0',
                    item_type:'radical,kanji',
                }
            }
        })
        .then(process_items);
    }

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

        'div.dpp-progress {margin-top:0; margin-right:0; padding-right:8px;}'+
        '.dpp-progress:not(.pct90) a {margin-top:4px;}'+
        '.dpp-progress a {position:relative;}'+
        '.dpp-progress.pct90 {background:#fff; border-radius:0; border-color:#777; border-style:solid; border-width:1px 0; padding-top:3px; padding-bottom:2px;}'+
        '.dpp-progress.pct90.pct90_left {border-left-width:1px; border-top-left-radius:7px; border-bottom-left-radius:7px; padding-left:3px; margin-left:-4px;}'+
        '.dpp-progress.pct90.pct90_right {border-right-width:1px; border-top-right-radius:7px; border-bottom-right-radius:7px; padding-right:3px;}'+
        '.level-progress-dashboard__content[data-hide-char="true"] .dpp-progress a {color:transparent; text-shadow:unset;}'+
        '.dpp-progress.dpp-noshow {display:none;}'+

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

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

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

    //========================================================================
    // 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() {
        $('.level-progress-dashboard__content').attr('data-hide-char', !settings.show_char);
        if (settings.enable_popup) {
            $('.level-progress-dashboard__items').popover({
                selector:'.dpp-progress',
                trigger:'hover',
                animation: false,
                html:true,
                content:generate_item_info,
                placement:place_item_info,
            });
        } else {
            $('.level-progress-dashboard__items').popover('destroy');
        }
        $('.level-progress-dashboard__items').attr('data-lock-icon', (settings.show_lock_icon ? 'y' : 'n'));
        $('.level-progress-dashboard__items').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.
    //-------------------------------------------------------------------
    let srs_stages = ['Lesson','Apprentice 1','Apprentice 2','Apprentice 3','Apprentice 4','Guru 1','Guru 2','Master','Enlightened','Burned'];
    srs_stages[-1] = 'Locked';
    function generate_item_info() {
        // Populate the next review date.
        let elem = $(this);
        let item = elem.data('item');
        let 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) {
            let 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') {
            let 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) {
            html.push('<span class="srs">SRS: '+srs_stages[item.assignments.srs_stage]+'</span>');
        }

        // Pass Date and Next Review
        let next = [];
        let date;
        if (item.assignments) {
            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 (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 (settings.show_next_review) {
                    date = formatDate(new Date(item.assignments.available_at), true /* is_next_date */);
                    next.push('Next: '+date);
                }
            } else if (item.assignments.unlocked_at) {
                next.push('Lesson: Available Now');
            } else {
                next.push('Locked!');
            }
        } 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() {
        let elem = this.$element.eq(0);
        let parent = elem.parent();
        return ((elem.position().left + elem.width() - parent.position().left) <= (parent.width()/2) ? 'right' : 'left');
    }

    //========================================================================
    // Determine whether the item is unlocked.
    //-------------------------------------------------------------------
    function is_unlocked(item) {
        return (item && item.assignments && item.assignments.unlocked_at ? true : false);
    }

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

    //========================================================================
    // Determine whether the item has been previously Guru'd.
    //-------------------------------------------------------------------
    function is_passed(item) {
        return (item && item.assignments && item.assignments.passed_at ? true : false);
    }

    //========================================================================
    // Populate level info from API.
    //-------------------------------------------------------------------
    function populate_item_info(itype) {
        let group,elems,items;
        let section_idx = Array.from(document.querySelectorAll('.level-progress-dashboard__content-title')).map((e)=>e.textContent.trim().toLowerCase().slice(0,3)).indexOf(itype.slice(0,3));
        if (section_idx < 0) return;
        group = $('.level-progress-dashboard__items').eq(section_idx);
        group.attr('data-type',itype);
        elems = group.find('.level-progress-dashboard__item');
        items = wkof.ItemData.get_index(gobj.items[itype], 'slug');

        // Populate item data.
        elems.each(function(idx, elem){
            elem = $(elem);
            let a = elem.find('a');
            a.removeAttr('title');
            let slug;
            if (itype === 'radical') {
                slug = decodeURIComponent(a.attr('href').split('/').slice(-1)[0]);
            } else {
                slug = decodeURIComponent(a[0].innerText.trim());
            }
            let item = items[slug];
            elem.data('item', item);

            elem.addClass('dpp-progress');

            // Populate 'data-srs-lvl', which is a styling selector.
            let 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.
        if (typeof elems.eq(0).data('original_position') !== 'number') {
            elems.each((idx, elem) => $(elem).data('original_position', idx));
        }
        let locked_last = (settings.locked_position === 'last');
        let srs_locked = (locked_last ? 10 : -1);
        elems.sort(function(a,b){
            let a_pos = $(a).data('original_position');
            let b_pos = $(b).data('original_position');
            if (itype === 'radical') {
                a = items[$(a).find('a').attr('href').split('/').slice(-1)[0]];
                b = items[$(b).find('a').attr('href').split('/').slice(-1)[0]];
            } else {
                a = items[a.innerText];
                b = items[b.innerText];
            }
            if (!locked_last) {
                let a_passed = is_passed(a);
                let b_passed = is_passed(b);
                if (!a_passed && b_passed) return -1;
                if (a_passed && !b_passed) return 1;
            }
            let a_srs = (a && a.assignments && a.assignments.srs_stage ? a.assignments.srs_stage : (is_initiate(a) ? 0 : srs_locked));
            let 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) {
                let a_avail = (a && a.assignments && a.assignments.available_at ?
                               new Date(a.assignments.available_at).getTime() : Number.MAX_SAFE_INTEGER);
                let 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;
            } else {
                if (settings.lesson_order === 'wanikani') {
                    return (a_pos < b_pos ? -1 : 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');
        let srslvl;
        switch (settings.visible_items) {
            case 'non_passed':
                elems.each(function(idx, elem){
                    elem = $(elem);
                    let item = items[elem.text()];
                    if (is_passed(item)) elem.addClass('dpp-noshow');
                });
                break;
            case 'passed':
                elems.each(function(idx, elem){
                    elem = $(elem);
                    let item = items[elem.text()];
                    if (!is_passed(item)) elem.addClass('dpp-noshow');
                });
                break;
        }

        if (settings.show_90percent && settings.visible_items !== 'passed' && itype === 'kanji') {
            // Add marker at 90%, indicating when level will be complete.
            let needed = Math.ceil(elems.length * 0.9);
            let locked = 0;
            let passed = 0;
            let passed_hidden = 0;
            let visible = 0;
            elems.each(function(idx, elem){
                elem = $(elem);
                let item = items[$(elem).text()];
                if (is_passed(item)) {
                    passed++;
                    if (elem.hasClass('dpp-noshow')) passed_hidden++;
                }
                if (!is_unlocked(item)) locked++;
            });

            let visible_elems = elems.filter(':not(.dpp-noshow)');
            let visible_len = visible_elems.length;

            let first = elems.length - needed;
            let last = first + (needed - 1) - passed_hidden;
            if (locked_last) {
                let shift = Math.min(first, locked);
                first -= shift;
                last -= shift;
            }
            if (first <= last) {
                visible_elems.eq(first).addClass('pct90_left');
                visible_elems.slice(first, last + 1).addClass('pct90');
                visible_elems.eq(last).addClass('pct90_right');
            }
        }
    }

    //========================================================================
    // Print date in pretty format.
    //-------------------------------------------------------------------
    function formatDate(d, is_next_date){
        let s = '';
        let now = new Date();
        let 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";
        let 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) {
            let 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);