Wanikani Levels Overview Plus

Improves the levels overview popup with progress indications

当前为 2020-04-07 提交的版本,查看 最新版本

// ==UserScript==
// @name          Wanikani Levels Overview Plus
// @namespace     Mercieral
// @description   Improves the levels overview popup with progress indications
// @include       /^https:\/\/(www|preview)\.wanikani\.com\/((?!review|lesson|login).)*$/
// @version       2.1.3
// @run-at        document-end
// @grant         none
// ==/UserScript==

/* global $ */

(function() {
    // Require 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,Settings');
    window.wkof.ready('document,ItemData,Settings')
        .then(load_settings)
        .then(initCSS)
        .then(sortData)
        .then(finishUI)
        .catch(loadError);

    /**
     * Loads the saved settings for this script
     */
    function load_settings () {
        return window.wkof.Settings.load('levelOverviewPlus', {
            showNextReview: true
        })
    }

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

                    // Increment 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++;
                    }

                    // Check if this is the next item for review for its level
                    let nextReviewDate = 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,
                        }
                        if (item.data.characters == null) {
                            levelStageCounts.nextReviewItem.imgUrl = item.data.character_images.filter((img) => {
                                return (img.content_type === 'image/svg+xml' && !img.metadata.inline_styles);
                            })[0].url;
                            levelStageCounts.nextReviewItem.slug = item.data.slug;
                        }
                    }
                }
            }
            return Promise.resolve();
        });
    }

    /**
     * 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) {
            overwriteLevelBlock(levelBlock);
        }
        createSettings();
        updateLevelheader();

        /**
         * Overwrite the level block with the custom elements and tooltip
         */
        function overwriteLevelBlock (levelBlock) {
            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;
            let loadImgData;
            if (nextReview != null) {
                // Get the time amount until review
                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" : '');
                }

                // Get the review item characters
                let itemClass = "guru-tooltip";
                if (nextReview.type === "radical") {
                    itemClass = "enlightened-tooltip";
                } else if (nextReview.type === "kanji") {
                    itemClass = "apprentice-tooltip";
                }
                if (nextReview.characters) {
                    nextReviewChars = `<span class="${itemClass} next-review-chars">${nextReview.characters}</span>`;
                }
                else if (nextReview.imgUrl && nextReview.slug) {
                    nextReviewChars = `<span class="${itemClass} next-review-chars" id="svg_${nextReview.slug}"></span>`;
                    loadImgData = nextReview;
                }
            }

            const nextReviewText = `<p class="tooltip-extra-info next-review-text">Next: ${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}`);

            if (loadImgData != null) {
                wkof.load_file(loadImgData.imgUrl).then((svgData) => {
                    const destSpan = $(`#svg_${loadImgData.slug}`);
                    if (destSpan.length > 0) {
                        destSpan.html(svgData);
                    }
                });
            }
        }

        /**
         * Create the settings button and panel with contents
         */
        function createSettings () {
            // Create the settings popop
            const showNextToggle = $(`<input type="checkbox">`);
            const showNextSetting = $(`<div class="settings-row"><span>Show Next Review</span></div>`);
            showNextSetting.prepend(showNextToggle);
            const settingsButton = $(`<div id="settings-button"></div>`);
            const settingsPanel = $(`<div id="settings-panel"><div id='settings-title'>Levels Overview Plus Settings</div></div>`);
            settingsPanel.append(showNextSetting);
            $('.sitemap__grouped-pages').append(settingsButton, settingsPanel);

            // Handling hiding/showing the settings panel
            let settingsPanelActive = false;
            function handleBodyClick (e) {
                if ($(e.target).closest("#settings-panel").length === 0 && e.target.id !== 'settings-button') {
                    $("#settings-panel").hide();
                    settingsButton.css("background-color", "");
                    $("body").off('mouseup', handleBodyClick);
                    settingsPanelActive = false;
                }
            }
            settingsButton.click(() => {
                settingsPanelActive = !settingsPanelActive;
                if (settingsPanelActive) {
                    settingsPanel.show();
                    settingsButton.css("background-color", "#b0b0b0");
                    $("body").on('mouseup', handleBodyClick);
                } else {
                    settingsPanel.hide();
                    settingsButton.css("background-color", "");
                    $("body").off('mouseup', handleBodyClick);
                }
            });

            // Handle the "toggle next review" checkbox
            const nextReviewTexts = $('.next-review-text');
            showNextToggle.change(function() {
                if(this.checked) {
                    nextReviewTexts.show();
                } else {
                    nextReviewTexts.hide();
                }
                window.wkof.settings.levelOverviewPlus.showNextReview = this.checked;
                window.wkof.Settings.save('levelOverviewPlus');
            });
            showNextToggle.prop("checked", window.wkof.settings.levelOverviewPlus.showNextReview).change();
            showNextSetting.click(() => {
                showNextToggle.prop("checked", !window.wkof.settings.levelOverviewPlus.showNextReview).change();
            });
        }

        /**
         * Append a "+" to the level nav text to indicate script success
         */
        function updateLevelheader () {
            $('.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 !important;
            }

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

            #settings-button {
                width: 28px;
                height: 28px;
                position: absolute;
                top: 8px;
                right: 8px;
                cursor: pointer;
                border-radius: 5px;
                background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAABaklEQVR4Xu2aTUoDQRCFv2yNP9cInsCtbkWCl9EraE4jwWXIVo8gXiIkxL0URNBON3Yyq6p6s00z9Hv1VVdPpUYkf0bJ9SMDREByB5QCyQHQIagUUAokd0Ap0ABgAjwBN8CZc0i+gCXwAHyUWmoEmPh34MK58HL7a+CqNKFmwAswDSb+R8681FYzYBMA+1b8tqW2mgG2aByUgC4DDJO7oAZYet//1pbpEFztDsHP/wyw360SPO/K4KlzGgz7BfAI/BFvunQRch7dwdsXAYMtdP4CEeA8gIO3LwIGW+j8BS0CLoEZcA2cONd4cD/AxL8B586FH90PeAVug4k/qB9gyHjHXv2AhgPqB6gfUHwSt8qg+gFBq8CeLF2Fs0S6pVMEiIDkDigFkgPQ/F9A/QD1A2LmRtd8gPoBmg/QfEDIISnNB2g+oFLZdBWOWe77VYmAfq9irhQBMePar0oE9HsVc6UIiBnXflXpCfgGC8dCQbbkoGgAAAAASUVORK5CYII=');
                background-position: center !important;
                background-repeat: no-repeat !important;
                background-size: 18px !important;
            }

            #settings-panel {
                display: none;
                position: absolute;
                padding: 10px 15px;
                background-color: #2a2a2a;
                border-radius: 4px;
                top: 40px;
                right: 8px;
                user-select: none;
            }

            #settings-title {
                font-weight: bold;
                font-size: 14px;
            }

            .settings-row {
                padding: 8px 0 0 10px;
                font-size: 12px;
                line-height: 20px;
                cursor: pointer;
            }

            .settings-row input {
                margin: 0 3px 0 0;
                width: 20px;
                height: 20px;
            }

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

            .tooltip-level-text {
                font-size: 16px;
                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;
                text-indent: -20px;
                padding-left: 20px;
                padding-bottom: 6px;
            }

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

            svg.radical {
                fill:none;
                stroke:#fff;
                stroke-linecap:square;
                stroke-miterlimit:2;
                stroke-width:68px;
                height: 15px;
                vertical-align: top;
                margin-top: 1.5px;
            }

            .tooltip-section {
                clear: both;
                overflow: auto;
                padding: 3px 10px;
            }

            .tooltip-section-title {
                float: left;
                margin: 0;
                font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif !important;
                font-size: 11px;
                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: 11px;
                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>`);
        return Promise.resolve();
    }

    /**
     * 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或关注我们的公众号极客氢云获取最新地址