Wanikani: Levels by SRS

Displays your level for each SRS stage.

// ==UserScript==
// @name         Wanikani: Levels by SRS
// @namespace    http://tampermonkey.net/
// @version      1.1.7
// @description  Displays your level for each SRS stage.
// @author       Kumirei
// @match        https://www.wanikani.com/dashboard
// @match        https://www.wanikani.com
// @include      *preview.wanikani.com*
// @grant        none
// ==/UserScript==
/*jshint esversion: 8 */

;(function () {
    //check that the Wanikani Framework is installed
    var script_name = 'Levels By SRS'
    if (!window.wkof) {
        if (
            confirm(
                script_name +
                    ' requires Wanikani Open Framework.\nDo you want to be forwarded to the installation instructions?',
            )
        )
            window.location.href =
                'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549'
        return
    }
    //if it's installed then do the stuffs
    else {
        wkof.include('Menu,Settings,ItemData')
        wkof.ready('Menu,Settings,ItemData').then(load_settings).then(install_menu).then(add_css).then(fetch_and_update)
    }

    // Fetches items and updates display
    function fetch_and_update() {
        fetch_items().then(process_items).then(update_display)
    }

    // Fetches the relevant items
    function fetch_items() {
        var [promise, resolve] = new_promise()
        var config = {
            wk_items: {
                options: { assignments: true },
                filters: { level: '1..+0' },
            },
        }
        wkof.ItemData.get_items(config).then(resolve)
        return promise
    }

    // Retreives the levels by srs
    function process_items(data) {
        // Sort by level
        var levels = {}
        let item
        for (var i = 0; i < data.length; i++) {
            item = data[i]
            var level = item.data.level
            if (!levels[level]) levels[level] = []
            levels[level].push(item)
        }
        // Go through items level by level
        var srs_levels = {
            Ini: [0, 0, 0],
            App: [0, 0, 0],
            Gur: [0, 0, 0],
            Mas: [0, 0, 0],
            Enl: [0, 0, 0],
            Bur: [0, 0, 0],
        }
        for (i = 1; i < wkof.user.level + 1; i++) {
            // Get counts for level
            var by_srs = { Ini: 0, App: 0, Gur: 0, Mas: 0, Enl: 0, Bur: 0, total: 0 }
            for (var j = 0; j < levels[i].length; j++) {
                item = levels[i][j]
                if (item.assignments)
                    by_srs[
                        ['Ini', 'App', 'App', 'App', 'App', 'Gur', 'Gur', 'Mas', 'Enl', 'Bur'][
                            item.assignments.srs_stage
                        ]
                    ]++
                else by_srs.Ini++
                by_srs.total++
            }
            // Check if srs_level should be increased
            var types = ['Ini', 'App', 'Gur', 'Mas', 'Enl', 'Bur']
            var cumulative = 0
            for (j = 0; j < types.length; j++) {
                var count = by_srs[types[j]]
                if (
                    1 - cumulative / by_srs.total >= wkof.settings.levels_by_srs.threshold / 100 &&
                    i == srs_levels[types[j]][0] + 1
                ) {
                    srs_levels[types[j]][0]++
                } else if (
                    1 - cumulative / by_srs.total <= wkof.settings.levels_by_srs.threshold / 100 &&
                    i == srs_levels[types[j]][0] + 1
                ) {
                    srs_levels[types[j]][1] = 1 - cumulative / by_srs.total
                    srs_levels[types[j]][2] = by_srs.total
                }
                cumulative += count
            }
        }
        return srs_levels
    }

    // Updates the display element
    function update_display(srs_levels) {
        var types = ['App', 'Gur', 'Mas', 'Enl', 'Bur']
        // If the element doesn't already exist, create it
        var display = $('#levels_by_srs')
        if (!display.length) {
            display = $('<div id="levels_by_srs"' + (is_dark_theme() ? ' class="dark_theme"' : '') + '></div>')
            for (var i = 0; i < types.length; i++)
                display.append(
                    '<div class="' +
                        types[i] +
                        '"><span class="level_label">Level: </span><span class="value"></span></div>',
                )
            $('.srs-progress').append(display)
        }
        // Update
        for (let i = 0; i < types.length; i++) {
            var elem = $(display).find('.' + types[i] + ' span.value')[0]
            elem.innerText = srs_levels[types[i]][0]
            elem.parentElement.setAttribute(
                'title',
                Math.floor(srs_levels[types[i]][1] * 100) +
                    '% to level ' +
                    (srs_levels[types[i]][0] + 1) +
                    ' (' +
                    Math.round(srs_levels[types[i]][1] * srs_levels[types[i]][2]) +
                    ' of ' +
                    srs_levels[types[i]][2] +
                    ')',
            )
        }
    }

    // Load stored settings or set defaults
    function load_settings() {
        var defaults = { threshold: 90 }
        return wkof.Settings.load('levels_by_srs', defaults)
    }

    // Installs the options button in the menu
    function install_menu() {
        var config = {
            name: 'levels_by_srs',
            submenu: 'Settings',
            title: 'Levels By SRS',
            on_click: open_settings,
        }
        wkof.Menu.insert_script_link(config)
    }

    // Create the options
    function open_settings(items) {
        var config = {
            script_id: 'levels_by_srs',
            title: 'Levels By SRS',
            on_save: fetch_and_update,
            content: {
                threshold: {
                    type: 'number',
                    label: 'Threshold',
                    hover_tip: 'Percentage to consider level done',
                    default: 90,
                },
            },
        }
        var dialog = new wkof.Settings(config)
        dialog.open()
    }

    // Adds the script's CSS to the page
    function add_css() {
        $('head').append(`<style id="levels_by_srs_CSS">
        #levels_by_srs {
            background: #434343;
            border-radius: 0 0 3px 3px;
            height: 30px;
            line-height: 30px;
            color: rgb(240, 240, 240);
            display: flex;
            justify-content: space-around;
        }
        #levels_by_srs > div {
            display: flex;
            flex: 1;
            justify-content: center;
        }
        #levels_by_srs .level_label {
            font-size: 16px;
            margin-right: 5px;
        }
        #levels_by_srs .value {
            font-size: 16px;
            font-weight: normal;
        }
        #levels_by_srs.dark_theme {
            background: #232629;
        }
        #levels_by_srs.dark_theme > div:not(:last-child) {
            border-right: 1px solid #31363b;
            margin-right: 5px;
        }
        #levels_by_srs.dark_theme > div:last-child {
            border-right: 1px solid transparent;
        }
        .srs-progress > ul > li {
            border-radius: 0 !important;
        }
        </style>`)
    }

    // Returns a promise and a resolve function
    function new_promise() {
        var resolve,
            promise = new Promise((res, rej) => {
                resolve = res
            })
        return [promise, resolve]
    }

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