Wanikani Integrated Dashboard Overhaul Settings

Settings Panel to modify the Dashboard Overhaul

// ==UserScript==
// @name         Wanikani Integrated Dashboard Overhaul Settings
// @namespace    http://tampermonkey.net/
// @version      0.8
// @description  Settings Panel to modify the Dashboard Overhaul
// @author       ToastedRice
// @include      /^https?://(www|preview).wanikani.com/(dashboard)?/
// @grant        none
// ==/UserScript==

(function() {

    // Add your fonts here!
    let fonts = [
        // Fonts that Wanikani uses
        'Arial',
        'FontAwesome',
        '"Helvetica Neue"',
        'Helvetica',
        'Noto Sans JP',
        '"Open Sans"',
        'sans-serif',
        'Source Sans Pro',

        // Default Windows fonts
        "Meiryo, メイリオ",
        "MS PGothic, MS Pゴシック, MS Gothic, MS ゴック",
        "MS PMincho, MS P明朝, MS Mincho, MS 明朝",
        "Yu Gothic, YuGothic",
        "Yu Mincho, YuMincho",

        // Default OS X fonts
        "Hiragino Kaku Gothic Pro, ヒラギノ角ゴ Pro W3",
        "Hiragino Maru Gothic Pro, ヒラギノ丸ゴ Pro W3",
        "Hiragino Mincho Pro, ヒラギノ明朝 Pro W3",

        // Common Linux fonts
        "Takao Gothic, TakaoGothic",
        "Takao Mincho, TakaoMincho",
        "Sazanami Gothic",
        "Sazanami Mincho",
        "Kochi Gothic",
        "Kochi Mincho",
        "Dejima Mincho",
        "Ume Gothic",
        "Ume Mincho",


        // Other Japanese fonts people use.
        // You might want to try some of these!
        "04KanjyukuGothic",
        "07YasashisaAntique",
        "851MkPOP",
        "A-OTF Shin Maru Go Pro",
        "aoyagireisyosimo2, AoyagiKouzanFont2OTF",
        "AppliMincho",
        "ArmedBanana",
        "aquafont",
        "'chifont+', chifont",
        "Chihaya Jyun",
        "ChihayaGothic",
        "cinecaption",
        "Corporate Logo Medium",
        "Corporate Logo Rounded",
        "DartsFont",
        "EPSON 行書体M",
        "EPSON 正楷書体M",
        "EPSON 教科書体M",
        "EPSON 太明朝体B",
        "EPSON 太行書体B",
        "EPSON 丸ゴシック体M",
        "FangSong",
        "FC-Flower",
        "GenEi Antique Pv5",
        "GenEi Nombre",
        "GenEi Web Honmon",
        "HakusyuGyosyoPro_kk",
        "HakusyuKaisyoExtraBold_kk",
        "HakusyuKaisyo_kk",
        "HakusyuReisyo_kk",
        "Honoka Antique-Kaku",
        "HonyaJi-Re",
        "Hosofuwafont",
        "JK Gothic M",
        "nagayama_kai",
        "Nchifont+",
        "Pomeranian",
        "Purisa",
        "RiiTegakiFude",
        "santyoume-font",
        "ShokakiSarariGyotai",
        "SoukouMincho",
        "Togalite",
        "umeboshi"
    ];
    let lesson = [0,0,0];
    let review = [0,0,0];
    let fetched = [false,false];

    function lesson_text() {return $('.lessons-and-reviews__lessons-button span')}
    function review_text() {return $('.lessons-and-reviews__reviews-button span')}

    window.window.addEventListener('load', check_fonts);

    if (window.wkof)
    {
        wkof.include('Menu,Settings');
        wkof.ready('Menu,Settings')
            .then(load)
            .then(install_menu)
            .then(install_css)
            .then(update_font);
    }
    else if (confirm('WK IDBO requires Wanikani Open Framework.\n\nDo you want to be forwarded to the installation instructions?')) {
        window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
    };

    // ================================================================================================================
    // Checks if fonts exist on system/browser/website. Function waits for page to load before proceeding to allow fonts time to be imported.

    function check_fonts()
    {
        let available = isFontAvailable(fonts);
        fonts = fonts.filter((font,i) => available[i]);
        fonts.forEach((font,i) => {font = font.replace(/'?"?/g,''); fonts[i] = font.charAt(0).toUpperCase() + font.slice(1)});
        fonts = fonts.sort();
    }

    // ================================================================================================================
    // Loads settings and setting defaults

    function load()
    {
        lesson_text().on('DOMSubtreeModified', fetch_lesson_details)
        review_text().on('DOMSubtreeModified', fetch_review_details)

        let default_settings =
            {
                style: 'jp_simple',
                font: 'Arial',
                font_weight: 5,
                font_size: 14.0,
                separator: 'space',
                size: 5,
                offset: 0,
            }
        return wkof.Settings.load('idbo_settings', default_settings);
    }

    // ================================================================================================================
    // Scans text for r/k/v counts

    function parse_details(input, output)
    {
        let rad = input.search('部');
        let kan = input.search('漢');
        let voc = input.search('語');
        if (rad==-1) rad = 0;
        if (kan==-1) kan = rad;
        if (voc==-1) voc = kan;
        output[0] = parseInt(input.substring(0,rad).replace(/\D/g,''),10);
        output[1] = parseInt(input.substring(rad,kan).replace(/\D/g,''),10);
        output[2] = parseInt(input.substring(kan,voc).replace(/\D/g,''),10);
    }

    // ================================================================================================================
    // Retrieves lesson r/k/v text

    function fetch_lesson_details()
    {
        if (lesson_text().text() && !fetched[0])
        {
            fetched[0] = true;
            parse_details(lesson_text().text(), lesson);
            update_lesson_details();
        }
    }

    // ================================================================================================================
    // Retrieves review r/k/v text

    function fetch_review_details()
    {
        if (review_text().text() && !fetched[1])
        {
            fetched[1] = true;
            parse_details(event.target.innerText, review);
            update_review_details();
        }
    }

    // ================================================================================================================
    // Updates both lesson and review r/k/v text

    function update_details()
    {
        update_lesson_details();
        update_review_details();
    }

    // ================================================================================================================
    // Updates review r/k/v text

    function update_lesson_details()
    {
        if (fetched[0]) lesson_text().html(detail_text(lesson))
    }

    // ================================================================================================================
    // Updates lesson r/k/v text

    function update_review_details()
    {
        if (fetched[1]) review_text().html(detail_text(review))
    }

    // ================================================================================================================
    // Adds menu entry to script menu list.

    function install_menu()
    {
        var menu =
            {
                name: 'integrated_dashboard_overhaul_settings',
                submenu: 'Settings',
                title: 'Integrated Dashboard Overhaul',
                on_click: open_menu
            };
        wkof.Menu.insert_script_link(menu);
    }

    // ================================================================================================================
    // Opens the script menu. Updates preview upon any changes.

    function open_menu()
    {
        var submenu =
        {
            script_id: 'idbo_settings',
            title:     'Integrated Dashboard Overhaul',
            pre_open:   update_preview,
            on_save:    update_font,
            on_cancel:  update_font,
            on_close:   update_font,
            content:
            {
                preview:
                {
                    type:  'html',
                    label: 'Preview:',
                    html:  '<div class="idbo-preview"><div>Lessons</div><span id="idbo-preview">' + preview().replace(/\n/g, "<br />") + '</span></div>'
                },

                font:
                {
                    type:     'dropdown',
                    label:    'Font:',
                    default:  'Arial',
                    on_change: update_preview,
                },

                font_size:
                {
                    type:     'number',
                    label:    'Font Size:',
                    default:  14,
                    min:       4,
                    max:      44,
                    on_change: update_preview,
                },

                font_weight:
                {
                    type:      'number',
                    label:     'Font Weight:',
                    hover_tip: 'Change how "bold" the font is from 1-9. Most fonts will only have 2 available weights',
                    default:   5,
                    min:       1,
                    max:       9,
                    on_change: update_preview,
                },

                style:
                {
                    type:      'list',
                    label:     'Appearance',
                    hover_tip: 'Choose the appearance of your lesson/review counts',
                    default:   'jp_simple',
                    on_change: function(value) { document.getElementById('idbo-preview').innerText = preview() },
                    content:
                    {
                        classic:    preview('classic'),
                        eng_simple: preview('eng_simple'),
                        eng_full:   preview('eng_full'),
                        jp_simple:  preview('jp_simple'),
                        jp_full:    preview('jp_full'),
                        hiragana:   preview('hiragana'),
                        katakana:   preview('katakana'),
                    }
                },

                separator:
                {
                    type:      'dropdown',
                    label:     'Subject Separator',
                    hover_tip: 'Choose symbol to separate subjects. Some symbols will not show up in some fonts',
                    default:   ' ',
                    on_change: function(value) { document.getElementById('idbo-preview').innerText = preview(); update_preview(); },
                    content:
                    {
                        space:  '(space)',
                        slash:  '/',
                        line:   '|',
                        plus:   '+',
                        dash:   '-',
                        long:   'ー',
                        tilde:  '~',
                        bullet: '•',
                        maru:   '・',
                        heart:  '♥',
                        spade:  '♠',
                        diam:   '♦',
                        club:   '♣',
                        tri:    '‣',
                        ast:    '*',
                        star:   '★',
                        star1:  '☆',
                        star2:  '✦',
                        star3:  '✧',
                        star4:  '✸',
                        sun:    '☀',
                        sun2:   '☼',
                        moon:   '☾',
                        cloud:  '☁',
                        umbrl:  '☂',
                        comma:  ',',
                        colon:  ':',
                        semi:   ';',
                        newline:'(new line) - Stack subjects vertically.'
                    }
                },

                size:
                {
                    type: 'number',
                    label: 'Border Size',
                    hover_tip: 'Enlarge or shrink the border. Default size is 5',
                    default: 5,
                    min: 0,
                    max: 20,
                    on_change: update_preview,
                },

                offset:
                {
                    type: 'number',
                    label: 'Vertical Offset',
                    hover_tip: 'Will shift the text up and down. Useful if font is not centered.',
                    default: 0,
                    min: -5 - wkof.settings.idbo_settings.size,
                    max: 5 + wkof.settings.idbo_settings.size,
                    on_change: update_preview,
                },
            }
        }
        submenu.content.font.content = fonts.reduce(function(output, value) {output[value] = value; return output;}, {});
        new wkof.Settings(submenu).open();
    }

    // ================================================================================================================
    // Generates preview text for settings panel

    function preview(style)
    {
        let rad = get_radical_style(style || wkof.settings.idbo_settings.style)
        let kan = get_kanji_style  (style || wkof.settings.idbo_settings.style)
        let voc = get_vocab_style  (style || wkof.settings.idbo_settings.style)
        let sep = get_separator();

        style = style?style:wkof.settings.idbo_settings.style;

        if (style == 'classic') return (12+34+56).toString();
        return 12+rad + sep + 34+kan + sep + 56+voc;
    }

    // ================================================================================================================
    // Generates r/k/v details text

    function detail_text(data)
    {
        if (data[0] + data[1] + data[2] == 0) return '0';

        let style = wkof.settings.idbo_settings.style
        let rad = get_radical_style(style || wkof.settings.idbo_settings.style)
        let kan = get_kanji_style  (style || wkof.settings.idbo_settings.style)
        let voc = get_vocab_style  (style || wkof.settings.idbo_settings.style)
        let sep = get_separator();
        if (sep == '\n') sep = '<br \\>';

        if (style == 'classic') return data[0]+data[1]+data[2];

        let output = '';
        output += data[0] ? data[0] + rad : '';
        output += ((data[0] && data[1]) || (data[0] && data[2])) ? sep : '';
        output += data[1] ? data[1] + kan : '';
        output += (data[1] && data[2]) ? sep : '';
        output += data[2] ? data[2] + voc : '';
        return output;
    }

    // ================================================================================================================
    // Converts settings separator into actual symbol for separator

    function get_separator(sep)
    {
        switch(sep || wkof.settings.idbo_settings.separator)
        {
            case 'space':   return ' '; break;
            case 'slash':   return '/'; break;
            case 'line':    return '|'; break;
            case 'plus':    return '+'; break
            case 'dash':    return '-'; break;
            case 'long':    return 'ー'; break;
            case 'tilde':   return '~'; break;
            case 'bullet':  return '•'; break;
            case 'maru':    return '・'; break;
            case 'tri':     return '‣'; break;
            case 'ast':     return '*'; break;
            case 'heart':   return '♥'; break;
            case 'spade':   return '♠'; break;
            case 'diam':    return '♦'; break;
            case 'club':    return '♣'; break;
            case 'star':    return '★'; break;
            case 'star1':   return '☆'; break;
            case 'star2':   return '✦'; break;
            case 'star3':   return '✧'; break;
            case 'star4':   return '✸'; break;
            case 'sun':     return '☀'; break;
            case 'sun2':    return '☼'; break;
            case 'moon':    return '☾'; break;
            case 'cloud':   return '☁'; break;
            case 'umbrl':   return '☂'; break;
            case 'comma':   return ','; break;
            case 'colon':   return ':'; break;
            case 'semi':    return ';'; break;
            case 'newline': return '\n'; break;
        }
    }

    // ================================================================================================================
    // Retrieves appropriate text for radical

    function get_radical_style(style)
    {
        switch(style || wkof.settings.idbo_settings.style)
        {
            case 'eng_full':   return ' Radical';
            case 'eng_simple': return 'R';
            case 'jp_full':    return '部首';
            case 'jp_simple':  return '部';
            case 'hiragana':   return ' ぶしゅ';
            case 'katakana':   return ' ブシュ';
        }
    }

    // ================================================================================================================
    // Retrieves appropriate text for kanji

    function get_kanji_style(style)
    {
        switch(style || wkof.settings.idbo_settings.style)
        {
            case 'eng_full':   return ' Kanji';
            case 'eng_simple': return 'K';
            case 'jp_full':    return '漢字';
            case 'jp_simple':  return '漢';
            case 'hiragana':   return ' かんじ';
            case 'katakana':   return ' カンジ';
        }
    }

    // ================================================================================================================
    // Retrieves appropriate text for vocab

    function get_vocab_style(style)
    {
        switch(style || wkof.settings.idbo_settings.style)
        {
            case 'eng_full':   return ' Vocabulary';
            case 'eng_simple': return 'V';
            case 'jp_full':    return '単語';
            case 'jp_simple':  return '語';
            case 'hiragana':   return ' たんご';
            case 'katakana':   return ' タンゴ';
        }
    }

    // ================================================================================================================
    // Installs text css styles

    function install_css()
    {
        document.getElementsByTagName('head')[0].insertAdjacentHTML('beforeend', `
        <style id="IDBO Font CSS"></style>
        <style id="IDBO Font Preview CSS"></style>`
        );
        update_font();
        update_preview();
    }

    // ================================================================================================================
    // Updates lesson/review detail font

    function update_font()
    {
        let settings = wkof.settings.idbo_settings;
        document.getElementById('IDBO Font Preview CSS').innerHTML = `
        .lessons-and-reviews__button span
        {
            font-weight: ` + (settings.font_weight * 100) + ` !important;
            font-family: ` + settings.font + ` !important;
            font-size: `   + settings.font_size + `px!important;
            padding: ` + (settings.size - settings.offset) + `px ` +
                         (settings.size + 5) + `px ` +
                         (settings.size + settings.offset) + `px !important;
            padding-top: 5px !important;
            padding-bottom: 5px !important;
            text-align: end !important;
        }`;
        update_details();
    }

    // ================================================================================================================
    // Updates the font in the preview

    function update_preview()
    {
        let settings = wkof.settings.idbo_settings;
        document.getElementById('IDBO Font Preview CSS').innerHTML = `
        #wkof_ds #idbo_settings_style
        {
            font-family: ` + settings.font + `;
        }
        .idbo-preview
        {
            display: block;
            cursor: pointer;
            position: relative;
            top: 0px;
            left: 0px;
            float: right;
            width: 380px;
            height: 149.6px;
            margin: 0px 0px 10px !important;
            background-color: #FF00AA;
            background-image: url(https://cdn.wanikani.com/assets/dashboard/buttons/bg_lessons-25-f813c200a3639f27d8bf9a84c0512db19c69b80701abfe18c9d961a353b2900f.png);
            background-repeat: no-repeat;
            background-size: auto 150px;
            background-position: center;
            border-radius: 5px;
            color: #FFF;
            line-height: 1;
            box-shadow: inset 0 -3px 1px rgba(0,0,0,0.2), inset 0 3px 1px rgba(0,0,0,0);
            transition: box-shadow 100ms;
        }
        .idbo-preview span
        {
            font-family: ` + settings.font + `;
            font-size: `   + settings.font_size + `px;
            font-weight: ` + settings.font_weight * 100 + `;
            padding: ` + (settings.size - settings.offset) + `px ` +
                         (settings.size + 5) + `px ` +
                         (settings.size + settings.offset) + `px !important;
            border-radius: 15px;
            border-color: #FFF;
            background-color: #FFF;
            color: #FF00AA;
            width: fit-content;
            position: absolute;
            bottom: 12px;
            right: 12px;
        }
        .idbo-preview div
        {
            position:absolute;
            bottom: 16px;
            left: 16px;
            font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
            font-size: 18px;
            font-weight: bold;
        }`;
    }

    // ================================================================================================================
    // Tests to see whether or not the font exists by checking the width of the output.

    function isFontAvailable (font) {
        var testString  = '~iomw-—l~ツ亻';
        var containerId = 'is-font-available-container';

        var fontArray = font instanceof Array;

        if (!fontArray) {
            font = [ font ];
        }

        var fontAvailability = [];

        var containerSel = '#' + containerId;
        var spanSel      = containerSel + ' span';

        var familySansSerif = 'sans-serif';
        var familyMonospace = 'monospace, monospace';
        // Why monospace twice? It's a bug in the Mozilla and Webkit rendering engines:

        // DOM:
        $('body').append('<div id="' + containerId + '"></div>');
        $(containerSel).append('<span></span>');
        $(spanSel).append(document.createTextNode(testString));

        // CSS:
        $(containerSel).css('visibility', 'hidden');
        $(containerSel).css('position', 'absolute');
        $(containerSel).css('left', '-9999px');
        $(containerSel).css('top', '0');
        $(containerSel).css('font-weight', 'bold');
        $(containerSel).css('font-size', '200px !important');

        jQuery.each( font, function (i, v) {
            $(spanSel).css('font-family', v + ',' + familyMonospace );
            var monospaceFallbackWidth = $(spanSel).width();
            var monospaceFallbackHeight = $(spanSel).height();

            $(spanSel).css('font-family', v + ',' + familySansSerif );
            var sansSerifFallbackWidth = $(spanSel).width();
            var sansSerifFallbackHeight = $(spanSel).height();

            fontAvailability[i] = true
            && monospaceFallbackWidth == sansSerifFallbackWidth
            && monospaceFallbackHeight == sansSerifFallbackHeight;
        } );

        $(containerSel).remove();

        if (!fontArray && fontAvailability.length == 1) {
            fontAvailability = fontAvailability[0];
        }

        return fontAvailability;
    }
})();