// ==UserScript==
// @name WaniKani Dashboard SRS and Leech Breakdown
// @namespace https://www.wanikani.com
// @description Show SRS and leech breakdown on dashboard
// @author seanblue
// @version 1.0.0
// @include https://www.wanikani.com/dashboard
// @include https://www.wanikani.com/
// @grant none
// ==/UserScript==
(function() {
'use strict';
if (!window.wkof) {
let response = confirm('WaniKani Dashboard SRS and Leech Breakdown 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>
.srs-progress-details {
position: relative;
color: #fff;
}
.dashboard section.srs-progress span {
margin-bottom: 0.2em;
}
.dashboard section.srs-progress .srs-progress-details .leech-count .leech-breakdown {
background-color: black;
font-size: 0.8em;
font-weight: 100;
opacity: 0.75;
display: none;
}
.dashboard section.srs-progress li:hover .srs-progress-details .leech-count .leech-breakdown {
display: inline;
}
.dashboard section.srs-progress .srs-progress-details .leech-count {
background-color: black;
position: absolute;
right: -1.0em;
bottom: -2.5em;
padding-left: 0.3em;
padding-right: 0.3em;
font-size: 1em;
opacity: 0.25;
font-weight: 100;
}
.dashboard section.srs-progress li:hover .srs-progress-details .leech-count {
opacity: 1.0;
}
</style>`
$('head').append(style);
const leechThreshold = 1;
const config = {
wk_items: {
options: {
review_statistics: true,
assignments: true
}
}
};
wkof.include('ItemData');
wkof.ready('ItemData').then(getItems).then(mapItemsToSrs).then(updatePage);
function getItems(items) {
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 <= 8;
}
function getSrsStage(assignments) {
return assignments.srs_stage;
}
function mapItemsToSrs(items) {
let itemsBySrs = [1, 2, 3, 4, 5, 6, 7, 8].reduce((result, srs) => {
result[srs] = {
total: 0,
leech: 0
};
return result;
}, {});
items.forEach(function(item) {
let srsStage = getSrsStage(item.assignments);
itemsBySrs[srsStage].total++;
if (isLeech(item)) {
itemsBySrs[srsStage].leech++;
}
});
return itemsBySrs;
}
function isLeech(item) {
if (item.review_statistics === undefined) {
return false;
}
let reviewStats = item.review_statistics;
let meaningScore = getLeechScore(reviewStats.meaning_incorrect, reviewStats.meaning_current_streak);
let readingScore = getLeechScore(reviewStats.reading_incorrect, reviewStats.reading_current_streak);
return meaningScore >= leechThreshold || readingScore >= leechThreshold;
}
function getLeechScore(incorrect, currentStreak) {
return incorrect / Math.pow((currentStreak || 0.5), 1.5);
}
function updatePage(itemsBySrs) {
displayDetailedSection('apprentice', itemsBySrs, [1, 2, 3, 4]);
displayDetailedSection('guru', itemsBySrs, [5, 6]);
displaySimpleSection('master', itemsBySrs, 7);
displaySimpleSection('enlightened', itemsBySrs, 8);
displayEmptySection('burned');
}
function displayDetailedSection(srsSectionId, itemsBySrs, srsLevelsArray) {
let srsProgressDetailsSection = addFilledTotalBreakdownSection(srsSectionId, itemsBySrs, srsLevelsArray);
addDetailedLeechSection(srsProgressDetailsSection, itemsBySrs, srsLevelsArray);
}
function displaySimpleSection(srsSectionId, itemsBySrs, srsLevel) {
let srsProgressDetailsSection = addEmptyTotalBreakdownSection(srsSectionId);
addSimpleLeechSection(srsProgressDetailsSection, itemsBySrs, srsLevel);
}
function displayEmptySection(srsSectionId) {
addEmptyTotalBreakdownSection(srsSectionId);
}
function addFilledTotalBreakdownSection(srsSectionId, itemsBySrs, srsLevelsArray) {
let totals = srsLevelsArray.map(srs => itemsBySrs[srs].total).join(' / ');
return addTotalBreakdownSection(srsSectionId, `${totals}`);
}
function addEmptyTotalBreakdownSection(srsSectionId) {
return addTotalBreakdownSection(srsSectionId, ' ');
}
function addTotalBreakdownSection(srsSectionId, sectionContent) {
let section = $(`<div class="srs-progress-details">${sectionContent}</div>`);
$(`#${srsSectionId} span`).after(section);
return section;
}
function addDetailedLeechSection(srsProgressDetailsSection, itemsBySrs, srsLevelsArray) {
let leechArray = srsLevelsArray.map(srsLevel => itemsBySrs[srsLevel].leech);
let leechBreakdown = leechArray.join(' / ');
let leechTotal = leechArray.reduce((total, val) => total + val, 0);
let sectionContent = `<span class="leech-breakdown">(${leechBreakdown}) </span>${leechTotal}`;
addLeechSection(srsProgressDetailsSection, sectionContent);
}
function addSimpleLeechSection(srsProgressDetailsSection, itemsBySrs, srsLevel) {
addLeechSection(srsProgressDetailsSection, `${itemsBySrs[srsLevel].leech}`);
}
function addLeechSection(srsProgressDetailsSection, sectionContent) {
let section = `<span class="leech-count">${sectionContent}</span>`;
srsProgressDetailsSection.append(section);
}
})();