WaniKani Quick Answers

Popup to display the answers and more

// ==UserScript==
// @name         WaniKani Quick Answers
// @namespace    quick-answers
// @version      1.4.2
// @description  Popup to display the answers and more
// @author       Mystery
// @match        https://www.wanikani.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    /* global wkof */
    'use strict';

    if (!window.wkof) {
        if (confirm('Better Lesson Picker requires Wanikani Open Framework.\nDo you want to be forwarded to the installation instructions')) {
            window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
        }
        return;
    }

    const scriptId = 'quickAnswers';

    let settings, indexedItemData, answerContainer, breakdownContainer, partOfSpeechContainer, infoPopup;
    let infoSet = false;
    const hiraToKata = {"め": "メ", "む": "ム", "ゃ": "ャ", "も": "モ", "ゅ": "ュ", "や": "ヤ", "ょ": "ョ", "ゆ": "ユ", "ら": "ラ", "よ": "ヨ", "る": "ル", "り": "リ", "ろ": "ロ", "れ": "レ", "わ": "ワ", "ん": "ン", "を": "ヲ", "あ": "ア", "い": "イ", "う": "ウ", "え": "エ", "か": "カ", "お": "オ", "き": "キ", "が": "ガ", "く": "ク", "ぎ": "ギ", "け": "ケ", "ぐ": "グ", "こ": "コ", "げ": "ゲ", "さ": "サ", "ご": "ゴ", "し": "シ", "ざ": "ザ", "す": "ス", "じ": "ジ", "せ": "セ", "ず": "ズ", "そ": "ソ", "ぜ": "ゼ", "た": "タ", "ぞ": "ゾ", "ち": "チ", "だ": "ダ", "っ": "ッ", "ぢ": "ヂ", "づ": "ヅ", "つ": "ツ", "で": "デ", "て": "テ", "ど": "ド", "と": "ト", "に": "ニ", "な": "ナ", "ね": "ネ", "ぬ": "ヌ", "は": "ハ", "の": "ノ", "ぱ": "パ", "ば": "バ", "び": "ビ", "ひ": "ヒ", "ふ": "フ", "ぴ": "ピ", "ぷ": "プ", "ぶ": "ブ", "べ": "ベ", "へ": "ヘ", "ほ": "ホ", "ぺ": "ペ", "ぽ": "ポ", "ぼ": "ボ", "み": "ミ", "ま": "マ"};

    let mainLoaded = false;
    wkof.on_pageload(
        [
            /\/subjects\/review.*\/?$/,
            /\/subject-lessons\/[\d-]+\/quiz.*\/?$/,
            /\/recent-mistakes\/[\d-]+\/quiz.*\/?$/,
            /\/subjects\/extra_study.*$/,
        ],
        main,
        unloadMain,
    );

    wkof.on_pageload(
        [
            '/',
            '/dashboard'
        ],
        installSettingsMenu,
    );

    function unloadMain() {
        mainLoaded = false;
    }

    async function installSettingsMenu() {
        await loadSettings();
        wkof.include('Menu');
        await wkof.ready('Menu');
        wkof.Menu.insert_script_link({
            name:      'quick_answers',
            title:     'Quick Answers',
            submenu:   'Settings',
            on_click:  open_settings,
        });
    }

    function open_settings() {
        const config = {
            script_id: scriptId,
            title: 'Quick Answers',
            content: {
                displayGroup: {
                    type: 'group',
                    label: 'Display',
                    content: {
                        show_on_wrong: {
                            type: 'checkbox',
                            label: 'Show On Wrong Answer',
                        },
                        show_on_correct: {
                            type: 'checkbox',
                            label: 'Show On Correct Answer',
                        },
                        show_on_multiple: {
                            type: 'checkbox',
                            label: 'Show On Multiple Answers Available',
                        },
                        show_on_bit_off: {
                            type: 'checkbox',
                            label: 'Show On Your Answer was a bit off',
                        },
                        alt_kanji_readings: {
                            type: 'checkbox',
                            label: 'Also Show Alternative Kanji Readings',
                        },
                        toggle_key: {
                            type: 'input',
                            label: 'Toggle Quick Answer Key',
                        }
                    }
                },
                breakdownGroup: {
                    type: 'group',
                    label: 'Breakdown',
                    content: {
                        show_breakdown: {
                            type: 'checkbox',
                            label: 'Show Breakdown',
                        },
                        show_part_of_speech: {
                            type: 'checkbox',
                            label: 'Show Word type',
                        },
                        breakdown_all_meanings: {
                            type: 'checkbox',
                            label: 'Show All Meanings in Breakdown',
                        },
                    }
                },
                miscGroup: {
                    type: 'group',
                    label: 'Misc',
                    content: {
                        katakana_onyomi: {
                            type: 'checkbox',
                            label: 'Use Katakana for Onyomi',
                        },
                        hide_popup: {
                            type: 'checkbox',
                            label: 'Hide Info Popup',
                        },
                    }
                }
            }
        }

        const dialog = new wkof.Settings(config);
        dialog.open();
    }

    async function loadSettings() {
        const defaults = {
            show__on_wrong: false,
            show_on_correct: false,
            show_on_multiple: true,
            show_on_bit_off: false,
            alt_kanji_readings: false,
            show_breakdown: true,
            show_part_of_speech: true,
            breakdown_all_meanings: true,
            katakana_onyomi: true,
            hide_popup: false,
            toggle_key: 'a',
        };

        wkof.include('Settings');
        await wkof.ready('Settings')
        settings = await wkof.Settings.load(scriptId, defaults);
    }

    async function loadItemData() {
        wkof.include('ItemData');
        await wkof.ready('ItemData');
        await wkof.ItemData.get_items()
            .then((items) => {
            indexedItemData = wkof.ItemData.get_index(items, 'subject_id');
        });
    }

    async function main() {
        if (mainLoaded) {
            return;
        }
        mainLoaded = true;
        await loadSettings();
        if (settings.show_breakdown) {
            await loadItemData();
            insertCss();
            addBreakdownContainer();
        }
        addAnswerContainer();
        setInfoPopup();
        addEventListeners();
    }

    function insertCss() {
        document.head.insertAdjacentHTML('beforeend',`
        <style name="quick_answers" type="text/css">
            .character-header__characters {
                margin-left: 20px;
                margin-right: 20px;
            }
            .qa-character-wrapper {
                width: 100%;
                display: flex;
            }
            .qa-info-container {
                flex: 1;
                margin-bottom: 16px;
                display: flex;
                max-height: 120px;
            }
            .qa-part-of-speech-container {
                align-items: center;
                justify-content: right;
                line-height: 24px;
                font-size: 20px;
            }
            .qa-breakdown-container {
                flex-direction: column;
                justify-content: center;
                gap: 7px;
            }
            .qa-item-container {
                display: flex;
                cursor: pointer;
                width: fit-content;
                font-size: 22px;
            }
            .qa-item-container.qa-radical:hover {
                color: var(--color-radical);
            }
            .qa-item-container.qa-kanji:hover {
                color: var(--color-kanji);
            }
            .qa-radical-image-container {
                width: 22px;
                display: inline-flex;
            }
            .qa-item-container.radical:hover .qa-radical-image {
                --color-text: var(--color-radical);
            }
            .qa-radical-image {
                flex: 0 0 22px;
                --color-text: white;
            }
        </style>
        `);

    }

    function addAnswerContainer() {
        answerContainer = document.createElement('div');
        answerContainer.className = 'quiz-input__input';
        answerContainer.style.display = 'none';
        document.querySelector('#user-response').after(answerContainer);
    }

    function addBreakdownContainer() {
        const wrapper = document.createElement('div');
        wrapper.className = 'qa-character-wrapper';
        partOfSpeechContainer = document.createElement('div');
        partOfSpeechContainer.className = 'qa-part-of-speech-container qa-info-container';
        partOfSpeechContainer.style.visibility = 'hidden';
        const characters = document.querySelector('.character-header__characters');
        breakdownContainer = document.createElement('div');
        breakdownContainer.className = 'qa-breakdown-container qa-info-container';
        breakdownContainer.style.visibility = 'hidden';
        characters.before(wrapper);
        wrapper.append(partOfSpeechContainer, characters, breakdownContainer);
    }

    function setInfoPopup() {
        if (settings.hide_popup) {
            infoPopup = document.querySelector('.answer-exception');
        }
    }

    function addEventListeners() {
        window.addEventListener('didAnswerQuestion', handleDidAnswerQuestion);
        window.addEventListener('willShowNextQuestion', handleWillShowNextQuestion);
        window.addEventListener('didUnanswerQuestion', handleDidUnanswerQuestion);
    }

    function handleDidAnswerQuestion(event) {
        setInfo(event);

        if (shouldShow(event)) {
            toggleOn();
        } else {
            document.addEventListener('keydown', handleKeyDown);
        }
    }

    function setInfo(event) {
        if (!infoSet) {
            setAnswer(event);
            if (settings.show_breakdown) {
                setBreakdown(event);
                if (settings.show_part_of_speech) {
                    setPartOfSpeech(event);
                }
            }
            infoSet = true;
        }
    }

    function handleWillShowNextQuestion() {
        clearInfo();
        reset();
    }

    function handleDidUnanswerQuestion() {
        reset();
    }

    function clearInfo() {
        answerContainer.innerText = '';
        if (settings.show_breakdown) {
            breakdownContainer.innerHTML = '';
            if (settings.show_part_of_speech) {
                partOfSpeechContainer.innerText = '';
            }
        }
        infoSet = false;
    }

    function reset() {
        toggleOff();

        if (settings.hide_popup) {
            infoPopup.style.display = 'block';
        }
        document.removeEventListener('keydown', handleKeyDown);
    }

    function shouldShow(event) {
        const results = event.detail.results;

        if (results.action == 'fail') {
            if (settings.show_on_wrong) {
                hidePopup();
                return true;
            }
            return false;
        }

        const message = results.message;
        if (message) {
            if (message.text.startsWith('Did you know this item has multiple possible') && settings.show_on_multiple) {
                hidePopup();
                return true;
            }
            if (message.text.startsWith('Your answer was a bit off. Check the') && settings.show_on_bit_off) {
                hidePopup();
                return true;
            }
        } else if (settings.show_on_correct) {
            return true;
        }

        return false;
    }

    function hidePopup() {
        if (settings.hide_popup) {
            infoPopup.style.display = 'none';
        }
    }

    function setAnswer(event) {
        const questionType = event.detail.questionType;
        const subject = event.detail.subjectWithStats.subject;

        if (questionType == 'meaning') {
            answerContainer.innerText = subject.meanings.filter(m => m.kind == 'primary' || m.kind == 'alternative').map(m => m.text).join(', ');
        } else {
            const subjectType = subject.type;
            if (subjectType == 'Kanji') {
                const primaryReadingType = subject.readings[0].type;
                answerContainer.innerText = subject.readings.filter(r => settings.alt_kanji_readings || r.type == primaryReadingType).map(r => (r.type == 'onyomi' && settings.katakana_onyomi) ? hiraganaToKatakana(r.text) : r.text).join(', ');
            } else {
                answerContainer.innerText = subject.readings.filter(r => r.kind == 'primary' || r.kind == 'alternative').map(r => r.text).join(', ');
            }
        }

    }

    function setBreakdown(event) {
        const subject = event.detail.subjectWithStats.subject;
        const itemData = indexedItemData[subject.id].data;
        if (subject.type != 'Radical' && subject.type != 'KanaVocabulary') {
            const components = itemData.component_subject_ids.map(id => createItemBreakdown(id, subject.type));
            breakdownContainer.append(...components);
        }
    }


    function createItemBreakdown(itemId, subjectType) {
        const itemContainer = document.createElement('div');
        if (subjectType == 'Vocabulary') {
            itemContainer.className = 'qa-item-container qa-kanji'
        } else if (subjectType == 'Kanji') {
            itemContainer.className = 'qa-item-container qa-radical'
        }
        const data = indexedItemData[itemId].data;
        itemContainer.addEventListener('click', () => {
            window.open(data.document_url, '_blank');
        });
        if (data.characters == null) {
            const characterImgUrl = data.character_images[data.character_images.length - 1].url;
            itemContainer.innerHTML = `<span class="qa-radical-image-container">
                                           <wk-character-image src="${characterImgUrl}" class="qa-radical-image"></wk-character-image>
                                       </span>
                                       <span>: ${toTitleCase(data.slug)}</span>`;
        } else {
            let meanings;
            if (settings.breakdown_all_meanings) {
                meanings = data.meanings.map(m => m.meaning).join(', ');
            } else {
                meanings = data.meanings[0].meaning;
            }
            itemContainer.innerHTML = `<span>${data.characters}: ${meanings}</span>`;
        }
        return itemContainer;

    }

    function setPartOfSpeech(event) {
        const subject = event.detail.subjectWithStats.subject;
        const partOfSpeech = indexedItemData[subject.id].data.parts_of_speech;
        if (settings.show_part_of_speech && partOfSpeech) {
            partOfSpeechContainer.innerText = partOfSpeech.map(toTitleCase).join('\n');
        }
    }

    function hiraganaToKatakana(hiragana) {
        return [...hiragana].map(char => hiraToKata[char] || char).join('');
    }

    function toTitleCase(str) {
        return str
            .toLowerCase()
            .split(/\s+/)
            .map(word => word.charAt(0).toUpperCase() + word.slice(1))
            .join(' ');
    }


    function handleKeyDown(event) {
        if (event.key == settings.toggle_key) {
            toggle();
        }
    }

    function toggleOn() {
        toggleAnswerOn();
        toggleBreakdownOn();
    }

    function toggleOff() {
        toggleAnswerOff();
        toggleBreakdownOff();
    }

    function toggle() {
        if (answerContainer.style.display == 'none') {
            toggleOn();
        } else {
            toggleOff();
        }
    }

    function toggleAnswerOn() {
        answerContainer.style.display = 'block';
    }

    function toggleAnswerOff() {
        answerContainer.style.display = 'none';
    }

    function toggleBreakdownOn() {
        if (settings.show_breakdown) {
            breakdownContainer.style.visibility = 'visible';
            if (settings.show_part_of_speech) {
                partOfSpeechContainer.style.visibility = 'visible';
            }
        }
    }

    function toggleBreakdownOff() {
        if (settings.show_breakdown) {
            breakdownContainer.style.visibility = 'hidden';
            if (settings.show_part_of_speech) {
                partOfSpeechContainer.style.visibility = 'hidden';
            }
        }
    }

})();