// ==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 += " ";
}
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) ? ", " : "";
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) ? ", " : "";
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">'+' '+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;
}
})();