// ==UserScript==
// @name WaniKani: Total Progress
// @namespace https://wanikani.com/
// @version 0.2.0
// @description A (smaller) re-implementation of https://community.wanikani.com/t/userscript-total-progress-bar-allows-level-progress-removalaka-2cool4progress/38899 using modern js
// @author KaHLK
// @include /^https://(www|preview).wanikani.com/(dashboard)?$/
// @grant none
// ==/UserScript==
// @ts-ignore
const SCRIPT_ID = "total_progress_bar";
const TOTAL_PROGRESS_STYLE = `
#${SCRIPT_ID} {
position: relative;
padding: 1em;
background: #f4f4f4;
border-radius: 5px;
}
#${SCRIPT_ID} #inner {
overflow: hidden;
}
#${SCRIPT_ID} .bar{
display: flex;
height: 2.5em;
background: transparent;
}
#${SCRIPT_ID} h5{
margin-bottom: 0;
margin-top: 1em;
}
#${SCRIPT_ID} .section{
display: flex;
justify-content: center;
align-items: center;
height: inherit;
color: #fafafa;
}
#${SCRIPT_ID} #tooltip {
position: absolute;
padding: .5em;
background-color: #222;
color: #fafafa;
border-radius: 5px;
transform: translate(0, -100%);
width: max-content;
visibility: hidden;
}
#${SCRIPT_ID} #tooltip::after {
content: "";
position: absolute;
bottom: 0;
left: 50%;
transform: translate(-50%, 100%);
height: 0;
width: 0;
border: 4px solid transparent;
border-top-color: #222;
}
`;
const EMPTY_MAP = {
locked: 0,
level: 0,
lesson: 0,
apprentice1: 0,
apprentice2: 0,
apprentice3: 0,
apprentice4: 0,
guru1: 0,
guru2: 0,
master: 0,
enlightened: 0,
burned: 0,
};
(function () {
'use strict';
let wkof;
if (!window.wkof) {
let response = confirm("\"WaniKani: Total Progress\" 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;
}
else {
wkof = window.wkof;
}
// Prepare variables
const total = { ...EMPTY_MAP };
const total_type = {
radical: { ...EMPTY_MAP },
kanji: { ...EMPTY_MAP },
vocabulary: { ...EMPTY_MAP },
};
const percent = { ...EMPTY_MAP };
const percent_type = {
radical: { ...EMPTY_MAP },
kanji: { ...EMPTY_MAP },
vocabulary: { ...EMPTY_MAP },
};
const amount = {
radical: 0,
kanji: 0,
vocabulary: 0,
};
const container = el("section");
container.id = SCRIPT_ID;
const inner = div("inner");
const bar = div();
const sections = {};
const bar_type = {
radical: div(),
kanji: div(),
vocabulary: div(),
};
const sections_type = {
radical: {},
kanji: {},
vocabulary: {},
};
const mouse_move = {};
const mouse_move_type = {
radical: {},
kanji: {},
vocabulary: {},
};
const tooltip = div("tooltip");
let dialog;
let item_total = 0;
let container_rect;
setup();
async function setup() {
wkof.include("Menu,Settings,ItemData");
await wkof.ready("Menu,Settings,ItemData");
dialog = prepare_dialog(update_bar);
dialog.load({
reverse: true,
sub_section: true,
locked_color: "#222222",
level_color: "#3a3a3a",
lesson_color: "#525252",
apprentice1_color: "#933172",
apprentice2_color: "#b32082",
apprentice3_color: "#d41092",
apprentice4_color: "#f500a3",
guru1_color: "#bc23b3",
guru2_color: "#a035bb",
master_color: "#3a5bde",
enlightened_color: "#009eee",
burned_color: "#fab623",
show_type_breakdown: false,
});
install_menu();
const cur_level = (await wkof.Apiv2.get_endpoint("user")).data.level;
const items = await wkof.ItemData.get_items({
wk_items: {
options: {
assignments: true,
}
}
});
item_total = items.length;
// Count the amount of items for each srs stage
const index = wkof.ItemData.get_index(items, "srs_stage");
for (let [stage_num, items] of Object.entries(index)) {
for (let item of items) {
let stage = stage_num_to_id(Number(stage_num));
if (stage === "locked" && item.data.level === cur_level) {
stage = "level";
}
const type = item.object;
amount[type]++;
total_type[type][stage]++;
total[stage]++;
}
}
// Setup divs for the bar and calculate the different srs percentages
document.querySelector(".srs-progress").before(container);
container.append(inner);
inner.append(bar);
setup_bar(total, item_total, percent, sections, bar);
for (let key of Object.keys(total_type)) {
const bar = bar_type[key];
inner.append(bar);
const title = el("h5");
title.innerHTML = key.charAt(0).toUpperCase() + key.slice(1);
bar.before(title);
setup_bar(total_type[key], amount[key], percent_type[key], sections_type[key], bar);
}
container.append(tooltip);
update_bar();
}
function setup_bar(items, amount, percent_arr, section_arr, bar) {
bar.classList.add("bar");
bar.addEventListener("mouseenter", mouseover);
bar.addEventListener("mouseleave", mouseout);
for (let [key, value] of Object.entries(items)) {
percent_arr[key] = value / amount * 100;
const section = div();
section.classList.add(key, "section");
section_arr[key] = section;
bar.append(section);
}
}
function mouseover() {
tooltip_show = true;
position_tooltip();
tooltip.style.visibility = "visible";
}
function mouseout() {
tooltip_show = false;
tooltip.style.visibility = "hidden";
}
let tooltip_show = false;
let tooltip_y = 0;
let tooltip_x = 0;
function mousemove(bar, section) {
const rect = bar.getBoundingClientRect();
const top = rect.top - container_rect.top - 6;
const offset = rect.left - container_rect.left;
const max = rect.width;
return (e) => {
tooltip.innerText = section.text;
const half_width = tooltip.getBoundingClientRect().width / 2;
const x = e.clientX - rect.x;
let resX = offset;
if (x > half_width && x < max - half_width) {
resX = x + offset - half_width;
}
else if (x >= max - half_width) {
resX = max - half_width * 2 + offset;
}
tooltip_y = top;
tooltip_x = resX;
};
}
function position_tooltip() {
tooltip.style.top = `${tooltip_y}px`;
tooltip.style.left = `${tooltip_x}px`;
if (tooltip_show) {
requestAnimationFrame(position_tooltip);
}
}
// Update the bar with values from the settings
function update_bar() {
const settings = wkof.settings[SCRIPT_ID];
// Reverse the bar
if (settings.reverse) {
bar.style.flexDirection = "row-reverse";
for (let el of Object.values(bar_type)) {
el.style.flexDirection = "row-reverse";
}
}
else {
bar.style.flexDirection = "row";
for (let el of Object.values(bar_type)) {
el.style.flexDirection = "row";
}
}
let [percent_values, percent_values_type] = map_values(percent, percent_type, settings);
let [total_values, total_values_type] = map_values(total, total_type, settings);
if (settings.show_type_breakdown) {
inner.style.height = "auto";
}
else {
inner.style.height = `${bar.getBoundingClientRect().height}px`;
}
container_rect = container.getBoundingClientRect();
// Update the width, text, and color of the sections
set_sections(bar, sections, percent_values, total_values, item_total, settings, mouse_move);
for (let key of Object.keys(sections_type)) {
set_sections(bar_type[key], sections_type[key], percent_values_type[key], total_values_type[key], amount[key], settings, mouse_move_type[key]);
}
}
function merge_sections(data, settings) {
if (settings.sub_section) {
return data;
}
let values = {};
for (let [key, value] of Object.entries(data)) {
let k = key;
if (key.startsWith("apprentice")) {
k = "apprentice4";
}
else if (k.startsWith("guru")) {
k = "guru2";
}
if (values[k] === undefined) {
values[k] = 0;
}
values[k] += value;
}
return values;
}
function map_values(data, data_by_type, settings) {
let values = merge_sections(data, settings);
let values_types = {};
for (let key of Object.keys(sections_type)) {
values_types[key] = merge_sections(data_by_type[key], settings);
}
return [values, values_types];
}
function set_sections(bar, sections, values, amount, total, settings, listeners) {
for (let [key, section] of Object.entries(sections)) {
let value = 0;
if (values[key] !== undefined) {
value = values[key];
}
section.style.width = `${value}%`;
section.style.background = settings[`${key}_color`];
section.text = `${stage_id_to_title(key, !settings.sub_section)}: ${value.toFixed(2)}% (${amount[key]} / ${total})`;
if (value > 4) {
section.innerHTML = `${value.toFixed(2)}%`;
}
else {
section.innerHTML = "";
}
section.removeEventListener("mousemove", listeners[key]);
listeners[key] = mousemove(bar, section);
section.addEventListener("mousemove", listeners[key]);
}
}
function install_menu() {
wkof.Menu.insert_script_link({
name: `${SCRIPT_ID}_settings`,
submenu: "Settings",
title: "Total Progress",
on_click: open_settings,
});
}
function open_settings() {
dialog.open();
}
// Add the required styles to the header
const style = el("style");
style.id = `${SCRIPT_ID}_style`;
style.innerHTML = TOTAL_PROGRESS_STYLE;
document.head.append(style);
})();
// A utility logger function that appends an easy to locate identifier before each log call
function log(...args) {
console.log("TP:", ...args);
}
// A utility function that maps a srs_stage number from the api to a number used for look-up in the maps
function stage_num_to_id(stage) {
switch (stage) {
case -2: return "lesson";
case 1: return "apprentice1";
case 2: return "apprentice2";
case 3: return "apprentice3";
case 4: return "apprentice4";
case 5: return "guru1";
case 6: return "guru2";
case 7: return "master";
case 8: return "enlightened";
case 9: return "burned";
}
return "locked";
}
function stage_id_to_title(stage, merge) {
switch (stage) {
case "locked": return "Locked";
case "lesson": return "Lesson";
case "lesson": return "Lesson";
case "apprentice1": if (!merge) {
return "Apprentice 1";
}
case "apprentice2": if (!merge) {
return "Apprentice 2";
}
case "apprentice3": if (!merge) {
return "Apprentice 3";
}
case "apprentice4": if (!merge) {
return "Apprentice 4";
}
else {
return "Apprentice";
}
case "guru1": if (!merge) {
return "Guru 1";
}
case "guru2": if (!merge) {
return "Guru 2";
}
else {
return "Guru";
}
case "master": return "Master";
case "enlightened": return "Enlightened";
case "burned": return "Burned";
}
return "locked";
}
function div(id = "") {
const d = el("div");
d.id = id;
return d;
}
function el(el) {
return document.createElement(el);
}
function prepare_dialog(update_bar) {
return new window.wkof.Settings({
script_id: SCRIPT_ID,
title: "Total Progress",
on_change: update_bar,
on_close: update_bar,
content: {
reverse: {
type: "checkbox",
label: "Reverse order",
hover_tip: "Reverses the order of the stages",
default: true,
},
sub_section: {
type: "checkbox",
label: "Subsection",
hover_tip: "Divide Apprentice and Guru stages into their subsections",
default: true,
},
show_type_breakdown: {
type: "checkbox",
label: "Show type breakdown",
hover_tip: "Show bars breakingdown the progress for each item type (radical, kanji, vocabulary)",
default: false,
},
color_section: {
type: "group",
label: "Colors",
content: {
locked_color: {
type: "color",
label: "Locked",
hover_tip: "The color of Locked items",
default: "#222222",
},
level_color: {
type: "color",
label: "Level",
hover_tip: "The color of items that is currently locked but will be available in the current level",
default: "#3a3a3a",
},
lesson_color: {
type: "color",
label: "Locked",
hover_tip: "The color of Lesson items",
default: "#525252",
},
apprentice1_color: {
type: "color",
label: "Apprentice 1 Color",
hover_tip: "The color of Apprentice 2 items",
default: "#933172",
},
apprentice2_color: {
type: "color",
label: "Apprentice 2 Color",
hover_tip: "The color of Apprentice 2 items",
default: "#b32082",
},
apprentice3_color: {
type: "color",
label: "Apprentice 3 Color",
hover_tip: "The color of Apprentice 3 items",
default: "#d41092",
},
apprentice4_color: {
type: "color",
label: "Apprentice 4 Color",
hover_tip: "The color of Apprentice 4 items",
default: "#f500a3",
},
guru1_color: {
type: "color",
label: "Guru 1 Color",
hover_tip: "The color of Guru 1 items",
default: "#bc23b3",
},
guru2_color: {
type: "color",
label: "Guru 2 Color",
hover_tip: "The color of Guru 2 items",
default: "#a035bb",
},
master_color: {
type: "color",
label: "Master Color",
hover_tip: "The color of Master items",
default: "#3a5bde",
},
enlightened_color: {
type: "color",
label: "Enlightened Color",
hover_tip: "The color of Enlightened items",
default: "#009eee",
},
burned_color: {
type: "color",
label: "Burned Color",
hover_tip: "The color of Burned items",
default: "#fab623",
},
},
},
}
});
}