WaniKani Review Summary

Adds a review summary tile to the dashboard

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         WaniKani Review Summary
// @namespace    rwesterhof
// @version      0.9.5
// @description  Adds a review summary tile to the dashboard
// @include      /^https:\/\/(www|preview)\.wanikani\.com(\/(#)?dashboard)?(\/)?$/
// @match        https://preview.wanikani.com/subjects/review*
// @match        https://www.wanikani.com/subjects/review*
// @require      https://greasyfork.org/scripts/410909-wanikani-review-cache/code/Wanikani:%20Review%20Cache.js?version=1183366
// @run-at       document-end
// @grant        none
// @license      GPL-3.0-or-later
// ==/UserScript==

;(function() {
    'use strict';
    /* global wkof, review_cache */

    var SCRIPT_ID = 'wk_reviewSummary';
    var msInDay = 24 * 60 * 60 * 1000; // milliseconds in day

    /*-------------------------------------------------------------------------------------------------------------------------------*/

   // Temporary measure to allow the review cache script to load on the review page to track reviews while the /reviews endpoint is unavailable
   if (/^https:\/\/(www|preview)\.wanikani\.com(\/(#)?dashboard)?(\/)?$/.test(window.location.href)) {

       // Make sure WKOF is installed
       if (!window.wkof) {
           let response = confirm('WaniKani Review Summary requires WaniKani Open Framework.\n Click "OK" to be forwarded to installation instructions.');

           if (response) {
               window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
           }

           return;
       }

    /*-------------------------------------------------------------------------------------------------------------------------------*/

       wkof.include('Menu,Settings,ItemData');
       wkof.ready('Menu,Settings')
           .then(load_settings)
           .then(install_menu)
           .then(add_css)
           .then(createPanel)
           .then(wkof.ready('ItemData').then(fetch_and_update));
   }
    /*-------------------------------------------------------------------------------------------------------------------------------*/

    var CACHE_VERSION = '0.2';
    var RS_CACHE_KEY = 'wk_rs_cache';
    var LS_CACHE_KEY = 'wk_ls_cache'; // future expandable to lesson summary
    function retrieveFromCache(cacheKey) {
        var cached_json = localStorage.getItem(cacheKey);
        if (cached_json) {
            var cached = JSON.parse(cached_json);
            if (cached.version == CACHE_VERSION) {
                return cached;
            }
        }
        return;
    }

    function storeInCache(cacheKey, itemBreakdown) {
        // cache the results for next page load
        var json = { version: CACHE_VERSION, itemBreakdown: itemBreakdown, currentTs: new Date().getTime() };
        localStorage.setItem(cacheKey, JSON.stringify(json));
    }

    /*-------------------------------------------------------------------------------------------------------------------------------*/

    const STAGE_DIVS = [ '', 'Apprentice I', 'Apprentice II', 'Apprentice III', 'Apprentice IV', 'Apprentice',
                             'Guru I', 'Guru II', 'Guru',
                             'Master', 'Enlightened', 'Burned'
                       ];
    function createPanel() {
        var reviewSummaryDiv = document.createElement("div");
        reviewSummaryDiv.id="reviewSummaryDiv";

        // first the access tile with the percentage
        var reviewSummaryTile = document.createElement("div");
        reviewSummaryTile.id="reviewSummaryTile";
        reviewSummaryTile.innerHTML =
            "<span id='rs_tile_span_header'>Review Summary</span><p></p>"
          + "<a id='rs_tile_windowlink' title='Open review summary' onclick='wk_rs_displayReviewSummary();'><span id='rs_tile_span_percentage'></span></a>"
          + "<a id='rs_tile_percentageToggle' title='Show/Hide percentage' onclick='wk_rs_togglePercentage(0);'>"
          + "<svg id='rs_tile_icon_percentage_down' class='wk-icon wk-icon--chevron-down hidden' viewBox='0 0 512 512' aria-hidden='true' style='color:var(--color-character-text);'><use href='#wk-icon__chevron-down'></use></svg>"
          + "<svg id='rs_tile_icon_percentage_up' class='wk-icon wk-icon--chevron-up hidden' viewBox='0 0 512 512' aria-hidden='true' style='color:var(--color-character-text);'><use href='#wk-icon__chevron-up'></use></svg>"
          + "</a>";
        reviewSummaryDiv.append(reviewSummaryTile);

        // then the detail window
        var reviewSummaryWindow = document.createElement("div");
        reviewSummaryWindow.id="reviewSummaryWindow";
        reviewSummaryWindow.classList.add("reviewSummaryWindow");
        reviewSummaryWindow.classList.add("hidden");
        reviewSummaryWindow.setAttribute("onclick", "wk_rs_fireClickEvent();");
        reviewSummaryWindow.innerHTML =
            "<span id='rs_window_span_header'>Review Summary Details</span>"
          + "<button id='rs_window_button_close' onclick='wk_rs_hideReviewSummary();' title='Close window' class='rs_iconButton'>"
          + "<svg class='wk-icon wk-icon--cross' viewBox='0 0 384 512' aria-hidden='true'><use href='#wk-icon__cross'></use></svg>"
          + "</button><p></p>"
          + "<span id='rs_window_span_percentage'></span>"
          + "<div id='rs_window_div_tablist' class='rs_window_tablist'>"
            + "<button id='rs_window_button_tab_all' class='rs_window_button_tab' onclick='wk_rs_showTab([true, true]);'>All</button>"
            + "<button id='rs_window_button_tab_incorrect' class='rs_window_button_tab' onclick='wk_rs_showTab([true, false]);'>Incorrect</button>"
            + "<button id='rs_window_button_tab_correct' class='rs_window_button_tab' onclick='wk_rs_showTab([false, true]);'>Correct</button>"
          + "</div>";
        reviewSummaryDiv.append(reviewSummaryWindow);
        var reviewSummaryDivIncorrect = document.createElement("div");
        reviewSummaryDivIncorrect.id=('rs_window_div_correct_0');
        reviewSummaryDivIncorrect.classList.add('rs_window_div_items');
        reviewSummaryDivIncorrect.innerHTML = "<span id='rs_window_span_count_incorrect'></span>";
        reviewSummaryWindow.append(reviewSummaryDivIncorrect);
        var reviewSummaryDivCorrect = document.createElement("div");
        reviewSummaryDivCorrect.id=('rs_window_div_correct_1');
        reviewSummaryDivCorrect.classList.add('rs_window_div_items');
        reviewSummaryDivCorrect.innerHTML = "<span id='rs_window_span_count_correct'></span>";
        reviewSummaryWindow.append(reviewSummaryDivCorrect);

        for (var stage = 1; stage < STAGE_DIVS.length; stage++) {
            var stageDiv = document.createElement("div");
            stageDiv.id=('rs_window_div_correct_0_' + stage);
            stageDiv.classList.add('rs_window_div_items_stage');
            stageDiv.innerHTML = "<span class='rs_window_div_items_stage_span'>" + STAGE_DIVS[stage]  + "</span>";
            reviewSummaryDivIncorrect.append(stageDiv);

            stageDiv = document.createElement("div");
            stageDiv.id=('rs_window_div_correct_1_' + stage);
            stageDiv.classList.add('rs_window_div_items_stage');
            stageDiv.innerHTML = "<span class='rs_window_div_items_stage_span'>" + STAGE_DIVS[stage]  + "</span>";
            reviewSummaryDivCorrect.append(stageDiv);
        }

        document.getElementsByClassName('dashboard__content')[0].append(reviewSummaryDiv);
        // position it over the review forecast
        reviewSummaryDiv.style['grid-row'] = "1 / 2";
        reviewSummaryDiv.style['grid-column'] = "5 / span 2";
        // and make space
        if (document.getElementsByClassName('dashboard__review-forecast')[0]) {
            document.getElementsByClassName('dashboard__review-forecast')[0].style['grid-row'] = "2 / 5";
        }
    }

    function applyVisibility(setting, panel) {
        if (setting) {
            panel.classList.remove('hidden');
        }
        else {
            panel.classList.add('hidden');
        }
    }
    window.wk_rs_displayReviewSummary=function() {
        applyVisibility(true, document.getElementById("reviewSummaryWindow"));
        var computedWidth = getComputedStyle(document.getElementById("reviewSummaryWindow")).width;
        var nrOfCells = 25;
        if (computedWidth.endsWith('px')) {
            nrOfCells = Math.floor((computedWidth.slice(0, -2) - 24) / 28);
        }
        else if(computedWidth.endsWith('em')) {
            // best effort guess
            nrOfCells = Math.floor(((parseFloat(computedWidth.slice(0.-2)) * 16) - 24) / 28);
        }
        // else stick with default
        [...document.querySelectorAll(".rs_window_div_items_stage_span")].forEach(span => { span.style["grid-column"]="1 / span " + nrOfCells });
    }
    window.wk_rs_hideReviewSummary=function() { applyVisibility(false, document.getElementById("reviewSummaryWindow")); }
    window.wk_rs_displayDetails=function(div) { applyVisibility(true, div); listenForClicks(div); }

    const DIV_ARRAY = [ 'incorrect', 'correct', 'all' ];
    function showTab(divArray) {
        var buttonClicked = 0;
        for (var index = 0; index < divArray.length; index++) {
            applyVisibility(divArray[index], document.getElementById("rs_window_div_correct_" + index));
            if (divArray[index]) { buttonClicked += (index+1); }
        }
        for (var i = 0; i < DIV_ARRAY.length; i++) {
            document.getElementById('rs_window_button_tab_' + DIV_ARRAY[i]).classList.remove("active");
        }
        document.getElementById('rs_window_button_tab_' + DIV_ARRAY[buttonClicked-1]).classList.add("active");
    }
    window.wk_rs_showTab=showTab;

    function togglePercentage(mode) {
        // mode can be 0 or 1 or it can be true or false, which we convert to 0 (false) and 1 (true) by forcing int conversion via calculations
        var chevrons = [ document.getElementById('rs_tile_icon_percentage_down'),
                         document.getElementById('rs_tile_icon_percentage_up') ];
        // remove all hiddens (prevent duplicate hidden classes)
        applyVisibility(1, chevrons[0]);
        applyVisibility(1, chevrons[1]);
        // hide the one we don't need
        applyVisibility(0, chevrons[((1-mode))]);
        document.getElementById('rs_tile_percentageToggle').onclick=function() { wk_rs_togglePercentage(((1-mode))); };
        applyVisibility(((0+mode)), document.getElementById('rs_tile_span_percentage'));
    }
    window.wk_rs_togglePercentage= togglePercentage;

    function fireClickEvent() {
        // fires a custom event to signal cockpit loaded
        const event = document.createEvent('Event');
        event.initEvent("review-summary-clicked", true, true);
        document.getElementById("reviewSummaryWindow").dispatchEvent(event);
    }
    window.wk_rs_fireClickEvent=fireClickEvent;

    function listenForClicks(div) {
        document.getElementById("reviewSummaryWindow").addEventListener('review-summary-clicked', { div: div, handleEvent:function(event) { closeOnClick(event, div) } }, { once: true });
    }

    // eventlistener that moves the graph button to the heatmap once it is loaded
    function closeOnClick(event, div) {
        applyVisibility(false, div);
    }

    /*-------------------------------------------------------------------------------------------------------------------------------*/

    async function fetch_and_update() {
        var cached = retrieveFromCache(RS_CACHE_KEY);
        console.log("Found " + (cached ? cached.itemBreakdown.counts[2] : 0) + " reviews in cache");
        var startAtTs = null;
        if (cached) {
            startAtTs = cached.currentTs;
        }
        // if no cache available, default to 24 hours
        else {
            startAtTs = new Date().getTime() - msInDay;
        }
        var data = await Promise.all([review_cache.get_reviews()]);
        var filteredReviews = data[0].filter(item => item[0] > startAtTs);
        var totalReviewCount = filteredReviews.length;
        console.log("Found " + totalReviewCount + " new reviews since " + startAtTs);
        if (totalReviewCount > 0) {
            // we have reviews since last page load!
            var errorReviews = filteredReviews.filter(item => (item[3]+item[4])>0);
            var errorReviewCount = errorReviews.length;
            var correctReviewCount = totalReviewCount - errorReviewCount;
            var itemBreakdown = { filteredReviews: filteredReviews, percentage: (100 * correctReviewCount / totalReviewCount).toFixed(0) + '%', counts: [ errorReviewCount, correctReviewCount, totalReviewCount ] };
            storeInCache(RS_CACHE_KEY, itemBreakdown);
            togglePercentage((wkof.settings.wk_reviewSummary.showReviewPercentage > '0'));
            display(itemBreakdown);
        }
        else {
            // no reviews since last load, use cached summary
            if (cached) {
                document.getElementById('rs_tile_span_percentage').innerHTML=cached.itemBreakdown.percentage;
                togglePercentage((wkof.settings.wk_reviewSummary.showReviewPercentage == '2'));
                display(cached.itemBreakdown);
            }
            else {
                console.log("No review summary to display");
            }
        }

        // and show
        applyVisibility(true, document.getElementById('reviewSummaryTile'));
    }

    const OBJECT_TYPE_ORDER = { 'radical': 1, 'kanji': 2, 'vocabulary': 3, 'kana_vocabulary': 3 };
    async function display(itemBreakdown) {
        document.getElementById('rs_tile_span_percentage').innerHTML=itemBreakdown.percentage;
        document.getElementById('rs_window_span_percentage').innerHTML=itemBreakdown.percentage;
        document.getElementById('rs_window_span_count_incorrect').innerHTML="Incorrect " + itemBreakdown.counts[0] + "/" + itemBreakdown.counts[2];
        document.getElementById('rs_window_span_count_correct').innerHTML="Correct " + itemBreakdown.counts[1] + "/" + itemBreakdown.counts[2];

        var reviewIds = itemBreakdown.filteredReviews.map(review => review[1]);
        var itemList = await wkof.ItemData.get_index(await wkof.ItemData.get_items('include_hidden'), 'subject_id');
        var svgs = await getSvgs(reviewIds, itemList);

        let nextStage = (review) => {
                let stageAfter = review[2] + (review[3] + review[4] == 0) - Math.ceil((review[3] + review[4]) / 2) * (review[2] < 5 ? 1 : 2);
                return stageAfter < 1 ? 1 : stageAfter;
            }

        var displayItems = itemBreakdown.filteredReviews.map(review =>
            { return { id: review[1],
                       type: itemList[review[1]].object,
                       level: itemList[review[1]].data.level,
                       stage: nextStage(review),
                       correct: (review[3]+review[4]==0),
                       display: getDisplay(review[1], itemList, svgs)
                     }
            });

        displayItems.sort((a, b) => {
            // level before type
            if (wkof.settings.wk_reviewSummary.itemSort == '0') {
                if (a.level != b.level) {
                    return 10000000 * (a.level - b.level);
                }
            }
            if (a.type != b.type) {
               return 1000000 * (OBJECT_TYPE_ORDER[a.type] - OBJECT_TYPE_ORDER[b.type]);
            }
            // level after type
            if (wkof.settings.wk_reviewSummary.itemSort == '1') {
                if (a.level != b.level) {
                    return 10000 * (a.level - b.level);
                }
            }
            return (a.id - b.id);
        });

        // clear previous display
       [...document.querySelectorAll('.rs_window_div_items_stage .rs_item_div')].forEach(node => node.remove());
       [...document.querySelectorAll('.rs_window_div_items_stage')].forEach(div => { div.style.display = 'grid' });

        // incorrect/correct split according to STAGE_DIVS (app1-4, App combined (5), Gur1-2, Gur combined (8), Mas,Enl,Bur)
        for(var answer = 0; answer <= 1; answer++) {
            const useAnswer = answer;
            var answerSplit = displayItems.filter(item => (item.correct == useAnswer));
            for (var stage = 1; stage <= 9; stage++) {
                const useStage = stage;
                var targetList = answerSplit.filter(item => item.stage == useStage);
                if (targetList.length > 0) console.log("Processing " + targetList.length + " for stage " + useStage);
                const targetDiv = document.getElementById('rs_window_div_correct_' + answer + '_' + (stage + Math.floor(stage/5) + Math.floor(stage/7)));
                if ((targetList.length == 0) || (!wkof.settings.wk_reviewSummary.detailStages && (stage < 7))) {
                    targetDiv.style.display='none';
                }
                else {
                    var stageSpan = targetDiv.querySelector(".rs_window_div_items_stage_span");
                    stageSpan.textContent = STAGE_DIVS[(stage + Math.floor(stage/5) + Math.floor(stage/7))] + ' (' + targetList.length + ')';
                    targetList.forEach(item => targetDiv.append(item.display));
                }
            }
            if (!wkof.settings.wk_reviewSummary.detailStages) {
                for (var combinedStage = 5; combinedStage <= 8; combinedStage += 3) {
                    const useCombinedStage = combinedStage;
                    targetList = answerSplit.filter(item => (Math.floor(item.stage/7) + 3*Math.floor(item.stage/5) + 5) == useCombinedStage);
                    const targetDiv = document.getElementById('rs_window_div_correct_' + answer + '_' + combinedStage);
                    if (targetList.length == 0) {
                        targetDiv.style.display='none';
                    }
                    else {
                        console.log("Processing " + targetList.length + " for stage " + useCombinedStage);
                        stageSpan = targetDiv.querySelector(".rs_window_div_items_stage_span");
                        stageSpan.textContent = STAGE_DIVS[combinedStage] + ' (' + targetList.length + ')';
                        targetList.forEach(item => targetDiv.append(item.display));
                    }
                }
            }
            else {
                for (combinedStage = 5; combinedStage <= 8; combinedStage += 3) {
                    const targetDiv = document.getElementById('rs_window_div_correct_' + answer + '_' + combinedStage);
                    targetDiv.style.display='none';
                }
            }
        }

        if (itemBreakdown.counts[0] == 0) {
            applyVisibility(false, document.getElementById('rs_window_button_tab_all'));
            applyVisibility(false, document.getElementById('rs_window_button_tab_incorrect'));
            showTab([false, true]);
        }
        else if (itemBreakdown.counts[1] == 0) {
            applyVisibility(false, document.getElementById('rs_window_button_tab_all'));
            applyVisibility(false, document.getElementById('rs_window_button_tab_correct'));
            showTab([true, false]);
        }
        else {
            showTab([wkof.settings.wk_reviewSummary.initialDisplay < 2, wkof.settings.wk_reviewSummary.initialDisplay != 1]);
        }
    }

    const SUBJECT_TYPE_MAP = { 'radical': 'radical', 'kanji': 'kanji', 'vocabulary': 'vocabulary', 'kana_vocabulary': 'vocabulary' };
    function getDisplay(reviewId, itemList, svgs) {
        let item = itemList[reviewId];
        var itemSize = (item.data.characters ? item.data.characters.length : 1);

        var itemImgLink = document.createElement("div");
        itemImgLink.classList.add("rs_item_div");
        itemImgLink.style["grid-column"]="span " + ((itemSize+1));

        var itemDetailDiv = document.createElement("div");
        itemDetailDiv.id="rs_item_window_" + reviewId;
        itemDetailDiv.classList.add("rs_item_window");
        itemDetailDiv.classList.add("hidden");

        var itemDetailTable = document.createElement("table");
        itemDetailTable.classList.add("rs_item_table");

        var itemDetailRow = document.createElement("tr");
        var itemDetailCell = document.createElement("td");
        itemDetailCell.setAttribute("colspan", "2");
        var itemDetailImg = document.createElement("a");
        itemDetailImg.classList.add("rs_item_table_character");
        itemDetailImg.setAttribute("href", item.data.document_url);
        itemDetailImg.setAttribute("target", "_blank");
        var itemDetailSpan = document.createElement("span");
        itemDetailSpan.classList.add("character-item--" + SUBJECT_TYPE_MAP[item.object]);
        itemDetailSpan.append(item.data.characters || svgs[reviewId].cloneNode(true));
        itemDetailImg.append(itemDetailSpan);
        itemDetailCell.append(itemDetailImg);
        itemDetailRow.append(itemDetailCell);
        itemDetailTable.append(itemDetailRow);

        itemDetailRow = document.createElement("tr");
        itemDetailCell = document.createElement("th");
        itemDetailCell.append('Meanings');
        itemDetailRow.append(itemDetailCell);
        itemDetailCell = document.createElement("td");
        itemDetailCell.append(item.data.meanings.map((i) => i.meaning).join(', '));
        itemDetailRow.append(itemDetailCell);
        itemDetailTable.append(itemDetailRow);

        itemDetailRow = document.createElement("tr");
        itemDetailCell = document.createElement("th");
        itemDetailCell.append('Readings');
        itemDetailRow.append(itemDetailCell);
        itemDetailCell = document.createElement("td");
        itemDetailCell.append(item.data.readings ? item.data.readings.map((i) => i.reading).join('、 ') : '-');
        itemDetailRow.append(itemDetailCell);
        itemDetailTable.append(itemDetailRow);

        itemDetailRow = document.createElement("tr");
        itemDetailCell = document.createElement("th");
        itemDetailCell.append('Level');
        itemDetailRow.append(itemDetailCell);
        itemDetailCell = document.createElement("td");
        itemDetailCell.append(item.data.level);
        itemDetailRow.append(itemDetailCell);
        itemDetailTable.append(itemDetailRow);
        itemDetailDiv.append(itemDetailTable);
        itemImgLink.append(itemDetailDiv);

        var itemImg = document.createElement("a");
        itemImg.classList.add("rs_item_character");
        itemImg.onclick=function() { wk_rs_fireClickEvent(); event.stopPropagation(); wk_rs_displayDetails(itemDetailDiv); };
        var itemSpan = document.createElement("span");
        itemSpan.classList.add("character-item--" + SUBJECT_TYPE_MAP[item.object]);
        itemSpan.append(item.data.characters || svgs[reviewId].cloneNode(true));
        itemImg.append(itemSpan);
        itemImgLink.append(itemImg);

        return itemImgLink;
    }

    // copied from the heatmap script
    async function getSvgs(ids, items_id) {
        const svgs = {};
        const svgPromises = [];
        for (var index in ids) {
            const id = ids[index];
            if (items_id[id].data.characters) { continue; }
            svgPromises.push(
                wkof.load_file(
                        items_id[id].data.character_images.find(
                            (a) => a.content_type == 'image/svg+xml' && a.metadata.inline_styles,
                        ).url,
                    )
                    .then((svg) => {
                        let svgElem = document.createElement('span')
                        svgElem.innerHTML = svg.replace(/<svg /, `<svg class="radical-svg" `)
                        svgs[id] = svgElem.firstChild
                    }),
            )
        }
        await Promise.allSettled(svgPromises);
        return svgs;
    }

    /*-------------------------------------------------------------------------------------------------------------------------------*/

    // Adds the script's CSS to the page
    function add_css() {
        var userStyle = document.createElement('style');
        userStyle.id = "wk_reviewSummary_CSS";
        userStyle.append(
                `#reviewSummaryTile {
                     grid-row: 1;
                     grid-column: 5 / span 2;
                     height: 100%;
                     background-color: #00aaff;
                     width: 150px;
                     text-align: center;
                     padding: 0;
                     border-radius: 5px;
                 }
                 #reviewSummaryTile i {
                     color: #fff;
                     position: relative;
                     float: right;
                     right: 6px;
                     top: 6px;
                 }
                 #reviewSummaryTile a {
                     cursor: pointer;
                 }
                 #reviewSummaryTile span {
                     position: relative;
                     color: #fff;
                     font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
                     font-weight: bold;
                 }
                 #reviewSummaryTile span#rs_tile_span_header {
                     font-size: 18px;
                     top: 8px;
                 }
                 #reviewSummaryTile span#rs_tile_span_percentage {
                     font-size: 36px;
                     top: 28px;
                 }
                 .rs_iconButton {
                     cursor:pointer;
                     position: relative;
                     float: right;
                     right: 6px;
                     border: 0px;
                     width: 24px;
                 }
                 #reviewSummaryWindow {
                     position: absolute;
                     z-index: 1;
                     background: #434343;
                     border-radius: 5px;
                     padding: 6px;
                     top: 300px;
                     left: 150px;
                     width: 70%;
                 }
                 .reviewSummaryWindow span {
                     color: #fff;
                     font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
                     padding: 4px 6px 4px;
                 }
                 #reviewSummaryWindow span#rs_window_span_header {
                     font-size: 18px;
                     font-weight: bold;
                 }
                 #reviewSummaryWindow span#rs_window_span_percentage {
                     font-size: 36px;
                     position: relative;
                     top: 6px;
                 }
                 #rs_tile_windowlink:hover {
                     text-decoration:none;
                 }
                 .reviewSummaryWindow div {
                     position: relative;
                     top: 12px;
                     padding: 6px;
                     margin-bottom: 8px;
                 }
                 div.rs_window_div_items_stage {
                     display: grid;
                     grid-template-columns: min-content;
                 }
                 div.rs_window_div_items_stage span {
                     grid-column: 1 / span 10;
                 }
                 #rs_window_span_count_correct {
                     background: #00aa00;
                 }
                 #rs_window_span_count_incorrect {
                     background: #ff0000;
                 }
                 .rs_window_button_tab {
                     border-style: solid;
                     border-color: #666666;
                     background: #666666;
                     color: #fff;
                 }
                 .rs_window_button_tab:hover {
                     border-color: #888888;
                     color: #fff;
                 }
                 .rs_window_button_tab.active {
                     border-color: #888888;
                     background: #888888;
                     color: #fff;
                 }
                 .rs_iconButton {
                     background: #666666;
                     color: #fff;
                 }
                 .rs_item_div {
                 }
                 .rs_item_table_character {
                     color: #fff;
                     font-size: 32px;
                 }
                 .rs_item_table th {
                     padding-right: 20px;
                 }
                 .rs_item_character {
                     color: #fff;
                     font-size: 24px;
                     padding: 4px;
                     margin-right: 6px;
                     white-space: nowrap;
                 }
                 div.rs_item_window {
                     background: #666666;
                     color: #fff;
                     position: absolute;
                     width: 300px;
                     z-index:2;
                     padding: 12px;
                     left: 16px;
                 }
                 .rs_item_character:hover {
                     text-decoration:none;
                     color: #fff;
                     cursor: pointer;
                 }
                 .rs_item_table_character:hover {
                     text-decoration:none;
                     color: #fff;
                 }
                 a.rs_item_table_character span {
                     display: block;
                     text-align: center;
                     width: 92%;
                     padding: 12px;
                 }
                 .rs_item_character:visited {
                     text-decoration:none;
                 }
                 .rs_item_table_character:visited {
                     text-decoration:none;
                 }
                 .character-item--radical{background-color:#0af;background-image:linear-gradient(to bottom, #0af, #0093dd);border-color:#88d7ff transparent #069 #88d7ff}
                 .character-item--kanji{background-color:#f0a;background-image:linear-gradient(to bottom, #f0a, #dd0093);border-color:#f6c transparent #c08 #f6c}
                 .character-item--vocabulary{background-color:#a0f;background-image:linear-gradient(to bottom, #a0f, #9300dd);border-width:1px 0;border-color:#c655ff transparent #80c #c655ff}
                 .rs_item_character svg.radical-svg {
                     filter: invert(1);
                     width: 24px;
                  }
                 `);
        document.getElementsByTagName('head')[0].append(userStyle);
    }

    /*-------------------------------------------------------------------------------------------------------------------------------*/

    // Load settings and set defaults
    function load_settings() {
        var defaults = {
            showLessonSummary: false,
            showReviewPercentage: '1',
            initialDisplay: '1',
            itemSort: '1',
            detailStages: false
        };
        return wkof.Settings.load(SCRIPT_ID, defaults);
    }

    // Installs the options button in the menu
    function install_menu() {
        var config = {
            name: 'wk_reviewSummary_settings',
            submenu: 'Settings',
            title: 'Review Summary',
            on_click: open_settings
        };
        wkof.Menu.insert_script_link(config);
    }

    // Create the options
    function open_settings(items) {
        var config = {
            script_id: SCRIPT_ID,
            title: 'Review Summary Settings',
            on_save: fetch_and_update,
            content: {
                mainPage: {
                    type: 'page',
                    label: 'Settings',
                    hover_tip: 'Settings for the Review Summary',
                    content: {
                        display_group: {
                            type: 'group',
                            label: 'Display',
                            content: {
                                showReviewPercentage: {
                                    type: 'dropdown',
                                    label: 'Show accuracy of last review',
                                    hover_tip: 'Display mode for the accuracy percentage',
                                    default: '1',
                                    content: {
                                        0: 'Never',
                                        1: 'Only when new',
                                        2: 'Always'
                                    }
                                },
                                initialDisplay: {
                                    type: 'dropdown',
                                    label: 'Information to initially display',
                                    hover_tip: 'Only affects which items are initially displayed when opening the summary details. You can still toggle between the views later',
                                    default: '1',
                                    content: {
                                        0: 'All',
                                        1: 'Incorrect items',
                                        2: 'Correct items'
                                    }
                                },
                                itemSort: {
                                    type: 'dropdown',
                                    label: 'Order of displayed items',
                                    hover_tip: 'Affects the order of display of the reviewed items',
                                    default: '1',
                                    content: {
                                        0: 'Stage > Level > Type',
                                        1: 'Stage > Type > Level'
                                    }
                                },
                                detailStages: {
                                    type: 'checkbox',
                                    label: 'Use detailed stages',
                                    hover_tip: 'Separate Guru I from Guru II, etc',
                                    default: false
                                }
                            }
                        }
                    }
                }
            }
        }

        var dialog = new wkof.Settings(config);
        dialog.open();
    }

})();