Wanikani Review Summary

Show a popup with statistics about the review session when returning to the dashboard

当前为 2023-08-24 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name Wanikani Review Summary
// @namespace https://tampermonkey.net/
// @version 0.3
// @license MIT
// @description Show a popup with statistics about the review session when returning to the dashboard
// @author leohumnew
// @match https://www.wanikani.com/subjects/review
// @grant none
// ==/UserScript==

(function() {
    'use strict';
    // Variables to store the statistics
    let questionsAnswered = 0;
    let itemsCorrect = 0;
    let itemsIncorrect = 0;
    let meaningCorrect = 0;
    let meaningIncorrect = 0;
    let readingCorrect = 0;
    let readingIncorrect = 0;
    let correctHistory = [];
    let itemsList = [];
    let currentQuestionType = "";
    let currentCategory = "";
    let currentWord = "";
    let currentSRSLevel = -1;
    let quizQueueSRS = [];
    let SRSLevelNames = ["Lesson", "Appr. I", "Appr. II", "Appr. III", "Appr. IV", "Guru I", "Guru II", "Master", "Enl.", "Burned", "Error"];

    // Create style element with popup styles and append it to the document head
    let style = document.createElement("style");
    style.textContent = ".summary-popup { position: fixed; width: 100%; height: 100%; z-index: 9999; color: var(--color-text); background-color: var(--color-dashboard-panel-content-background, #eee); padding: 50px; overflow-y: auto; font-size: var(--font-size-large); }";
    style.textContent += ".summary-popup > a { background-color: transparent; text-decoration: none; text-align: center; margin: 30px 50px; position: absolute; top: 0px; right: 0px; cursor: pointer; padding: 10px; border-radius: 5px; outline: 1px solid var(--color-tertiary, black); color: var(--color-text) } .summary-popup > a:hover { color: var(--color-tertiary, #bbb); }";
    style.textContent += ".summary-popup table { border-collapse: collapse; width: 100%; background-color: var(--color-dashboard-panel-background, #000); } .summary-popup td { border: none; padding: 5px; text-align: center; }";
    style.textContent += ".summary-popup h1 { margin-bottom: 10px; font-weight: bold; font-size: var(--font-size-xlarge); } .summary-popup h2 { font-weight: bold; margin-top: 20px; padding: 20px; color: #fff; font-size: var(--font-size-large); border-radius: 5px 5px 0 0; }";
    style.textContent += ".summary-popup ul { background-color: var(--color-dashboard-panel-background, #fff); padding: 0 5px; } .summary-popup li { display: inline-block; } .summary-popup li a { display: block; margin: 10px 5px; padding: 10px; color: var(--color-text-dark, #fff); font-size: 1.5rem; height: 2.6rem; border-radius: 5px; text-decoration: none; } .summary-popup li a img { height: 1.5rem; vertical-align: middle; }";
    style.textContent += ".summary-popup .summary-popup__popup { background-color: var(--color-menu, #ddd); color: var(--color-text, #fff); text-decoration: none; padding: 10px; border-radius: 5px; position: fixed; z-index: 9999; display: none; font-size: var(--font-size-medium); box-shadow: 0 2px 3px rgba(0, 0, 0, 0.5); width: max-content; line-height: 1.3; }";
    style.textContent += ".summary-popup .summary-popup__popup:after { content: ''; position: absolute; top: -8px; margin-left: -10px; width: 0; height: 0; border-left: 10px solid transparent; border-right: 10px solid transparent; border-bottom: 10px solid var(--color-menu, #ddd); }";
    style.textContent += ".summary-popup .summary-popup__popup--left:after { right: 15px; } .summary-popup .summary-popup__popup--right:after { left: 25px; }";
    style.textContent += ".summary-popup .accuracy-graph { height: 150px; width: 100%; background-color: var(--color-dashboard-panel-background, #fff); padding: 25px 0; } .summary-popup .accuracy-graph__line { position: relative; transform-origin: 0 0; height: 2px; background-color: var(--color-text); float: left; }";

    document.head.appendChild(style);

    // Function to calculate the percentage
    function percentage(numerator, denominator) {
        return Math.round(numerator / denominator * 100) + "%";
    }

    // Function to get quiz queue SRS
    function getQuizQueueSRS() {
        quizQueueSRS = JSON.parse(document.querySelectorAll("#quiz-queue script[data-quiz-queue-target='subjectIdsWithSRS']")[0].innerHTML);
    }
    getQuizQueueSRS();

    // Clear the data-quiz-queue-done-url-value parameter on #quiz-queue
    document.getElementById("quiz-queue").setAttribute("data-quiz-queue-done-url-value", "");

    // Function to create a popup element
    function createPopup(content) {
        // Create a div element with some styles
        let popup = document.createElement("div");
        popup.className = "summary-popup";

        // Create a close button with some styles and functionality
        let closeButton = document.createElement("a");
        closeButton.textContent = "Dashboard";
        closeButton.href = "https://www.wanikani.com/dashboard";

        // Append the content and the close button to the popup
        popup.appendChild(content);
        popup.appendChild(closeButton);

        return popup;
    }

    // Function to create a table element with some data
    function createTable(data) {
        // Create a table
        let table = document.createElement("table");
        let row = document.createElement("tr");
        let row2 = document.createElement("tr");
        let row3 = document.createElement("tr");

        // Loop through the data array
        for (let i = 0; i < data.length; i++) {
            // Create table cell elements
            let cell = document.createElement("td");
            cell.textContent = data[i][0];
            cell.style.fontSize = "var(--font-size-xxlarge)";
            cell.style.fontWeight = "bold";
            cell.style.padding = "25px 0 0 0";
            row.appendChild(cell);

            let cell2 = document.createElement("td");
            cell2.textContent = data[i][1];
            cell2.style.fontSize = "var(--font-size-small)";
            cell2.style.fontStyle = "italic";
            cell2.style.color = "var(--color-text-mid, #999)";
            cell2.style.padding = "0 0 10px 0";
            row2.appendChild(cell2);

            let cell3 = document.createElement("td");
            cell3.textContent = data[i][2];
            cell3.style.fontSize = "var(--font-size-medium)";
            cell3.style.paddingBottom = "25px";
            row3.appendChild(cell3);
        }
        // Append the rows to the table
        table.append(row, row2, row3);

        // Return the table element
        return table;
    }

    // Function to show the statistics when returning to the dashboard
    function showStatistics() {
        console.log(itemsList);
        // Check if there are any items reviewed
        if (questionsAnswered > 0) {
            // Create a heading element with some text and styles
            let headingText = document.createElement("h1");
            headingText.textContent = " Review Summary";
            let headingImage = document.createElement("div");
            headingImage.classList = "wk-icon fa-solid fa-square-check";
            headingImage.style.float = "left";
            headingImage.style.margin = "2px 6px 0 0";
            let heading = document.createElement("div");
            heading.append(headingImage, headingText);

            // Create an unordered list element
            let listCorrect = document.createElement("ul");
            let listIncorrect = document.createElement("ul");

            // Loop through the items list array
            let srsUpNum = 0;
            let typeNum = [0, 0, 0];
            for (let i = 0; i < itemsList.length; i++) {
                // Create a list item element with the character or image
                let listItem = document.createElement("li");
                let listItemLink = document.createElement("a");
                if(itemsList[i].characters.url == null) listItemLink.textContent = itemsList[i].characters;
                else {
                    let listItemImage = document.createElement("img");
                    listItemImage.src = itemsList[i].characters.url;
                    listItemLink.appendChild(listItemImage);
                }
                if (itemsList[i].type === "Radical") {
                    listItemLink.style.backgroundColor = "var(--color-radical, #00aaff)";
                    listItemLink.href = "https://www.wanikani.com/radicals/" + itemsList[i].meanings[0];
                } else if (itemsList[i].type === "Kanji") {
                    listItemLink.style.backgroundColor = "var(--color-kanji, #ff00aa)";
                    listItemLink.href = "https://www.wanikani.com/kanji/" + itemsList[i].characters;
                }
                else {
                    listItemLink.style.backgroundColor = "var(--color-vocabulary, #aa00ff)";
                    listItemLink.href = "https://www.wanikani.com/vocabulary/" + itemsList[i].characters;
                }

                // Create popup with meaning and reading info on hover
                let popup = document.createElement("div");
                popup.className = "summary-popup__popup";
                popup.innerHTML = "Meaning: <strong>" + itemsList[i].meanings.slice(0, 2).join(", ") + "</strong>";
                if(itemsList[i].type == "Kanji") {
                    typeNum[1]++;
                    for (let k = 0; k < itemsList[i].readings.length; k++) { // Nanori, Onyomi, Kunyomi readings if kanji
                        if (itemsList[i].readings[k] != null) {
                            let label = "";
                            switch (k) {
                            case 0:
                                label = "Nanori";
                                break;
                            case 1:
                                label = "Onyomi";
                                break;
                            case 2:
                                label = "Kunyomi";
                                break;
                            }
                            popup.innerHTML += "<br>" + label + ": <strong>" + itemsList[i].readings[k].join(", ") + "</strong>";
                        }
                    }
                }
                else if(itemsList[i].type != "Radical" && itemsList[i].readings[0] != null) { // Reading if vocabulary
                    typeNum[2]++;
                    popup.innerHTML += "<br>Reading: <strong>" +
                                    itemsList[i].readings.map(r => r.reading).join(", ") +
                                    "</strong>";
                } else { // No reading if radical
                    typeNum[0]++;
                }
                popup.innerHTML += "<br>SRS: " + SRSLevelNames[itemsList[i].oldSRS] + " -> " + SRSLevelNames[itemsList[i].newSRS];

                popup.style.display = "block";
                popup.style.visibility = "hidden";
                listItemLink.addEventListener("mouseover", function(e) {
                    // Position the popup element relative to the parent item element: to the right of the parent unless that would cause the popup to go off the screen
                    let infoPos = listItemLink.getBoundingClientRect();
                    let popupPos = popup.getBoundingClientRect();
                    if (infoPos.left + popupPos.width + 5 > window.innerWidth) {
                        popup.style.right = window.innerWidth - infoPos.right + "px";
                        popup.style.removeProperty("left");
                        popup.className = "summary-popup__popup summary-popup__popup--left";
                    } else {
                        popup.style.left = infoPos.left + "px";
                        popup.style.removeProperty("right");
                        popup.className = "summary-popup__popup summary-popup__popup--right";
                    }
                    popup.style.top = infoPos.bottom + 5 + "px";
                    popup.style.visibility = "visible";
                });

                listItemLink.addEventListener("mouseout", function(e) {
                    popup.style.visibility = "hidden";
                });
                popup.style.visibility = "hidden";

                // Append the list item to the list
                listItemLink.appendChild(popup);
                listItem.appendChild(listItemLink);
                if (itemsList[i].SRSUp) {
                    listCorrect.appendChild(listItem);
                    srsUpNum++;
                }
                else listIncorrect.appendChild(listItem);
            }

            // Create a header table with main stats
            let data = [
                [itemsList.length, "R: " + typeNum[0] + " / K: " + typeNum[1] + " / V: " + typeNum[2], "Items Completed"],
                [percentage(itemsCorrect, questionsAnswered), itemsCorrect + " out of " + questionsAnswered , "Questions Answered Correctly"],
                [percentage(meaningCorrect, meaningCorrect + meaningIncorrect), meaningCorrect + " out of " + (meaningCorrect + meaningIncorrect), "Meanings Correct"],
                [percentage(readingCorrect, readingCorrect + readingIncorrect), readingCorrect + " out of " + (readingCorrect + readingIncorrect), "Readings Correct"]
            ];
            let table = createTable(data);

            // Create h2 titles for the lists
            let correctTitle = document.createElement("h2");
            correctTitle.textContent = srsUpNum + " Items SRS Up";
            correctTitle.style.backgroundColor = "var(--color-quiz-correct-background, #88cc00)";

            let incorrectTitle = document.createElement("h2");
            incorrectTitle.textContent = (itemsList.length - srsUpNum) + " Items SRS Down";
            incorrectTitle.style.backgroundColor = "var(--color-quiz-incorrect-background, #ff0033)";

            // Create a div element to wrap everything
            let content = document.createElement("div");
            content.append(heading, table, incorrectTitle, listIncorrect, correctTitle, listCorrect);

            // Create a graph showing accuracy throughout the session using the correctHistory array, with an average of 3 elements
            if(itemsList.length > 4) {
                // Title h2
                let graphTitle = document.createElement("h2");
                graphTitle.textContent = "Session Accuracy";
                graphTitle.style.backgroundColor = "var(--color-menu, #777)";
                // Graph
                let graph = document.createElement("div");
                graph.classList = "accuracy-graph";

                // Wrapper
                let graphWrapper = document.createElement("div");
                graphWrapper.append(graphTitle, graph);
                content.appendChild(graphWrapper);
            }

            // Create a popup element with the content
            let popup = createPopup(content);
            document.body.appendChild(popup);

            // Fill the graph with the correctHistory array
            if(itemsList.length > 4) {
                let graphDiv = document.querySelector(".accuracy-graph");
                // Calculate graph data
                let graphData = [];
                for (let i = 1; i < correctHistory.length - 1; i++) {
                    graphData.push((correctHistory[i-1] + correctHistory[i] + correctHistory[i+1]) / 3);
                }
                let graphHeight = 100;
                let graphWidth = graphDiv.getBoundingClientRect().width;
                let graphStep = graphWidth / graphData.length;
                graphDiv.style.paddingLeft = graphStep / 2 + "px";
                let prevPos = {x: null, y: null};
                for (let i = 0; i < graphData.length; i++) {
                    let x = graphStep * i;
                    let y = graphHeight - (graphData[i] * graphHeight);
                    if(prevPos.x != null) {
                        let line = document.createElement("div");
                        line.classList = "accuracy-graph__line";
                        let lineWidth = Math.sqrt(Math.pow(x - prevPos.x, 2) + Math.pow(y - prevPos.y, 2));
                        line.style.width = lineWidth + "px";
                        let rotation = Math.atan2(y - prevPos.y, x - prevPos.x);
                        line.style.transform = "rotate(" + rotation + "rad)";
                        if(rotation != 0) line.style.marginRight = Math.cos(rotation) * lineWidth - lineWidth + "px";
                        line.style.top = prevPos.y + "px";
                        graphDiv.appendChild(line);
                    }
                    prevPos = {x: x, y: y};
                }
            }

            // Reset the statistics variables
            questionsAnswered = 0;
            itemsCorrect = 0;
            itemsIncorrect = 0;
            meaningCorrect = 0;
            meaningIncorrect = 0;
            readingCorrect = 0;
            readingIncorrect = 0;
            itemsList = [];
        }
    }

    let eventToObserve = window.doublecheck == null ? "didAnswerQuestion" : "didFinalAnswer";
    // Add an event listener for the didAnswerQuestion event
    window.addEventListener(eventToObserve, function(e) {
        // Check if the answer was correct or not by looking for the correct attribute
        let correct = document.querySelector(".quiz-input__input-container[correct='true']") !== null;

        // Push value to correct history array: 1 if correct, 0 if incorrect
        correctHistory.push(correct);

        // Increment the questions answered and correct/incorrect counters
        questionsAnswered++;
        if (currentQuestionType === "meaning") {
            correct ? meaningCorrect++ : meaningIncorrect++;
        } else if (currentQuestionType === "reading") {
            correct ? readingCorrect++ : readingIncorrect++;
        }
        if (correct) itemsCorrect++;
        else itemsIncorrect++;
    });

    // Add an event listener for the didCompleteSubject event
    window.addEventListener("didCompleteSubject", function(e) {
        // Get the subject data from the event detail
        let subject = e.detail.subjectWithStats.subject;
        let didSRSUp = e.detail.subjectWithStats.stats.meaning.incorrect === 0 && e.detail.subjectWithStats.stats.reading.incorrect === 0;
        let reading = null;
        if(subject.type == "Vocabulary" || subject.type == "KanaVocabulary") {
            reading = subject.readings;
        } else if (subject.type == "Kanji") {
            reading = [null, null, null];
            if(subject.nanori.length > 0) {
                reading[0] = subject.nanori;
            }
            if(subject.onyomi.length > 0) {
                reading[1] = subject.onyomi;
            }
            if(subject.kunyomi.length > 0) {
                reading[2] = subject.kunyomi;
            }
        }

        // Calculate the new SRS level
        let newSRSLevel = didSRSUp ? currentSRSLevel + 1 : (currentSRSLevel < 2 ? currentSRSLevel : (currentSRSLevel < 5 ? currentSRSLevel - 1 : currentSRSLevel - 2));
        console.log(subject.characters + " - Old SRS Level: " + SRSLevelNames[currentSRSLevel] + " New SRS Level: " + SRSLevelNames[newSRSLevel]);

        // Push the subject data to the items list array
        let subjectInfoToSave = { characters: subject.characters, type: subject.type, id: subject.id, SRSUp: didSRSUp, meanings: subject.meanings, readings: reading, oldSRS: currentSRSLevel, newSRS: newSRSLevel };
        itemsList.push(subjectInfoToSave);
    });

    // Add an event listener for the willShowNextQuestion event
    window.addEventListener("willShowNextQuestion", function(e) {
        // Set current question variables with event info
        currentQuestionType = e.detail.questionType;
        currentCategory = e.detail.subject.type;
        currentWord = e.detail.subject.characters;

        currentSRSLevel = quizQueueSRS.find(function(element) { return element[0] == e.detail.subject.id; })[1];
        if(currentSRSLevel == null) {
            getQuizQueueSRS();
            currentSRSLevel = quizQueueSRS.find(function(element) { return element[0] == e.detail.subject.id; })[1];
            if(currentSRSLevel == null) currentSRSLevel = 10;
        }
    });

    // Add an event listener for the turbo before-visit event
    window.addEventListener("turbo:before-visit", function(e) {       
        showStatistics();
    });

    // Home button override
    let homeButton = document.querySelector(".summary-button");
    homeButton.setAttribute("title", "Show statistics and return to dashboard");
    homeButton.addEventListener("click", function(e) {
        // Prevent the default behavior of the button
        if(questionsAnswered > 0) e.preventDefault();

        showStatistics();
    });

})();