[Userscript]: Anime Context Sentences

same problem. 0 selected and unable to open the selection boxes to add selection. kiwi browser on Android.

As of today, the script does not seem to be working anymore in new lessons

1 Like

If you install the newest version of Item Info Injector, the Anime Context Sentences script will use this version instead of its own outdated one.

4 Likes

Thank you, that fixed it!

2 Likes

I had the same problem but your suggestion fixed it-- thank you so much! :pray:

1 Like

The script seems to overflowing into the next section, does anyone else have the same problem?

image

It seems the settings button has disappeared, for me on both Chrome on PC and Safari. It is still there for other scripts, just not this one

1 Like

I have the same problem

1 Like

Found out a workaround, we can change the settings by modifying the script itself under “let state”, the various options to change here are listed later under “funcion openSettings”
Here’s mine where I’ve changed the playback speed to natural, and added in all the shows:

let state = {
        settings: {
            playbackRate: 1,
            showEnglish: 'onhover',
            showJapanese: 'always',
            showFurigana: 'onhover',
            sentenceLengthSort: 'asc',
            filterWaniKaniLevel: true,
            filterAnimeShows: {0: true, 1: true, 2: true, 3: true, 4: true, 5: true, 6: true, 8: true, 9: true, 10: true, 11: true, 12: true, 13: true, 14: true, 15: true, 16: true, 17: true, 18: true,
                               19: true, 20: true, 21: true, 22: true, 23: true, 24: true, 25: true, 26: true, 28: true, 29: true, 30: true, 31: true, 32: true, 33: true, 34: true, 35: true, 36: true, 37: true, 38: true, 39: true, 40: true, 41: true, 42: true, 43: true,},
            filterAnimeMovies: {0: true, 1: true, 2: true, 3: true, 4: true, 5: true, 6: true},
            // Some Ghibli films are enabled by default
            filterGhibli: {0: true, 1: true, 2: true, 3: true, 4: true, 5: true, 6: true, 7: true, 8: true, 9: true, 10: true, 11: true, 12: true},
        },
1 Like

I took 5min to look it up. The problem is that the icon (as in, the actual icon from the font library) isn’t there anymore. So it now inserts an empty html element in the page. My guess is that it was using a google font icon but the google font library was removed from Wanikani when they made the font changes not long ago.

In any case, you can add this line after line 171. It will add a string “Settings” in the content of the icon element. It’s a simple temporary solution.

settingsBtn.textContent = “Settings”;

image

3 Likes

thanks, works for me

1 Like

I’ll raise your improvement with a unicode emoji
settingsBtn.textContent = '⚙️';

A few more additions:
The line #170 with settingsBtn.setAttribute("class", "fa fa-gear"); is no longer necessary and therefore can be removed.
Changing line #169 with const settingsBtn = document.createElement("i"); to some other element type makes the text (and therefore, now the :gear: emoji) no longer italicized. FYI, I used the following:
const settingsBtn = document.createElement("svg");

3 Likes

Anyone else getting this error?

Help with setting up? I am new to using userscripts and it doesn’t seem to be working. I have went through all the instruction and troubleshooting in the post but nothing has changed/ I am not getting the context sentences. I have the user script for the open framework as well as this one installed on tampermonkey. The Tampermonkey extension is set to enabled, I was using edge have also tried again on chrome. The first time after setting the script up on edge I got a popup asking to generate a personal token. Not sure if this is an indicator of what the issue is. Thanks in advance for any help on this

Ok, I was able to get working now with some more troubleshooting and digging in this thread. Realized it was working on vocab page but not lessons/reviews and downloaded the latest version of [‘Item Info Injector’]
This was able to solve my issue. thnks @ [Sinyaven]

1 Like

Edit: Check out my fork instead.

Original post remains below.


I made quite a few changes to my local script because this script appears to create an issue on chromium-based browsers by not properly disposing of the audio elements.
By the way, the issue I’m referencing is that the regular audio for the items begins to fail to play for the remainder of the session after enough of these elements have been created.
(Edit: I’ve come to the conclusion that, while this was a major culprit of the issue for me, there is still another contributor out there—which could even be something in the main site itself for all I know; I ended up encountering a form of the issue in a recent long session once I was nearing 300 reviews completion)

Besides fixing that issue, the changes I made include:

  • Stop the currently playing audio when clicking again on the selection
    • When selecting a different entry, will also stop the previous audio beforehand
  • Re-implemented a speaker icon (:speaker:) [I think the forum renders these differently…]
    • Moved the icon to after the title of the anime so as to not take up as much space
    • Made the icon change to :loud_sound: for the active playing selection and reduced the other options’ opacity
  • Clicking on elements that are set to toggle their hidden state “On Click” no longer triggers the audio to play

That’s all I can remember that I changed at the moment, but it took a while to figure out the simplest way of accomplishing it with relative efficiency.

To make it easier for people who want to just use my solution as-is, I’ll copy entire functions (even though some of it remains the same).
Essentially, you’ll want to replace the whole renderSentences function with my code, as well the createStyles function for the CSS. I’ll separate them into two code blocks below. I’ll also include the changes to the addAnimeSentences function I mentioned earlier in this topic.

Let me know if I forgot anything or if it doesn’t work for you.

Here’re my changes. Just expand the sections below to view them.

The new `renderSentences` function, and a `removeAudioElement` function (used as a helper in the former)

Replace the entire existing renderSentences function.

    function renderSentences() {
        // Called from immersionkit response, and on settings save
        let examples = state.immersionKitData;
        const exampleLenBeforeFilter = examples.length;

        // Exclude non-selected titles
        let desiredTitles = getDesiredShows();
        examples = examples.filter(ex => desiredTitles.includes(ex.deck_name));
        if (state.settings.sentenceLengthSort === 'asc') {
            examples.sort((a, b) => a.sentence.length - b.sentence.length);
        } else {
            examples.sort((a, b) => b.sentence.length - a.sentence.length);
        }

        let showJapanese = state.settings.showJapanese;
        let showEnglish = state.settings.showEnglish;
        let showFurigana = state.settings.showFurigana;
        let playbackRate = state.settings.playbackRate;

        let html = '';
        let exampleLimit = Math.min(examples.length, 50);

        if (exampleLenBeforeFilter === 0) {
            html = 'No sentences found.';
        } else if (examples.length === 0 && exampleLenBeforeFilter > 0) {
            // TODO show which titles have how many examples
            html = 'No sentences found for your selected movies & shows.';
        } else {
            for (let i = 0; i < exampleLimit; i++) {
                const example = examples[i];

                let japaneseText = state.settings.showFurigana === 'never' ?
                    example.sentence :
                    new Furigana(example.sentence_with_furigana).ReadingHtml;

                html += `
    <div class="anime-example">
        <img src="${example.image_url}" alt="">
        <div class="anime-example-text">
            <div class="title" title="${example.id}">${example.deck_name}
            <span><button class="audio-btn audio-idle">🔈</button></span>
            </div>
            <div class="ja">
                <span class="${showJapanese === 'onhover' ? 'show-on-hover' : ''} ${showFurigana === 'onhover' ? 'show-ruby-on-hover' : ''}  ${showJapanese === 'onclick' ? 'show-on-click' : ''}">${japaneseText}</span>
            </div>
            <div class="en">
                <span class="${showEnglish === 'onhover' ? 'show-on-hover' : ''} ${showEnglish === 'onclick' ? 'show-on-click' : ''}">${example.translation}</span>
            </div>
        </div>
    </div>`
            }
        }

        let sentencesEl = state.sentencesEl;
        sentencesEl.innerHTML = html;

        const audioIdleClass = "audio-idle";
        const audioPlayClass = "audio-play";

        const animeSentencesContainer = document.querySelector("#anime-sentences-parent");
        const audioButtons = document.querySelectorAll(".anime-example .audio-btn");
        for (let i = 0; i < audioButtons.length; i++) {
            let audioContainer;
            const button = audioButtons[i];
            const onPlay = () => {button.classList.replace(audioIdleClass,audioPlayClass);button.textContent='🔊';};
            const onStop = () => {button.classList.replace(audioPlayClass,audioIdleClass);button.textContent='🔈';removeAudioElement(audioContainer);};
            button.onclick = function () {
                if ((audioContainer = document.querySelector("#anime-sentences-parent audio")) !== null) {
                    let prevSource = audioContainer.src;
                    audioContainer.pause();
                    if (prevSource === examples[i].sound_url) {
                        return;
                    }
                }
                audioContainer = document.createElement("audio");
                audioContainer.src = examples[i].sound_url;
                audioContainer.playbackRate = playbackRate;
                audioContainer.onplay = onPlay;
                audioContainer.onpause = onStop;
                audioContainer.onended = onStop;
                audioContainer.onabort = onStop;
                animeSentencesContainer.append(audioContainer);
			    audioContainer.play();
            };
        }

        // Click anywhere plays the audio
        let exampleEls = document.querySelectorAll("#anime-sentences-parent .anime-example");
        exampleEls.forEach((a) => {
            a.onclick = function () {
                let button = this.querySelector('.audio-btn');
                button.click();
            };
        });

        // Assigning onclick function to .show-on-click elements
        document.querySelectorAll(".show-on-click").forEach((a) => {
            a.onclick = function (e) {
                e.stopPropagation(); // prevent this click from triggering the audio to play
                a.classList.toggle('show-on-click');
            };
        });
    }

    function removeAudioElement(element) {
        if (element === undefined || element === null) {
            return;
        }
        element.src = "";
        element.remove();
    }
The new `createStyle` function, for a few minor CSS changes (including some QoL changes)

Replace the entire existing createStyle function.

    function createStyle() {
        const style = document.createElement("style");
        style.setAttribute("id", "anime-sentences-style");
        // language=CSS
        style.innerHTML = `
            #anime-sentences-parent > div {
                overflow-y: auto;
                max-height: 280px;
            }

            #anime-sentences-parent .fa-solid {
                border: none;
                font-size: 100%;
            }

            .anime-example {
                display: flex;
                align-items: center;
                margin-bottom: 1em;
                cursor: pointer;
            }

            .anime-example .audio-btn {
                background-color: transparent;
            }

            .anime-example .audio-btn.audio-idle {
                opacity: 50%;
            }

            /* Make text and background color the same to hide text */
            .anime-example-text .show-on-hover, .anime-example-text .show-on-click {
                background: #ccc;
                color: transparent;
                text-shadow: none;
            }

            .anime-example-text .show-on-hover:hover {
                background: inherit;
                color: inherit
            }

            /* Furigana hover*/
            .anime-example-text .show-ruby-on-hover ruby rt {
                visibility: hidden;
            }

            .anime-example-text:hover .show-ruby-on-hover ruby rt {
                visibility: visible;
            }

            .anime-example .title {
                font-weight: 700;
            }

            .anime-example .ja {
                font-size: 2em;
            }

            .anime-example img {
                margin-right: 1em;
                max-width: 200px;
            }
        `;

        document.querySelector("head").append(style);
    }
The updated `addAnimeSentences` function, for those who may've missed the changes mentioned earlier in this topic

Replace the entire existing addAnimeSentences function.

    function addAnimeSentences() {
        const parentEl = document.createElement("div");
        parentEl.setAttribute("id", 'anime-sentences-parent');

        let header = ['Anime Sentences'];

        const settingsBtn = document.createElement("svg");
        settingsBtn.setAttribute("style", "font-size: 14px; cursor: pointer; vertical-align: middle; margin-left: 10px;");
        settingsBtn.textContent = '⚙️';
        settingsBtn.onclick = openSettings;
        let sentencesEl = document.createElement("div");
        sentencesEl.innerText = 'Loading...';

        header.push(settingsBtn);
        parentEl.append(sentencesEl);
        state.sentencesEl = sentencesEl;

        if (state.item.injector) {
            if (state.item.on === 'lesson') {
                state.item.injector.appendAtTop(header, parentEl);
            } else { // itemPage, review
                state.item.injector.append(header, parentEl);
            }
        }

        const queryString = state.item.characters.replace('〜', '');  // for "counter" kanji
        const wkLevelFilter = state.settings.filterWaniKaniLevel ? state.userLevel : '';
        let url = `https://api.immersionkit.com/look_up_dictionary?keyword=${queryString}&tags=&jlpt=&wk=${wkLevelFilter}&sort=shortness&category=anime`;
        fetch(url)
            .then(response => response.json())
            .then(data => {
                state.immersionKitData = data.data[0].examples;
                renderSentences();
            });
    }
Updated the version of the referenced Item Info Injector library (Thanks for the reminder @Revenant)

Added to the top of the script

// @require      https://greasyfork.org/scripts/430565-wanikani-item-info-injector/code/WaniKani%20Item%20Info%20Injector.user.js?version=1343973

Here is the entire updated script contents for the above changes if you'd rather just overwrite everything
// ==UserScript==
// @name         Wanikani Anime Sentences
// @description  Adds example sentences from anime movies and shows for vocabulary from immersionkit.com
// @version      1.1.3.1
// @author       psdcon
// @namespace    wkanimesentences

// @match        https://www.wanikani.com/*
// @match        https://preview.wanikani.com/*

// @require      https://greasyfork.org/scripts/430565-wanikani-item-info-injector/code/WaniKani%20Item%20Info%20Injector.user.js?version=1343973
// @copyright    2021+, Paul Connolly
// @license      MIT; http://opensource.org/licenses/MIT
// @run-at       document-end
// @grant        none
// @downloadURL https://update.greasyfork.org/scripts/433994/Wanikani%20Anime%20Sentences.user.js
// @updateURL https://update.greasyfork.org/scripts/433994/Wanikani%20Anime%20Sentences.meta.js
// ==/UserScript==


(() => {

    //--------------------------------------------------------------------------------------------------------------//
    //-----------------------------------------------INITIALIZATION-------------------------------------------------//
    //--------------------------------------------------------------------------------------------------------------//
    const wkof = window.wkof;

    const scriptId = "anime-sentences";
    const scriptName = "Anime Sentences";

    let state = {
        settings: {
            playbackRate: 1,
            showEnglish: 'onhover',
            showJapanese: 'always',
            showFurigana: 'onhover',
            sentenceLengthSort: 'asc',
            filterWaniKaniLevel: true,
            filterAnimeShows: {0: true, 1: true, 2: true, 3: true, 4: true, 5: true, 6: true, 8: true, 9: true, 10: true, 11: true, 12: true, 13: true, 14: true, 15: true, 16: true, 17: true, 18: true,
                               19: true, 20: true, 21: true, 22: true, 23: true, 24: true, 25: true, 26: true, 28: true, 29: true, 30: true, 31: true, 32: true, 33: true, 34: true, 35: true, 36: true, 37: true, 38: true, 39: true, 40: true, 41: true, 42: true, 43: true},
            filterAnimeMovies: {0: true, 1: true, 2: true, 3: true, 4: true, 5: true, 6: true},
            // Some Ghibli films are enabled by default
            filterGhibli: {0: true, 1: true, 2: true, 3: true, 4: true, 5: true, 6: true, 7: true, 8: true, 9: true, 10: true, 11: true, 12: true},
        },
        item: null, // current vocab from wkinfo
        userLevel: '', // most recent level progression
        immersionKitData: null, // cached so sentences can be re-rendered after settings change
        sentencesEl: null, // referenced so sentences can be re-rendered after settings change
    };

    // Titles taken from https://www.immersionkit.com/information
    const animeShows = {
        0: "Angel Beats!",
        1: "Anohana: The Flower We Saw That Day",
        2: "Assassination Classroom Season 1",
        3: "Bakemonogatari",
        4: "Boku no Hero Academia Season 1",
        5: "Cardcaptor Sakura",
        6: "Chobits",
        7: "Clannad",
        8: "Clannad After Story",
        9: "Code Geass Season 1",
        10: "Daily Lives of High School Boys",
        11: "Death Note",
        12: "Durarara!!",
        13: "Erased",
        14: "Fairy Tail",
        15: "Fate Stay Night UBW Season 1",
        16: "Fate Stay Night UBW Season 2",
        17: "Fate Zero",
        18: "From the New World",
        19: "Fruits Basket Season 1",
        20: "Fullmetal Alchemist Brotherhood",
        21: "God's Blessing on this Wonderful World!",
        22: "Haruhi Suzumiya",
        23: "Hunter × Hunter",
        24: "Is The Order a Rabbit",
        25: "K-On!",
        26: "Kanon (2006)",
        27: "Kill la Kill",
        28: "Kino's Journey",
        29: "Kokoro Connect",
        30: "Little Witch Academia",
        31: "Mahou Shoujo Madoka Magica",
        32: "My Little Sister Can't Be This Cute",
        33: "New Game!",
        34: "No Game No Life",
        35: "Noragami",
        36: "One Week Friends",
        37: "Psycho Pass",
        38: "Re:Zero − Starting Life in Another World",
        39: "Shirokuma Cafe",
        40: "Steins Gate",
        41: "Sword Art Online",
        42: "Toradora!",
        43: "Wandering Witch The Journey of Elaina",
        44: "Your Lie in April",
    };

    const animeMovies = {
        0: "Only Yesterday",
        1: "The Garden of Words",
        2: "The Girl Who Leapt Through Time",
        3: "The World God Only Knows",
        4: "Weathering with You",
        5: "Wolf Children",
        6: "Your Name",
    };

    const ghibliTitles = {
        0: "Castle in the sky",
        1: "From Up on Poppy Hill",
        2: "Grave of the Fireflies",
        3: "Howl's Moving Castle",
        4: "Kiki's Delivery Service",
        5: "My Neighbor Totoro",
        6: "Princess Mononoke",
        7: "Spirited Away",
        8: "The Cat Returns",
        9: "The Secret World of Arrietty",
        10: "The Wind Rises",
        11: "When Marnie Was There",
        12: "Whisper of the Heart",
    };

    main();

    function main() {
        init(() => wkItemInfo.forType(`vocabulary,kanaVocabulary`).under(`examples`).notify(
            (item) => onExamplesVisible(item))
        );
    }

    function init(callback) {
        createStyle();

        if (wkof) {
            wkof.include("ItemData,Settings");
            wkof
                .ready("Apiv2,Settings")
                .then(loadSettings)
                .then(processLoadedSettings)
                .then(getLevel)
                .then(callback);
        } else {
            console.warn(
                `${scriptName}: You are not using Wanikani Open Framework which this script utilizes to provide the settings dialog for the script. You can still use ${scriptName} normally though`
            );
            callback();
        }
    }

    function getLevel() {
        wkof.Apiv2.fetch_endpoint('level_progressions', (window.unsafeWindow ?? window).options ?? analyticsOptions).then((response) => {
            state.userLevel = response.data[response.data.length - 1].data.level;
        });
    }

    function onExamplesVisible(item) {
        state.item = item; // current vocab item
        addAnimeSentences();
    }

    function addAnimeSentences() {
        const parentEl = document.createElement("div");
        parentEl.setAttribute("id", 'anime-sentences-parent');

        let header = ['Anime Sentences'];

        const settingsBtn = document.createElement("svg");
        settingsBtn.setAttribute("style", "font-size: 14px; cursor: pointer; vertical-align: middle; margin-left: 10px;");
        settingsBtn.textContent = '⚙️';
        settingsBtn.onclick = openSettings;
        let sentencesEl = document.createElement("div");
        sentencesEl.innerText = 'Loading...';

        header.push(settingsBtn);
        parentEl.append(sentencesEl);
        state.sentencesEl = sentencesEl;

        if (state.item.injector) {
            if (state.item.on === 'lesson') {
                state.item.injector.appendAtTop(header, parentEl);
            } else { // itemPage, review
                state.item.injector.append(header, parentEl);
            }
        }

        const queryString = state.item.characters.replace('〜', '');  // for "counter" kanji
        const wkLevelFilter = state.settings.filterWaniKaniLevel ? state.userLevel : '';
        let url = `https://api.immersionkit.com/look_up_dictionary?keyword=${queryString}&tags=&jlpt=&wk=${wkLevelFilter}&sort=shortness&category=anime`;
        fetch(url)
            .then(response => response.json())
            .then(data => {
                state.immersionKitData = data.data[0].examples;
                renderSentences();
            });
    }

    function getDesiredShows() {
        // Convert settings dictionaries to array of titles
        let titles = [];
        for (const [key, value] of Object.entries(state.settings.filterAnimeShows)) {
            if (value === true) {
                titles.push(animeShows[key]);
            }
        }
        for (const [key, value] of Object.entries(state.settings.filterAnimeMovies)) {
            if (value === true) {
                titles.push(animeMovies[key]);
            }
        }
        for (const [key, value] of Object.entries(state.settings.filterGhibli)) {
            if (value === true) {
                titles.push(ghibliTitles[key]);
            }
        }
        return titles;
    }

    function renderSentences() {
        // Called from immersionkit response, and on settings save
        let examples = state.immersionKitData;
        const exampleLenBeforeFilter = examples.length;

        // Exclude non-selected titles
        let desiredTitles = getDesiredShows();
        examples = examples.filter(ex => desiredTitles.includes(ex.deck_name));
        if (state.settings.sentenceLengthSort === 'asc') {
            examples.sort((a, b) => a.sentence.length - b.sentence.length);
        } else {
            examples.sort((a, b) => b.sentence.length - a.sentence.length);
        }

        let showJapanese = state.settings.showJapanese;
        let showEnglish = state.settings.showEnglish;
        let showFurigana = state.settings.showFurigana;
        let playbackRate = state.settings.playbackRate;

        let html = '';
        let exampleLimit = Math.min(examples.length, 50);

        if (exampleLenBeforeFilter === 0) {
            html = 'No sentences found.';
        } else if (examples.length === 0 && exampleLenBeforeFilter > 0) {
            // TODO show which titles have how many examples
            html = 'No sentences found for your selected movies & shows.';
        } else {
            for (let i = 0; i < exampleLimit; i++) {
                const example = examples[i];

                let japaneseText = state.settings.showFurigana === 'never' ?
                    example.sentence :
                    new Furigana(example.sentence_with_furigana).ReadingHtml;

                html += `
    <div class="anime-example">
        <img src="${example.image_url}" alt="">
        <div class="anime-example-text">
            <div class="title" title="${example.id}">${example.deck_name}
            <span><button class="audio-btn audio-idle">🔈</button></span>
            </div>
            <div class="ja">
                <span class="${showJapanese === 'onhover' ? 'show-on-hover' : ''} ${showFurigana === 'onhover' ? 'show-ruby-on-hover' : ''}  ${showJapanese === 'onclick' ? 'show-on-click' : ''}">${japaneseText}</span>
            </div>
            <div class="en">
                <span class="${showEnglish === 'onhover' ? 'show-on-hover' : ''} ${showEnglish === 'onclick' ? 'show-on-click' : ''}">${example.translation}</span>
            </div>
        </div>
    </div>`
            }
        }

        let sentencesEl = state.sentencesEl;
        sentencesEl.innerHTML = html;

        const audioIdleClass = "audio-idle";
        const audioPlayClass = "audio-play";

        const animeSentencesContainer = document.querySelector("#anime-sentences-parent");
        const audioButtons = document.querySelectorAll(".anime-example .audio-btn");
        for (let i = 0; i < audioButtons.length; i++) {
            let audioContainer;
            const button = audioButtons[i];
            const onPlay = () => {button.classList.replace(audioIdleClass,audioPlayClass);button.textContent='🔊';};
            const onStop = () => {button.classList.replace(audioPlayClass,audioIdleClass);button.textContent='🔈';removeAudioElement(audioContainer);};
            button.onclick = function () {
                if ((audioContainer = document.querySelector("#anime-sentences-parent audio")) !== null) {
                    let prevSource = audioContainer.src;
                    audioContainer.pause();
                    if (prevSource === examples[i].sound_url) {
                        return;
                    }
                }
                audioContainer = document.createElement("audio");
                audioContainer.src = examples[i].sound_url;
                audioContainer.playbackRate = playbackRate;
                audioContainer.onplay = onPlay;
                audioContainer.onpause = onStop;
                audioContainer.onended = onStop;
                audioContainer.onabort = onStop;
                animeSentencesContainer.append(audioContainer);
			    audioContainer.play();
            };
        }

        // Click anywhere plays the audio
        let exampleEls = document.querySelectorAll("#anime-sentences-parent .anime-example");
        exampleEls.forEach((a) => {
            a.onclick = function () {
                let button = this.querySelector('.audio-btn');
                button.click();
            };
        });

        // Assigning onclick function to .show-on-click elements
        document.querySelectorAll(".show-on-click").forEach((a) => {
            a.onclick = function (e) {
                e.stopPropagation(); // prevent this click from triggering the audio to play
                a.classList.toggle('show-on-click');
            };
        });
    }

    function removeAudioElement(element) {
        if (element === undefined || element === null) {
            return;
        }
        element.src = "";
        element.remove();
    }

    //--------------------------------------------------------------------------------------------------------------//
    //----------------------------------------------SETTINGS--------------------------------------------------------//
    //--------------------------------------------------------------------------------------------------------------//

    function loadSettings() {
        return wkof.Settings.load(scriptId, state.settings);
    }

    function processLoadedSettings() {
        state.settings = wkof.settings[scriptId];
    }

    function openSettings(e) {
		e.stopPropagation();
        let config = {
            script_id: scriptId,
            title: scriptName,
            on_save: updateSettings,
            content: {
                general: {
                    type: "section",
                    label: "General"
                },
                sentenceLengthSort: {
                    type: "dropdown",
                    label: "Sentence Order",
                    hover_tip: "",
                    content: {
                        asc: "Shortest first",
                        desc: "Longest first"
                    },
                    default: state.settings.sentenceLengthSort
                },
                playbackRate: {
                    type: "number",
                    label: "Playback Speed",
                    step: 0.1,
                    min: 0.5,
                    max: 2,
                    hover_tip: "Speed to play back audio.",
                    default: state.settings.playbackRate
                },
                showJapanese: {
                    type: "dropdown",
                    label: "Show Japanese",
                    hover_tip: "When to show Japanese text. Hover enables transcribing a sentences first (play audio by clicking the image to avoid seeing the answer).",
                    content: {
                        always: "Always",
                        onhover: "On Hover",
                        onclick: "On Click",
                    },
                    default: state.settings.showJapanese
                },
                showFurigana: {
                    type: "dropdown",
                    label: "Show Furigana",
                    hover_tip: "These have been autogenerated so there may be mistakes.",
                    content: {
                        always: "Always",
                        onhover: "On Hover",
                        never: "Never",
                    },
                    default: state.settings.showFurigana
                },
                showEnglish: {
                    type: "dropdown",
                    label: "Show English",
                    hover_tip: "Hover or click allows testing your understanding before seeing the answer.",
                    content: {
                        always: "Always",
                        onhover: "On Hover",
                        onclick: "On Click",
                    },
                    default: state.settings.showEnglish
                },
                tooltip: {
                    type: "section",
                    label: "Filters"
                },
                filterGhibli: {
                    type: "list",
                    label: "Ghibli Movies",
                    multi: true,
                    size: 6,
                    hover_tip: "Select which Studio Ghibli movies you'd like to see examples from.",
                    default: state.settings.filterGhibli,
                    content: ghibliTitles
                },
                filterAnimeMovies: {
                    type: "list",
                    label: "Anime Movies",
                    multi: true,
                    size: 6,
                    hover_tip: "Select which anime movies you'd like to see examples from.",
                    default: state.settings.filterAnimeMovies,
                    content: animeMovies
                },
                filterAnimeShows: {
                    type: "list",
                    label: "Anime Shows",
                    multi: true,
                    size: 6,
                    hover_tip: "Select which anime shows you'd like to see examples from.",
                    default: state.settings.filterAnimeShows,
                    content: animeShows
                },
                filterWaniKaniLevel: {
                    type: "checkbox",
                    label: "WaniKani Level",
                    hover_tip: "Only show sentences with maximum 1 word outside of your current WaniKani level.",
                    default: state.settings.filterWaniKaniLevel,
                },
                credits: {
                    type: "section",
                    label: "Powered by immersionkit.com"
                },
            }
        };
        let dialog = new wkof.Settings(config);
        dialog.open();
    }

    // Called when the user clicks the Save button on the Settings dialog.
    function updateSettings() {
        state.settings = wkof.settings[scriptId];
        renderSentences();
    }

    //--------------------------------------------------------------------------------------------------------------//
    //-----------------------------------------------STYLES---------------------------------------------------------//
    //--------------------------------------------------------------------------------------------------------------//

    function createStyle() {
        const style = document.createElement("style");
        style.setAttribute("id", "anime-sentences-style");
        // language=CSS
        style.innerHTML = `
            #anime-sentences-parent > div {
                overflow-y: auto;
                max-height: 280px;
            }

            #anime-sentences-parent .fa-solid {
                border: none;
                font-size: 100%;
            }

            .anime-example {
                display: flex;
                align-items: center;
                margin-bottom: 1em;
                cursor: pointer;
            }

            .anime-example .audio-btn {
                background-color: transparent;
            }

            .anime-example .audio-btn.audio-idle {
                opacity: 50%;
            }

            /* Make text and background color the same to hide text */
            .anime-example-text .show-on-hover, .anime-example-text .show-on-click {
                background: #ccc;
                color: transparent;
                text-shadow: none;
            }

            .anime-example-text .show-on-hover:hover {
                background: inherit;
                color: inherit
            }

            /* Furigana hover*/
            .anime-example-text .show-ruby-on-hover ruby rt {
                visibility: hidden;
            }

            .anime-example-text:hover .show-ruby-on-hover ruby rt {
                visibility: visible;
            }

            .anime-example .title {
                font-weight: 700;
            }

            .anime-example .ja {
                font-size: 2em;
            }

            .anime-example img {
                margin-right: 1em;
                max-width: 200px;
            }
        `;

        document.querySelector("head").append(style);
    }


    //--------------------------------------------------------------------------------------------------------------//
    //----------------------------------------------FURIGANA--------------------------------------------------------//
    //--------------------------------------------------------------------------------------------------------------//
    // https://raw.githubusercontent.com/helephant/Gem/master/src/Gem.Javascript/gem.furigana.js
    function Furigana(reading) {
        var segments = ParseFurigana(reading || "");

        this.Reading = getReading();
        this.Expression = getExpression();
        this.Hiragana = getHiragana();
        this.ReadingHtml = getReadingHtml();

        function getReading() {
            var reading = "";
            for (var x = 0; x < segments.length; x++) {
                reading += segments[x].Reading;
            }
            return reading.trim();
        }

        function getExpression() {
            var expression = "";
            for (var x = 0; x < segments.length; x++)
                expression += segments[x].Expression;
            return expression;
        }

        function getHiragana() {
            var hiragana = "";
            for (var x = 0; x < segments.length; x++) {
                hiragana += segments[x].Hiragana;
            }
            return hiragana;
        }

        function getReadingHtml() {
            var html = "";
            for (var x = 0; x < segments.length; x++) {
                html += segments[x].ReadingHtml;
            }
            return html;
        }
    }

    function FuriganaSegment(baseText, furigana) {
        this.Expression = baseText;
        this.Hiragana = furigana.trim();
        this.Reading = baseText + "[" + furigana + "]";
        this.ReadingHtml = "<ruby><rb>" + baseText + "</rb><rt>" + furigana + "</rt></ruby>";
    }

    function UndecoratedSegment(baseText) {
        this.Expression = baseText;
        this.Hiragana = baseText;
        this.Reading = baseText;
        this.ReadingHtml = baseText;
    }

    function ParseFurigana(reading) {
        var segments = [];

        var currentBase = "";
        var currentFurigana = "";
        var parsingBaseSection = true;
        var parsingHtml = false;

        var characters = reading.split('');

        while (characters.length > 0) {
            var current = characters.shift();

            if (current === '[') {
                parsingBaseSection = false;
            } else if (current === ']') {
                nextSegment();
            } else if (isLastCharacterInBlock(current, characters) && parsingBaseSection) {
                currentBase += current;
                nextSegment();
            } else if (!parsingBaseSection)
                currentFurigana += current;
            else
                currentBase += current;
        }

        nextSegment();

        function nextSegment() {
            if (currentBase)
                segments.push(getSegment(currentBase, currentFurigana));
            currentBase = "";
            currentFurigana = "";
            parsingBaseSection = true;
            parsingHtml = false;
        }

        function getSegment(baseText, furigana) {
            if (!furigana || furigana.trim().length === 0)
                return new UndecoratedSegment(baseText);
            return new FuriganaSegment(baseText, furigana);
        }

        function isLastCharacterInBlock(current, characters) {
            return !characters.length ||
                (isKanji(current) !== isKanji(characters[0]) && characters[0] !== '[');
        }

        function isKanji(character) {
            return character && character.charCodeAt(0) >= 0x4e00 && character.charCodeAt(0) <= 0x9faf;
        }

        return segments;
    }

})();

And this is the entire thing again with a variety of other modifications—including an updated anime list (about 10 more added; take note of which ones you've enabled prior to using this, because they will become mixed up until you reassign them) based on ImmersionKit's actual search results, more settings for filtering, and coloring of the keyword within the example, similar to how immersionkit's UI does it. Subject to possible future changes. I also updated the namespace, so it should be able to be installed side-by-side with the official one.
// ==UserScript==
// @name         Wanikani Anime Sentences
// @description  Adds example sentences from anime movies and shows for vocabulary from immersionkit.com
// @version      1.1.3.9
// @author       psdcon, edited by Inserio (with inspirations from polv and others)
// @namespace    wkanimesentences/inserio
// @match        https://www.wanikani.com/*
// @match        https://preview.wanikani.com/*
// @require      https://greasyfork.org/scripts/430565-wanikani-item-info-injector/code/WaniKani%20Item%20Info%20Injector.user.js?version=1343973
// @copyright    2021+, Paul Connolly
// @license      MIT; http://opensource.org/licenses/MIT
// @run-at       document-end
// @grant        none
// ==/UserScript==
(() => {
    //--------------------------------------------------------------------------------------------------------------//
    //-----------------------------------------------INITIALIZATION-------------------------------------------------//
    //--------------------------------------------------------------------------------------------------------------//
    const wkof = window.wkof, scriptId = "anime-sentences-2", scriptName = "Anime Sentences";
    const state = {
        settings: {
            maxBoxHeight: 320,
            playbackRate: 1,
            exampleLimit: 100,
            showEnglish: 'onhover',
            showJapanese: 'always',
            showFurigana: 'onhover',
            sentenceSorting: 'shortness',
            filterExactSearch: true,
            filterJLPTLevel: 0,
            filterWaniKaniLevel: true,
            // Enable all shows and movies by default
            filterAnimeShows: [true, true, true, true, true, true, true, true, true, true,
                                true, true, true, true, true, true, true, true, true, true,
                                true, true, true, true, true, true, true, true, true, true,
                                true, true, true, true, true, true, true, true, true, true,
                                true, true, true, true, true, true, true, true, true, true,
                                true, true, true, true, true],
            filterAnimeMovies: [true, true, true, true, true, true],
            filterGhibli: [true, true, true, true, true, true, true, true, true, true,
                           true, true, true, true],
            // Disable all dramas, games, literature, and news by default
            filterDramas: [false, false, false, false, false, false, false, false, false, false,
                           false, false, false, false],
            filterGames: [false, false, false, false, false, false],
            filterLiterature: [false, false, false, false, false, false, false, false, false, false,
                               false, false, false, false, false, false, false, false, false, false,
                               false, false, false, false, false, false, false, false, false, false,
                               false, false, false, false, false, false, false, false, false, false,
                               false, false, false, false, false, false, false, false, false, false,
                               false, false, false, false, false, false, false, false, false, false,
                               false, false, false, false, false, false, false, false, false, false,
                               false, false, false, false, false, false, false, false, false, false,
                               false, false, false, false, false, false, false, false, false, false,
                               false],
            filterNews: [false, false, false, false, false],
        },
        desiredTitles: null,
        item: null, // current vocab from wkinfo
        userLevel: '', // most recent level progression
        immersionKitData: null, // cached so sentences can be re-rendered after settings change
        sentencesEl: null, // referenced so sentences can be re-rendered after settings change
    };
    // Titles taken from https://www.immersionkit.com/information and modified after testing a few example search results
    // All anime/movies/Ghibli are enabled by default, and all dramas, games, literature, and news are disabled by default
    const animeShows = [
        "Angel Beats!",
        "Anohana the flower we saw that day",
        "Assassination Classroom Season 1",
        "Bakemonogatari",
        "Boku no Hero Academia Season 1",
        "Bunny Drop",
        "Cardcaptor Sakura",
        "Chobits",
        "Clannad",
        "Clannad After Story",
        "Code Geass Season 1",
        "Daily Lives of High School Boys",
        "Death Note",
        "Demon Slayer - Kimetsu no Yaiba",
        "Durarara!!",
        "Erased",
        "Fairy Tail",
        "Fate Stay Night Unlimited Blade Works",
        "Fate Zero",
        "From the New World",
        "Fruits Basket Season 1",
        "Fullmetal Alchemist Brotherhood",
        "God's Blessing on this Wonderful World!",
        "Haruhi Suzumiya",
        "Hunter × Hunter",
        "Hyouka",
        "Is The Order a Rabbit",
        "K-On!",
        "Kakegurui",
        "Kanon (2006)",
        "Kill la Kill",
        "Kino's Journey",
        "Kokoro Connect",
        "Little Witch Academia",
        "Lucky Star",
        "Mahou Shoujo Madoka Magica",
        "Mob Psycho 100",
        "Mononoke",
        "My Little Sister Can't Be This Cute",
        "New Game!",
        "Nisekoi",
        "No Game No Life",
        "Noragami",
        "One Week Friends",
        "Psycho Pass",
        "Re:Zero − Starting Life in Another World",
        "ReLIFE",
        "Shirokuma Cafe",
        "Sound! Euphonium",
        "Steins Gate",
        "Sword Art Online",
        "The Pet Girl of Sakurasou",
        "Toradora!",
        "Wandering Witch The Journey of Elaina",
        "Your Lie in April",
    ], animeMovies = [
        "The Garden of Words",
        "The Girl Who Leapt Through Time",
        "The World God Only Knows",
        "Weathering with You",
        "Wolf Children",
        "Your Name",
    ], ghibliTitles = [
        "Castle in the sky",
        "From Up on Poppy Hill",
        "Grave of the Fireflies",
        "Howl's Moving Castle",
        "Kiki's Delivery Service",
        "My Neighbor Totoro",
        "Only Yesterday",
        "Princess Mononoke",
        "Spirited Away",
        "The Cat Returns",
        "The Secret World of Arrietty",
        "The Wind Rises",
        "When Marnie Was There",
        "Whisper of the Heart",
    ], dramasList = [
        "1 Litre of Tears",
        "Border",
        "Good Morning Call Season 1",
        "Good Morning Call Season 2",
        "I am Mita, Your Housekeeper",
        "I'm Taking the Day Off",
        "Legal High Season 1",
        "Million Yen Woman",
        "Overprotected Kahoko",
        "Quartet",
        "Sailor Suit and Machine Gun (2006)",
        "Smoking",
        "The Journalist",
        "Weakest Beast",
    ], gamesList = [ // commented-out entries are not queryable via the API (but maybe someday???)
        "Cyberpunk 2077",
        // "NieR:Automata",
        // "NieR Re[in]carnation",
        "Skyrim",
        "Witcher 3",
        // "Zelda: Breath of the Wild"
    ], literatureList = [
        "黒猫",
        "おおかみと七ひきのこどもやぎ",
        "マッチ売りの少女",
        "サンタクロースがやってきた",
        "君死にたまふことなかれ 与謝野 晶子",
        "蝉",
        "胡瓜",
        "若鮎について",
        "黒足袋 吉井 勇",
        "柿",
        "お母さんの思ひ出",
        "砂をかむ",
        "虻のおれい",
        "がちゃがちゃ",
        "犬のいたずら",
        "犬と人形",
        "懐中時計",
        "きのこ会議",
        "お金とピストル 夢野 久作",
        "梅のにおい",
        "純真",
        "声と人柄",
        "心の調べ",
        "愛",
        "期待と切望",
        "空の美 宮本 百合子",
        "いちょうの実",
        "虔十公園林",
        "クねずみ",
        "おきなぐさ",
        "さるのこしかけ 宮沢 賢治",
        "セロ弾きのゴーシュ",
        "ざしき童子のはなし",
        "秋の歌 寺田 寅彦",
        "赤い船とつばめ 小川 未明",
        "赤い蝋燭と人魚 小川 未明",
        "赤い魚と子供",
        "秋が きました 小川 未明",
        "青いボタン",
        "ある夜の星たちの話",
        "いろいろな花",
        "からすとかがし 小川 未明",
        "片田舎にあった話",
        "金魚売り",
        "小鳥と兄妹",
        "おじいさんが捨てたら",
        "おかめどんぐり 小川 未明",
        "お母さん",
        "お母さんのお乳 小川 未明",
        "おっぱい",
        "少年と秋の日",
        "金のくびかざり 小野 浩",
        "愛よ愛 岡本 かの子",
        "気の毒な奥様",
        "新茶",
        "初夏に座す",
        "三角と四角",
        "赤い蝋燭",
        "赤とんぼ",
        "飴だま 新美 南吉",
        "あし",
        "がちょうのたんじょうび 新美 南吉",
        "ごん狐 新美 南吉",
        "蟹のしょうばい 新美 南吉",
        "カタツムリノ ウタ",
        "木の祭り",
        "こぞうさんのおきょう",
        "去年の木",
        "おじいさんのランプ",
        "王さまと靴屋",
        "落とした一銭銅貨",
        "サルト サムライ",
        "里の春、山の春 新美 南吉",
        "ウサギ 新美 南吉",
        "あひるさん と 時計",
        "川へおちた玉ねぎさん",
        "小ぐまさんのかんがへちがひ",
        "お鍋とお皿とカーテン",
        "お鍋とおやかんとフライパンのけんくわ",
        "ひらめの学校",
        "狐物語 林 芙美子",
        "桜の樹の下には 梶井 基次郎",
        "瓜子姫子",
        "ああしんど",
        "葬式の行列",
        "風",
        "子どものすきな神さま",
        "喫茶店にて",
        "子供に化けた狐 野口 雨情",
        "顔",
        "四季とその折々 黒島 傳冶",
    ], newsList = [
        "平成30年阿蘇神社で甘酒の仕込み始まる",
        "フレッシュマン!5月号阿蘇広域行政事務組合",
        "フレッシュマン!7月号春工房、そば処ゆう雀",
        "フレッシュマン!11月号内牧保育園",
        "山田小学校で最後の稲刈り",
    ];

    main();

    function main() {
        init(() => wkItemInfo.forType(`vocabulary,kanaVocabulary`).under(`examples`).notify((item) => onExamplesVisible(item)));
    }

    function init(callback) {
        if (wkof) {
            wkof.include("ItemData,Settings");
            wkof
                .ready("Apiv2,Settings")
                .then(loadSettings)
                .then(processLoadedSettings)
                .then(createStyle)
                .then(getLevel)
                .then(updateDesiredShows)
                .then(callback);
        } else {
            console.warn(`${scriptName}: You are not using Wanikani Open Framework which this script utilizes to provide the settings dialog for the script. You can still use ${scriptName} normally though`);
            createStyle();
            updateDesiredShows();
            callback();
        }
    }

    function getLevel() {
        wkof.Apiv2.fetch_endpoint('level_progressions', (window.unsafeWindow ?? window).options ?? analyticsOptions).then((response) => {
            state.userLevel = response.data[response.data.length - 1].data.level;
        });
    }

    function onExamplesVisible(item) {
        state.item = item; // current vocab item
        addAnimeSentences();
    }

    function addAnimeSentences() {
        const parentEl = document.createElement("div"), sentencesEl = document.createElement("div"),
            settingsBtn = document.createElement("svg"), header = ['Anime Sentences'];
        parentEl.setAttribute("id", 'anime-sentences-parent');
        settingsBtn.setAttribute("style", "font-size: 14px; cursor: pointer; vertical-align: middle; margin-left: 10px;");
        settingsBtn.textContent = '⚙️';
        settingsBtn.onclick = openSettings;
        sentencesEl.innerText = 'Loading...';

        header.push(settingsBtn);
        parentEl.append(sentencesEl);
        state.sentencesEl = sentencesEl;

        if (state.item.injector) {
            if (state.item.on === 'lesson') {
                state.item.injector.appendAtTop(header, parentEl);
            } else { // itemPage, review
                state.item.injector.append(header, parentEl);
            }
        }

        fetchImmersionKitData().then(examples => {
            state.immersionKitData = examples;
            renderSentences(state.immersionKitData);
        });
    }

    function fetchImmersionKitData() {
        let keyword = state.item.characters.replace('〜', '');  // for "counter" kanji
        if (state.settings.filterExactSearch)
            keyword = `「${keyword}」`;
        // TODO: Add &tags=
        const sentenceSorting = state.settings.sentenceSorting !== 'none' ? `&sort=${state.settings.sentenceSorting}` : '',
            jlptFilter = state.settings.filterJLPTLevel !== 0 ? `&jlpt=${state.settings.filterJLPTLevel}` : '',
            wkLevelFilter = state.settings.filterWaniKaniLevel ? `&wk=${state.userLevel}` : '', tags = '',
            // animeOnly = (state.settings.filterDramas.length === 0 && state.settings.filterGames.length === 0 && state.settings.filterLiterature.length === 0) ? "&category=anime" : '', // this could create a false-negative if the user selects Mob Psycho 100 but nothing else in the non-anime categories so I'll remove the filter
            url = `https://api.immersionkit.com/look_up_dictionary?keyword=${keyword}${sentenceSorting}${tags}${jlptFilter}${wkLevelFilter}`;
        return fetch(url)
            .then(response => response.json())
            .then(data => {
                return data.data[0].examples;
            });
    }

    function updateDesiredShows() {
        // Combine settings objects to a single hashmap of titles
        const maxLength = Math.max(state.settings.filterAnimeShows.length, state.settings.filterAnimeMovies.length, state.settings.filterGhibli.length, state.settings.filterDramas.length, state.settings.filterGames.length, state.settings.filterLiterature.length, state.settings.filterNews.length);
        const titles = new Map();
        // ordering doesn't matter, so we'll add from all lists simultaneously
        for (let i = 0; i < maxLength; i++) {
            if (i < state.settings.filterAnimeShows.length && state.settings.filterAnimeShows[i])
                titles.set(animeShows[i],true);
            if (i < state.settings.filterAnimeMovies.length && state.settings.filterAnimeMovies[i])
                titles.set(animeMovies[i],true);
            if (i < state.settings.filterGhibli.length && state.settings.filterGhibli[i])
                titles.set(ghibliTitles[i],true);
            if (i < state.settings.filterDramas.length && state.settings.filterDramas[i])
                titles.set(dramasList[i],true);
            if (i < state.settings.filterGames.length && state.settings.filterGames[i])
                titles.set(gamesList[i],true);
            if (i < state.settings.filterLiterature.length && state.settings.filterLiterature[i])
                titles.set(literatureList[i],true);
            if (i < state.settings.filterNews.length && state.settings.filterNews[i])
                titles.set(newsList[i],true);
        }
        state.desiredTitles = titles;

    }

    function getInnerHtml(examples, sentences) {
        const showJapanese = state.settings.showJapanese,
            showEnglish = state.settings.showEnglish,
            showFurigana = state.settings.showFurigana,
            exampleLimit = ((state.settings.exampleLimit === 0) ? examples.length : Math.min(examples.length, state.settings.exampleLimit));
        let innerHtml = '';
        if (sentences.length === 0)
            return 'No sentences found.';
        if (examples.length === 0 && sentences.length > 0) {
            // TODO show which titles have how many examples
            return 'No sentences found for your selected movies & shows.';
        }
        const keyword = state.item.characters.replace('〜', ''),
            keyRegex = new RegExp(keyword.split('').join('\\s*'), 'g'); // intersperse whitespace quantifier to match awkwardly spaced out sentences
        for (let i = 0; i < exampleLimit; i++) {
            const example = examples[i],
                japaneseText = new Furigana(example.sentence_with_furigana);
            keyRegex.lastIndex = 0;
            if (keyRegex.test(example.sentence)) {
                let startIndex = -1, endIndex = 0, match;
                for (let j = 0; j < japaneseText.Segments.length; j++) {
                    const segment = japaneseText.Segments[j];
                    keyRegex.lastIndex = 0;
                    if (keyRegex.test(segment.Expression)) {
                        // the entire match is contained within segment
                        segment.Expression = segment.Expression.replace(keyRegex, '<keyword>$&</keyword>');
                        startIndex = -1;
                        endIndex = 0;
                        continue;
                    }

                    // match is likely split between multiple segments, so we'll parse ahead to find them
                    if (endIndex <= 0 || startIndex <= -1) {
                        // no match has been found yet
                        let combinedExpression = "";
                        for (let k = j; k < japaneseText.Segments.length; k++) {
                            combinedExpression += japaneseText.Segments[k].Expression;
                            match = keyRegex.exec(combinedExpression);
                            if (match !== null) {
                                startIndex = match.index;
                                endIndex = startIndex + match[0].length;
                                break;
                            }
                        }
                    }
                    if (endIndex <= 0 || startIndex <= -1) break; // no match was found through the remainder of the sentence
                    // complete match was found within index 'j' to index 'k' by combining the segments

                    if (startIndex - segment.Expression.length >= 0) {
                        // the current segment is prior to the start of the match
                        startIndex -= segment.Expression.length;
                        endIndex -= segment.Expression.length;
                        continue;
                    }

                    // match is present within the current segment
                    // new expression should consist of (current segment prior to the match) (open tag) (match) (close tag) (remainder of segment)
                    let newSegment;
                    if (endIndex - segment.Expression.length < 0) {
                        // the current segment contains the remainder of the match
                        newSegment = `${segment.Expression.substring(0, startIndex)}<keyword>${segment.Expression.substring(startIndex, endIndex - startIndex)}</keyword>${segment.Expression.substring(endIndex)}`;
                    } else {
                        // match continues to the following segment
                        newSegment = `${segment.Expression.substring(0, startIndex)}<keyword>${segment.Expression.substring(startIndex)}</keyword>`;
                    }

                    startIndex = 0; // start any subsequent segments with an open tag at the beginning
                    endIndex -= segment.Expression.length; // subtract used segment from index

                    // replace the current segment with a new expression with surrounding tags
                    segment.Expression = newSegment;
                }
            }
            innerHtml += `
    <div class="anime-example">
        <img src="${example.image_url}" alt="">
        <div class="anime-example-text">
            <div class="title" title="${example.id}">${example.deck_name}
                <span><button title="Play Audio" class="audio-btn audio-idle">🔈</button></span>
            </div>
            <div class="ja">
                <span class="${showJapanese === 'onhover' ? 'show-on-hover' : ''} ${showFurigana === 'onhover' ? 'show-ruby-on-hover' : ''}  ${showJapanese === 'onclick' ? 'show-on-click' : ''}">${showFurigana === 'never' ? japaneseText.Expression : japaneseText.ReadingHtml}</span>
            </div>
            <div class="en">
                <span class="${showEnglish === 'onhover' ? 'show-on-hover' : ''} ${showEnglish === 'onclick' ? 'show-on-click' : ''}">${example.translation}</span>
            </div>
        </div>
    </div>`;
        }
        return innerHtml;
    }

    // Called from immersionkit response, and on settings save
    function renderSentences(sentences) {
        // Exclude non-selected titles
        const examples = [];
        for (let i = 0; i < sentences.length; i++) {
            const ex = sentences[i];
            // if (state.desiredTitles !== null && (state.desiredTitles[ex.deck_name] || state.desiredTitles[ex.deck_name_japanese]))
            if (state.desiredTitles.has(ex.deck_name) || state.desiredTitles.has(ex.deck_name_japanese))
                examples.push(ex);
        }
        // This is necessary until the API is fixed to allow the sorting preference to happen before the deck sorting
        switch (state.settings.sentenceSorting) {
            case 'shortness':
                examples.sort((a, b) => a.sentence.length - b.sentence.length);
                break;
            case 'longness':
                examples.sort((a, b) => b.sentence.length - a.sentence.length);
                break;
        }
        const sentencesEl = state.sentencesEl, playbackRate = state.settings.playbackRate;
        sentencesEl.innerHTML = getInnerHtml(examples, sentences);
        const animeSentencesContainer = document.querySelector("#anime-sentences-parent"),
            audioButtons = document.querySelectorAll(".anime-example .audio-btn");
        for (let i = 0; i < audioButtons.length; i++) {
            const button = audioButtons[i];
            configureAudioButton(animeSentencesContainer, button, examples[i], playbackRate);
        }

        // Click anywhere plays the audio
        const exampleEls = document.querySelectorAll("#anime-sentences-parent .anime-example");
        for (let i = 0; i < exampleEls.length; i++) {
            exampleEls[i].onclick = function () {
                const button = this.querySelector('.audio-btn');
                button.click();
            };
        }

        // Assigning onclick function to .show-on-click elements
        const showOnClickElements = document.querySelectorAll(".show-on-click");
        for (let i = 0; i < showOnClickElements.length; i++) {
            const el = showOnClickElements[i];
            el.onclick = e => {
                e.stopPropagation(); // prevent this click from triggering the audio to play
                el.classList.toggle('show-on-click');
            };
        }
    }

    const audioIdleClass = "audio-idle", audioPlayClass = "audio-play";
    function configureAudioButton(container, element, example, playbackRate) {
        let audioContainer;
        const onPlay = () => {
            element.classList.replace(audioIdleClass, audioPlayClass);
            element.textContent = '🔊';
        }, onStop = () => {
            element.classList.replace(audioPlayClass, audioIdleClass);
            element.textContent = '🔈';
            removeAudioElement(audioContainer);
        };
        element.onclick = function(e) {
            e.stopPropagation(); // prevent this click from triggering twice in some scenarios
            if ((audioContainer = container.querySelector("audio")) !== null) {
                const prevSource = audioContainer.src;
                audioContainer.pause();
                if (prevSource === example.sound_url)
                    return;
            }
            audioContainer = document.createElement("audio");
            audioContainer.src = example.sound_url;
            audioContainer.playbackRate = playbackRate;
            audioContainer.onplay = onPlay;
            audioContainer.onpause = onStop;
            audioContainer.onended = onStop;
            audioContainer.onabort = onStop;
            container.append(audioContainer);
            audioContainer.play();
        };
    }

    function removeAudioElement(element) {
        if (element === undefined || element === null)
            return;
        element.src = "";
        element.remove();
    }

    //--------------------------------------------------------------------------------------------------------------//
    //----------------------------------------------SETTINGS--------------------------------------------------------//
    //--------------------------------------------------------------------------------------------------------------//

    function arrayValuesEqual(a, b) {
        if (a === b) return true;
        if (a == null || b == null) return false;
        let aValues = Object.values(a), bValues = Object.values(b);
        if (aValues.length !== bValues.length) return false;
        for (let i = 0; i < aValues.length; ++i) {
            if (aValues[i] !== bValues[i]) return false;
        }
        return true;
    }

    function loadSettings() {
        return wkof.Settings.load(scriptId, state.settings);
        //return wkof.Settings.load(scriptId, structuredClone(state.settings)); // need to clone because WaniKani Open Framework appears to update values by reference
    }

    function processLoadedSettings(settings) {
        // need to use Object.assign() in order to avoid updating the settings object byref whenever it's saved
        Object.assign(state.settings, settings);
    }

    function openSettings(e) {
        e.stopPropagation();
        const config = {
            script_id: scriptId, title: scriptName, on_save: updateSettings, content: {
                general: {
                    type: "section", label: "General"
                }, maxBoxHeight: {
                    type: "number",
                    label: "Box Height",
                    step: 1,
                    min: 0,
                    hover_tip: "Set the maximum height of the container box in pixels (px).",
                    default: state.settings.maxBoxHeight
                }, sentenceSorting: {
                    type: "dropdown", label: "Sentence Sorting Order", hover_tip: "", content: {
                        none: "Anime Title (alphabetical)", shortness: "Shortest first", longness: "Longest first"
                    },
                    default: state.settings.sentenceSorting
                }, exampleLimit: {
                    type: "number",
                    label: "Example Limit",
                    step: 1,
                    min: 0,
                    hover_tip: "Limit the number of entries that may appear. Set to 0 to show as many as possible.",
                    default: state.settings.exampleLimit
                }, playbackRate: {
                    type: "number",
                    label: "Playback Speed",
                    step: 0.1,
                    min: 0.5,
                    max: 2,
                    hover_tip: "Speed to play back audio.",
                    default: state.settings.playbackRate
                }, showJapanese: {
                    type: "dropdown",
                    label: "Show Japanese",
                    hover_tip: "When to show Japanese text. Hover enables transcribing a sentences first (play audio by clicking the image to avoid seeing the answer).",
                    content: {
                        always: "Always", onhover: "On Hover", onclick: "On Click",
                    },
                    default: state.settings.showJapanese
                }, showFurigana: {
                    type: "dropdown",
                    label: "Show Furigana",
                    hover_tip: "These have been autogenerated so there may be mistakes.",
                    content: {
                        always: "Always", onhover: "On Hover", never: "Never",
                    },
                    default: state.settings.showFurigana
                }, showEnglish: {
                    type: "dropdown",
                    label: "Show English",
                    hover_tip: "Hover or click allows testing your understanding before seeing the answer.",
                    content: {
                        always: "Always", onhover: "On Hover", onclick: "On Click",
                    },
                    default: state.settings.showEnglish
                }, tooltip: {
                    type: "section", label: "Filters"
                }, filterExactSearch: {
                    type: "checkbox",
                    label: "Exact Search",
                    hover_tip: "Text must match term exactly",
                    default: state.settings.filterExactSearch
                }, filterAnimeShows: {
                    type: "list",
                    label: "Anime Shows",
                    multi: true,
                    size: 6,
                    hover_tip: "Select which anime shows you'd like to see examples from.",
                    default: state.settings.filterAnimeShows,
                    content: animeShows
                }, filterAnimeMovies: {
                    type: "list",
                    label: "Anime Movies",
                    multi: true,
                    size: 4,
                    hover_tip: "Select which anime movies you'd like to see examples from.",
                    default: state.settings.filterAnimeMovies,
                    content: animeMovies
                }, filterGhibli: {
                    type: "list",
                    label: "Ghibli Movies",
                    multi: true,
                    size: 4,
                    hover_tip: "Select which Studio Ghibli movies you'd like to see examples from.",
                    default: state.settings.filterGhibli,
                    content: ghibliTitles
                }, filterDramas: {
                    type: "list",
                    label: "Dramas",
                    multi: true,
                    size: 4,
                    hover_tip: "Select which dramas you'd like to see examples from.",
                    default: state.settings.filterDramas,
                    content: dramasList
                }, filterGames: {
                    type: "list",
                    label: "Games",
                    multi: true,
                    size: 3,
                    hover_tip: "Select which video games you'd like to see examples from.",
                    default: state.settings.filterGames,
                    content: gamesList
                }, filterLiterature: {
                    type: "list",
                    label: "Literature",
                    multi: true,
                    size: 6,
                    hover_tip: "Select which pieces of literature you'd like to see examples from.",
                    default: state.settings.filterLiterature,
                    content: literatureList
                }, filterNews: {
                    type: "list",
                    label: "News",
                    multi: true,
                    size: 3,
                    hover_tip: "Select which news sources you'd like to see examples from.",
                    default: state.settings.filterNews,
                    content: newsList
                }, filterJLPTLevel: {
                    type: "dropdown",
                    label: "JLPT Level",
                    hover_tip: "Only show sentences matching a particular JLPT Level or easier.",
                    content: {
                        0: "No Filter", 1: "N1", 2: "N2", 3: "N3", 4: "N4", 5: "N5"
                    },
                    default: state.settings.filterJLPTLevel,
                }, filterWaniKaniLevel: {
                    type: "checkbox",
                    label: "WaniKani Level",
                    hover_tip: "Only show sentences with maximum 1 word outside of your current WaniKani level.",
                    default: state.settings.filterWaniKaniLevel,
                }, credits: {
                    type: "section", label: "Powered by immersionkit.com"
                },
            }
        };
        const dialog = new wkof.Settings(config);
        dialog.open();
    }

    // Called when the user clicks the Save button on the Settings dialog.
    function updateSettings(updatedSettings) {
        const {filterAnimeShows, filterAnimeMovies, filterGhibli, filterDramas, filterGames, filterLiterature, filterNews, filterExactSearch, filterJLPTLevel, filterWaniKaniLevel, exampleLimit, playbackRate, sentenceSorting, showFurigana, showJapanese, showEnglish, maxBoxHeight} = state.settings,
              {filterAnimeShows: newFilterAnimeShows, filterAnimeMovies: newFilterAnimeMovies, filterGhibli: newFilterGhibli, filterDramas: newFilterDramas, filterGames: newFilterGames, filterLiterature: newFilterLiterature, filterNews: newFilterNews, filterExactSearch: newFilterExactSearch, filterJLPTLevel: newFilterJLPTLevel, filterWaniKaniLevel: newFilterWaniKaniLevel, exampleLimit: newExampleLimit, playbackRate: newPlaybackRate, sentenceSorting: newSentenceSorting, showFurigana: newShowFurigana, showJapanese: newShowJapanese, showEnglish: newShowEnglish, maxBoxHeight: newMaxBoxHeightt} = updatedSettings,
              animeShowsListsDiffer = !arrayValuesEqual(filterAnimeShows,newFilterAnimeShows),
              animeMoviesListsDiffer = !arrayValuesEqual(filterAnimeMovies,newFilterAnimeMovies),
              ghibliListsDiffer = !arrayValuesEqual(filterGhibli,newFilterGhibli),
              dramasListsDiffer = !arrayValuesEqual(filterDramas,newFilterDramas),
              gamesListsDiffer = !arrayValuesEqual(filterGames,newFilterGames),
              literatureListsDiffer = !arrayValuesEqual(filterLiterature,newFilterLiterature),
              newsListsDiffer = !arrayValuesEqual(filterNews,newFilterNews);
        // avoid many issues by updating the values manually exactly as desired
        state.settings.filterExactSearch = newFilterExactSearch;
        state.settings.filterJLPTLevel = newFilterJLPTLevel;
        state.settings.filterWaniKaniLevel = newFilterWaniKaniLevel;
        state.settings.playbackRate = newPlaybackRate;
        state.settings.sentenceSorting = newSentenceSorting;
        state.settings.showFurigana = newShowFurigana;
        state.settings.showJapanese = newShowJapanese;
        state.settings.showEnglish = newShowEnglish;
        state.settings.maxBoxHeight = newMaxBoxHeightt;
        if (maxBoxHeight !== newMaxBoxHeightt) {
            updateContainerMaxBoxHeight();
        }
        if (animeShowsListsDiffer || animeMoviesListsDiffer || ghibliListsDiffer || dramasListsDiffer || gamesListsDiffer || literatureListsDiffer || newsListsDiffer) {
            state.settings.filterAnimeShows = animeShowsListsDiffer ? Object.values(newFilterAnimeShows) : filterAnimeShows;
            state.settings.filterAnimeMovies = animeMoviesListsDiffer ? Object.values(newFilterAnimeMovies) : filterAnimeMovies;
            state.settings.filterGhibli = ghibliListsDiffer ? Object.values(newFilterGhibli) : filterGhibli;
            state.settings.filterDramas = dramasListsDiffer ? Object.values(newFilterDramas) : filterDramas;
            state.settings.filterGames = gamesListsDiffer ? Object.values(newFilterGames) : filterGames;
            state.settings.filterLiterature = literatureListsDiffer ? Object.values(newFilterLiterature) : filterLiterature;
            state.settings.filterNews = newsListsDiffer ? Object.values(newFilterNews) : filterNews;
            updateDesiredShows();
        }
        if (filterExactSearch === newFilterExactSearch && filterJLPTLevel === newFilterJLPTLevel && filterWaniKaniLevel === newFilterWaniKaniLevel) {
            // TODO: Move (sentenceSorting === newSentenceSorting) to the prior if statement when the API sorts the results properly
            if (animeShowsListsDiffer || animeMoviesListsDiffer || ghibliListsDiffer || dramasListsDiffer || gamesListsDiffer || literatureListsDiffer || newsListsDiffer ||
                exampleLimit !== newExampleLimit || playbackRate !== newPlaybackRate || showFurigana !== newShowFurigana ||
                showJapanese !== newShowJapanese || showEnglish !== newShowEnglish || sentenceSorting !== newSentenceSorting) {
                renderSentences(state.immersionKitData);
            }
            return;
        }
        fetchImmersionKitData().then(examples => {
            state.immersionKitData = examples;
            renderSentences(state.immersionKitData);
        });
    }

    //--------------------------------------------------------------------------------------------------------------//
    //-----------------------------------------------STYLES---------------------------------------------------------//
    //--------------------------------------------------------------------------------------------------------------//

    function createStyle() {
        const style = document.createElement("style");
        style.setAttribute("id", "anime-sentences-style");
        // language=CSS
        style.innerHTML = `
            #anime-sentences-parent > div {
                overflow-y: auto;
                max-height: ${state.settings.maxBoxHeight}px;
            }

            #anime-sentences-parent .fa-solid {
                border: none;
                font-size: 100%;
            }

            .anime-example {
                display: flex;
                align-items: center;
                margin-bottom: 1em;
                cursor: pointer;
            }

            .anime-example .audio-btn {
                background-color: transparent;
            }

            .anime-example .audio-btn.audio-idle {
                opacity: 50%;
            }

            /* Make text and background color the same to hide text */
            .anime-example .anime-example-text .show-on-hover, .anime-example-text .show-on-click {
                background: #ccc;
                color: transparent;
                text-shadow: none;
            }

            .anime-example .anime-example-text .show-on-hover:hover {
                background: inherit;
                color: inherit
            }

            /* Color the keyword in the example sentence */
            .anime-example .anime-example-text .ja span keyword {
                color: darkcyan;
            }

            /* Furigana hover */
            .anime-example .anime-example-text .ja .show-ruby-on-hover ruby rt {
                visibility: hidden;
            }

            .anime-example .anime-example-text:hover .ja .show-ruby-on-hover ruby rt {
                visibility: visible;
            }

            .anime-example .title {
                font-weight: var(--font-weight-bold);
            }

            .anime-example .ja {
                font-size: var(--font-size-xlarge);
            }

            .anime-example img {
                margin-right: 1em;
                max-width: 200px;
            }
        `;

        document.querySelector("head").append(style);
    }

    function updateContainerMaxBoxHeight() {
        document.querySelector('#anime-sentences-parent > div').style.maxHeight = `${state.settings.maxBoxHeight}px`;
    }


    //--------------------------------------------------------------------------------------------------------------//
    //----------------------------------------------FURIGANA--------------------------------------------------------//
    //--------------------------------------------------------------------------------------------------------------//
    // https://raw.githubusercontent.com/helephant/Gem/master/src/Gem.Javascript/gem.furigana.js
    function Furigana(reading) {
        this.Segments = ParseFurigana(reading || "");
        this.Reading = (() => ({
            toString: () => {
                let reading = "";
                for (let x = 0; x < this.Segments.length; x++) {
                    reading += this.Segments[x].Reading;
                }
                return reading.trim();
            }
        }))();
        this.Expression = (() => ({
            toString: () => {
                let expression = "";
                for (let x = 0; x < this.Segments.length; x++) expression += this.Segments[x].Expression;
                return expression;
            }
        }))();
        this.Hiragana = (() => ({
            toString: () => {
                let hiragana = "";
                for (let x = 0; x < this.Segments.length; x++) {
                    hiragana += this.Segments[x].Hiragana;
                }
                return hiragana;
            }
        }))();
        this.ReadingHtml = (() => ({
            toString: () => {
                let html = "";
                for (let x = 0; x < this.Segments.length; x++) {
                    html += this.Segments[x].ReadingHtml;
                }
                return html;
            }
        }))();
    }

    function FuriganaSegment(baseText, furigana) {
        this.Expression = baseText;
        this.Hiragana = furigana.trim();
        this.Reading = (() => ({toString: () => `${this.Expression}[${this.Hiragana}]`}))();
        this.ReadingHtml = (() => ({toString: () => `<ruby>${this.Expression}<rp>[</rp><rt>${this.Hiragana}</rt><rp>]</rp></ruby>`}))();
    }

    function UndecoratedSegment(baseText) {
        this.Expression = baseText;
        this.Hiragana = (() => ({toString: () => this.Expression}))();
        this.Reading = (() => ({toString: () => this.Expression}))();
        this.ReadingHtml = (() => ({toString: () => this.Expression}))();
    }

    function ParseFurigana(reading) {
        let currentBase = "", currentFurigana = "", parsingBaseSection = true, parsingHtml = false;
        const segments = [], characters = reading.split('');
        while (characters.length > 0) {
            const current = characters.shift();

            if (current === '[') {
                parsingBaseSection = false;
            } else if (current === '<') {
                parsingHtml = true;
            } else if (current === ']') {
                nextSegment();
            } else if (parsingBaseSection && !parsingHtml && isLastCharacterInBlock(current, characters)) {
                currentBase += current;
                nextSegment();
            } else if (parsingHtml && current === '>') {
                parsingHtml = false;
            } else if (!parsingBaseSection && !parsingHtml) {
                currentFurigana += current;
            } else {
                currentBase += current;
            }
        }

        nextSegment();

        function nextSegment() {
            if (currentBase) segments.push(getSegment(currentBase, currentFurigana));
            currentBase = "";
            currentFurigana = "";
            parsingBaseSection = true;
            parsingHtml = false;
        }

        function getSegment(baseText, furigana) {
            if (!furigana || furigana.trim().length === 0) return new UndecoratedSegment(baseText);
            return new FuriganaSegment(baseText, furigana);
        }

        function isLastCharacterInBlock(current, characters) {
            return !characters.length || (isKanji(current) !== isKanji(characters[0]) && characters[0] !== '[');
        }

        function isKanji(character) {
            return character && character.charCodeAt(0) >= 0x4e00 && character.charCodeAt(0) <= 0x9faf;
        }

        return segments;
    }
})();

Changelog for this modified version:

  • 1.1.3.2: Initial version.
  • 1.1.3.3: Made a bit of a better system for coloring the keywords (previously it would fail when using furigana in various scenarios).
  • 1.1.3.4: Greatly improved the system for coloring the keywords (will now successfully color the keywords in sentences that have random spaces in the middle of a keyword, and it will now properly match longer vocab with mixed okurigana).
  • 1.1.3.5: Fixed small oversight in the segment parser for the colorizer in 1.1.3.4.
  • 1.1.3.6: No longer needlessly queries the API when saving of the settings results in no changes necessary to re-query the API (also similarly won’t recreate the list if no changes have been made). Made a bunch of other code simplifications/optimizations.
  • 1.1.3.7: Added new search categories [dramas, games, literature, news] (all disabled by default) and added many entries for them (news and literature are shown with Japanese titles in the settings; hint: ctrl+a works to select all once a single entry has been selected). Added Mob Psycho 100 to the anime list (despite it being in the “Drama” category on the site for some reason). Changed the order of the selection boxes and resized a few of them. Fixed onclick for the speaker icon. Now allows changing the Example Limit to 0, which makes it return as many results as possible. Improved some array lookups by using hashmaps instead. Fixed and improved the checks for whether the filter lists have been updated or not and fixed some issues that were present when recreating the list; changing the filters now updates the list extremely fast. Most things involving the settings are being handled in a more graceful manner now.
  • 1.1.3.8: Added a setting to configure the max height of the container box. Moved “Only Yesterday” to Ghibli films as was mentioned earlier in the thread. Changed the scriptId referenced by WaniKani Open Framework to be unique to this version of the script (will consequently “reset” settings to default).
  • 1.1.3.9: Added regex.lastIndex=0 before each regex.test(string) usage to prevent inconsistent behavior when using the global regex flag in Javascript; this fixes an issue that would occasionally prevent a keyword from getting identified to be colored.
5 Likes

I can’t seem to see the element to change which animes context sentences are displayed from. I remember it used to be a gear icon in the corner. How can I fix this?

Check my post above you or Maxinoume’s post or my response for alternative solutions you can manually implement for this specific problem.

However, do also take note that for the past few days I’ve noticed some inconsistencies in the backend (I’m guessing the database) for Immersion Kit, resulting in frequently retrieving no results, which is likely your actual issue, since the text is supposed to say No sentences found for your selected movies & shows. if your settings are preventing any that actually exist from showing.

I don’t see any option to configure? I can’t change which series to draw from etc. I only have the WK Open Framework and below that this script. Do I need anything else, like this item injector plugin as well? I don’t see any configuration button or the like anywhere.

You may have answered your own question. @Sinyaven mentioned earlier in this thread that the Item Injector Plugin version was outdated in this script and so installing it locally fixed that issue. I forgot about that since I did it so long ago, but it’s very likely your main problem. (Also, I did update the referenced version in my script modifications but forgot to mention it, so I’ll edit to add that as well)