Jitai (字体): The font randomizer that fits

Thank you so much - the update seems to have fixed everything on my end:


^ (it does feel like delta gothic is even more bold than in the settings preview, but that may just be me seeing it for the first time)

Other things I am noticing off the bat (not sure if these are issues on my end/related to the update(s)/etc:

  • This was while using 3.3.0, but I got the initial popup about not having any fonts installed right on the WK login page, and I think again on the dashboard page? its seemed a bit aggressive/confusing, especially since the “cog in the top left” isn’t present on either of those windows. I have fonts added now so I’m unable to trigger the popup again, but I could remove those and try to re-test with 3.3.1 if that would be helpful for troubleshooting?
  • It looks like the link to download armedbanana is not resolving, at least on my end

Thanks for making/updating/maintaining this extension, I think I’m going to get a ton of use out of it!

1 Like

Okay I don’t know what happened (if it’s a glitch) but I can reroll the fonts during lessons, too? I noticed first that the font was different. So I hit Ctrl+J and it rerolled. Thought Jitai only worked for reviews but this is an unexpected behavior. assuming this is glitch


UPDATE: Did a few more tests.

  1. Wish I got more screenshot but before I did a page refresh, I seem to be able to re-roll the fonts in the “quiz” after a batch of lesson.
  2. Seems like it’s only a manual re-roll. Meaning, it does not automatically re-roll if I go to a new vocab/item. This item has the same font as above before I hit Ctrl+J.
  3. Refreshed the page and the magic is gone :thinking: Maybe it is a glitch. I hope it was since I remember this note:
1 Like

Hm, that’s a very weird behavior. Could it be that you did Reviews just before the Lessons? I can imagine that since Jitai installs the font styles into the DOM during review session, that it keeps persistent even when you move to Lessons after. If that’s the case, a fresh reload of the page after review should fix it in Lessons. But I’ll check it today evening if that is really what is happening.
Turbo (the thing the WaniKani team has shifted to and that broke many scripts) is also very new to me, and I have to learn more about it I guess.

1 Like

Glad the modifications seemed to work for you (still wondering what especially fixed it though). Let me know if this happens again to you. If so, send me a screenshot of your browsers dev console (F12 key), there should be some logs (and hopefully errors) that help me to find the root cause.

The popup will not appear anymore on other pages than review, extra study, and recent mistakes page.

Thanks all for the feedback! Those detailed bug reports help a lot.

1 Like

I did do that. I’ll take note of this scenario: Review then going to Lessons immediately. I’ll try this out on my other account (since it has pending reviews and some lessons) to test it after work today.

Not quite sure I understand this technically. But if that may be the case, doesn’t the page reload when the reviews are finished? And going to the lessons also “reloads” a new page for it? Which, in my mind, means that there’s a refresh after review is finished, and before the lesson page loads up.

The lightning bolt in the reviews that skips the mini animation in regular session?

1 Like

Just putting this out there before I go to bed. It appears that my assumption was incorrect about the document.body stuff. Thinking about it now, I’m surprised it didn’t occur to me earlier. Since it’s not the document.body element that gets replaced, it’s the #turbo-body element just beneath that in the DOM.

Unfortunately, it isn’t as simple as adding the key listeners to that element instead, however, since they don’t appear to capture them for me. The way I see it, there’s two options:

  1. (easy but not ideal) Add the pageRegex test to the rest of the listeners
    • I would really like to avoid this though, because we’re dealing with keyEvent listeners here, which could really slowdown the user experience if they’re not properly managed
  2. (slightly more complicated) Add an on_pageload for one of the following:
    a. (ideal) navigating away from the reviews page (Pretty sure this is what I had in my full example above, before the edit, but I’m not sure if it applies as well with using simply WKOF)
    b. (easy) navigating to any page
    • Whichever is chosen, the callback should attempt to deregister any and all listeners. Consequently, if it is added before the other on_pageload, it should have no negative side effects, even if option b is chosen.

I’m likely to be forgetting something, but I’m about to go to sleep, so I’ll just leave it at this for now.

Edit:

I'll paste my earlier version here for easier comparisons
// ==UserScript==
// @name        Jitai
// @author      @marciska
// @namespace   marciska
// @description Displays your WaniKani reviews with randomized fonts (based on original by @obskyr with various fixes applied)
// @version     3.1.3
// @icon        https://raw.github.com/marciska/Jitai/master/imgs/jitai.ico
// @match       https://www.wanikani.com/*
// @match       https://preview.wanikani.com/*
// @license     MIT; http://opensource.org/licenses/MIT
// @run-at      document-end
// @grant       none
// ==/UserScript==

(function(global) {
	'use strict';

    /* eslint no-multi-spaces: off */
    /* global wkof */

    //===================================================================
    // Variables
    //-------------------------------------------------------------------
    const script_id = "jitai";
    const script_name = "Jitai";
    const listenerOptions = { passive: true };
    const pageRegex = /^https:\/\/www\.wanikani\.com\/subjects\/(?:review.*\/?|extra_study\?queue_type=(?:recent_lessons|burned_items))$/;
    let item_element;
    let style_element;
    let setup_complete = false;

    new MutationObserver((mutations, observer) => {
        for (const mutation of mutations) {
            for (const addedNode of mutation.addedNodes) {
                if (addedNode.nodeName !== 'IFRAME' || !['rikaitan-popup','yomichan-popup'].includes(addedNode.className))
                    continue;
                window.onblur = () => { if (document.activeElement === addedNode || document.activeElement === addedNode.parentElement) { window.setTimeout(() => addedNode.blur(), 1); }};
                observer.disconnect();
                return;
            }
        }
    }).observe(document.firstElementChild, { childList: true, subtree: true });

    // ----- Fonts -----
    const example_sentence = '質問:私立探偵 (P.I.) はどんな靴を履いていますか?<br>答え:・・・スニーカー。(笑)';
    let font_default;
    let font_randomized;

    // available fonts
    let font_pool = {
        // Default OSX fonts
        "Hiragino-Kaku-Gothic-Pro" : {full_font_name: "Hiragino Kaku Gothic Pro, ヒラギノ角ゴ Pro W3", display_name: "Hiragino Kaku Gothic Pro", url: 'local', recommended: false},
        "Hiragino-Maru-Gothic-Pro" : {full_font_name: "Hiragino Maru Gothic Pro, ヒラギノ丸ゴ Pro W3", display_name: "Hiragino Maru Gothic Pro", url: 'local', recommended: false},
        "Hiragino-Mincho-Pro" : {full_font_name: "Hiragino Mincho Pro, ヒラギノ明朝 Pro W3", display_name: "Hiragino Mincho Pro", url: 'local', recommended: false},
        // Default Windows fonts
        "Meiryo" : {full_font_name: "Meiryo, メイリオ", display_name: "Meiryo", url: 'local', recommended: false},
        "MS-PGothic" : {full_font_name: "MS PGothic, MS Pゴシック, MS Gothic, MS ゴック", display_name: "MS Gothic", url: 'local', recommended: false},
        "MS-PMincho" : {full_font_name: "MS PMincho, MS P明朝, MS Mincho, MS 明朝", display_name: "MS Mincho", url: 'local', recommended: false},
        "Yu-Gothic" : {full_font_name: "Yu Gothic, YuGothic", display_name: "Yu Gothic", url: 'local', recommended: false},
        "Yu-Mincho" : {full_font_name: "Yu Mincho, YuMincho", display_name: "Yu Mincho", url: 'local', recommended: false},
        // GoogleFonts
        "Zen-Kurenaido" : {full_font_name: "Zen Kurenaido", display_name: "Zen Kurenaido", url: 'https://fonts.googleapis.com/css?family=Zen+Kurenaido&subset=japanese', recommended: false},
        "Kaisei-Opti" : {full_font_name: "Kaisei Opti", display_name: "Kaisei Opti", url: 'https://fonts.googleapis.com/css?family=Kaisei+Opti&subset=japanese', recommended: false},
        "Reggae-One" : {full_font_name: "Reggae One", display_name: "Reggae One", url: 'https://fonts.googleapis.com/css?family=Reggae+One&subset=japanese', recommended: false},
        "New-Tegomin" : {full_font_name: "New Tegomin", display_name: "New Tegomin", url: 'https://fonts.googleapis.com/css?family=New+Tegomin&subset=japanese', recommended: false},
        "Yuji-Boku" : {full_font_name: "Yuji Boku", display_name: "Yuji Boku", url: 'https://fonts.googleapis.com/css?family=Yuji+Boku&subset=japanese', recommended: false},
        "Yuji-Mai" : {full_font_name: "Yuji Mai", display_name: "Yuji Mai", url: 'https://fonts.googleapis.com/css?family=Yuji+Mai&subset=japanese', recommended: false},
        "Yuji-Syuku" : {full_font_name: "Yuji Syuku", display_name: "Yuji Syuku", url: 'https://fonts.googleapis.com/css?family=Yuji+Syuku&subset=japanese', recommended: false},
        "DotGothic16" : {full_font_name: "DotGothic16", display_name: "DotGothic16", url: 'https://fonts.googleapis.com/css?family=DotGothic16&subset=japanese', recommended: true},
        "Hachi-Maru-Pop" : {full_font_name: "Hachi Maru Pop", display_name: "Hachi Maru Pop", url: 'https://fonts.googleapis.com/css?family=Hachi+Maru+Pop&subset=japanese', recommended: true},
        "Yomogi" : {full_font_name: "Yomogi", display_name: "Yomogi", url: 'https://fonts.googleapis.com/css?family=Yomogi&subset=japanese', recommended: false},
        "Potta-One" : {full_font_name: "Potta One", display_name: "Potta One", url: 'https://fonts.googleapis.com/css?family=Potta+One&subset=japanese', recommended: false},
        "Dela-Gothic-One" : {full_font_name: "Dela Gothic One", display_name: "Dela Gothic One", url: 'https://fonts.googleapis.com/css?family=Dela+Gothic+One&subset=japanese', recommended: true},
        "RocknRoll-One" : {full_font_name: "RocknRoll One", display_name: "RocknRoll One", url: 'https://fonts.googleapis.com/css?family=RocknRoll+One&subset=japanese', recommended: false},
        "Stick" : {full_font_name: "Stick", display_name: "Stick", url: 'https://fonts.googleapis.com/css?family=Stick&subset=japanese', recommended: true},
        "Yusei-Magic" : {full_font_name: "Yusei Magic", display_name: "Yusei Magic", url: 'https://fonts.googleapis.com/css?family=Yusei+Magic&subset=japanese', recommended: false},
        "Kaisei-Decol" : {full_font_name: "Kaisei Decol", display_name: "Kaisei Decol", url: 'https://fonts.googleapis.com/css?family=Kaisei+Decol&subset=japanese', recommended: false},
        "Kaisei-Tokumin" : {full_font_name: "Kaisei Tokumin", display_name: "Kaisei Tokumin", url: 'https://fonts.googleapis.com/css?family=Kaisei+Tokumin&subset=japanese', recommended: false},
        // Other popular fonts
        "ArmedBanana" : {full_font_name: "ArmedBanana", display_name: "Armed Banana", url: 'https://marciska.github.io/Jitai/ArmedBanana.css', recommended: true},
        "ArmedLemon" : {full_font_name: "ArmedLemon", display_name: "Armed Lemon", url: 'local', recommended: false},
        "AoyagiReisyosimo-AoyagiKouzan" : {full_font_name: "aoyagireisyosimo2, AoyagiKouzanFont2OTF", display_name: "Aoyagi Kouzan", url: 'local', recommended: false},
        "Aquafont" : {full_font_name: "aquafont", display_name: "Aquafont", url: 'local', recommended: false},
        "Shin-Maru-Go-Pro" : {full_font_name: "A-OTF Shin Maru Go Pro", display_name: "Shin Maru Go Pro", url: 'local', recommended: false},
        "Chifont" : {full_font_name: "'chifont+', chifont", display_name: "Chifont", url: 'local', recommended: false},
        "Cinecaption" : {full_font_name: "cinecaption", display_name: "Cinecaption", url: 'local', recommended: false},
        "Darts" : {full_font_name: "darts font", display_name: "Darts", url: 'https://marciska.github.io/Jitai/Darts.css', recommended: false},
        "EPSON-行書体M" : {full_font_name: "EPSON 行書体M", display_name: "EPSON 行書体M", url: 'local', recommended: false},
        "EPSON-正楷書体M" : {full_font_name: "EPSON 正楷書体M", display_name: "EPSON 正楷書体M", url: 'local', recommended: false},
        "EPSON-教科書体M" : {full_font_name: "EPSON 教科書体M", display_name: "EPSON 教科書体M", url: 'local', recommended: false},
        "EPSON-太明朝体B" : {full_font_name: "EPSON 太明朝体B", display_name: "EPSON 太明朝体B", url: 'local', recommended: false},
        "EPSON-太行書体B" : {full_font_name: "EPSON 太行書体B", display_name: "EPSON 太行書体B", url: 'local', recommended: false},
        "EPSON-丸ゴシック体M" : {full_font_name: "EPSON 丸ゴシック体M", display_name: "EPSON 丸ゴシック体M", url: 'local', recommended: false},
        "FC-Flower" : {full_font_name: "FC-Flower", display_name: "FC-Flower", url: 'https://marciska.github.io/Jitai/FCFlower.css', recommended: false},
        "HakusyuKaisyoExtraBold_kk" : {full_font_name: "HakusyuKaisyoExtraBold_kk", display_name: "Hakusyu Kaisyo (Extra Bold)", url: 'local', recommended: false},
        "Hosofuwafont" : {full_font_name: "Hosofuwafont", display_name: "Hoso Fuwa", url: 'https://marciska.github.io/Jitai/HosoFuwa.css', recommended: false},
        "Nagayama-Kai" : {full_font_name: "nagayama_kai", display_name: "Nagayama Kai", url: 'https://marciska.github.io/Jitai/NagayamaKai.css', recommended: false},
        "Pop-Rum-Cute" : {full_font_name: "PopRumCute", display_name: "Pop Rum Cute", url: 'https://marciska.github.io/Jitai/PopRumCute.css', recommended: false},
        "San-Chou-Me" : {full_font_name: "santyoume-font", display_name: "San Chou Me", url: 'https://marciska.github.io/Jitai/SanChouMe.css', recommended: false},
    };

    // fonts that are selected by user to be shown
    let font_pool_selected = [];

    // bool indicating if hovering effect is flipped
    let hover_flipped = false;

    // bool indicating if a modifier key is being held down
    let modifier_held = false;

    //===================================================================
    // Settings related stuff
    //-------------------------------------------------------------------
    function installSettingsMenu() {
        wkof.Menu.insert_script_link({
            name:      script_id,
            submenu:   'Settings',
            title:     script_name,
            on_click:  settingsOpen
        });
    }

    function settingsPrepare(dialog) {
        dialog.dialog({width:720});
    }
    async function settingsSave(settings) {
        await wkof.Settings.save(script_id);
        settingsApply(settings);
        settingsClose(settings);
    }
    async function settingsLoad() {
        const settings = await wkof.Settings.load(script_id);
        settingsApply(settings);
    }
    function settingsClose(settings) {
        // Remove all urls to fonts we don't use
        for (const [fontkey, value] of Object.entries(font_pool)) {
            if (!(fontkey in settings)) { continue; }
            if (!settings[fontkey]) { // check if font is disabled
                // if it is a webfont, uninstall webfont
                if (value.url !== 'local') {
                    uninstallWebfont(value.full_font_name, value.url);
                }
            }
        }
    }
    function settingsApply(settings) {
        // clear cache of selected fonts
        font_pool_selected = [];
        // now refill the pool of selected fonts
        for (const [fontkey, value] of Object.entries(font_pool)) {
            if (!(fontkey in settings)) { continue; }
            if (settings[fontkey]) { // check if font is enabled
                // if it is a webfont, install webfont
                if (value.url !== 'local') {
                    installWebfont(value.full_font_name, value.url);
                    // recheck if font is installed on machine
                    // if (!isFontInstalled(value.full_font_name)) { continue; }
                } else { // check if local font is installed on machine
                    if (!isFontInstalled(value.full_font_name)) { continue; }
                }
                // put fonts in selected fonts
                let frequency = settings[fontkey+'_frequency'];
                if (frequency === undefined) { frequency = 1; } // if script started first time, the value might be undefined
                frequency = Math.ceil(frequency);
                for (let i = 0; i < frequency; i++) {
                    font_pool_selected.push(value.full_font_name);
                }
            }
        }

        // randomly shuffle font pool
        shuffleArray(font_pool_selected);

        if (setup_complete && pageRegex.test(document.URL)) {
            updateRandomFont();
            setflippedFontState();
        }
    }

    function settingsOpen() {
        // install webfonts, and remove non-accesible local fonts for selection
        for (const [fontkey, value] of Object.entries(font_pool)) {
            if (value.url !== 'local') { // install webfonts
                installWebfont(value.full_font_name, value.url);
            } else if (!isFontInstalled(value.full_font_name)) { // remove local fonts that are not installed for selection
                delete font_pool[fontkey];
            }
        }

        // order fonts alphabetically
        const fontkeys = Object.keys(font_pool).sort((a, b) =>
            font_pool[a].display_name.localeCompare(font_pool[b].display_name, undefined, {sensitivity: 'base'})
        );

        // prepare selection option for every font
        const font_selector = Object.fromEntries(fontkeys.map(fontkey => ['BOX_'+fontkey, {
            type: 'group',
            label: `<span class="font_label${font_pool[fontkey].recommended ? ' font_recommended' : ''}">${font_pool[fontkey].display_name}</span>`,
            content: {
                sampletext: {
                    type: 'html',
                    html: `<p class="font_example" style="font-family:'${font_pool[fontkey].full_font_name}'">${example_sentence}</p>`
                },
                [fontkey]: {
                    type: 'checkbox',
                    label: 'Use font in '+script_name,
                    default: false,
                },
                [fontkey+'_frequency']: {
                    type: 'number',
                    label: 'Frequency',
                    hover_tip: 'The higher the value, the more often you see this font during review. It is affected by how many fonts you have enabled.',
                    default: 1,
                    min: 1,
                    step: 1,
                }
            }
        }]));

        // prepare configuration dialog
        let dialog = new wkof.Settings({
            script_id:  script_id,
            title:      script_name+' Settings',
            pre_open:   settingsPrepare,
            on_save:    settingsSave,
            on_close:   settingsClose,
            content: {
                currentfont: {
                    type: 'group',
                    label: `<span class="font_label">Current Font: ${font_randomized}</span>`,
                    content: {
                        sampletext: {
                            type: 'html',
                            html: `<p class="font_example" style="font-family:'${font_randomized}'">${example_sentence}</p>`
                        }
                    }
                },
                legend: {
                    type: 'html',
                    html: `<div class="font_legend"><span class="font_recommended">: Recommended Font</span></div>`
                },
                divider: {
                    type: 'section',
                    label: `Filter Fonts (${fontkeys.length} available)`
                },
                ...font_selector
            }
        });
        dialog.open();
    }

    //===================================================================
    // Main Script Functionality
    //-------------------------------------------------------------------

    function getDefaultFont(item) {
        return getComputedStyle(item).fontFamily;
    }

    function isFontInstalled(font_name) {
        // Approach from kirupa.com/html5/detect_whether_font_is_installed.htm - thanks!
        // Will return false for the browser's default monospace font, sadly.
        const canvas = document.createElement('canvas');
        const context = canvas.getContext('2d');
        const text = "wim-—l~ツ亻".repeat(100); // Characters with widths that often vary between fonts.

        context.font = "72px monospace";
        const defaultWidth = context.measureText(text).width;

        // Microsoft Edge raises an error when a context's font is set to a string
        // containing certain special characters... so that needs to be handled.
        try {
            context.font = "72px " + font_name + ", monospace";
        } catch (e) {
            return false;
        }
        const testWidth = context.measureText(text).width;

        return testWidth !== defaultWidth;
    }

    function isCanvasBlank(canvas) {
        return !canvas.getContext('2d', { willReadFrequently: true })
            .getImageData(0, 0, canvas.width, canvas.height).data
            .some(channel => channel !== 0);
    }
    function canRepresentGlyphs(fontName, glyphs) {
        const canvas = document.createElement('canvas');
        canvas.width = 50;
        canvas.height = 50;
        const context = canvas.getContext("2d", { willReadFrequently: true });
        context.textBaseline = 'top';

        context.font = "24px " + fontName;

        let result = true;
        for (let i = 0; i < glyphs.length; i++) {
            context.fillText(glyphs[i], 0, 0);
            if (isCanvasBlank(canvas)) {
                result = false;
                break;
            }
            context.clearRect(0, 0, canvas.width, canvas.height);
        }

        return result;
    }

    function installWebfont(font_name, url) {
        // If webfont already installed on local machine, don't need to reinstall
        if (isFontInstalled(font_name)) { return; }

        // install webfont
        const link = document.querySelector(`link[href="${url}"]`);
        if (!link) {
            const newlink = document.createElement("link");
            newlink.href = url;
            newlink.rel = "stylesheet";
            document.head.append(newlink);
        }
    }
    function uninstallWebfont(font_name, url) {
        const link = document.querySelector(`link[href="${url}"]`);
        if (!link) {
            link.remove();
        }
    }
    function addPreconnectLinks() {
        // add preconnect links to GoogleFonts servers
        let googleApiLink = document.querySelector(`link[href="https://fonts.googleapis.com"]`);
        if (!googleApiLink) {
            googleApiLink = document.createElement("link");
            googleApiLink.rel = "preconnect";
            googleApiLink.href = "https://fonts.googleapis.com";
            document.head.append(googleApiLink);
        }
        let gstaticLink = document.querySelector(`link[href="https://fonts.gstatic.com"]`);
        if (!gstaticLink) {
            gstaticLink = document.createElement("link");
            gstaticLink.rel = "preconnect";
            gstaticLink.href = "https://fonts.gstatic.com";
            gstaticLink.crossOrigin = true;
            document.head.append(gstaticLink);
        }
    }

    function shuffleArray(array) {
        for (let i = array.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [array[i], array[j]] = [array[j], array[i]];
        }
    }

    function updateRandomFont() {
        // choose new random font
        const glyphs = item_element.innerText;
        if (font_pool_selected.length === 0) {
            console.log(script_name+': empty font pool!');
            font_randomized = font_default;
        } else {
            do {
                font_randomized = font_pool_selected[Math.floor(Math.random() * font_pool_selected.length)];
            } while (!canRepresentGlyphs(font_randomized, glyphs));
        }
        style_element.innerHTML = style_element.innerHTML.replace(/(--font-family-japanese:).*;([\s\S]*?--font-family-japanese-hover:).*;/,`$1 ${font_randomized};$2 ${font_default};`);
    }

    function setflippedFontState() {
        const flipped = hover_flipped ? !modifier_held : modifier_held;
        item_element.classList.toggle('flipped', flipped);
    }

    function insertStyle() {
        // insert CSS
        if ((style_element = document.getElementById(`${script_id}-style`)) != null) return;
        style_element = document.createElement('style');
        style_element.setAttribute('id', `${script_id}-style`);
        style_element.innerHTML = `
.font_label {
    font-size: 1.2em;
    display: flex;
    align-items: center;
}
.font_legend {
    text-align: center;
    margin: 15px !important;
}
.font_example {
    margin: 5px 10px 10px 10px !important;
    font-size: 1.6em;
    line-height: 1.1em;
}
.font_recommended::before {
    content: '⭐️';
    font-size: 1.4em;
}
.character-header__characters {
    --font-family-japanese: ;
    --font-family-japanese-hover: ;
    font-family: var(--font-family-japanese);
}
.character-header__characters:hover { font-family: var(--font-family-japanese-hover); }
.character-header__characters.flipped { font-family: var(--font-family-japanese-hover); }
.character-header__characters.flipped:hover { font-family: var(--font-family-japanese); }
`;
        document.head.appendChild(style_element);
    }

    function cacheDefaultElementStyles() {
        if (font_default !== undefined && font_randomized !== undefined) return;
        item_element = document.getElementsByClassName("character-header__characters")[0];
        if (!item_element) return;
        font_default = getDefaultFont(item_element);
        font_randomized = font_default;
    }

    function onKeyDown(event) {
        if (event.repeat) return;
        switch (event.key) {
            // on holding ctrl and shift, swap the shown font
            case 'Control':
            case 'Shift':
                if (!event.ctrlKey || !event.shiftKey) return;
                modifier_held = true;
                setflippedFontState();
                break;
            // on alt+j, update to a new random font
            case 'j':
                if (!event.altKey) return;
                updateRandomFont();
                setflippedFontState();
                break;
        }
    }
    function onKeyUp(event) {
        if (event.repeat) return;
        switch (event.key) {
            case 'Control':
            case 'Shift':
                modifier_held = false;
                setflippedFontState();
                break;
        }
    }
    function onLostFocus() {
        modifier_held = false;
        setflippedFontState();
    }
    function onDidAnswerQuestion() {
        hover_flipped = true;
        setflippedFontState();
    }
    function onDidUnanswerQuestion() {
        hover_flipped = false;
        updateRandomFont();
        setflippedFontState();
    }
    function onWillShowNextQuestion() {
        hover_flipped = false;
        if (setup_complete) {
            updateRandomFont();
            setflippedFontState();
        }
    }

    function registerJitaiEvents() {
        // on answer submission, invert hovering event
        //  - normal  : default font
        //  - hovering: randomized font
        global.addEventListener("didAnswerQuestion", onDidAnswerQuestion, listenerOptions);

        // on advancing to next item question, randomize font again
        global.addEventListener("willShowNextQuestion", onWillShowNextQuestion, listenerOptions);

        // on reverting an answer by DoubleCheckScript, reroll random font and fix inverting of hovering
        global.addEventListener("didUnanswerQuestion", onDidUnanswerQuestion, listenerOptions);

        // add event to show regular font
        // add event to reroll randomized font
        global.addEventListener("keydown", onKeyDown, listenerOptions);
        global.addEventListener("keyup", onKeyUp, listenerOptions);
        // when page loses focus, revert any temporary modifications
        global.addEventListener("blur", onLostFocus, listenerOptions);
    }

    function unregisterJitaiEvents() {
        global.removeEventListener("didAnswerQuestion", onDidAnswerQuestion, listenerOptions);
        global.removeEventListener("willShowNextQuestion", onWillShowNextQuestion, listenerOptions);
        global.removeEventListener("didUnanswerQuestion", onDidUnanswerQuestion, listenerOptions);
        global.removeEventListener("keydown", onKeyDown, listenerOptions);
        global.removeEventListener("keyup", onKeyUp, listenerOptions);
        global.removeEventListener("blur", onLostFocus, listenerOptions);
    }

    //===================================================================
    // Script Startup
    //-------------------------------------------------------------------
    function startup() {
        // initialization of the Wanikani Open Framework
        if (!wkof) {
            if (confirm(script_name+' requires Wanikani Open Framework.\nDo you want to be forwarded to the installation instructions?')) {
                global.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
            }
            return;
        }
        const wkof_modules = 'Settings';
        wkof.include(wkof_modules);
        wkof.ready(wkof_modules)
            .then(addPreconnectLinks)
            .then(settingsLoad)
            .then(() => {setup_complete = true;});
    }

    function onReviewsPage() {
        const wkof_modules = 'Menu';
        wkof.include(wkof_modules);
        wkof.ready(wkof_modules)
            .then(cacheDefaultElementStyles)
            .then(insertStyle)
            .then(registerJitaiEvents)
            .then(installSettingsMenu);
    }

    function onNonReviewsPage() {
        unregisterJitaiEvents();
    }

    function onLoad(event) {
        if (pageRegex.test(event.detail.url))
            onReviewsPage();
        else
            onNonReviewsPage();
    }

    startup();

    const loadedUrl = (window.Turbo?.session.history.pageLoaded ? window.Turbo.session.history.location : (document.readyState === "complete" ? document.URL : null));
    if (loadedUrl !== null) {
        setup_complete = true;
        onLoad({detail:{url:loadedUrl},target:document.documentElement});
    }
    document.documentElement.addEventListener('turbo:load', (event) => {setTimeout(onLoad(event), 0);});

})(window);

In particular, the unregisterJitaiEvents() function is kinda what I’m talking about.

2 Likes

Okay this is what I did. I reviewed today, then immediately afterwards, I lesson picked ~2 vocabs.
This time I used Alt+J on my windows laptop (same setup as in my firefox in mac) and tried it for 3 times.

1st vocab:



2nd vocab: it’s set in the current font of the first vocab

After 1 reroll:

Refreshed the browser. It’s back to normal:

1 Like

Alright it’s a new day.

I looked at the Open Framework source again and there’s no inherent way to have on_pageload trigger for every page navigation. However, passing an empty regex should easily accomplish the same thing, so new RegExp() would seems good enough if we wanted to go that route.

However, there’s a third option I only just now thought of, which is to have the listener remove itself instead of simply returning if the pageRegex test fails. I still don’t think this is as ideal as removing the listener on a page navigation, since it means that it’ll still do a regex test for every key press and release on a reviews page, but at least it would only trigger once on other pages.

Anyway, going back to option 2a or 2b from my previous post. We might as well just use our own turbo:load listener for the removal, since that lets us negate the result of the regex test as well as ignore setTimeout, since we don’t need to wait for any DOM elements to load, besides the document.body which is already there.

Thus, I think something like this would suffice.

    function unregisterJitaiEvents() {
        window.removeEventListener("didAnswerQuestion", onDidAnswerQuestion, listenerOptions);
        window.removeEventListener("willShowNextQuestion", onWillShowNextQuestion, listenerOptions);
        window.removeEventListener("didUnanswerQuestion", onDidUnanswerQuestion, listenerOptions);
        document.body.removeEventListener("keydown", onKeyDown, listenerOptions);
        document.body.removeEventListener("keyup", onKeyUp, listenerOptions);
        document.body.removeEventListener("blur", onLostFocus, listenerOptions);
    }
    startup().then(() => {
        document.documentElement.addEventListener('turbo:load', (event) => {
            if (!pageRegex.test((new URL(event.detail.url)).pathname)) unregisterJitaiEvents();
        });
        wkof.on_pageload(pageRegex, onReviewsPage);
    });

Another positive side effect of this is that the pageRegex test can then be removed from all three of the listeners that it was previously added to.

P.s., I’m thinking of requesting that this be a feature of WKOF, since having an easier way for scripts to react when the user leaves the intended page is a benefit to everyone.


Tangentially. I typically like to keep my console clear of extraneous logging (I technically have 146 userscripts installed in Tampermonkey, and even if only just over half of them are enabled, that’s still like 80 scripts that could be logging to the console). So, I would like to request a configurable setting to toggle on/off debug logging (it could even be on by default; I don’t mind that).

1 Like

Thanks for the very detailed option and other alternatives! I’ll have a look at it sometime later this week, got a bit busy with other stuff.

@maddness Sorry for the bug with the Lessons having the random font applied when you came from the review quiz before – I’ll definitely fix it this week as by the proposals @Inserio gave, please give me some time :saluting_face: The bug only appears when you did a review before, so if that’s the case (for now) please manually refresh the page after a review session before continuing to lessons.

Ahh I am sorry for the technical talk in my previous post :laughing:

In more simpler words: The page will not automatically reload anymore on WaniKani, even though it looks for us (the user) like that. In fact, WaniKani has become a single page only, and only the written text on it changes as if you are watching an interactive film. That is what “Turbo” is – just a handy tool for web developers to show content on your computer’s browser screen in a faster and more compact manner by only updating content that changed, and leaving content that is reused untouched. (I’m also new to Turbo, but that’s what I understood from my limited Google Search; sorry if the exact technical details are wrong :slight_smile: )
Most userscripts at WaniKani (like Jitai) are programmed in a way to activate themselves during a page load on a specific page of WaniKani, but with Turbo this fresh page load doesn’t happen anymore. So the scripts have to become a lot more smarter in detecting that a page has changed. For userscript developers that may take a while until every script is fully updated and tested.

Fair, the logging I had added last version for debugging went definitely over the limit… sorry for that! I have quickly changed the logging level to debug (how it should be), to at least hide the blasting of logs from your eye as debug won’t show without manual selection. There’s also the possibility to filter out logs with the regex expression -Jitai, since that is how I will always start the logs with. At some point I’ll remove those excessive logs, after I got enough trust into how the new Turbo system on WK works :slight_smile: If the debug level is still too much, or the wait until I remove it is too long, then I’ll find the time to hack it together this week.

The change is in the latest version on Github, which also includes another person’s very helpful PR that added more fonts and fixed some wrong font download links.

1 Like

Hey don’t worry about this and take your time. I didn’t even notice this because I was too engrossed in the lessons until about 3 vocabs in, and I was like… why is the font different? :joy_cat: I’m here to provide feedback for any bugs found :slightly_smiling_face:

Ahh thanks for the detailed explanation! I am quite new with the userscripts and upon installing them for the first time, a few weeks back, I didn’t understand why some worked, and some didn’t. So I had to go through a pile of replies to see if there were updates to the script like this.

I really appreciate you and @Inserio for giving some of your precious time to maintain this script! :bowing_woman:t4:

1 Like

@marciska would it be a lot of work to add a button somewere on the interface to reroll the font on mobile browsers?
Using keyboard shortcuts on mobile can be quite inconvenient.
I’d rather not attempt it myself with my limited js knwoledge to avoid breaking things.

What about tapping (or double-clicking) on the word itself?
Might that be preferable? Adding more UI elements is a tricky game to play.

Tapping now functions as hovering on desktop, at least on firefox on android, which is definitely useful. I don’t know how that could work with some kind of double tapping.
If a double click on the word could trigger the reroll, maybe a single tap could still show the default font.

1 Like

:rocket: Released V3.3.4

  • Bugfix: random font not anymore shown on lessons page (hopefully that solves it @maddness)
  • Bugfix: Having no fonts selected triggered Jitai alert, which stopped the settings cog from appearing
  • New feature: re-roll font button (request by @Seppiola)
    • Note: After enabling the button in the settings, please refresh the page once for it to appear
  • New Fonts (contributed by @Seppiola on GitHub, thanks for that!):
    • Train One
    • Kiwi Maru
    • Mochiy Pop One
    • …and more!

Thanks for your quick code guide as always @Inserio ! It was missing to reset the font after finishing a review, so the lesson session was still showing the random font. Thanks to your active request on the WKOF developer thread a new WKOF version was by now available that had the ability to fire an unload event, so I went with that instead :smiling_face:

1 Like

Working flawlessly on ff mobile, thanks!

Maybe I can show the local fonts with some screenshots later, since it can be pretty annoying to preview a font before downloading it.

2 Likes

I would greatly appreciate that!

1 Like

I just did my reviews and did some lessons immediately. I didn’t even realize until the last lesson, haha. It fixed it! Thanks so much~ :sparkles:

1 Like

Great! Yep, this release feels like it is working quite stable now. Also passed the Dashboard → Review → Lesson → Review test. So I believe no need to refresh the page for Jitai anymore (ignoring the new reroll font button here).
But ready for anybody to prove me otherwise and destroy my feeling :joy:

1 Like

This might happen only if WK destroys revamps the dashboard again :laughing:

1 Like