WK Custom SRS

Add custom word packs to WaniKani!

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         WK Custom SRS
// @namespace    leohumnew.wk
// @version      0.3.7
// @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://greasyfork.org/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: 60%;
    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 }
    &:has(#pack-select [value="import"]:checked) .pack-box {
        display: none }
    &:has(#pack-select [value="import"]:checked) .import-box {
        display: grid !important }
    &:has(#pack-lvl-type [value="internal"]:checked) .pack-lvl-specific {
        display: grid !important }
    &: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 {
    hr {
        margin: 0;
        border-color: var(--color-wk-panel-background, gray)
    }
    .ctx-sentence-div {
        display: flex;
        justify-content: space-between;
        margin-bottom: 0.5rem;
    }
    .item-info-edit-container {
        padding: 1rem;
        border-radius: 3px;
        background-color: var(--color-wk-panel-content-background, white);
        button {
            margin-left: auto }
        input, label {
            margin-right: 0.5rem }
        label {
            opacity: 0.5 }
    }
    .component-div {
        display: grid;
        grid-template-columns: 1fr 0.2fr;
    }

    &:has(#item-type [value="Radical"]:checked) .item-radical-specific {
        display: grid !important }
    &:has(#item-type [value="Kanji"]:checked) .item-kanji-specific {
        display: grid !important }
    &:has(#item-type [value="Vocabulary"]:checked) .item-vocab-specific {
        display: grid !important }
    &:has(#item-type [value="KanaVocabulary"]:checked) .item-kanavocab-specific {
        display: grid !important }

    &:has(#component-type [value="internal"]:checked), &:has(#component-type [value="wk"]:checked) {
        #component-type-container {
            display: none !important }
        #component-id-container {
            display: block !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-info-edit-container item-kanavocab-specific item-vocab-specific" style="display: none; grid-column: 1 / span 2">
                    <p>Context Sentences</p>
                    <button id="ctx-add-btn">${Icons.customIconTxt("plus")}</button>
                    <div id="item-context-sentences-container" style="grid-column: 1 / span 2"></div>
                </div>
                <div class="item-info-edit-container item-vocab-specific item-radical-specific" style="display: none; grid-column: 1 / span 2">
                    <p style="grid-column: 1 / span 2">Kanji Components</p>
                    <span>
                        <span id="component-type-container">
                            <label for="component-type" style="float: left">Type:</label>
                            <select id="component-type">
                                <option value=""><i>Select type</i></option>
                                <option value="internal">This Pack</option>
                                <!--<option value="wk">WaniKani</option>-->
                            </select>
                        </span>
                        <span id="component-id-container" style="display: none">
                            <label id="component-id-label" for="component-id" style="float: left">Kanji</label>
                            <input id="component-id" type="text">
                        </span>
                    </span>
                    <button id="component-add-btn">${Icons.customIconTxt("plus")}</button>
                    <p style="display: none; grid-column: 1 / span 2"><i>Failed to find component.</i></p>
                    <hr style="grid-column: 1 / span 2">
                    <div id="components-container" style="grid-column: 1 / span 2"></div>
                </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";
        document.getElementById("ctx-add-btn").onclick = (e) => {
            e.preventDefault();
            document.getElementById("item-context-sentences-container").innerHTML += buildContextSentenceEditHTML("", "");
        };
        document.getElementById("component-add-btn").onclick = (e) => { // Handle adding kanji components
            e.preventDefault();
            let type = document.getElementById("component-type").value;
            let id = document.getElementById("component-id").value;
            if(type === "" || id === "") return;
            // Check if component exists. When type is internal id is the item character to search for
            switch(type) {
                case "internal":
                    let type = document.getElementById("component-id-label").innerText;
                    let itemID = activePackProfile.customPacks[document.querySelector("#pack-select").value].getItemID(type, document.getElementById("component-id").value);
                    if(itemID) {
                        document.getElementById("component-add-btn").nextElementSibling.style.display = "none";
                        document.getElementById("components-container").innerHTML += buildKanjiComponentEditHTML(type, document.querySelector("#pack-select").value, itemID);
                        document.getElementById("component-type").value = "";
                        document.getElementById("component-id").value = "";
                    } else {
                        document.getElementById("component-add-btn").nextElementSibling.style.display = "block";
                    }
                    break;
                case "wk":
                    // TODO: Add WaniKani component check
                    break;
            }
        };
        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.ctx_jp) {
                document.getElementById("item-context-sentences-container").innerHTML = editItemInfo.ctx_jp.map((s, i) => {
                    return buildContextSentenceEditHTML(s, editItemInfo.ctx_en[i]);
                }).join("");
            }
            if(editItemInfo.kanji) {
                document.getElementById("components-container").innerHTML = editItemInfo.kanji.map((k) => {
                    return buildKanjiComponentEditHTML(k[0], k[1], k[2]);
                }).join("");
            }
            document.querySelector("#tab-4__content button[type='submit']").innerText = "Save";
        } else {
            ["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";
            document.querySelector("#item-context-sentences-container").innerHTML = "";
            document.querySelector("#components-container").innerHTML = "";
        }
        // 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];
            let ctxDivs = document.querySelector("#item-context-sentences-container").children;

            // Add or edit item
            switch(itemType) {
                case "Radical":
                    infoStruct.category = infoStruct.type;
                    if(document.getElementById("components-container").children.length > 0) {
                        infoStruct.kanji = [];
                        let container = document.getElementById("components-container");
                        for(let i = 0; i < container.children.length; i++) {
                            let [type, pack, id] = container.children[i].querySelector(".component-info").innerText.split(",");
                            infoStruct.kanji.push([type, parseInt(pack), parseInt(id)]);
                        }
                    }
                    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-container").children.length > 0) {
                        infoStruct.ctx_jp = [];
                        infoStruct.ctx_en = [];
                        for(let i = 0; i < ctxDivs.length; i++) {
                            let ctxDiv = ctxDivs[i];
                            infoStruct.ctx_jp.push(ctxDiv.children[0].value);
                            infoStruct.ctx_en.push(ctxDiv.children[1].value);
                        }
                    }
                    if(document.getElementById("components-container").children.length > 0) {
                        infoStruct.kanji = [];
                        let container = document.getElementById("components-container");
                        for(let i = 0; i < container.children.length; i++) {
                            let [type, pack, id] = container.children[i].querySelector(".component-info").innerText.split(",");
                            infoStruct.kanji.push([type, parseInt(pack), parseInt(id)]);
                        }
                    }
                    break;
                case "KanaVocabulary":
                    infoStruct.category = "Vocabulary";
                    infoStruct.readings = document.querySelector("#item-readings").value.split(",").map(s => s.trim());
                    if(document.querySelector("#item-context-sentences-container").children.length > 0) {
                        infoStruct.ctx_jp = [];
                        infoStruct.ctx_en = [];
                        for(let i = 0; i < ctxDivs.length; i++) {
                            let ctxDiv = ctxDivs[i];
                            infoStruct.ctx_jp.push(ctxDiv.children[0].value);
                            infoStruct.ctx_en.push(ctxDiv.children[1].value);
                        }
                    }
                    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
                savePack();
                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();
        savePack();
        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);
            }
        }
    };

    function savePack() {
        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");
    }
}

