WaniKani: Total Progress

A (smaller) re-implementation of https://community.wanikani.com/t/userscript-total-progress-bar-allows-level-progress-removalaka-2cool4progress/38899 using modern js

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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",
                    },
                },
            },
        }
    });
}