Wanikani Number of Learned Kanji, Vocab

Shows how many Kanji, Vocabulary, and/or radical items you've learned in the dashboard. See SETTINGS.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name          Wanikani Number of Learned Kanji, Vocab
// @namespace     https://www.wanikani.com
// @description   Shows how many Kanji, Vocabulary, and/or radical items you've learned in the dashboard. See SETTINGS.
// @author        Saimin
// @version       0.8.3
// @include       /^https://(www|preview).wanikani.com/(dashboard)?$/
// @grant         none
// ==/UserScript==

// Introduction to this script, explanation of settings, leave feedback:
//   https://community.wanikani.com/t/userscript-number-of-learned-kanji-vocabulary/41042

// some code adapted with thanks from following scripts (which this is compatible with):
// Progress Percentages by Kumirei
//   https://community.wanikani.com/t/userscript-progress-percentages/35080
// SRS and Leech breakdown script by seanblue.
//   https://community.wanikani.com/t/userscript-wanikani-dashboard-srs-and-leech-breakdown/32756


(function() {
    'use strict';

        // --- don't change this (see Settings below) --- //
        const sectionsToPlaceAround = {
            reviewStatus: "review-status",
            srsProgress: "wk-panel wk-panel--level-progress",
            levelProgression: "progression",
        }
        const placements = {
            before: 0,
            after: 1
        }
        // --- don't change above --- //

        // --- SETTINGS (change things here) --- //
        let placeBeforeOrAfterSection = placements.after; // you can change the "before" to "after" to place the section after the section you specify below
        let sectionToPlaceAround = sectionsToPlaceAround.srsProgress; // you can change the "srsProgress" to e.g. "progression" (see above) to place this section before/after the progression section

        let srsStageLearned = 5; // 5: kanji counts as learned when it's Guru+. To set it to apprentice+, set this to 1. burned is 9.
        let srsStageLearnedDescription = "(Guru+)";

        let showKanjiLearned = 1; // set to 0 to not show number of kanji learned
        let showVocabLearned = 1; // set to 0 to not show number of vocabulary items learned
        let showRadicalsLearned = 1; // set to 0 to not show number of radicals learned

        let learnedPrefix = "Learned"; // you can change this to any text. To remove this text completely, set it to "".
        // --- SETTINGS end --- //

    const config = {
        wk_items: {
            options: {
                assignments: true
            }
        }
    };

	if (!window.wkof) {
		let response = confirm('WK Number of Learned Items script 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;
    }


     let style =
		`<style id="number-of-learned-items">
.number-of-learned-items {
	//position: relative;
    margin: 5px 0 5px;
    //grid-column: 1/span 6;
    //grid-row:  / 5;
}

.number-of-learned-items > div {
    border-radius: 3px;
    background: #F4F4F4 !important;
    padding: 5px 0px;
}
.noli-item {
    display: inline-block;
    //width: 40%;
    text-align: left;
}
.noli-first-item {
    margin-left: 20px;
}
.noli span {
    display: inline;
    font-size: 14px;
    border-radius: 5px;
}
.number-of-learned-items .item-description {
    //font-weight: bold;
}
.number-of-learned-items .items-learned-number {
    font-weight: bold;
}

.dashboard section.srs-progress span {
    margin-bottom: 0.0em;
}

</style>`

    const head = document.head;
    head.insertAdjacentHTML( 'beforeend', style);

    if (is_dark_theme()) head.insertAdjacentHTML( 'beforeend', '<style id="number-of-learned-items_dark">'+
    '.number-of-learned-items {'+
    '    box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.7), 2px 2px 2px rgba(0, 0, 0, 0.7);'+
    '}'+
    '.number-of-learned-items > div {'+
    '    background: #232629 !important;'+
    '}'+
    '.noli-item {'+
    '    color: rgb(240,240,240);'+
    '}'+
    '</style>');

	wkof.include('ItemData');
	wkof.ready('ItemData').then(getItems).then(mapItemsToSrs).then(addNumberOfLearnedItemsSection);

	function getItems(items) {
        return wkof.ItemData.get_items(config);
		//return wkof.ItemData.get_items(config).then(filterToActiveAssignments);
	}

	function filterToActiveAssignments(items) {
		return items.filter(itemIsActiveAssignment);
	}

	function itemIsActiveAssignment(item) {
		let assignments = item.assignments;
		if (assignments === undefined) {
			return false;
		}

		let srsStage = getSrsStage(assignments);

		return srsStage >= 1 && srsStage <= 9;
	}

	function getSrsStage(assignments) {
        if (!assignments) {
            return 0; // not yet learned
        }
		return assignments.srs_stage;
	}

	function mapItemsToSrs(items) {
		let itemsBySrs = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].reduce((result, srs) => { // stage 0 = not yet learned
			result[srs] = {
                kanji: 0,
                vocab: 0,
                radicals: 0
            };

			return result;
        }, {});
        // separation by srs stage may be necessary in future if we want to show kanji learned per SRS stage etc.

        itemsBySrs.kanjiLearned = 0;
        itemsBySrs.radicalsLearned = 0;
        itemsBySrs.vocabLearned = 0;

        itemsBySrs.totalKanji = 0;
        itemsBySrs.totalRadicals = 0;
        itemsBySrs.totalVocab = 0;

		items.forEach(function(item) {
			let srsStage = getSrsStage(item.assignments);
            if (showKanjiLearned && item.object == 'kanji') {
                itemsBySrs[srsStage].kanji++;
                itemsBySrs.totalKanji++;
                if (srsStage >= srsStageLearned) {
                    itemsBySrs.kanjiLearned++;
                }
            } else if (showVocabLearned && item.object == 'vocabulary') {
                itemsBySrs[srsStage].vocab++;
                itemsBySrs.totalVocab++;
                if (srsStage >= srsStageLearned) {
                    itemsBySrs.vocabLearned++;
                }
            } else if (showRadicalsLearned && item.object == 'radical') {
                itemsBySrs[srsStage].radicals++;
                itemsBySrs.totalRadicals++;
                if (srsStage >= srsStageLearned) {
                    itemsBySrs.radicalsLearned++;
                }
            }
		});

		return itemsBySrs;
	}

	/*function addNumberOfLearnedItemsSection(itemsBySrs) {
        /*let itemInfo = {
            kanjiLearned = 0,
            vocabLearned = 0,
            radicalsLearned = 0,
            totalKanji = 0,
            totalVocab = 0,
            totalRadicals = 0
        }
        let kanjiLearned = 0;
        let vocabLearned = 0;
        let radicalsLearned = 0;

        let totalKanji = 0;
        let totalVocab = 0;
        let totalRadicals = 0;
        for (let i=1; i<=9; i++) { // default: i=5 (guru)
            itemInfo.totalKanji += itemsBySrs[i].kanji;
            itemInfo.totalVocab += itemsBySrs[i].vocab;
            itemInfo.totalRadicals += itemsBySrs[i].radicals;
        }

        if (i >= srsStageLearned) {
            itemInfo.kanjiLearned += itemsBySrs[i].kanji;
            itemInfo.vocabLearned += itemsBySrs[i].vocab;
            itemInfo.radicalsLearned += itemsBySrs[i].radicals;
        }

		return addKanjiAndVocabSection(itemInfo);
	}*/

	function addNumberOfLearnedItemsSection(itemsBySrs) {
        if (learnedPrefix != "") {
            learnedPrefix += "&nbsp;";
        }
        let kanjiLearnedDescription = "Kanji";
        let vocabLearnedDescription = "Vocab items";
        let radicalsLearnedDescription = "Radicals";

        let kanjiLearnedHoverText = `${itemsBySrs.kanjiLearned}/${itemsBySrs.totalKanji} WK-Kanji learned`;
        let vocabLearnedHoverText = `${itemsBySrs.vocabLearned}/${itemsBySrs.totalVocab} WK-Vocab learned`;
        let radicalsLearnedHoverText = `${itemsBySrs.radicalsLearned}/${itemsBySrs.totalRadicals} WK-Radicals learned`;

        // Add elements
		var section = document.createElement('section');
        section.className = 'number-of-learned-items'; // number of learned items

        var list = document.createElement('div');
        list.insertAdjacentHTML( 'beforeend', '<div class="noli-item" id="">'+
            '<span class="noli-item noli-first-item">'+learnedPrefix+'</span></div>');
        if (showKanjiLearned) {
            const separatorSuffix = (showVocabLearned || showRadicalsLearned) ? ",&nbsp;" : "";
            list.insertAdjacentHTML( 'beforeend', '<div class="noli-item" id="" title="'+kanjiLearnedHoverText+'">'+
                '<span class="items-learned-number">'+itemsBySrs.kanjiLearned+'</span>'+
                '<span class="item-description">'+' '+kanjiLearnedDescription+separatorSuffix+'</span></div>');
        }
        if (showVocabLearned) {
            const separatorSuffix = (showRadicalsLearned) ? ",&nbsp;" : "";
            list.insertAdjacentHTML( 'beforeend',
                '<div class="noli-item" id="" title="'+vocabLearnedHoverText+'">'+
                '<span class="items-learned-number">'+itemsBySrs.vocabLearned+'</span>'+
                '<span class="item-description">'+' '+vocabLearnedDescription+separatorSuffix+'</span></div>');
        }
        if (showRadicalsLearned) {
            list.insertAdjacentHTML( 'beforeend', '<div class="noli-item" id="" title="'+radicalsLearnedHoverText+'">'+
                '<span class="items-learned-number">'+itemsBySrs.radicalsLearned+'</span>'+
                '<span class="item-description">'+' '+radicalsLearnedDescription+'</span></div>');
        }
        if (srsStageLearnedDescription != "" && srsStageLearnedDescription != null) {
            list.insertAdjacentHTML( 'beforeend', '<div class="noli-item" id="">'+
            '<span class="noli-item">'+'&nbsp;'+srsStageLearnedDescription+'</span></div>');
        }

        section.appendChild(list);

        const sectionToPlaceAroundElement = document.getElementsByClassName(sectionToPlaceAround)[0];
        if (placeBeforeOrAfterSection == placements.before) {
            sectionToPlaceAroundElement.before(section);
        } else {
            sectionToPlaceAroundElement.after(section);
        }

		return section;
    }

    // Handy little function that rfindley wrote. Checks whether the theme is dark. Fixed to not use jquery.
	function is_dark_theme() {
		// Grab the <html> background color, average the RGB.  If less than 50% bright, it's dark theme.
        const style = window.getComputedStyle(document.body);
		return style.getPropertyValue('background-color').match(/\((.*)\)/)[1].split(',').slice(0,3).map(str => Number(str)).reduce((a, i) => a+i)/(255*3) < 0.5;
	}
})();