// ---------- Item info procedural edit structures ----------
function buildKanjiComponentEditHTML(type, pack, id) {
    return /*html*/ `
    <div class="component-div">
        <p>${type == "wk" ? "WaniKani" : "This Pack"} Kanji. ID: ${id} Character: ${activePackProfile.getPack(pack).getItem(id).info.characters}</p>
        <button class="delete-component" title="Delete Component" onclick="this.parentElement.remove()">${Icons.customIconTxt("cross")}</button>
        <span class="component-info" style="display: none">${type},${pack},${id}</span>
    </div>
    `;
}

function buildContextSentenceEditHTML(jp, en) {
    return /*html*/ `
    <div class="ctx-sentence-div">
        <input type="text" value="${jp}" placeholder="Japanese" required>
        <input type="text" value="${en}" placeholder="English" required>
        <button class="delete-sentence" title="Delete Sentence" onclick="this.parentElement.remove()">${Icons.customIconTxt("cross")}</button>
    </div>
    `;
}
// ---------- Item details ----------
function buildKanjiComponentHTML(type, pack, id) {
    let item;
    if(pack >= 0) item = activePackProfile.getPack(pack).getItem(id);
    else item = null; // TODO: Add WaniKani component check
    return /*html*/ `
    <li class="subject-character-grid__item">
        <a class="subject-character subject-character--${type.toLowerCase()} subject-character--grid ${item.info.srs_lvl > 8 ? "subject-character--burned" : ""}" data-turbo-frame="_blank">
            <div class="subject-character__content">
                <span class="subject-character__characters" lang="ja">${item.info.characters}</span>
                <div class="subject-character__info">
                    <span class="subject-character__reading">${item.primary_reading_type == "onyomi" ? item.info.onyomi[0] : item.primary_reading_type == "kunyomi" ? item.info.kunyomi[0] : item.info.nanori[0]}</span>
                    <span class="subject-character__meaning">${item.info.meanings[0]}</span>
                </div>
            </div>
        </a>
    </li>
    `;
}
function buildContextSentencesHTML(ctxArrayJP, ctxArrayEN) {
    let out = "";
    for(let i = 0; i < ctxArrayJP.length; i++) {
        out += `
        <div class="subject-section__text subject-section__text--grouped">
            <p lang="ja">${ctxArrayJP[i]}</p>
            <p>${ctxArrayEN[i]}</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">
                                ${item.info.kanji ? item.info.kanji.map(k => buildKanjiComponentHTML(k[0], k[1], k[2])).join('') : "No found in kanji set."}
                            </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.ctx_jp ? buildContextSentencesHTML(item.info.ctx_jp, item.info.ctx_en) : "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">
                                ${item.info.kanji ? item.info.kanji.map(k => buildKanjiComponentHTML(k[0], k[1], k[2])).join('') : "No kanji components set."}
                            </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.ctx_jp ? buildContextSentencesHTML(item.info.ctx_jp, item.info.ctx_en) : "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 || ctx_jp, ctx_en, reading_expl, kanji
    // KanaVocabulary: || crx_jp, ctx_en
    info;

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

    isReadyForReview(levelingType, level) { // 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" && this.info.lvl && CustomSRSSettings.userSettings.lastKnownLevel < this.info.lvl)) {
                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 TODO: remove after a few weeks
            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);

        if(item.info.context_sentences) { // Convert context_sentences to ctx_jp and ctx_en TODO: remove after a few weeks
            for(let i = 0; i < item.info.context_sentences.length; i++) {
                item.info.ctx_jp = [];
                item.info.ctx_en = [];
                if(i % 2 == 0) item.info.ctx_jp.push(item.info.context_sentences[i]);
                else item.info.ctx_en.push(item.info.context_sentences[i]);
            }
            delete item.info.context_sentences;
        }

        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);
    }
    getItemID(itemType, itemChar) {
        let item = this.items.find(item => item.info.characters === itemChar && item.info.type === itemType);
        if(item) return item.id;
        else return null;
    }
    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);
}
})();