WK Custom SRS

Add custom word packs to WaniKani!

当前为 2024-03-18 提交的版本,查看 最新版本

// ==UserScript==
// @name         WK Custom SRS
// @namespace    leohumnew.wk
// @version      0.3.5
// @description  Add custom word packs to WaniKani!
// @author       leohumnew
// @match        https://www.wanikani.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=wanikani.com
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM_xmlhttpRequest
// @run-at       document-start
// @require      https://gf.qytechs.cn/scripts/489759-wk-custom-icons/code/CustomIcons.js?version=1344498


// ==/UserScript==
(async() => {
// ------------------------ Define and create custom HTML structures ------------------------
const srsNames = ["Lesson", "Apprentice 1", "Apprentice 2", "Apprentice 3", "Apprentice 4", "Guru 1", "Guru 2", "Master", "Enlightened", "Burned"];

// --------- Main popup ---------
let overviewPopup = document.createElement("dialog");
overviewPopup.id = "overview-popup";

let overviewPopupStyle = document.createElement("style");
// Styles copied in from styles.css
overviewPopupStyle.innerHTML = /*css*/ `
/* General styling */
.content-box {
    background-color: var(--color-wk-panel-background);
    border-radius: 3px;
    padding: 1rem;
}

/* Main popup styling */
#overview-popup {
    background-color: var(--color-menu, white);
    width: 60%;
    max-width: 50rem;
    height: 50%;
    max-height: 40rem;
    border: none;
    border-radius: 3px;
    box-shadow: 0 0 1rem rgb(0 0 0 / .5);

    &:focus-visible {
        border: none }
    & p {
        margin: 0 }
    & input, & select {
        margin-bottom: 0.5rem }
    & button {
        cursor: pointer;
        background-color: transparent;
        border: none;

        &[type="submit"], &.outline-button {
            border: 1px solid var(--color-text);
            border-radius: 5px;
            padding: 0.2rem 0.8rem;
        }
    }
    & button:hover {
        color: var(--color-tertiary, #a5a5a5);

        &[type="submit"], &.outline-button {
            border-color: var(--color-tertiary, #a5a5a5) }
    }
    & > header {
        display: flex;
        justify-content: space-between;
        border-bottom: 1px solid var(--color-tertiary, --color-text);
        margin-bottom: 1rem;

        & > h1 {
            font-size: x-large;
            color: var(--color-tertiary, --color-text);
        }
        & > button {
            border: none;
            color: var(--color-tertiary, --color-text);
            font-size: x-large;

            &:hover {
                color: var(--color-text) }
            &:focus-visible {
                outline: none }
        }
    }
    &::backdrop {
        background-color: rgba(0, 0, 0, 0.5) }
}

/* Styling for top tabs */
#tabs {
    display: flex;
    flex-wrap: wrap;

    & > input {
        display: none }
    & > label {
        cursor: pointer;
        padding: 0.5rem 1rem;
        max-width: 20%;
    }
    & > div {
        display: none;
        padding: 1rem;
        order: 1;
        width: 100%;
    }
    & > input:checked + label {
        color: var(--color-tertiary, --color-text);
        border-bottom: 2px solid var(--color-tertiary, gray);
    }
    & > input:checked + label + div {
        display: initial }
}

/* Styling for the overview tab */
#tab-1__content > div {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 1.5rem;
    text-align: center;

    & p {
        font-size: xx-large }
}

/* Styling for packs in the packs tab */
#tabs .pack {
    display: flex;
    justify-content: space-between;
    margin-bottom: 1rem;

    & span {
        font-style: italic }
    & > div {
        margin: auto 0 }
    & button {
        margin-left: 10px }
    & > div > span, & > div > input {
        margin: 0;
        vertical-align: middle;
    }
}

/* Styling for the pack edit tab */
#tab-3__content > .content-box {
    margin: 1rem 0;

    & input, & ul {
        margin-left: 0 }
    & > div {
        margin-top: 1.5rem }
    & div {
        display: flex;
        justify-content: space-between;
        margin-bottom: 0.5rem;
    }
    & li {
        margin: 0.25rem;
        justify-content: space-between;
        display: flex;

        & button {
            margin-left: 10px }
    }
    & li:hover {
        color: var(--color-tertiary, rgb(165, 165, 165));
    }
}
#tab-3__content:has(#pack-select [value="new"]:checked) .content-box :is(div, ul) {
    display: none }
#tab-3__content:has(#pack-select [value="import"]:checked) .pack-box {
    display: none }
#tab-3__content:has(#pack-select [value="import"]:checked) .import-box {
    display: grid !important }
#tab-3__content:has(#pack-lvl-type [value="internal"]:checked) .pack-lvl-specific {
    display: grid !important }
#tab-3__content:has(#pack-lvl-type [value="wk"]:checked) .wk-lvl-warn {
    display: grid !important }

#pack-items {
    background-color: var(--color-menu, white) }

/* Styling for the item edit tab */
#tab-4__content:has(#item-type [value="Vocabulary"]:checked) .item-vocab-specific {
    display: grid !important }
#tab-4__content:has(#item-type [value="KanaVocabulary"]:checked) .item-kanavocab-specific {
    display: grid !important }
#tab-4__content:has(#item-type [value="Kanji"]:checked) .item-kanji-specific {
    display: grid !important }
#tab-4__content .content-box, #tab-4__content .content-box > div, #tab-3__content > .content-box, #tab-3__content > .content-box > div {
    display: grid;
    gap: 0.5rem;
    grid-template-columns: 1fr 1fr;
    align-items: center;

    & input, & select {
        justify-self: end }
}

/* Styling for the settings tab */
#tab-5__content {
    & label {
        margin-right: 1rem;
        float: left;
    }
}
`;

overviewPopup.innerHTML = /*html*/ `
    <header>
        <h1>WaniKani Custom SRS</h1>
        <button class="close-button" onclick="document.getElementById('overview-popup').close();">${Icons.customIconTxt("cross")}</button>
    </header>
    <div id="tabs">
        <input type="radio" name="custom-srs-tab" id="tab-1" checked>
        <label for="tab-1">Overview</label>
        <div id="tab-1__content">
            <div>
                <div class="content-box">
                    <h2>Lessons</h2>
                    <p>0</p>
                </div>
                <div class="content-box">
                    <h2>Reviews</h2>
                    <p></p>
                </div>
            </div>
        </div>

        <input type="radio" name="custom-srs-tab" id="tab-2">
        <label for="tab-2">Packs</label>
        <div id="tab-2__content"></div>

        <input type="radio" name="custom-srs-tab" id="tab-3">
        <label for="tab-3">Edit Pack</label>
        <div id="tab-3__content">
            <label for="pack-select">Pack:</label>
            <select id="pack-select"></select><br>
            <form class="content-box pack-box">
                <label for="pack-name">Name:</label>
                <input id="pack-name" required type="text">
                <label for="pack-author">Author: </label>
                <input id="pack-author" required type="text">
                <label for="pack-version">Version:</label>
                <input id="pack-version" required type="number" step="0.1">
                <label for="pack-lvl-type">Pack Levelling Type:</label>
                <select id="pack-lvl-type" required>
                    <option value="none">No Levels</option>
                    <option value="internal">Pack Levels</option>
                    <option value="wk">WaniKani Levels</option>
                </select>
                <div class="wk-lvl-warn" style="display: none; grid-column: 1 / span 2; margin: 0; color: red">
                    <p style="grid-column: 1 / span 2"><i>Warning: Make sure API key is set in Custom SRS settings.</i></p>
                </div>
                <div class="pack-lvl-specific" style="display: none; grid-column: 1 / span 2; margin: 0">
                    <label for="pack-lvl">Pack Level (start at 1 recommended):</label>
                    <input id="pack-lvl" required type="number">
                </div>
                <div style="grid-column: 1 / span 2">
                    <p>Items:</p>
                    <button id="new-item-button" title="Add Item" type="button" style="margin-left: auto">${Icons.customIconTxt("plus")}</button>
                </div>
                <ul style="grid-column: 1 / span 2" class="content-box" id="pack-items"></ul>
                <button style="grid-column: 1 / span 2" type="submit">Save</button>
            </form>
            <form class="content-box import-box" style="display: none;">
                <label for="item-type">Paste Pack JSON here:</label>
                <textarea id="pack-import" required></textarea>
                <button style="grid-column: 1 / span 2" type="submit">Import</button>
            </form>
        </div>

        <input type="radio" name="custom-srs-tab" id="tab-4">
        <label for="tab-4">Edit Item</label>
        <div id="tab-4__content">
            <div>Select item from Pack edit tab.</div>
            <form class="content-box" style="display: none;">
                <label for="item-type">Type:</label>
                <select id="item-type">
                    <option value="Radical">Radical</option>
                    <option value="Kanji">Kanji</option>
                    <option value="Vocabulary">Vocabulary</option>
                    <option value="KanaVocabulary">Kana Vocabulary</option>
                </select>
                <label for="item-characters">Characters:</label>
                <input id="item-characters" required type="text">
                <label for="item-meanings">Meanings (comma separated):</label>
                <input id="item-meanings" required type="text">
                <div class="item-vocab-specific item-kanavocab-specific" style="display: none; grid-column: 1 / span 2">
                    <label for="item-readings">Readings (comma separated):</label>
                    <input id="item-readings" type="text">
                </div>
                <div class="item-kanji-specific" style="display: none; grid-column: 1 / span 2">
                    <label for="knaji-primary-reading">Primary Reading:</label>
                    <select id="kanji-primary-reading">
                        <option value="onyomi">On'yomi</option>
                        <option value="kunyomi">Kun'yomi</option>
                        <option value="nanori">Nanori</option>
                    </select>
                    <p style="grid-column: 1 / span 2"><i>Please enter at least one of the three readings:</i></p>
                    <label for="kanji-onyomi">On'yomi:</label>
                    <input id="kanji-onyomi" type="text">
                    <label for="kanji-kunyomi">Kun'yomi:</label>
                    <input id="kanji-kunyomi" type="text">
                    <label for="kanji-nanori">Nanori:</label>
                    <input id="kanji-nanori" type="text">
                </div>
                <label for="item-srs-stage">SRS Stage:</label>
                <select id="item-srs-stage">
                    <option value="0">Lesson</option>
                    <option value="1">Apprentice 1</option>
                    <option value="2">Apprentice 2</option>
                    <option value="3">Apprentice 3</option>
                    <option value="4">Apprentice 4</option>
                    <option value="5">Guru 1</option>
                    <option value="6">Guru 2</option>
                    <option value="7">Master</option>
                    <option value="8">Enlightened</option>
                    <option value="9">Burned</option>
                </select>
                <h3 style="grid-column: 1 / span 2">Optional</h3> <!-- Optional elements -->
                <label for="item-level">Item Unlock Level:</label>
                <input id="item-level" type="number">
                <label for="item-meaning-explanation">Meaning Explanation:</label>
                <input id="item-meaning-explanation" type="text">
                <div class="item-kanji-specific item-vocab-specific" style="display: none; grid-column: 1 / span 2">
                    <label for="item-reading-explanation">Reading Explanation:</label>
                    <input id="item-reading-explanation" type="text">
                </div>
                <div class="item-kanavocab-specific item-vocab-specific" style="display: none; grid-column: 1 / span 2">
                    <label for="item-context-sentences">Context Sentences (comma separated - each jp,en):</label>
                    <input id="item-context-sentences" type="text">
                </div>
                <button style="grid-column: 1 / span 2" type="submit">Add</button>
            </form>
        </div>

        <input type="radio" name="custom-srs-tab" id="tab-5">
        <label for="tab-5">Settings</label>
        <div id="tab-5__content">
            <label for="settingsShowDueTime">Show item due times</label>
            <input type="checkbox" id="settingsShowDueTime" checked><br>
            <label for="settingsExportSRSData">Include SRS data in exports</label>
            <input type="checkbox" id="settingsExportSRSData"><br>
            <label for="settingsItemQueueMode">Position to insert custom items in reviews</label>
            <select id="settingsItemQueueMode">
                <option value="start">Start</option>
                <option value="weighted-start">Random, weighted towards start</option>
                <option value="random">Random</option>
            </select><br>
            <label for="settingsWKAPIKey">WaniKani API Key</label>
            <input type="text" id="settingsWKAPIKey" placeholder="API key">
        </div>
    </div>
`;

// --------- Popup open button ---------
let overviewPopupButton, buttonLI;
if (window.location.pathname.includes("/dashboard") || window.location.pathname === "/") {
    overviewPopupButton = document.createElement("button");
    overviewPopupButton.classList = "sitemap__section-header";
    overviewPopupButton.style = `
        display: flex;
        align-items: center;
    `;
    let buttonSpan = document.createElement("span");
    buttonSpan.classList = "font-sans";
    buttonSpan.innerText = "WK Custom SRS";
    overviewPopupButton.appendChild(buttonSpan);
    buttonLI = document.createElement("li");
    buttonLI.classList = "sitemap__section";
    buttonLI.appendChild(overviewPopupButton);
    overviewPopupButton.title = "Custom SRS";
    overviewPopupButton.onclick = () => {
        changeTab(1);
        overviewPopup.showModal();
    };

    // --------- Add custom elements to page ---------
    document.addEventListener("DOMContentLoaded", () => {
        document.head.appendChild(overviewPopupStyle);
        document.body.appendChild(overviewPopup);
        // Add event listeners for buttons etc.
        for(let i = 1; i <= 5; i++) {
            document.querySelector(`#tab-${i}`).onchange = () => {
                changeTab(i) };
        }
        document.querySelector("#pack-select").onchange = () => {
            loadPackEditDetails(document.querySelector("#pack-select").value) };
        document.querySelector("#new-item-button").onclick = () => {
            changeTab(4, null) };
        // Add popup button to page
        if (window.location.pathname.includes("/dashboard") || window.location.pathname === "/") {
            document.querySelector("#sitemap").prepend(buttonLI);
        }
    });
}


// ---------- Change tab ----------
function changeTab(tab, data) {
    document.querySelector(`#tab-${tab}`).checked = true;
    switch(tab) {
        case 1:
            updateOverviewTab();
            break;
        case 2:
            updatePacksTab();
            break;
        case 3:
            updateEditPackTab(data);
            break;
        case 4:
            updateEditItemTab(data);
            break;
        case 5:
            updateSettingsTab();
            break;
    }
}

// ---------- Update popup content ----------
function updateOverviewTab() {
    //document.querySelector("#tab-1__content .content-box:first-child p").innerText = activePackProfile.getActiveLessons().length;
    document.querySelector("#tab-1__content .content-box:last-child p").innerText = activePackProfile.getNumActiveReviews();
}

function updatePacksTab() {
    let packsTab = document.querySelector("#tab-2__content");
    packsTab.innerHTML = "";
    for(let i = 0; i < activePackProfile.customPacks.length; i++) {
        let pack = activePackProfile.customPacks[i];
        let packElement = document.createElement("div");
        packElement.classList = "pack content-box";
        packElement.innerHTML = /*html*/ `
            <h3>${pack.name}: <span>${pack.items.length} items</span><br><span>${pack.author}</span></h3>
            <div>
                <span>Active: </span>
                <input type="checkbox" id="pack-${i}-active" ${pack.active ? "checked" : ""}>
                <button class="edit-pack" title="Edit Pack">${Icons.customIconTxt("edit")}</button>
                <button class="export-pack" title="Export Pack">${Icons.customIconTxt("download")}</button>
                <button class="delete-pack" title="Delete Pack">${Icons.customIconTxt("cross")}</button>
            </div>
        `;
        packElement.querySelector(".edit-pack").onclick = () => { // Pack edit button
            changeTab(3, i);
        };
        packElement.querySelector(".export-pack").onclick = () => { // Pack export button to make JSON and then copy it to the clipboard
            let data = StorageManager.packToJSON(activePackProfile.customPacks[i]);
            navigator.clipboard.writeText(data).then(() => {
                alert("Pack JSON copied to clipboard");
            });
        };
        packElement.querySelector(".delete-pack").onclick = () => { // Pack delete button
            activePackProfile.removePack(i);
            StorageManager.savePackProfile(activePackProfile, "main");
            changeTab(2);
        };
        packElement.querySelector(`#pack-${i}-active`).onchange = () => { // Pack active checkbox
            activePackProfile.customPacks[i].active = !activePackProfile.customPacks[i].active;
            StorageManager.savePackProfile(activePackProfile, "main");
        };
        packsTab.appendChild(packElement);
    }
    // New pack button
    let newPackButton = document.createElement("button");
    newPackButton.classList = "outline-button";
    newPackButton.style = "width: 48%";
    newPackButton.innerHTML = "New Pack";
    newPackButton.onclick = () => {
        changeTab(3, "new");
    };
    let importPackButton = document.createElement("button");
    importPackButton.classList = "outline-button";
    importPackButton.style = "width: 48%; float: right;";
    importPackButton.innerHTML = "Import Pack";
    importPackButton.onclick = () => {
        changeTab(3, "import");
    };
    packsTab.append(newPackButton, importPackButton);
}

function updateEditPackTab(editPack) {
    let packSelect = document.querySelector("#pack-select");
    packSelect.innerHTML = "<option value='new'>New Pack</option><option value='import'>Import Pack</option>";
    for(let i = 0; i < activePackProfile.customPacks.length; i++) {
        let pack = activePackProfile.customPacks[i];
        packSelect.innerHTML += `<option value="${i}">${pack.name} - ${pack.author}</option>`;
    }
    if(editPack !== undefined) packSelect.value = editPack;
    else packSelect.value = "new";
    packSelect.onchange();
}

function updateEditItemTab(editItem) {
    if(editItem !== undefined) {
        // Show add item edit tab and make sure inputs are empty
        document.querySelector("#tab-4__content > form").style.display = "grid";
        document.querySelector("#tab-4__content > div").style.display = "none";
        if(editItem !== null) {
            let editItemInfo = activePackProfile.customPacks[document.querySelector("#pack-select").value].getItem(editItem).info;
            document.querySelector("#item-srs-stage").value = editItemInfo.srs_lvl;
            document.querySelector("#item-type").value = editItemInfo.type;
            document.querySelector("#item-characters").value = editItemInfo.characters;
            document.querySelector("#item-meanings").value = editItemInfo.meanings.join(", ");
            if(editItemInfo.lvl) document.querySelector("#item-level").value = editItemInfo.lvl;
            if(editItemInfo.meaning_expl) document.querySelector("#item-meaning-explanation").value = editItemInfo.meaning_expl;
            if(editItemInfo.readings) document.querySelector("#item-readings").value = editItemInfo.readings.join(", ");
            if(editItemInfo.primary_reading_type) document.querySelector("#kanji-primary-reading").value = editItemInfo.primary_reading_type;
            if(editItemInfo.onyomi) document.querySelector("#kanji-onyomi").value = editItemInfo.onyomi.join(", ");
            if(editItemInfo.kunyomi) document.querySelector("#kanji-kunyomi").value = editItemInfo.kunyomi.join(", ");
            if(editItemInfo.nanori) document.querySelector("#kanji-nanori").value = editItemInfo.nanori.join(", ");
            if(editItemInfo.reading_expl) document.querySelector("#item-reading-explanation").value = editItemInfo.reading_expl;
            if(editItemInfo.context_sentences) document.querySelector("#item-context-sentences").value = editItemInfo.context_sentences.join(", ");
            document.querySelector("#tab-4__content button[type='submit']").innerText = "Save";
        } else {
            ["item-context-sentences", "item-reading-explanation", "item-meaning-explanation", "item-characters", "item-meanings", "item-readings", "kanji-onyomi", "kanji-kunyomi", "item-level", "kanji-nanori"].forEach((s) => {
                document.getElementById(s).value = "";
            });
            document.querySelector("#tab-4__content button[type='submit']").innerText = "Add";
            document.querySelector("#item-srs-stage").value = "0";
        }
        // Add event listener to form
        document.querySelector("#tab-4__content form").onsubmit = (e) => {
            e.preventDefault();

            let itemType = document.querySelector("#item-type").value;

            let infoStruct = {
                type: itemType,
                characters: document.querySelector("#item-characters").value,
                meanings: document.querySelector("#item-meanings").value.split(",").map(s => s.trim()),
                srs_lvl: document.querySelector("#item-srs-stage").value
            };
            if(document.querySelector("#item-meaning-explanation").value != "") infoStruct.meaning_expl = document.querySelector("#item-meaning-explanation").value;
            if(document.querySelector("#item-level").value != "") infoStruct.lvl = parseInt(document.querySelector("#item-level").value);

            let pack = activePackProfile.customPacks[document.querySelector("#pack-select").value];

            // Add or edit item
            switch(itemType) {
                case "Radical":
                    infoStruct.category = infoStruct.type;
                    break;
                case "Kanji":
                    infoStruct.category = infoStruct.type;
                    infoStruct.primary_reading_type = document.querySelector("#kanji-primary-reading").value;
                    if(document.querySelector("#kanji-onyomi").value != "") infoStruct.onyomi = document.querySelector("#kanji-onyomi").value.split(",").map(s => s.trim());
                    if(document.querySelector("#kanji-kunyomi").value != "") infoStruct.kunyomi = document.querySelector("#kanji-kunyomi").value.split(",").map(s => s.trim());
                    if(document.querySelector("#kanji-nanori").value != "") infoStruct.nanori = document.querySelector("#kanji-nanori").value.split(",").map(s => s.trim());
                    if(document.querySelector("#item-reading-explanation").value != "") infoStruct.reading_expl = document.querySelector("#item-reading-explanation").value;
                    break;
                case "Vocabulary":
                    infoStruct.category = infoStruct.type;
                    infoStruct.readings = document.querySelector("#item-readings").value.split(",").map(s => s.trim());
                    if(document.querySelector("#item-reading-explanation").value != "") infoStruct.reading_expl = document.querySelector("#item-reading-explanation").value;
                    if(document.querySelector("#item-context-sentences").value != "") infoStruct.context_sentences = document.querySelector("#item-context-sentences").value.split(",").map(s => s.trim());
                    break;
                case "KanaVocabulary":
                    infoStruct.category = "Vocabulary";
                    infoStruct.readings = document.querySelector("#item-readings").value.split(",").map(s => s.trim());
                    if(document.querySelector("#item-context-sentences").value != "") infoStruct.context_sentences = document.querySelector("#item-context-sentences").value.split(",").map(s => s.trim());
                    break;
                default:
                    console.error("Invalid item type");
                    return;
            }
            if(editItem !== null) pack.editItem(editItem, infoStruct);
            else pack.addItem(infoStruct);

            document.querySelector("#tab-4__content > form").style.display = "none";
            document.querySelector("#tab-4__content > div").style.display = "block";
            loadPackEditDetails(document.querySelector("#pack-select").value);
            StorageManager.savePackProfile(activePackProfile, "main");
            changeTab(3, document.querySelector("#pack-select").value);
        };
    } else {
        // Hide add item edit tab
        document.querySelector("#tab-4__content > form").style.display = "none";
        document.querySelector("#tab-4__content > div").style.display = "block";
    }
}

function updateSettingsTab() {
    document.querySelector("#settingsShowDueTime").checked = CustomSRSSettings.userSettings.showItemDueTime;
    document.querySelector("#settingsShowDueTime").onchange = () => {
        CustomSRSSettings.userSettings.showItemDueTime = document.querySelector("#settingsShowDueTime").checked;
        StorageManager.saveSettings();
    };
    document.querySelector("#settingsItemQueueMode").value = CustomSRSSettings.userSettings.itemQueueMode ? CustomSRSSettings.userSettings.itemQueueMode : "start";
    document.querySelector("#settingsItemQueueMode").onchange = () => {
        CustomSRSSettings.userSettings.itemQueueMode = document.querySelector("#settingsItemQueueMode").value;
        StorageManager.saveSettings();
    };
    document.querySelector("#settingsExportSRSData").checked = CustomSRSSettings.userSettings.exportSRSData;
    document.querySelector("#settingsExportSRSData").onchange = () => {
        CustomSRSSettings.userSettings.exportSRSData = document.querySelector("#settingsExportSRSData").checked;
        StorageManager.saveSettings();
    };
    document.querySelector("#settingsWKAPIKey").value = CustomSRSSettings.userSettings.apiKey;
    document.querySelector("#settingsWKAPIKey").onchange = () => {
        CustomSRSSettings.userSettings.apiKey = document.querySelector("#settingsWKAPIKey").value;
        StorageManager.saveSettings();
    };
}

// ---------- Tabs details ----------
function loadPackEditDetails(i) {
    let packNameInput = document.querySelector("#pack-name");
    let packAuthorInput = document.querySelector("#pack-author");
    let packVersionInput = document.querySelector("#pack-version");
    let packLvlTypeInput = document.querySelector("#pack-lvl-type");
    let packLvlInput = document.querySelector("#pack-lvl");
    let packItems = document.querySelector("#pack-items");
    let importBox = document.querySelector("#pack-import");
    if(i === "new") { // If creating a new pack
        packNameInput.value = "";
        packAuthorInput.value = "";
        packVersionInput.value = 0.1;
        packLvlTypeInput.value = "none";
        packLvlInput.value = 1;
    } else if(i === "import") { // If importing a pack
        importBox.value = "";
    } else { // If editing an existing pack
        let pack = activePackProfile.customPacks[i];
        packNameInput.value = pack.name;
        packAuthorInput.value = pack.author;
        packVersionInput.value = pack.version;
        packLvlTypeInput.value = pack.lvlType;
        packLvlInput.value = pack.lvl;
        packItems.innerHTML = "";
        for(let j = 0; j < pack.items.length; j++) {
            let item = pack.items[j];
            let itemElement = document.createElement("li");
            itemElement.classList = "pack-item";
            itemElement.innerHTML = `
                ${item.info.characters} - ${item.info.meanings[0]} - ${item.info.type} ${CustomSRSSettings.userSettings.showItemDueTime ? "- Due: " + pack.getItemTimeUntilReview(j) : ""}
                <div>
                    <button class="edit-item" title="Edit Item" type="button">${Icons.customIconTxt("edit")}</button>
                    <button class="delete-item" title="Delete Item" type="button">${Icons.customIconTxt("cross")}</button>
                </div>
            `;
            itemElement.querySelector(".edit-item").onclick = () => { // Item edit button
                changeTab(4, item.id);
            };
            itemElement.querySelector(".delete-item").onclick = () => { // Item delete button
                pack.removeItem(j);
                loadPackEditDetails(i);
            };
            packItems.appendChild(itemElement);
        }
    }
    document.querySelector("#tab-3__content form.pack-box").onsubmit = (e) => { // Pack save button
        e.preventDefault();

        if(i === "new") {
            let pack = new CustomItemPack(packNameInput.value, packAuthorInput.value, packVersionInput.value, packLvlTypeInput.value, parseInt(packLvlInput.value));
            activePackProfile.addPack(pack);
            changeTab(3, activePackProfile.customPacks.length - 1);
        } else {
            activePackProfile.customPacks[i].name = packNameInput.value;
            activePackProfile.customPacks[i].author = packAuthorInput.value;
            activePackProfile.customPacks[i].version = packVersionInput.value;
            activePackProfile.customPacks[i].lvlType = packLvlTypeInput.value;
            activePackProfile.customPacks[i].lvl = packLvlInput.value;
        }
        StorageManager.savePackProfile(activePackProfile, "main");
        changeTab(2);
    };
    document.querySelector("#tab-3__content form.import-box").onsubmit = (e) => { // Pack import button
        e.preventDefault();
        let pack = JSON.parse(importBox.value);

        let packExistingStatus = activePackProfile.doesPackExist(pack.name, pack.author, pack.version); // Check if pack already exists
        if(packExistingStatus == "exists") {
            alert("Import failed: A pack with the same name, author, and version already exists.");
        } else if(packExistingStatus == "no") {
            activePackProfile.addPack(StorageManager.packFromJSON(pack));
            StorageManager.savePackProfile(activePackProfile, "main");
            changeTab(2);
        } else {
            if(confirm("A pack with the same name and author but different version already exists. Do you want to update it?")) {
                activePackProfile.updatePack(packExistingStatus, pack);
                StorageManager.savePackProfile(activePackProfile, "main");
                changeTab(2);
            }
        }
    };
}

// ---------- Item details ----------
function buildContextSentencesHTML(ctxArray) {
    let out = "";
    for(let i = 0; i < ctxArray.length; i += 2) {
        out += `
        <div class="subject-section__text subject-section__text--grouped">
            <p lang="ja">${ctxArray[i]}</p>
            <p>${ctxArray[i+1]}</p>
        </div>
        `;
    }
    return out;
}
function makeDetailsHTML(item) {
    switch(item.info.type) {
        case "Radical":
        return /*html*/ `
        <turbo-frame class="subject-info" id="subject-info">
            <div class="container">
                <section class="subject-section subject-section--meaning subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{&quot;auto_expand_question_types&quot;:[&quot;meaning&quot;]}">
                    <a class='wk-nav__anchor' id='information'></a>
                    <h2 class='subject-section__title'>
                        <a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-meaning">
                            <span class="subject-section__toggle-icon">${Icons.customIconTxt("chevron-right")}</span>
                            <span class='subject-section__title-text'>Name</span>
                        </a>
                    </h2>
                    <section id="section-meaning" class="subject-section__content" data-toggle-target="content" hidden="hidden">
                        <section class="subject-section__subsection">
                            <div class='subject-section__meanings'>
                                <h2 class='subject-section__meanings-title'>Primary</h2>
                                <p class='subject-section__meanings-items'>${item.info.meanings[0]}</p>
                            </div>
                            ${item.info.meanings.length > 1 ? `
                            <div class="subject-section__meanings">
                                <h2 class="subject-section__meanings-title">Alternatives</h2>
                                <p class="subject-section__meanings-items">${item.info.meanings.slice(1).join(', ')}</p>
                            </div>` : ''}
                            <!--<div class='subject-section__meanings'>
                                <h2 class='subject-section__meanings-title'>User Synonyms</h2>
                                <p class='subject-section__meanings-items'><i>User synonyms are currently disabled for custom items.</i></p>
                            </div>-->
                        </section>
                        <section class="subject-section__subsection">
                            <h3 class='subject-section__subtitle'>Mnemonic</h3>
                            <p class="subject-section__text">${item.info.meaning_expl ? item.info.meaning_expl : "This item does not have a meaning explanation. Good luck!"}</p>
                            <!--<aside class="subject-hint">
                                <h3 class="subject-hint__title">
                                    <i class="subject-hint__title-icon" aria-hidden="true">${Icons.customIconTxt("circle-info")}</i>
                                    <span class="subject-hint__title-text">Hints</span>
                                </h3>
                                <p class="subject-hint__text"></p>
                            </aside>-->
                        </section>
                        <section class="subject-section__subsection">
                            <h3 class='subject-section__subtitle'>Note</h3>
                            <p class="subject-section__text"><i>Notes are currently disabled for custom items.</i></p>
                        </section>
                    </section>
                </section>

                <section class="subject-section subject-section--amalgamations subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{&quot;auto_expand_question_types&quot;:[]}">
                    <a class='wk-nav__anchor' id='amalgamations'></a>
                    <h2 class='subject-section__title'>
                        <a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-amalgamations">
                            <span class="subject-section__toggle-icon">${Icons.customIconTxt("chevron-right")}</span>
                            <span class='subject-section__title-text'>Found In Kanji</span>
                        </a>
                    </h2>
                    <section id="section-amalgamations" class="subject-section__content" data-toggle-target="content" hidden="hidden">
                        <div class="subject-character-grid">
                            <ol class="subject-character-grid__items">
                                <!--<li class="subject-character-grid__item">
                                    <a class="subject-character subject-character--kanji subject-character--grid subject-character--burned" title="じょう" href="https://www.wanikani.com/kanji/%E4%B8%8A" data-turbo-frame="_blank">
                                        <div class="subject-character__content">
                                            <span class="subject-character__characters" lang="ja">上</span>
                                            <div class="subject-character__info">
                                                <span class="subject-character__reading">じょう</span>
                                                <span class="subject-character__meaning">Above</span>
                                            </div>
                                        </div>
                                    </a>
                                </li>-->
                            </ol>
                        </div>
                    </section>
                </section>
            </div>
        </turbo-frame>
        `;
        case "Kanji":
        return /*html*/ `
        <turbo-frame class="subject-info" id="subject-info">
            <div class="container">
                <!-- Radical combination -->
                <section class="subject-section subject-section--components subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{&quot;auto_expand_question_types&quot;:[&quot;meaning&quot;]}">
                    <h2 class="subject-section__title">
                        <a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-components" data-controller-connected="true">
                            <span class="subject-section__toggle-icon" aria-hidden="true">${Icons.customIconTxt("chevron-right")}</span>
                            <span class="subject-section__title-text">Radical Combination</span>
                        </a>
                    </h2>
                    <section id="section-components" class="subject-section__content" data-toggle-target="content">
                        <div class="subject-list subject-list--with-separator">
                            <ul class="subject-list__items">
                                <!--<li class="subject-list__item">
                                    <a class="subject-character subject-character--radical subject-character--small-with-meaning subject-character--burned subject-character--expandable" title="Head" href="https://www.wanikani.com/radicals/head" data-turbo-frame="_blank">
                                        <div class="subject-character__content">
                                            <span class="subject-character__characters" lang="ja">冂</span>
                                            <div class="subject-character__info">
                                                <span class="subject-character__meaning">Head</span>
                                            </div>
                                        </div>
                                    </a>
                                </li>-->
                            </ul>
                        </div>
                    </section>
                </section>
                <!-- Meaning -->
                <section class="subject-section subject-section--meaning subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{&quot;auto_expand_question_types&quot;:[&quot;meaning&quot;]}">
                    <a class='wk-nav__anchor' id='meaning'></a>
                    <h2 class='subject-section__title'>
                        <a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-meaning">
                            <span class="subject-section__toggle-icon">${Icons.customIconTxt("chevron-right")}</span>
                            <span class='subject-section__title-text'>Meaning</span>
                        </a>
                    </h2>
                    <section id="section-meaning" class="subject-section__content" data-toggle-target="content" hidden="hidden">
                        <section class="subject-section__subsection">
                            <div class='subject-section__meanings'>
                                <h2 class='subject-section__meanings-title'>Primary</h2>
                                <p class='subject-section__meanings-items'>${item.info.meanings[0]}</p>
                            </div>
                            ${item.info.meanings.length > 1 ? `
                            <div class="subject-section__meanings">
                                <h2 class="subject-section__meanings-title">Alternative</h2>
                                <p class="subject-section__meanings-items">${item.info.meanings.slice(1).join(', ')}</p>
                            </div>` : ''}
                            <!--<div class='subject-section__meanings'>
                                <h2 class='subject-section__meanings-title'>User Synonyms</h2>
                                <p class='subject-section__meanings-items'><i>User synonyms are currently disabled for custom items.</i></p>
                            </div>-->
                        </section>
                        <section class="subject-section__subsection">
                            <h3 class='subject-section__subtitle'>Mnemonic</h3>
                            <p class="subject-section__text">${item.info.meaning_expl ? item.info.meaning_expl : "This item does not have a reading explanation. Good luck!"}</p>
                            <!--<aside class="subject-hint">
                                <h3 class="subject-hint__title">
                                    <i class="subject-hint__title-icon" aria-hidden="true">${Icons.customIconTxt("circle-info")}</i>
                                    <span class="subject-hint__title-text">Hints</span>
                                </h3>
                                <p class="subject-hint__text"></p>
                            </aside>-->
                        </section>
                        <section class="subject-section__subsection">
                            <h3 class='subject-section__subtitle'>Note</h3>
                            <p class="subject-section__text"><i>Notes are currently disabled for custom items.</i></p>
                        </section>
                    </section>
                </section>
                <!-- Reading -->
                <section class="subject-section subject-section--reading subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{&quot;auto_expand_question_types&quot;:[&quot;reading&quot;]}">
                    <a class='wk-nav__anchor' id='reading'></a>
                    <h2 class='subject-section__title'>
                        <a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-reading">
                            <span class="subject-section__toggle-icon">${Icons.customIconTxt("chevron-right")}</span>
                            <span class='subject-section__title-text'>Reading</span>
                        </a>
                    </h2>
                    <section id="section-reading" class="subject-section__content" data-toggle-target="content" hidden="hidden">
                        <section class="subject-section__subsection">
                            <div class="subject-readings">
                                <div class="subject-readings__reading ${item.info.primary_reading_type == "onyomi" ? "subject-readings__reading--primary" : ""}">
                                    <h3 class="subject-readings__reading-title">On’yomi</h3>
                                    <p class="subject-readings__reading-items" lang="ja">
                                        ${item.info.onyomi && item.info.onyomi.length > 0 ? item.info.onyomi.join(', ') : "None"}
                                    </p>
                                </div>
                                <div class="subject-readings__reading ${item.info.primary_reading_type == "kunyomi" ? "subject-readings__reading--primary" : ""}">
                                    <h3 class="subject-readings__reading-title">Kun’yomi</h3>
                                    <p class="subject-readings__reading-items" lang="ja">
                                        ${item.info.kunyomi && item.info.kunyomi.length > 0 ? item.info.kunyomi.join(', ') : "None"}
                                    </p>
                                </div>
                                <div class="subject-readings__reading ${item.info.primary_reading_type == "nanori" ? "subject-readings__reading--primary" : ""}">
                                    <h3 class="subject-readings__reading-title">Nanori</h3>
                                    <p class="subject-readings__reading-items" lang="ja">
                                        ${item.info.nanori && item.info.nanori.length > 0 ? item.info.nanori.join(', ') : "None"}
                                    </p>
                                </div>
                            </div>
                        </section>
                        <section class="subject-section__subsection">
                            <h3 class='subject-section__subtitle'>Mnemonic</h3>
                            <p class="subject-section__text">${item.info.reading_expl ? item.info.reading_expl : "This item does not have a reading explanation. Good luck!"}</p>
                            <!--<aside class="subject-hint">
                                <h3 class="subject-hint__title">
                                    <i class="subject-hint__title-icon" aria-hidden="true">${Icons.customIconTxt("circle-info")}</i>
                                    <span class="subject-hint__title-text">Hints</span>
                                </h3>
                                <p class="subject-hint__text"></p>
                            </aside>-->
                        </section>
                        <section class="subject-section__subsection">
                            <h3 class='subject-section__subtitle'>Note</h3>
                            <p class="subject-section__text"><i>Notes are currently disabled for custom items.</i></p>
                        </section>
                    </section>
                </section>
                <!-- Found in vocabulary -->
                <section class="subject-section subject-section--amalgamations subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{&quot;auto_expand_question_types&quot;:[]}">
                    <a class="wk-nav__anchor" id="amalgamations"></a>
                    <h2 class="subject-section__title">
                        <a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-amalgamations" data-controller-connected="true">
                            <span class="subject-section__toggle-icon" aria-hidden="true">${Icons.customIconTxt("chevron-right")}</span>
                            <span class="subject-section__title-text">Found In Vocabulary</span>
                        </a>
                    </h2>
                    <section id="section-amalgamations" class="subject-section__content" data-toggle-target="content">
                        <div class="subject-character-grid subject-character-grid--single-column">
                            <ol class="subject-character-grid__items">
                                <!--<li class="subject-character-grid__item">
                                    <a class="subject-character subject-character--vocabulary subject-character--grid subject-character--burned" title="うち" href="https://www.wanikani.com/vocabulary/%E5%86%85" data-turbo-frame="_blank">
                                        <div class="subject-character__content">
                                            <span class="subject-character__characters" lang="ja">内</span>
                                            <div class="subject-character__info">
                                                <span class="subject-character__reading">うち</span>
                                                <span class="subject-character__meaning">Inside</span>
                                            </div>
                                        </div>
                                    </a>
                                </li>-->
                            </ol>
                        </div>
                    </section>
                </section>
            </div>
        </turbo-frame>
        `;
        case "Vocabulary":
        return /*html*/ `
        <turbo-frame class="subject-info" id="subject-info">
            <div class="container">
                <!-- Meaning -->
                <section class="subject-section subject-section--meaning subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{&quot;auto_expand_question_types&quot;:[&quot;meaning&quot;]}">
                    <a class='wk-nav__anchor' id='meaning'></a>
                    <h2 class='subject-section__title'>
                        <a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-meaning">
                            <span class="subject-section__toggle-icon">${Icons.customIconTxt("chevron-right")}</span>
                            <span class='subject-section__title-text'>Meaning</span>
                        </a>
                    </h2>
                    <section id="section-meaning" class="subject-section__content" data-toggle-target="content" hidden="hidden">
                        <section class="subject-section__subsection">
                            <div class='subject-section__meanings'>
                                <h2 class='subject-section__meanings-title'>Primary</h2>
                                <p class='subject-section__meanings-items'>${item.info.meanings[0]}</p>
                            </div>
                            ${item.info.meanings.length > 1 ? `
                            <div class="subject-section__meanings">
                                <h2 class="subject-section__meanings-title">Alternatives</h2>
                                <p class="subject-section__meanings-items">${item.info.meanings.slice(1).join(', ')}</p>
                            </div>` : ''}
                            <!--<div class='subject-section__meanings'>
                                <h2 class='subject-section__meanings-title'>User Synonyms</h2>
                                <p class='subject-section__meanings-items'><i>User synonyms are currently disabled for custom items.</i></p>
                            </div>-->
                            <!--<div class="subject-section__meanings">
                                <h2 class="subject-section__meanings-title">Word Type</h2>
                                <p class="subject-section__meanings-items">noun, の adjective</p>
                            </div>-->
                        </section>
                        <section class="subject-section__subsection">
                            <h3 class='subject-section__subtitle'>Explanation</h3>
                            <p class="subject-section__text">${item.info.meaning_expl ? item.info.meaning_expl : "This item does not have a meaning explanation. Good luck!"}</p>
                            <!--<aside class="subject-hint">
                                <h3 class="subject-hint__title">
                                    <i class="subject-hint__title-icon" aria-hidden="true">${Icons.customIconTxt("circle-info")}</i>
                                    <span class="subject-hint__title-text">Hints</span>
                                </h3>
                                <p class="subject-hint__text"></p>
                            </aside>-->
                        </section>
                        <section class="subject-section__subsection">
                            <h3 class='subject-section__subtitle'>Note</h3>
                            <p class="subject-section__text"><i>Notes are currently disabled for custom items.</i></p>
                        </section>
                    </section>
                </section>
                <!-- Reading -->
                <section class="subject-section subject-section--reading subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{&quot;auto_expand_question_types&quot;:[&quot;reading&quot;]}">
                    <a class='wk-nav__anchor' id='reading'></a>
                    <h2 class='subject-section__title'>
                        <a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-reading">
                            <span class="subject-section__toggle-icon">${Icons.customIconTxt("chevron-right")}</span>
                            <span class='subject-section__title-text'>Reading</span>
                        </a>
                    </h2>
                    <section id="section-reading" class="subject-section__content" data-toggle-target="content" hidden="hidden">
                        <section class="subject-section__subsection">
                            <div class="subject-readings-with-audio">
                                <div class="subject-readings-with-audio__item">
                                    <div class="reading-with-audio">
                                        <div class="reading-with-audio__reading" lang='ja'>${item.info.readings[0]}</div>
                                        <ul class="reading-with-audio__audio-items">
                                        </ul>
                                    </div>
                                </div>
                            </div>
                        </section>
                        <section class="subject-section__subsection">
                            <h3 class='subject-section__subtitle'>Explanation</h3>
                            <p class="subject-section__text">${item.info.reading_expl ? item.info.reading_expl : "This item does not have a reading explanation. Good luck!"}</p>
                            <!--<aside class="subject-hint">
                                <h3 class="subject-hint__title">
                                    <i class="subject-hint__title-icon" aria-hidden="true">${Icons.customIconTxt("circle-info")}</i>
                                    <span class="subject-hint__title-text">Hints</span>
                                </h3>
                                <p class="subject-hint__text"></p>
                            </aside>-->
                        </section>
                        <section class="subject-section__subsection">
                            <h3 class='subject-section__subtitle'>Note</h3>
                            <p class="subject-section__text"><i>Notes are currently disabled for custom items.</i></p>
                        </section>
                    </section>
                </section>
                <!-- Context -->
                <section class="subject-section subject-section--context subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{&quot;auto_expand_question_types&quot;:[&quot;meaning&quot;]}">
                    <a class="wk-nav__anchor" id="context"></a>
                    <h2 class="subject-section__title">
                        <a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-context" data-controller-connected="true">
                            <span class="subject-section__toggle-icon" aria-hidden="true">${Icons.customIconTxt("chevron-right")}</span>
                            <span class="subject-section__title-text">Context</span>
                        </a>
                    </h2>
                    <section id="section-context" class="subject-section__content" data-toggle-target="content">
                        <!--<section class="subject-section__subsection">
                            <div class="subject-collocations" data-controller="tabbed-content" data-tabbed-content-next-tab-hotkey-value="s" data-tabbed-content-previous-tab-hotkey-value="w" data-hotkey-registered="true">
                                <div class="subject-collocations__patterns">
                                    <h3 class="subject-collocations__title subject-collocations__title--patterns">Pattern of Use</h3>
                                    <div class="subject-collocations__pattern-names">
                                        <a class="subject-collocations__pattern-name" data-tabbed-content-target="tab" data-action="tabbed-content#changeTab" aria-controls="collocations-710736400-0" aria-selected="true" role="tab" lang="ja" href="#collocations-710736400-0">農業を〜</a>
                                    </div>
                                </div>
                                <div class="subject-collocations__collocations">
                                    <h3 class="subject-collocations__title">Common Word Combinations</h3>
                                    <ul class="subject-collocations__pattern-collocations">
                                        <li class="subject-collocations__pattern-collocation" id="collocations-710736400-0" data-tabbed-content-target="content" role="tabpanel">
                                            <div class="context-sentences">
                                                <p class="wk-text" lang="ja">農業を行う</p>
                                                <p class="wk-text">to carry out farming</p>
                                            </div>
                                        </li>
                                    </ul>
                                </div>
                            </div>
                        </section>-->
                        <section class="subject-section__subsection">
                            <h3 class="subject-section__subtitle">Context Sentences</h3>
                            ${item.info.context_sentences && item.info.context_sentences.length > 0 ? buildContextSentencesHTML(item.info.context_sentences) : "No context sentences set."}
                        </section>
                    </section>
                </section>
                <!-- Kanji Composition -->
                <section class="subject-section subject-section--components subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{&quot;auto_expand_question_types&quot;:[]}">
                    <a class="wk-nav__anchor" id="components"></a>
                    <h2 class="subject-section__title">
                        <a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-components" data-controller-connected="true">
                            <span class="subject-section__toggle-icon" aria-hidden="true">${Icons.customIconTxt("chevron-right")}</span>
                            <span class="subject-section__title-text">Kanji Composition</span>
                        </a>
                    </h2>
                    <section id="section-components" class="subject-section__content" data-toggle-target="content">
                        <div class="subject-character-grid">
                            <ol class="subject-character-grid__items">
                                <!--<li class="subject-character-grid__item">
                                    <a class="subject-character subject-character--kanji subject-character--grid subject-character--burned" title="のう" href="https://www.wanikani.com/kanji/%E8%BE%B2" data-turbo-frame="_blank">
                                        <div class="subject-character__content">
                                            <span class="subject-character__characters" lang="ja">農</span>
                                            <div class="subject-character__info">
                                                <span class="subject-character__reading">のう</span>
                                                <span class="subject-character__meaning">Farming</span>
                                            </div>
                                        </div>
                                    </a>
                                </li>-->
                            </ol>
                        </div>
                    </section>
                </section>
            </div>
        </turbo-frame>
        `;
        case "KanaVocabulary":
        return /*html*/ `
        <turbo-frame class="subject-info" id="subject-info">
            <div class="container">
                <!-- Meaning -->
                <section class="subject-section subject-section--meaning subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{&quot;auto_expand_question_types&quot;:[&quot;meaning&quot;]}">
                    <a class='wk-nav__anchor' id='meaning'></a>
                    <h2 class='subject-section__title'>
                        <a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-meaning">
                            <span class="subject-section__toggle-icon">${Icons.customIconTxt("chevron-right")}</span>
                            <span class='subject-section__title-text'>Meaning</span>
                        </a>
                    </h2>
                    <section id="section-meaning" class="subject-section__content" data-toggle-target="content" hidden="hidden">
                        <section class="subject-section__subsection">
                            <div class='subject-section__meanings'>
                                <h2 class='subject-section__meanings-title'>Primary</h2>
                                <p class='subject-section__meanings-items'>${item.info.meanings[0]}</p>
                            </div>
                            ${item.info.meanings.length > 1 ? `
                            <div class="subject-section__meanings">
                                <h2 class="subject-section__meanings-title">Alternatives</h2>
                                <p class="subject-section__meanings-items">${item.info.meanings.slice(1).join(', ')}</p>
                            </div>` : ''}
                            <!--<div class='subject-section__meanings'>
                                <h2 class='subject-section__meanings-title'>User Synonyms</h2>
                                <p class='subject-section__meanings-items'><i>User synonyms are currently disabled for custom items.</i></p>
                            </div>-->
                            <!--<div class="subject-section__meanings">
                                <h2 class="subject-section__meanings-title">Word Type</h2>
                                <p class="subject-section__meanings-items">noun, suffix</p>
                            </div>-->
                        </section>
                        <section class="subject-section__subsection">
                            <h3 class='subject-section__subtitle'>Explanation</h3>
                            <p class="subject-section__text">${item.info.meaning_expl ? item.info.meaning_expl : "This item does not have a meaning explanation. Good luck!"}</p>
                            <!--<aside class="subject-hint">
                                <h3 class="subject-hint__title">
                                    <i class="subject-hint__title-icon" aria-hidden="true">${Icons.customIconTxt("circle-info")}</i>
                                    <span class="subject-hint__title-text">Hints</span>
                                </h3>
                                <p class="subject-hint__text"></p>
                            </aside>-->
                        </section>
                        <section class="subject-section__subsection">
                            <h3 class='subject-section__subtitle'>Note</h3>
                            <p class="subject-section__text"><i>Notes are currently disabled for custom items.</i></p>
                        </section>
                    </section>
                </section>
                <!-- Pronunciation -->
                <section class="subject-section subject-section--reading subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{&quot;auto_expand_question_types&quot;:[&quot;reading&quot;]}">
                    <a class='wk-nav__anchor' id='reading'></a>
                    <h2 class='subject-section__title'>
                        <a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-reading">
                            <span class="subject-section__toggle-icon">${Icons.customIconTxt("chevron-right")}</span>
                            <span class='subject-section__title-text'>Pronunciation</span>
                        </a>
                    </h2>
                    <section id="section-reading" class="subject-section__content" data-toggle-target="content" hidden="hidden">
                        <section class="subject-section__subsection">
                            <div class="subject-readings-with-audio">
                                <div class="subject-readings-with-audio__item">
                                    <div class="reading-with-audio">
                                        <div class="reading-with-audio__reading" lang='ja'>${item.info.readings[0]}</div>
                                        <ul class="reading-with-audio__audio-items">
                                        </ul>
                                    </div>
                                </div>
                            </div>
                        </section>
                    </section>
                </section>
                <!-- Context -->
                <section class="subject-section subject-section--context subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{&quot;auto_expand_question_types&quot;:[&quot;meaning&quot;]}">
                    <a class="wk-nav__anchor" id="context"></a>
                    <h2 class="subject-section__title">
                        <a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-context" data-controller-connected="true">
                            <span class="subject-section__toggle-icon" aria-hidden="true">${Icons.customIconTxt("chevron-right")}</span>
                            <span class="subject-section__title-text">Context</span>
                        </a>
                    </h2>
                    <section id="section-context" class="subject-section__content" data-toggle-target="content">
                        <!--<section class="subject-section__subsection">
                            <div class="subject-collocations" data-controller="tabbed-content" data-tabbed-content-next-tab-hotkey-value="s" data-tabbed-content-previous-tab-hotkey-value="w" data-hotkey-registered="true">
                                <div class="subject-collocations__patterns">
                                    <h3 class="subject-collocations__title subject-collocations__title--patterns">Pattern of Use</h3>
                                    <div class="subject-collocations__pattern-names">
                                        <a class="subject-collocations__pattern-name" data-tabbed-content-target="tab" data-action="tabbed-content#changeTab" aria-controls="collocations-710736400-0" aria-selected="true" role="tab" lang="ja" href="#collocations-710736400-0">農業を〜</a>
                                    </div>
                                </div>
                                <div class="subject-collocations__collocations">
                                    <h3 class="subject-collocations__title">Common Word Combinations</h3>
                                    <ul class="subject-collocations__pattern-collocations">
                                        <li class="subject-collocations__pattern-collocation" id="collocations-710736400-0" data-tabbed-content-target="content" role="tabpanel">
                                            <div class="context-sentences">
                                                <p class="wk-text" lang="ja">農業を行う</p>
                                                <p class="wk-text">to carry out farming</p>
                                            </div>
                                        </li>
                                    </ul>
                                </div>
                            </div>
                        </section>-->
                        <section class="subject-section__subsection">
                            <h3 class="subject-section__subtitle">Context Sentences</h3>
                            ${item.info.context_sentences && item.info.context_sentences.length > 0 ? buildContextSentencesHTML(item.info.context_sentences) : "No context sentences set."}
                        </section>
                    </section>
                </section>
            </div>
        </turbo-frame>
        `;
    }
}
const srsGaps = [0, 4*60*60*1000, 8*60*60*1000, 23*60*60*1000, 47*60*60*1000, 167*60*60*1000, 335*60*60*1000, 730*60*60*1000, 2920*60*60*1000];

class CustomItem {
    // Root variables
    id;
    last_reviewed_at = 0;

    // Item main info. Should always contain at least:
    // type (KanaVocabulary, Vocabulary, Kanji, Radical), category (Vocabulary, Kanji, Radical), srs_lvl, characters, meanings, aux_meanings
    // Optional: meaning_expl, lvl
    // Radicals: --
    // Kanji: primary_reading_type, onyomi, kunyomi, nanori || reading_expl
    // Vocabulary: readings, aux_readings || context_sentences, reading_expl
    // KanaVocabulary: || context_sentences
    info;

    constructor(id, info) {
        this.id = id;
        this.info = info;
        this.last_reviewed_at = Date.now();
    }

    isReadyForReview(levelingType = "none", level = 0) { // levelingType: none, internal, wk
        if(this.last_reviewed_at < Date.now() - srsGaps[this.info.srs_lvl] && this.info.srs_lvl > -1) { // TODO: Change SRS stage check to > 0 once lessons are implemented
            if(this.info.srs_lvl > 0) return true; // If item is already in SRS, ignore levels
            else if(levelingType == "none") return true;
            else if(levelingType == "internal" && (!this.info.lvl || level >= this.info.lvl)) return true;
            else if(levelingType == "wk" && (!this.info.lvl || CustomSRSSettings.userSettings.lastKnownLevel >= this.info.lvl)) return true;
        }
        return false;
    }
    getTimeUntilReview(levelingType, level) { // In hours, rounded to integer
        if(this.isReadyForReview(levelingType, level)) {
            return "Now";
        } else {
            if((levelingType == "internal" && this.info.lvl && level < this.info.lvl) || (levelingType == "wk" && level < CustomSRSSettings.userSettings.lastKnownLevel)) {
                return "Locked";
            } else return Math.round((srsGaps[this.info.srs_lvl] - (Date.now() - this.last_reviewed_at)) / (60*60*1000)) + "h";
        }
    }

    incrementSRS() {
        if(this.info.srs_lvl < 9) this.info.srs_lvl++;
        this.last_reviewed_at = Date.now();
        StorageManager.savePackProfile(activePackProfile, "main");
    }
    decrementSRS() {
        if(this.info.srs_lvl > 1) {
            if(this.info.srs_lvl < 5) this.info.srs_lvl--;
            else this.info.srs_lvl -= 2;
        }
        this.last_reviewed_at = Date.now();
        StorageManager.savePackProfile(activePackProfile, "main");
    }
    getSRS(packID) {
        return [Utils.cantorNumber(packID, this.id), parseInt(this.info.srs_lvl)];
    }

    getQueueItem(packID) {
        switch(this.info.type) {
            case "Radical":
                return {
                    id: Utils.cantorNumber(packID, this.id),
                    type: this.info.type,
                    subject_category: this.info.category,
                    characters: this.info.characters,
                    meanings: this.info.meanings,
                    auxiliary_meanings: this.info.aux_meanings || [],
                    kanji: this.info.kanji || []
                };
            case "Kanji":
                return {
                    id: Utils.cantorNumber(packID, this.id),
                    type: this.info.type,
                    subject_category: this.info.category,
                    characters: this.info.characters,
                    meanings: this.info.meanings,
                    auxiliary_meanings: this.info.aux_meanings || [],
                    primary_reading_type: this.info.primary_reading_type,
                    onyomi: this.info.onyomi || [],
                    kunyomi: this.info.kunyomi || [],
                    nanori: this.info.nanori || [],
                    auxiliary_readings: this.info.aux_readings || [],
                    radicals: this.info.radicals || [],
                    vocabulary: this.info.vocabulary || []
                };
            case "Vocabulary":
                return {
                    id: Utils.cantorNumber(packID, this.id),
                    type: this.info.type,
                    subject_category: this.info.category,
                    characters: this.info.characters,
                    meanings: this.info.meanings,
                    auxiliary_meanings: this.info.aux_meanings || [],
                    readings: this.info.readings.map(reading => ({"reading": reading, "pronunciations": []})),
                    auxiliary_readings: this.info.aux_readings || [],
                    kanji: this.info.kanji || []
                };
            case "KanaVocabulary":
                return {
                    id: Utils.cantorNumber(packID, this.id),
                    type: this.info.type,
                    subject_category: this.info.category,
                    characters: this.info.characters,
                    meanings: this.info.meanings,
                    auxiliary_meanings: this.info.aux_meanings || [],
                    readings: this.info.readings.map(reading => ({"reading": reading, "pronunciations": []}))
                };
        }
    }

    static fromObject(object) {
        let item;
        if(!object.info) { // If item from before update
            let newInfo = {};
            newInfo.type = object.type;
            newInfo.category = object.subject_category;
            newInfo.srs_lvl = object.srs_stage;
            newInfo.characters = object.characters;
            newInfo.meanings = object.meanings;
            if(object.readings) newInfo.readings = object.readings;
            if(object.auxiliary_readings && object.auxiliary_readings.length > 0) newInfo.aux_readings = object.auxiliary_readings;
            if(object.auxiliary_meanings && object.auxiliary_meanings.length > 0) newInfo.aux_meanings = object.auxiliary_meanings;
            if(object.meaning_explanation) newInfo.meaning_expl = object.meaning_explanation;
            if(object.reading_explanation && (object.type == "Vocabulary" || object.type == "Kanji")) newInfo.reading_expl = object.reading_explanation;
            if(object.primary_reading_type) newInfo.primary_reading_type = object.primary_reading_type;
            if(object.onyomi) newInfo.onyomi = object.onyomi;
            if(object.kunyomi) newInfo.kunyomi = object.kunyomi;
            if(object.nanori) newInfo.nanori = object.nanori;
            item = new CustomItem(object.id, newInfo);
        }
        else item = new CustomItem(object.id, object.info);

        item.last_reviewed_at = object.last_reviewed_at;
        return item;
    }
}

class CustomItemPack {
    name;
    author;
    version;
    items = [];
    active = true;
    nextID = 0;
    lvlType = "none"; // "none", "internal", "wk"
    lvl = 1;

    constructor(name, author, version, lvlType, lvl = 1) {
        this.name = name;
        this.author = author;
        this.version = version;
        this.lvlType = lvlType;
        this.lvl = lvl;
    }

    getItem(id) {
        return this.items.find(item => item.id === id);
    }
    addItem(itemInfo) {
        let id = this.nextID++;
        let item = new CustomItem(id, itemInfo);
        this.items.push(item);
    }
    editItem(id, itemInfo) {
        let item = this.getItem(id);
        delete item.info;
        item.info = itemInfo;
    }

    removeItem(position) {
        this.items.splice(position, 1);
    }

    getActiveReviews(packID) { // Get all items that were last reviewed more than 24 hours ago
        if(!this.active) return [];
        return this.items.filter(item => item.isReadyForReview(this.lvlType, this.lvl)).map(item => item.getQueueItem(packID));
    }
    getActiveReviewsSRS(packID) {
        if(!this.active) return [];
        return this.items.filter(item => item.isReadyForReview(this.lvlType, this.lvl)).map(item => item.getSRS(packID));
    }
    getNumActiveReviews() {
        if(!this.active) return 0;
        let num = 0;
        for(let item of this.items) {
            if(item.isReadyForReview(this.lvlType, this.lvl)) num++;
        }
        return num;
    }
    getItemTimeUntilReview(itemIndex) {
        return this.items[itemIndex].getTimeUntilReview(this.lvlType, this.lvl);
    }

    static fromObject(object) {
        let pack = new CustomItemPack(object.name, object.author, object.version, (object.lvlType ? object.lvlType : "none"), (object.lvl ? object.lvl : 1)); // TODO: Remove lvlType and lvl checks after a few weeks
        pack.items = object.items.map(item => CustomItem.fromObject(item));
        pack.active = object.active;
        pack.nextID = (object.nextID || pack.items.length); // If lastID is not present, use the length of the items array
        return pack;
    }
}

class CustomPackProfile {
    customPacks = [];

    getPack(id) {
        return this.customPacks[id];
    }
    addPack(newPack) {
        this.customPacks.push(newPack);
    }
    removePack(id) {
        this.customPacks.splice(id, 1);
    }

    doesPackExist(packName, packAuthor, packVersion) {
        for(let i = 0; i < this.customPacks.length; i++) {
            let pack = this.customPacks[i];
            if(pack.name === packName && pack.author === packAuthor) {
                if(pack.version === packVersion) return "exists";
                else return i;
            }
        }
        return "no";
    }
    updatePack(id, newPack) { // Update pack but keeping the SRS stages of items that are in both the old and new pack
        let oldPack = this.customPacks[id];
        newPack = StorageManager.packFromJSON(newPack);
        for(let i = 0; i < newPack.items.length; i++) {
            let newItem = newPack.items[i];
            let oldItem = oldPack.items.find(item => item.id === newItem.id);
            if(oldItem) {
                newItem.info.srs_lvl = oldItem.info.srs_lvl;
                newItem.last_reviewed_at = oldItem.last_reviewed_at;
            }
        }
        this.customPacks[id] = newPack;
    }

    getActiveReviews() {
        let activeReviews = [];
        for(let i = 0; i < this.customPacks.length; i++) {
            activeReviews.push(...this.customPacks[i].getActiveReviews(i));
        }
        return activeReviews;
    }
    getNumActiveReviews() {
        return this.customPacks.reduce((acc, pack) => acc + pack.getNumActiveReviews(), 0);
    }
    getActiveReviewsSRS() {
        let activeReviewsSRS = [];
        for(let i = 0; i < this.customPacks.length; i++) {
            activeReviewsSRS.push(...this.customPacks[i].getActiveReviewsSRS(i));
        }
        return activeReviewsSRS;
    }

    getSubjectInfo(cantorNum) { // Get details of custom item for review page details display
        let [packID, itemID] = Utils.reverseCantorNumber(cantorNum);
        let item = this.getPack(packID).getItem(itemID);
        return makeDetailsHTML(item);
    }

    submitReview(cantorNum, meaningIncorrectNum, readingIncorrectNum) {
        let [packID, itemID] = Utils.reverseCantorNumber(cantorNum);
        let item = this.customPacks[packID].getItem(itemID);
        if(meaningIncorrectNum > 0 || readingIncorrectNum > 0) {
            item.decrementSRS();
        } else {
            item.incrementSRS();
            // Check if pack should level up
            let pack = this.customPacks[packID];
            if(pack.lvlType == "internal") {
                for(let item of pack.items) {
                    if((!item.info.lvl || item.info.lvl <= pack.lvl) && item.info.srs_lvl < 5) break;
                }
                pack.lvl++;
            }
        }
    }

    static fromObject(object) {
        let packProfile = new CustomPackProfile();
        packProfile.customPacks = object.customPacks.map(pack => CustomItemPack.fromObject(pack));
        return packProfile;
    }
}

// ------------------- Utility classes -------------------
class Utils {
    static cantorNumber(a, b) {
        return -(0.5 * (a + b) * (a + b + 1) + b) - 1;
    }
    static reverseCantorNumber(z) {
        z = -z - 1;
        let w = Math.floor((Math.sqrt(8 * z + 1) - 1) / 2);
        let y = z - ((w * w + w) / 2);
        let x = w - y;
        return [x, y];
    }
    static async get_controller(name) {
        let controller;
        while(!controller) {
            try {
                controller = Stimulus.getControllerForElementAndIdentifier(document.querySelector(`[data-controller~="${name}"]`),name);
            } catch(e) {
                console.log("Waiting for controller " + name);
            }
            await new Promise(r => setTimeout(r, 50));
        }
        return controller;
    }
    static async wkAPIRequest(endpoint, method = "GET", data = null) {
        if(!CustomSRSSettings.userSettings.apiKey) console.error("CustomSRS: No API key set");
        let url = "https://api.wanikani.com/v2/" + endpoint;
        let headers = new Headers({
            Authorization: "Bearer " + CustomSRSSettings.userSettings.apiKey,
        });
        let apiRequest = new Request(url, {
            method: method,
            headers: headers
        });
        if(data) apiRequest.body = JSON.stringify(data);

        let response = await fetch(apiRequest);
        return response.json();
    }
}

class CustomSRSSettings {
    static defaultUserSettings = {
        showItemDueTime: true,
        itemQueueMode: "start",
        exportSRSData: false,
        lastKnownLevel: 0,
        apiKey: null
    };
    static userSettings = this.defaultUserSettings;
    static savedData = {
        capturedWKReview: null
    };
    static validateSettings() {
        for(let setting in this.defaultUserSettings) {
            if(this.userSettings[setting] === undefined) this.userSettings[setting] = this.defaultUserSettings[setting];
        }
    }
}

class StorageManager {
    // Get custom packs saved in GM storage
    static async loadPackProfile(profileName) {
        let savedPackProfile = CustomPackProfile.fromObject(await GM.getValue("customPackProfile_" + profileName, new CustomPackProfile()));
        return savedPackProfile;
    }

    // Save custom packs to GM storage
    static async savePackProfile(packProfile, profileName) {
        GM.setValue("customPackProfile_" + profileName, packProfile);
    }

    // Settings
    static async saveSettings() {
        GM.setValue("custom_srs_user_data", CustomSRSSettings.userSettings);
        GM.setValue("custom_srs_saved_data", CustomSRSSettings.savedData);
    }
    static async loadSettings() {
        CustomSRSSettings.userSettings = await GM.getValue("custom_srs_user_data", CustomSRSSettings.userSettings);
        CustomSRSSettings.validateSettings();
        CustomSRSSettings.savedData = await GM.getValue("custom_srs_saved_data", CustomSRSSettings.savedData);
    }

    static packFromJSON(json) {
        let pack = CustomItemPack.fromObject(json);
        return pack;
    }
    static packToJSON(pack) {
        let packJSON = JSON.parse(JSON.stringify(pack));
        if(!CustomSRSSettings.userSettings.exportSRSData) {
            packJSON.items.forEach(item => {
                item.last_reviewed_at = 0;
                item.info.srs_lvl = 0;
            });
        }
        return JSON.stringify(packJSON);
    }
}
let activePackProfile = await StorageManager.loadPackProfile("main");
await StorageManager.loadSettings();
let quizStatsController;

// ----------- If on review page -----------
if (window.location.pathname.includes("/review")) {
    if(activePackProfile.getNumActiveReviews() !== 0) {
        // Add style to root to prevent header flash
        let headerStyle = document.createElement("style");
        headerStyle.innerHTML = `
        .character-header__characters {
            transition: opacity 0.15s;
        }
        .character-header__loading .character-header__characters {
            opacity: 0;
        }
        `;
        document.head.append(headerStyle);
    }

    // Add custom items to the quiz queue and update captured WK review
    document.addEventListener("DOMContentLoaded", () => {
        let changedFirstItem = false;
        let queueEl = document.getElementById('quiz-queue');
        let parentEl = queueEl.parentElement;
        queueEl.remove();
        let cloneEl = queueEl.cloneNode(true);
        let queueElement = JSON.parse(cloneEl.querySelector("script[data-quiz-queue-target='subjects']").innerHTML);
        let SRSElement = JSON.parse(cloneEl.querySelector("script[data-quiz-queue-target='subjectIdsWithSRS']").innerHTML);
        // Remove captured WK review from queue
        if(queueElement.length === 1 || (CustomSRSSettings.savedData.capturedWKReview && queueElement[1].id === CustomSRSSettings.savedData.capturedWKReview.id)) {
            CustomSRSSettings.savedData.capturedWKReview = queueElement.shift();
            SRSElement.shift();
            changedFirstItem = true;
            console.log("CustomSRS: Captured first item from queue.");
        } else {
            CustomSRSSettings.savedData.capturedWKReview = queueElement[1];
            queueElement.splice(1, 1);
            SRSElement.splice(1, 1);
            console.log("CustomSRS: Captured second item from queue.");
        }

        // Add custom items to queue
        if(activePackProfile.getNumActiveReviews() !== 0) {
            switch(CustomSRSSettings.userSettings.itemQueueMode) {
                case "weighted-start":
                    let reviewsToAddW = activePackProfile.getActiveReviews();
                    let reviewsSRSToAddW = activePackProfile.getActiveReviewsSRS();
                    for(let i = 0; i < reviewsToAddW.length; i++) {
                        let pos = Math.floor(Math.random() * queueElement.length / 4);
                        if(pos === 0) changedFirstItem = true;
                        queueElement.splice(pos, 0, reviewsToAddW[i]);
                        SRSElement.splice(pos, 0, reviewsSRSToAddW[i]);
                    }
                    break;
                case "random":
                    let reviewsToAdd = activePackProfile.getActiveReviews();
                    let reviewsSRSToAdd = activePackProfile.getActiveReviewsSRS();
                    for(let i = 0; i < reviewsToAdd.length; i++) {
                        let pos = Math.floor(Math.random() * queueElement.length);
                        if(pos === 0) changedFirstItem = true;
                        queueElement.splice(pos, 0, reviewsToAdd[i]);
                        SRSElement.splice(pos, 0, reviewsSRSToAdd[i]);
                    }
                    break;
                case "start":
                    changedFirstItem = true;
                    queueElement = activePackProfile.getActiveReviews().concat(queueElement);
                    SRSElement = activePackProfile.getActiveReviewsSRS().concat(SRSElement);
                    break;
            }
        }
        cloneEl.querySelector("script[data-quiz-queue-target='subjects']").innerHTML = JSON.stringify(queueElement);
        cloneEl.querySelector("script[data-quiz-queue-target='subjectIdsWithSRS']").innerHTML = JSON.stringify(SRSElement);

        parentEl.appendChild(cloneEl);
        StorageManager.saveSettings();

        if(changedFirstItem) {
            let headerElement = document.querySelector(".character-header");
            headerElement.classList.add("character-header__loading");
            for(let className of headerElement.classList) { // Fix header colour issues
                if(className.includes("character-header--")) {
                    headerElement.classList.remove(className);
                    headerElement.classList.add("character-header--" + activePackProfile.getActiveReviews()[0].subject_category.toLowerCase());
                    setTimeout(() => {
                        headerElement.classList.remove("character-header__loading");
                    }, 500);
                    break;
                }
            }
        }

        loadControllers();
    });

    // Catch submission fetch and stop it if submitted item is a custom item
    const { fetch: originalFetch } = unsafeWindow;
    unsafeWindow.fetch = async (...args) => {
        let [resource, config] = args;
        if (resource.includes("/subjects/review") && config != null && config.method === "POST") {
            let payload = JSON.parse(config.body);
            // Check if submitted item is a custom item
            if(payload.counts && payload.counts[0].id < 0) {
                // Update custom item SRS
                activePackProfile.submitReview(payload.counts[0].id, payload.counts[0].meaning, payload.counts[0].reading);
                return new Response("{}", { status: 200 });
            } else {
                if(payload.counts[0].id == CustomSRSSettings.savedData.capturedWKReview.id) { // Check if somehow the captured WK review is being submitted
                    CustomSRSSettings.savedData.capturedWKReview = null;
                    StorageManager.saveSettings();
                }
                return originalFetch(...args);
            }
        // Catch subject info fetch and return custom item details if the number at the end of the url is negative
        } else if (resource.includes("/subject_info/") && config && config.method === "get" && resource.split("/").pop() < 0) {
            // Submit original fetch but to different URL to get usable headers
            args[0] = "https://www.wanikani.com/subject_info/1";
            let response = await originalFetch(...args);
            let subjectId = resource.split("/").pop();
            let subjectInfo = activePackProfile.getSubjectInfo(subjectId);
            return new Response(subjectInfo, {
                status: response.status,
                headers: response.headers
            });
        } else {
            return originalFetch(...args);
        }
    };

// ----------- If on lessons page -----------
} else if (window.location.pathname.includes("/lessons")) {
    // TODO

// ----------- If on dashboard page -----------
} else if (window.location.pathname.includes("/dashboard") || window.location.pathname === "/") {
    // Catch lesson / review count fetch and update it with custom item count
    const { fetch: originalFetch } = unsafeWindow;
    unsafeWindow.fetch = async (...args) => {
        let [resource, config] = args;
        if (resource.includes("lesson-and-review-count") && config != null && config.method === "get") {
            let response = await originalFetch(...args);
            let data = await response.text();
            let res = new Response(updateLessonReviewCountData(data), {
                status: response.status,
                headers: response.headers
            });
            return res;
        } else {
            return originalFetch(...args);
        }
    };
    // Catch document load to edit review count on dashboard
    document.addEventListener("DOMContentLoaded", () => {
        let reviewNumberElement = document.querySelector(".reviews-dashboard .reviews-dashboard__count-text span");
        reviewNumberElement.innerHTML = parseInt(reviewNumberElement.innerHTML) + activePackProfile.getNumActiveReviews() + (CustomSRSSettings.savedData.capturedWKReview ? -1 : 0);
        console.log("Captured review item: " + (CustomSRSSettings.savedData.capturedWKReview ? CustomSRSSettings.savedData.capturedWKReview.id : "none"));

        let reviewTile = document.querySelector("div.reviews-dashboard");
        if(reviewTile.querySelector(".reviews-dashboard__buttons") === null && activePackProfile.getNumActiveReviews() > 0) { // If failed to catch WK review and custom items are due, display error message
            reviewTile.querySelector(".reviews-dashboard__text .wk-text").innerHTML = "CustomSRS Error. Please wait for WK review item to be available.";
        } else if(parseInt(reviewTile.querySelector(".count-bubble").innerHTML) === 0) { // If no custom items are due, update review tile to remove buttons
            reviewTile.querySelector(".reviews-dashboard__buttons").remove();
            reviewTile.classList.add("reviews-dashboard--complete");
            reviewTile.querySelector(".reviews-dashboard__text .wk-text").innerHTML = "There are no more reviews to do right now.";
        }
    });

    // Update the stored user level
    let response = await Utils.wkAPIRequest("user");
    if(response && response.data && response.data.level) {
        CustomSRSSettings.userSettings.lastKnownLevel = response.data.level;
        StorageManager.saveSettings();
    }
} else {
    // Catch lesson / review count fetch and update it with custom item count
    const { fetch: originalFetch } = unsafeWindow;
    unsafeWindow.fetch = async (...args) => {
        let [resource, config] = args;
        if (resource.includes("lesson-and-review-count") && config != null && config.method === "get") {
            let response = await originalFetch(...args);
            let data = await response.text();
            let res = new Response(updateLessonReviewCountData(data), {
                status: response.status,
                headers: response.headers
            });
            return res;
        } else {
            return originalFetch(...args);
        }
    };
}

// ----------- UTILITIES -----------
function parseHTML(html) {
    var t = document.createElement('template');
    t.innerHTML = html;
    return t.content;
}

function updateLessonReviewCountData(data) {
    data = parseHTML(data);

    let reviewCountElement = data.querySelector("a[href='/subjects/review'] .lesson-and-review-count__count");
    // If reviewCountElement is null, replace the span .lesson-and-review-count__item with some custom HTML
    let numActiveReviews = activePackProfile.getNumActiveReviews();
    if(reviewCountElement === null && numActiveReviews > 0) {
        let reviewTile = data.querySelector(".lesson-and-review-count__item:nth-child(2)");
        reviewTile.outerHTML = `
        <a class="lesson-and-review-count__item" target="_top" href="/subjects/review">
            <div class="lesson-and-review-count__count">${numActiveReviews}</div>
            <div class="lesson-and-review-count__label">Reviews</div>
        </a>
        `;
    } else {
        if(numActiveReviews > 0 || (!CustomSRSSettings.savedData.capturedWKReview && parseInt(reviewCountElement.innerHTML) > 0) || parseInt(reviewCountElement.innerHTML) > 1) reviewCountElement.innerHTML = parseInt(reviewCountElement.innerHTML) + numActiveReviews + (CustomSRSSettings.savedData.capturedWKReview ? -1 : 0);
        else {
            let reviewTile = data.querySelector(".lesson-and-review-count__item:nth-child(2)");
            reviewTile.outerHTML = `
            <span class="lesson-and-review-count__item" target="_top">
                <div class="lesson-and-review-count__count lesson-and-review-count__count--zero">0</div>
                <div class="lesson-and-review-count__label">Reviews</div>
            </span>
            `;
        }
    }

    // Convert the DocumentFragment back to a string and return it as a Response
    return (new XMLSerializer()).serializeToString(data);
}

async function loadControllers() {
    quizStatsController = await Utils.get_controller('quiz-statistics');
    quizStatsController.remainingCountTarget.innerText = parseInt(quizStatsController.remainingCountTarget.innerText) + activePackProfile.getNumActiveReviews() + (CustomSRSSettings.savedData.capturedWKReview ? -1 : 0);
}
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址