Wanikani Levels Overview Plus

Improves the levels overview popup with progress indications

当前为 2020-01-06 提交的版本,查看 最新版本

// ==UserScript==
// @name          Wanikani Levels Overview Plus
// @namespace     Mercieral
// @description   Improves the levels overview popup with progress indications
// @include       https://www.wanikani.com/*
// @version       2.0.0
// @run-at        document-end
// @grant         none
// ==/UserScript==

/* global $ */

(function() {
    // Reqire the WK open resource
    if (!window.wkof) {
        alert('"Wanikani Levels Overview Plus" script requires Wanikani Open Framework.\nYou will now be forwarded to installation instructions.');
        window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
        return;
    }

    // Initialize the level stage data array
    const levelCounts = [];
    for (let i = 0; i < 61; i++) {
        levelCounts.push({
            locked: 0,
            apprentice: 0,
            guru: 0,
            master: 0,
            enlighten: 0,
            burn: 0,
            nextReviewItem: null
        });
    }

    window.wkof.include('ItemData');
    window.wkof.ready('document,ItemData').then(startup).catch(loadError);

    /**
     * Parse the data once the document and WKOF data is ready
     */
    function startup () {
        initCSS();
        if (window.wkof.ItemData != null) {
            const config = {
                wk_items: {
                    options: {assignments: true}
                }
            };
            window.wkof.ItemData.get_items(config).then(function (processItems) {
                if (processItems != null) {
                    for (let i = 0, itemsLen = processItems.length; i < itemsLen; i++) {
                        const item = processItems[i];
                        if (item != null) {
                        }
                        const srsLevel = item && item.assignments && item.assignments.srs_stage;
                        const level = item && item.data && item.data.level;
                        const levelStageCounts = levelCounts[level];
                        if (levelStageCounts == null) {
                            // skip this item...
                            continue;
                        }

                        // Incremement the appropriate level's stage counter for this item
                        if (srsLevel == null || srsLevel === 0) {
                            levelStageCounts.locked++;
                        } else if (srsLevel < 5) {
                            levelStageCounts.apprentice++;
                        } else if (srsLevel < 7) {
                            levelStageCounts.guru++;
                        } else if (srsLevel === 7) {
                            levelStageCounts.master++;
                        } else if (srsLevel === 8) {
                            levelStageCounts.enlighten++;
                        } else if (srsLevel === 9) {
                            levelStageCounts.burn++;
                        }

                        let nextReviewDate = item && item.assignments && item.assignments.available_at && new Date(item.assignments.available_at);
                        if (nextReviewDate != null && (levelStageCounts.nextReviewItem == null || nextReviewDate < levelStageCounts.nextReviewItem.reviewDate)) {
                            levelStageCounts.nextReviewItem = {
                                reviewDate: nextReviewDate,
                                characters: item.data.characters,
                                type: item.object,
                            }
                        }
                    }
                    finishUI();
                }
                else {
                    loadError();
                }
            }).catch(loadError);
        }
        else {
            loadError();
        }
    };

    /**
     * Create the UI once the data has been parsed into the data array
     */
    function finishUI () {
        // Get the HTML square elements for each level in the popout
        const levelBlocks = $('.sitemap__expandable-chunk--levels > .sitemap__grouped-pages > ol > li > a');
        for (let levelBlock of levelBlocks) {
            levelBlock = $(levelBlock);
            levelBlock.addClass('level-block');
            const originalLevelText = levelBlock.text();
            const levelStageCounts = levelCounts[Number(originalLevelText)];
            const levelTotal = levelStageCounts.locked + levelStageCounts.apprentice + levelStageCounts.guru + levelStageCounts.master + levelStageCounts.enlighten + levelStageCounts.burn;

            // Create the overlay elements
            const levelText = `<span class="level-block-text">${originalLevelText}</span>`;
            const lockedDiv = `<div class="level-block-item level-block-locked" style="width:${levelStageCounts.locked/levelTotal*100}%"></div>`;
            const apprenticeDiv = `<div class="level-block-item level-block-apprentice" style="width:${levelStageCounts.apprentice/levelTotal*100}%"></div>`;
            const guruDiv = `<div class="level-block-item level-block-guru" style="width:${levelStageCounts.guru/levelTotal*100}%"></div>`;
            const masterDiv = `<div class="level-block-item level-block-master" style="width:${levelStageCounts.master/levelTotal*100}%"></div>`;
            const enlightenedDiv = `<div class="level-block-item level-block-enlightened" style="width:${levelStageCounts.enlighten/levelTotal*100}%"></div>`;
            const burnDiv = `<div class="level-block-item level-block-burn" style="width:${levelStageCounts.burn/levelTotal*100}%"></div>`;

            // Create the tooltip
            const lockedText = `<div class="locked-tooltip tooltip-section"><p class="tooltip-section-title">Locked</p><p class="tooltip-section-count">${levelStageCounts.locked}</p></div>`;
            const apprenticeText = `<div class="apprentice-tooltip tooltip-section"><p class="tooltip-section-title">Apprentice</p><p class="tooltip-section-count">${levelStageCounts.apprentice}</p></div>`;
            const guruText = `<div class="guru-tooltip tooltip-section"><p class="tooltip-section-title">Guru</p><p class="tooltip-section-count">${levelStageCounts.guru}</p></div>`;
            const masterText = `<div class="master-tooltip tooltip-section"><p class="tooltip-section-title">Master</p><p class="tooltip-section-count">${levelStageCounts.master}</p></div>`;
            const enlightenedText = `<div class="enlightened-tooltip tooltip-section"><p class="tooltip-section-title">Enlightened</p><p class="tooltip-section-count">${levelStageCounts.enlighten}</p></div>`;
            const burnText = `<div class="burn-tooltip tooltip-section"><p class="tooltip-section-title">Burn</p><p class="tooltip-section-count">${levelStageCounts.burn}</p></div>`;
            const totalText = `<div class="total-tooltip tooltip-section"><p class="tooltip-section-title">Total</p><p class="tooltip-section-count">${levelTotal}</p></div>`;
            let nextReviewDateText = "N/A"
            let nextReviewChars = "N/A";
            const nextReview = levelStageCounts.nextReviewItem;
            if (nextReview != null) {
                const nextReviewMinutes = (new Date(nextReview.reviewDate) - new Date()) / (1000 * 60);
                const nextReviewHours = nextReviewMinutes / 60;
                if (nextReviewHours <= 0) {
                    nextReviewDateText = "now";
                } else if (nextReviewHours <= 1) {
                    const minutes = Math.floor(nextReviewMinutes);
                    nextReviewDateText = minutes + " minute" + (minutes !== 1 ? "s" : '');
                } else if (nextReviewHours >= 24) {
                    const days = Math.floor(nextReviewHours / 24);
                    nextReviewDateText = days + " day" + (days !== 1 ? "s" : '');
                } else {
                    const hours = Math.floor(nextReviewHours);
                    nextReviewDateText = hours + " hour" + (hours !== 1 ? "s" : '');
                }

                let itemClass = "guru-tooltip";
                if (nextReview.type === "radical") {
                    itemClass = "enlightened-tooltip";
                } else if (nextReview.type === "kanji") {
                    itemClass = "apprentice-tooltip";
                }
                nextReviewChars = `<span class="${itemClass} next-review-chars">${nextReview.characters}</span>`;
            }

            const nextReviewText = `<p class="tooltip-extra-info">Next Review: ${nextReview ? `${nextReviewChars} (${nextReviewDateText})` : "N/A"}</p>`
            const tooltip = `<div class="level-tooltip"><p class="tooltip-level-text">Level ${originalLevelText}</p>${lockedText}${apprenticeText}${guruText}${masterText}${enlightenedText}${burnText}${totalText}${nextReviewText}</div>`;

            if (levelStageCounts.burn === levelTotal) {
                // Fully burned level, add the checkbox to the div
                levelBlock.addClass('level-block-complete');
            }

            if (levelStageCounts.locked === levelTotal) {
                // Fully locked level, add the padlock to the div
                levelBlock.addClass('level-block-full-locked');
            }

            // Rewrite the level block's HTML with the custom elements
            levelBlock.html(`<div class="level-block-container">${apprenticeDiv}${guruDiv}${masterDiv}${enlightenedDiv}${burnDiv}${lockedDiv}</div>${levelText}${tooltip}`);
        }

        // Append a "+" to the level nav text to indicate script success
        $('.navigation > .sitemap > li:first-child > .sitemap__section-header').children().append('+');
    };

    /**
     * Create the CSS style sheet and append it to the document
     */
    function initCSS() {
        $('head').append(`
        <style>
            .level-block {
                position: relative;
                height: 46px;
                border: 1px solid black;
            }

            .level-block-text {
                width: 100%;
                position: absolute;
                top: 0;
                left: 0;
                text-align: center;
                line-height: 46px;
            }

            .level-tooltip {
                color: #eeeeee;
                visibility: hidden;
                background-color: rgba(0,0,0,0.8);
                text-align: center;
                padding: 0 14px 14px 14px;
                margin-left: 48px;
                margin-top: -5px;
                border-radius: 6px;
                position: absolute;
                z-index: 2;
                width: 190px;
                font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif !important;
            }

            .tooltip-level-text {
                font-size: 20px;
                margin: 5px 0;
                font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif !important;
            }

            .tooltip-extra-info {
                font-size: 12px;
                text-align: left;
                margin: 0;
                font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif !important;
                white-space: normal;
                text-shadow: none;
            }

            .next-review-chars {
                text-shadow: none;
                padding: 0 3px;
                font-size: 14px;
            }

            .tooltip-section {
                clear: both;
                overflow: auto;
                padding: 5px 15px;
            }

            .tooltip-section-title {
                float: left;
                margin: 0;
                font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif !important;
                font-size: 13px;
                font-weight: bold;
                text-shadow: none;
            }

            .tooltip-section-count {
                float: right;
                margin: 0;
                font-weight: bold;
                font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif !important;
                font-size: 13px;
                font-weight: bold;
                text-shadow: none;
            }

            .locked-tooltip {
                background-color: #666;
                background-image: linear-gradient(to bottom, #666, #444);
            }

            .apprentice-tooltip {
                background-color: #dd0093;
                background-image: linear-gradient(to bottom, #ff00aa, #b30077);
            }

            .guru-tooltip {
                background-color: #882d9e;
                background-image: linear-gradient(to bottom, #aa38c7, #662277);
            }

            .master-tooltip {
                background-color: #294ddb;
                background-image: linear-gradient(to bottom, #516ee1, #2142c4);
            }

            .enlightened-tooltip {
                background-color: #0093dd;
                background-image: linear-gradient(to bottom, #00aaff, #0077b3);
            }

            .burn-tooltip {
                background-color: #fbc042;
                background-image: linear-gradient(to bottom, #fbc550, #c88a04);
                color: #ffffff;
            }

            .total-tooltip {
                background-color: #efefef;
                background-image: linear-gradient(to bottom, #efefef, #cfcfcf);
                color: #000000;
                margin-bottom: 10px;
            }

            .level-block:hover .level-tooltip {
                visibility: visible;
            }

            .sitemap__page--current-level > a > span{
                line-height: 42px !important;
            }

            .sitemap__pages--levels .sitemap__page--current-level a {
                border: 2px solid black !important;
            }

            .sitemap__pages--levels .sitemap__page a:hover {
                background-color: rgba(255,255,255,0.5);
            }

            .sitemap__grouped-pages {
                overflow: visible !important;
            }

            .level-block-container {
                height: 100%;
                width: 100%;
                position: absolute;
                top: 0;
                left: 0;
                overflow: hidden;
                border-radius: 4px;
            }

            .level-block-complete {
                background-position: center !important;
                background-repeat: no-repeat !important;
                background-size: 34px !important;
                background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE2LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPg0KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCINCgkgd2lkdGg9IjM1Mi42MnB4IiBoZWlnaHQ9IjM1Mi42MnB4IiB2aWV3Qm94PSIwIDAgMzUyLjYyIDM1Mi42MiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMzUyLjYyIDM1Mi42MjsiDQoJIHhtbDpzcGFjZT0icHJlc2VydmUiIGZpbGw9IiMwMDY2MDAiIHN0cm9rZT0iIzAwNjYwMCI+DQo8Zz4NCgk8cGF0aCBkPSJNMzM3LjIyMiwyMi45NTJjLTE1LjkxMi04LjU2OC0zMy42Niw3Ljk1Ni00NC4wNjQsMTcuNzQ4Yy0yMy44NjcsMjMuMjU2LTQ0LjA2Myw1MC4xODQtNjYuNzA4LDc0LjY2NA0KCQljLTI1LjA5MiwyNi45MjgtNDguMzQ4LDUzLjg1Ni03NC4wNTIsODAuMTczYy0xNC42ODgsMTQuNjg4LTMwLjYsMzAuNi00MC4zOTIsNDguOTZjLTIyLjAzMi0yMS40MjEtNDEuMDA0LTQ0LjY3Ny02NS40ODQtNjMuNjQ4DQoJCWMtMTcuNzQ4LTEzLjQ2NC00Ny4xMjQtMjMuMjU2LTQ2LjUxMiw5LjE4YzEuMjI0LDQyLjIyOSwzOC41NTYsODcuNTE3LDY2LjA5NiwxMTYuMjhjMTEuNjI4LDEyLjI0LDI2LjkyOCwyNS4wOTIsNDQuNjc2LDI1LjcwNA0KCQljMjEuNDIsMS4yMjQsNDMuNDUyLTI0LjQ4LDU2LjMwNC0zOC41NTZjMjIuNjQ1LTI0LjQ4LDQxLjAwNS01Mi4wMjEsNjEuODEyLTc3LjExMmMyNi45MjgtMzMuMDQ4LDU0LjQ2OC02NS40ODUsODAuNzg0LTk5LjE0NQ0KCQlDMzI2LjIwNiw5Ni4zOTIsMzc4LjIyNiw0NC45ODMsMzM3LjIyMiwyMi45NTJ6IE0yNi45MzcsMTg3LjU4MWMtMC42MTIsMC0xLjIyNCwwLTIuNDQ4LDAuNjExDQoJCWMtMi40NDgtMC42MTEtNC4yODQtMS4yMjQtNi43MzItMi40NDhsMCwwQzE5LjU5MywxODQuNTIsMjIuNjUzLDE4NS4xMzIsMjYuOTM3LDE4Ny41ODF6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==') !important;
            }

            .level-block-full-locked {
                background-position: center !important;
                background-repeat: no-repeat !important;
                background-size: 36px !important;
                background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE2LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPg0KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCINCgkgd2lkdGg9IjM0cHgiIGhlaWdodD0iMzRweCIgdmlld0JveD0iMCAwIDM0IDM0IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAzNCAzNDsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHN0cm9rZT0iIzY2NjY2NiIgZmlsbD0iIzY2NjY2NiI+DQo8Zz4NCgk8cGF0aCBkPSJNMjYuODMzLDEzLjMzM1Y5YzAtNC45NjItNC4wMzgtOS05LTloLTEuNjY2Yy00Ljk2MiwwLTksNC4wMzgtOSw5djQuMzMzSDQuMzMzVjM0aDI1LjMzNFYxMy4zMzNIMjYuODMzeiBNMTEuMTY3LDkNCgkJYzAtMi43NTcsMi4yNDMtNSw1LTVoMS42NjZjMi43NTcsMCw1LDIuMjQzLDUsNXY0LjMzM0gxMS4xNjdWOXoiLz4NCjwvZz4NCjwvc3ZnPg0K') !important;
            }

            .level-block-item {
                height: 100%;
                display: inline-block;
            }

            .level-block-locked {
                background-color: rgba(0, 0, 0, 0.3);
            }

            .level-block-apprentice {
                background-color: rgba(221, 0, 147, 0.4);
            }

            .level-block-guru {
                background-color: rgba(136, 45, 158, 0.4);
            }

            .level-block-master {
                background-color: rgba(41, 77, 219, 0.4);
            }

            .level-block-enlightened {
                background-color: rgba(0, 147, 221, 0.4);
            }

            .level-block-burn {
                background-color: rgba(251, 192, 66, 0.4);
            }

        </style>`);
    }

    /**
     * log an error if any part of the wkof data request failed
     * @param {*} [e] - The error to log if it exists
     */
    function loadError (e) {
        console.error('Failed to load data from WKOF for "Wanikani Levels Overview Plus"', e);
    };
})();

QingJ © 2025

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