[AO3] Kat's Tweaks: Settings Manager

Controls the storage and modification of various settings for all Kat's Tweaks scripts.

// ==UserScript==
// @name         [AO3] Kat's Tweaks: Settings Manager
// @author       Katstrel
// @description  Controls the storage and modification of various settings for all Kat's Tweaks scripts.
// @version      1.2.0
// @history      1.2.0 - added settings for Tag Colors module
// @history      1.1.0 - added settings for Bookmarking module
// @namespace    https://github.com/Katstrel/Kats-Tweaks-and-Skins
// @include      https://archiveofourown.org/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=archiveofourown.org
// @require      https://cdnjs.cloudflare.com/ajax/libs/jscolor/2.5.2/jscolor.min.js
// @grant        none
// ==/UserScript==
"use strict";
let DEBUG = true;

/*
    This userscript is to be used in conjuntion with the other scripts I've released.
    Set all scripts to update automatically and they should be good to go!
    Do NOT edit settings here, edit them in the header bar on AO3
*/

let LOADED_SETTINGS = {};
let DEFAULT_SETTINGS = {
    debugMode: false,
    reversi: false,
    readTime: {
        enabled: true,
        wordsPerMinute: 200,
        levels: [
            {
                id: "Level_0",
                name: "Level_0",
                mins: 0,
                color: '#80ff8080',
            },
            {
                id: "Level_1",
                name: "Level_1",
                mins: 60,
                color: '#ffff8080',
            },
            {
                id: "Level_2",
                name: "Level_2",
                mins: 180,
                color: '#ff808080',
            },
            {
                id: "Level_3",
                name: "24 Hours",
                mins: 1440,
                color: '#ff80ff80',
            },
        ],
    },
    bookmarking: {
        enabled: true,
        dateFormat: "Month/Year",
        defaultNote: "No Notes",
        details: "Tracking",
        includeFandom: false,
        newBookmarksPrivate: true,
        newBookmarksRec: false,
        hideDefaultToreadBtn: true,
        showUpdatedBookmarks: true,
        databaseInfo: [
            {
                keyID: "Bookmarked",
                tagLabel: "Bookmarked",
                enabled: true,
            },
            {
                keyID: "Checked",
                tagLabel: "Checked",
                enabled: true,
            },
            {
                keyID: "Commented",
                tagLabel: "Commented",
                enabled: false,
            },
            {
                keyID: "Kudosed",
                tagLabel: "Kudosed",
                enabled: false,
            },
            {
                keyID: "Series",
                tagLabel: "Series",
                enabled: true,
            },
            {
                keyID: "Subscribed",
                tagLabel: "Subscribed",
                enabled: false,
            },
        ],
        databaseTags: [
            {
                keyID: "toread",
                tagLabel: "To Read",
                posLabel: "📚 Mark as To Read",
                negLabel: "🧹 Remove from To Read",
                btnHeader: true,
                btnFooter: false,
            },
            {
                keyID: "awaitupdate",
                tagLabel: "Awaiting Update",
                posLabel: "📖 Add to Awaiting Update",
                negLabel: "📕 Remove from Awaiting Update",
                btnHeader: false,
                btnFooter: true,
            },
            {
                keyID: "finished",
                tagLabel: "Finished Reading",
                posLabel: "✔️ Mark as Finished",
                negLabel: "🗑️ Remove from Finished",
                btnHeader: false,
                btnFooter: true,
            },
            {
                keyID: "favorite",
                tagLabel: "Favorite",
                posLabel: "❤️ Add to Favorites",
                negLabel: "💔 Remove from Favorites",
                btnHeader: true,
                btnFooter: true,
            },
        ],
        databaseWord: [
            {
                keyID: "short",
                tagLabel: "Short Story | Under 10k",
                wordMin: 0,
                wordMax: 10000,
            },
            {
                keyID: "novella",
                tagLabel: "Novella | 10k to 50k",
                wordMin: 10000,
                wordMax: 50000,
            },
            {
                keyID: "novel",
                tagLabel: "Novel | 50k to 100k",
                wordMin: 50000,
                wordMax: 100000,
            },
            {
                keyID: "longfic",
                tagLabel: "Longfic | Over 100k",
                wordMin: 100000,
                wordMax: Infinity,
            },
        ],
    },
    tagColor: {
        enabled: true,
        cssMode: false,
        databaseWarn: [
            {
                keyID: "no-warn",
                keyName: 'No Warnings',
                priority: 0,
                tagNames: ["No Archive Warnings Apply"],
                color: '#80ff8080',
                css: `background-color: #80ff8080 !important;`,
            },
            {
                keyID: "chose-not",
                keyName: 'Chose Not To Use Warnings',
                priority: 0,
                tagNames: ["Chose Not To Use"],
                color: '#ffff8080',
                css: `background-color: #ffff8080 !important;`,
            },
            {
                keyID: "violence",
                keyName: 'Graphic Violence',
                priority: 0,
                tagNames: ["Violence"],
                color: '#ff808080',
                css: `background-color: #ff808080 !important;`,
            },
            {
                keyID: "mcd",
                keyName: 'Major Character Death',
                priority: 0,
                tagNames: ["Death"],
                color: '#ff80ff80',
                css: `background-color: #ff80ff80 !important;`,
            },
            {
                keyID: "noncon",
                keyName: 'Non-Con',
                priority: 0,
                tagNames: ["Non-Con"],
                color: '#8080ff80',
                css: `background-color: #8080ff80 !important;`,
            },
            {
                keyID: "underage",
                keyName: 'Underage',
                priority: 0,
                tagNames: ["Underage"],
                color: '#80ffff80',
                css: `background-color: #80ffff80 !important;`,
            },
        ],
        databaseShip: [
            {
                keyID: "example",
                keyName: 'Example',
                priority: 0,
                tagNames: ['Example & Example'],
                color: '#80808080',
                css: `background-color: #80808080 !important;`,
            },
        ],
        databaseChar: [
            {
                keyID: "example",
                keyName: 'Example',
                priority: 0,
                tagNames: ['Example Character'],
                color: '#80808080',
                css: `background-color: #80808080 !important;`,
            },
        ],
        databaseFree: [
            {
                keyID: "example",
                keyName: 'Example',
                priority: 0,
                tagNames: ['Example Tag'],
                color: '#80808080',
                css: `background-color: #80808080 !important;`,
            },
        ],
    }
};

class SettingsManager {
    constructor() {
        console.info(`[Kat's Tweaks] Initializing Settings Manager:`, LOADED_SETTINGS);
        this.dropMenu = this.createHeader();
        this.dropMenu.append(
            this.getMenuButton(`— Welcome to Kat's Tweaks —`),
            this.getMenuButton('Main Settings | Import/Export', this.manageSettings),
            this.getMenuButton('Report Issue/Request Feature', function () {
                window.open("https://github.com/Katstrel/Kats-Tweaks-and-Skins/issues/new", '_blank').focus();
            }),
            this.getMenuButton('— Tweaks Modules —'),
            this.getMenuButton('Bookmarking', this.initBookmarking),
            this.getMenuButton('Read Time & Word Count', this.initReadTime),
            this.getMenuButton('Tag Color', this.initTagColor),
        );
    }

    manageSettings() {
        let container = StyleManager.SETM_SettingsContainer();
        new SettingsMain(container);
    }

    initReadTime() {
        let container = StyleManager.SETM_SettingsContainer();
        new SettingsReadTime(container);
    }

    initBookmarking() {
        let container = StyleManager.SETM_SettingsContainer();
        new SettingsBookmarking(container);
    }

    initTagColor() {
        let container = StyleManager.SETM_SettingsContainer();
        new SettingsTagColor(container);
    }

    getMenuButton(text, func) {
        let button = document.createElement('li');
        let label = document.createElement('a');
        button.append(label);

        //button.className = 'menu dropdown-menu';
        label.textContent = text;

        if (func) {
            button.addEventListener("click", (func));
            button.classList.add('KT-SETM-menu-setting');
        }
        else {
            button.classList.add('KT-SETM-menu-header');
        }
        return button;
    }

    createHeader() {
        let header = document.querySelector('ul.primary.navigation.actions');
        let menu = document.createElement('li');
        header.querySelector('li.search').before(menu);

        let label = document.createElement('a');
        let drop = document.createElement('ul');
        menu.append(label);
        menu.append(drop);

        menu.id = 'KT-SETM-dropdown';
        menu.className = 'dropdown';
        label.textContent = "Kat's Tweaks";
        drop.className = 'menu dropdown-menu';
        
        return drop;
    }
}

class SettingsMenu {
    simpleTrueFalse(label, id, setting) {
        let container = this.menuOptionContainer(this.container);

        let objInput = Object.assign(document.createElement(`input`), {
            id: `${this.id}-${id}`,
            type: 'checkbox',
            checked: setting,
        });
        let objLabel = Object.assign(document.createElement(`label`), {
            htmlFor: `${this.id}-${id}`,
            innerHTML: `<span class="optionlabel">${label}</span>`,
        });

        container.append(objLabel);
        container.append(objInput);
    }

    simpleText(label, id, setting) {
        let container = this.menuOptionContainer(this.container);
        Object.assign(this.menuSpanInLine(container), {
            innerText: `${label}`,
        })

        let objInput = Object.assign(document.createElement(`input`), {
            id: `${this.id}-${id}`,
            type: 'text',
            value: setting,
        });

        container.append(objInput);
    }

    menuActionsMenu(container) {
        let footer = Object.assign(document.createElement('p'), {
            className: 'actions',
        });

        let saveButton = Object.assign(document.createElement('input'), {
            type: 'button',
            id: 'KT-SETM-optionssave',
            value: 'Save',
        });

        let closeButton = Object.assign(document.createElement('input'), {
            type: 'button',
            id: 'KT-SETM-optionsclose',
            value: 'Close',
        });
        closeButton.addEventListener("click", () => {
            container.remove();
            document.getElementById('KT-SETM-optionsbackground').remove();
        });

        container.append(footer);
        footer.append(saveButton, closeButton);
    }

    menuOptionContainer(container, id, className) {
        let p = Object.assign(document.createElement('p'), {
            className: 'KT-SETM-setting-container',
            id: id || "",
            className: className || "",
        });
        container.append(p);
        return p;
    }

    menuHardRule(container) {
        container.append(Object.assign(document.createElement('hr'), {
            className: 'big-hr',
        }));
    }

    menuSpanInLine(container) {
        let span = Object.assign(document.createElement('span'), {
            className: 'optionlabel',
        });
        container.append(span);
        return span;
    }

    menuSectionText(container, heading, paragraph) {
        container.append(Object.assign(document.createElement('h2'), {
            textContent: heading,
        }));
        container.append(Object.assign(document.createElement('p'), {
            textContent: paragraph,
        }));
    }
    
    createColorPick(container, moduleID, elementID, itemID, itemColor) {
        container.append(Object.assign(document.createElement('span'), {
            id: `${moduleID}-colorSpan-${itemID}`,
        }));
        document.getElementById(`${moduleID}-colorSpan-${itemID}`).innerHTML += `<input id="${moduleID}-colorPick-${itemID}" data-jscolor="{}" value="#80808080">`
        jscolor.install() // recognizes new inputs and installs jscolor on them
        
        // Color Picker
        let colorPick = document.getElementById(`${moduleID}-colorPick-${itemID}`);
        colorPick.jscolor.alphaChannel = true;
        colorPick.jscolor.format = 'any';
        colorPick.jscolor.fromString(`${itemColor}`);
        colorPick.addEventListener("input", function(e) {
            document.getElementById(`${moduleID}-${elementID}-${itemID}`).style.background = colorPick.jscolor.toHEXAString();
        });
        if (LOADED_SETTINGS.reversi) {
            colorPick.jscolor.backgroundColor = 'rgb(51, 51, 51)';
            colorPick.jscolor.borderColor = 'rgb(1, 1, 1)';
            colorPick.jscolor.controlBorderColor = 'rgb(1, 1, 1)';
        }
    }

    createNumberBox(container, moduleID, elementID, itemID, defaultNumber) {
        container.append(Object.assign(document.createElement(`input`), {
            id: `${moduleID}-${elementID}-${itemID}`,
            type: 'text',
            value: defaultNumber,
        }));
        StyleManager.setInputFilter(document.getElementById(`${moduleID}-${elementID}-${itemID}`), function(value) {
            return /^\d*\.?\d*$/.test(value);
        }, "Only numbers are allowed!");
    }

    createRemoveItem(container, moduleID, elementID, itemID, database, textValue) {
        container.append(Object.assign(document.createElement('input'), {
            type: 'button',
            id: `${moduleID}-removeItem-${itemID}`,
            className: 'removeItem',
            value: textValue || 'Remove',
        }));
        document.querySelectorAll(`#${moduleID}-removeItem-${itemID}`).forEach(button => {
            button.addEventListener('click', () => {
                database.forEach(({keyID}, index, array) => {
                    if (keyID == itemID) {
                        array.splice(index, 1);
                        DEBUG && console.log(`[Kat's Tweaks] Item Remove ${itemID} | New Item List: `, database);
                        document.querySelectorAll(`#${moduleID}-${elementID}-${itemID}`).forEach(function() {
                            document.getElementById(`${moduleID}-${elementID}-${itemID}`).remove();
                        })
                    }
                });
            })
        });
    }

    createUniqueItemInput(container, moduleID) {
        container.append(Object.assign(document.createElement('input'), {
            type: 'text',
            id: `${moduleID}-addItem-text`,
            value: ""
        }));
        StyleManager.setInputFilter(document.getElementById(`${moduleID}-addItem-text`), function(value) {
            return /^[a-zA-Z0-9\-\_]{0,12}$/.test(value);
        }, "Only letters, dashes(-), and underscores(_) up to 12 characters are allowed!");

        container.append(Object.assign(document.createElement('input'), {
            type: 'button',
            id: `${moduleID}-addItem`,
            value: 'Add Key ID',
        }));
    }

}

class SettingsMain extends SettingsMenu {
    constructor(container) {
        super();
        this.id = "KT-SETM";
        this.settings = LOADED_SETTINGS;
        this.container = container;

        let text = document.createElement('p');
        text.textContent = `WORK IN PROGRESS\nWill be added in the future!`;
        this.container.append(text);

        // Actions Footer
        this.menuActionsMenu(this.container);
        document.getElementById('KT-SETM-optionssave').addEventListener("click", () => {
            this.saveSettings();
        });
    }

    saveSettings() {
        let confirmed = confirm('Sure you want to save these settings?');

        

        if (confirmed) {
            LOADED_SETTINGS = this.settings;
            DEBUG && console.log(`[Kat's Tweaks] Settings Saved:`, LOADED_SETTINGS);
        }
    }
}

// Read Time & Word Count Module
class SettingsReadTime extends SettingsMenu {
    constructor(container) {
        super();
        this.id = "KT-RTWC";
        this.container = container;
        this.settings = this.moduleSettingValidation();
        
        let title = Object.assign(document.createElement('h1'), {
            textContent: "Read Time & Word Count",
        });
        this.container.append(title);

        this.simpleTrueFalse('Module Enabled', 'enabled', this.settings.enabled);
        this.menuHardRule(this.container);

        this.container.append(Object.assign(document.createElement('h2'), {
            textContent: "Reading Speed",
        }));
        this.container.append(Object.assign(document.createElement('p'), {
            textContent: `What is reading speed? You can get your reading speed by dividing the number of words a work has over by how many minutes it took to read it! This script will use your value to calculate how long it should take to read works.`,
        }));
        this.wordSpeed();
        this.menuHardRule(this.container);

        this.container.append(Object.assign(document.createElement('h2'), {
            textContent: "Time Levels",
        }));
        this.container.append(Object.assign(document.createElement('p'), {
            textContent: `Levels are super customizable for color coding how long it should take to read works! Simply enter the number of minutes it should take before the color code is used and change the color if you prefer.`,
        }));
        this.readingLevels();
        this.menuHardRule(this.container);
        
        // Actions Footer
        this.menuActionsMenu(this.container);
        document.getElementById('KT-SETM-optionssave').addEventListener("click", () => {
            this.saveSettings();
        });

    }
    
    wordSpeed() {
        let container = this.menuOptionContainer(this.container);
        Object.assign(this.menuSpanInLine(container), {
            innerText: "Words per Minute:",
        })

        let words = Object.assign(document.createElement(`input`), {
            id: `${this.id}-wpm`,
            type: 'text',
            onkeydown: "return StyleManager.isNumberKey(event)",
            value: this.settings.wordsPerMinute,
            min: 0,
        });

        container.append(words);

        StyleManager.setInputFilter(document.getElementById(`${this.id}-wpm`), function(value) {
            return /^\d*\.?\d*$/.test(value); // Allow digits and '.' only, using a RegExp.
        }, "Only numbers are allowed!");
    }

    readingLevels() {
        let container = this.menuOptionContainer(this.container);

        // Create the existing Levels
        let levelContainer = this.menuOptionContainer(container);
        console.info(`[Kat's Tweaks] Settings Manager - Levels: `, this.settings.levels)
        this.settings.levels.forEach(({id, name, mins, color}) => {
            this.drawLevel(levelContainer, this.id, id, name, mins, color);
        });

        // Add Levels Input
        container.append(Object.assign(document.createElement('input'), {
            type: 'text',
            id: `${this.id}-addlevel-text`,
            value: ""
        }));
        StyleManager.setInputFilter(document.getElementById(`${this.id}-addlevel-text`), function(value) {
            return /^[a-zA-Z0-9\-\_]{0,12}$/.test(value);
        }, "Only letters, dashes(-), and underscores(_) up to 12 characters are allowed!");

        container.append(Object.assign(document.createElement('input'), {
            type: 'button',
            id: `${this.id}-addlevel`,
            value: 'Add Level',
        }));
        document.getElementById(`${this.id}-addlevel`).addEventListener("click", () => {
            let textValue = document.getElementById(`${this.id}-addlevel-text`).value;
            let alreadyUsed = false;
            this.settings.levels.forEach(({id}) => {
                DEBUG && console.log(`[Kat's Tweaks] Testing id: `, id);
                if (id == `${textValue}`) {
                    alreadyUsed = true;
                }
            });

            if (`${textValue}` == "") {
                DEBUG && console.log(`[Kat's Tweaks] Level ID is empty!`);
                let box = document.getElementById(`${this.id}-addlevel-text`);
                box.classList.add("input-error");
                box.setCustomValidity("ID can't be empty!");
                box.reportValidity();
                return;
            }
            else if (alreadyUsed) {
                DEBUG && console.log(`[Kat's Tweaks] Level ID already in use!`);
                let box = document.getElementById(`${this.id}-addlevel-text`);
                box.classList.add("input-error");
                box.setCustomValidity("ID is already in use!");
                box.reportValidity();
                return;
            }
            else {
                this.settings.levels.push({
                    id: `${textValue}`,
                    name: `${textValue}`,
                    mins: 0,
                    color: '#80808080',
                });
                this.drawLevel(levelContainer, this.id, `${textValue}`, `${textValue}`, 0, '#80808080');
                DEBUG && console.log(`[Kat's Tweaks] Levels: `, this.settings.levels);
            }
        });
    }
    
    drawLevel(levelContainer, moduleID, levelID, levelName, levelMins, levelColor) {
        jscolor.init();
        let newLevel = this.menuOptionContainer(levelContainer, `${moduleID}-levelContainer-${levelID}`);
        levelContainer.append(newLevel);
        let label = Object.assign(this.menuSpanInLine(newLevel), {
            innerText: `${levelName}`,
            id: `${moduleID}-levelLabel-${levelID}`,
        })
        label.style.backgroundColor = levelColor;

        // Minutes
        newLevel.append(Object.assign(document.createElement(`input`), {
            id: `${moduleID}-level-${levelID}`,
            type: 'text',
            value: levelMins,
        }));
        StyleManager.setInputFilter(document.getElementById(`${moduleID}-level-${levelID}`), function(value) {
            return /^\d*\.?\d*$/.test(value);
        }, "Only numbers are allowed!");

        // Create Color Picker
        newLevel.append(Object.assign(document.createElement('span'), {
            id: `${moduleID}-colorSpan-${levelID}`,
        }));
        document.getElementById(`${moduleID}-colorSpan-${levelID}`).innerHTML += `<input id="${moduleID}-colorPick-${levelID}" data-jscolor="{}" value="#80808080">`
        jscolor.install() // recognizes new inputs and installs jscolor on them
        
        // Color Picker
        let colorPick = document.getElementById(`${moduleID}-colorPick-${levelID}`);
        colorPick.jscolor.alphaChannel = true;
        colorPick.jscolor.format = 'any';
        colorPick.jscolor.fromString(`${levelColor}`);
        colorPick.addEventListener("input", function(e) {
            document.getElementById(`${moduleID}-levelLabel-${levelID}`).style.background = colorPick.jscolor.toHEXAString();
        });
        if (LOADED_SETTINGS.reversi) {
            colorPick.jscolor.backgroundColor = 'rgb(51, 51, 51)';
            colorPick.jscolor.borderColor = 'rgb(1, 1, 1)';
            colorPick.jscolor.controlBorderColor = 'rgb(1, 1, 1)';
        }

        DEBUG && console.log(`[Kat's Tweaks] Levels this. check: `, this.settings.levels);

        // Rename Level
        newLevel.append(Object.assign(document.createElement('input'), {
            type: 'button',
            id: `${moduleID}-renameLevel-${levelID}`,
            value: 'Rename',
        }));
        document.getElementById(`${moduleID}-renameLevel-${levelID}`).addEventListener("click", () => {
            this.settings.levels.forEach(({id}, index, array) => {
                if (id == levelID) {
                    let newName = prompt(`[Kat's Tweaks] Renaming ${levelName} (${levelID})\nEnter New Name:`);
                    array[index].name = newName;
                    document.getElementById(`${moduleID}-levelLabel-${levelID}`).innerText = newName;
                }
            });
        });
        
        // Remove Level
        newLevel.append(Object.assign(document.createElement('input'), {
            type: 'button',
            id: `${moduleID}-removeLevel-${levelID}`,
            className: 'removeLevel',
            value: 'Remove',
        }));


        document.querySelectorAll(`#${moduleID}-removeLevel-${levelID}`).forEach(button => {
            button.addEventListener('click', () => {
                this.settings.levels.forEach(({id}, index, array) => {
                    if (id == levelID) {
                        array.splice(index, 1);
                        DEBUG && console.log(`[Kat's Tweaks] Level Remove ${levelID} | New Levels List: `, this.settings.levels);
                        document.querySelectorAll(`#${moduleID}-levelContainer-${levelID}`).forEach(function() {
                            document.getElementById(`${moduleID}-levelContainer-${levelID}`).remove();
                        })
                    }
                });
            })
        });

        newLevel.append(document.createElement('hr'));
    }

    saveSettings() {
        let confirmed = confirm('Sure you want to save these settings?');

        this.settings.enabled = document.getElementById(`${this.id}-enabled`).checked;
        this.settings.wordsPerMinute = document.getElementById(`${this.id}-wpm`).value;

        // forEach (function (values, index, array) => {}) WHY AM I ONLY NOW LEARNING THIS?!
        this.settings.levels.forEach(({id}, index, array) => {
            array[index].name = document.getElementById(`${this.id}-levelLabel-${id}`).innerText;
            array[index].mins = document.getElementById(`${this.id}-level-${id}`).value;
            array[index].color = document.getElementById(`${this.id}-colorPick-${id}`).jscolor.toHEXAString();
        });

        if (confirmed) {
            LOADED_SETTINGS.readTime = this.settings;
            localStorage.setItem('KT-SavedSettings', JSON.stringify(LOADED_SETTINGS));
            DEBUG && console.log(`[Kat's Tweaks] Settings Saved:`, LOADED_SETTINGS);
            window.location.reload();
        }
    }

    moduleSettingValidation() {
        const setDefault = DEFAULT_SETTINGS.readTime;
        const setLoaded = LOADED_SETTINGS.readTime;
        let settings = setLoaded || setDefault;

        try { settings.wordsPerMinute = setLoaded.wordsPerMinute; }
        catch { settings.wordsPerMinute = setDefault.wordsPerMinute; }

        try { settings.levels = setLoaded.levels; }
        catch { settings.levels = setDefault.levels; }

        return settings;
    }
}

class SettingsBookmarking extends SettingsMenu {
    constructor(container) {
        super();
        this.id = "KT-BOOK";
        this.container = container;
        this.settings = this.moduleSettingValidation();
        
        // Header
        let title = Object.assign(document.createElement('h1'), {
            textContent: "Bookmarking",
        });
        this.container.append(title);

        this.simpleTrueFalse('Module Enabled', 'enabled', this.settings.enabled);
        this.menuHardRule(this.container);

        // Bookmark Settings
        this.container.append(Object.assign(document.createElement('h2'), {
            textContent: "General Settings",
        }));
        this.container.append(Object.assign(document.createElement('p'), {
            textContent: `This mostly handles the note and if the bookmark is private and or rec.`,
        }));

        this.simpleText('Default Note:', 'usernote', this.settings.defaultNote);
        this.simpleText('Tracking Details Title:', 'details', this.settings.details);
        this.simpleTrueFalse('Include Fandom in Note:', 'includeFandom', this.settings.includeFandom);
        this.simpleTrueFalse('Private New Bookmarks:', 'newprivate', this.settings.newBookmarksPrivate);
        this.simpleTrueFalse('Rec New Bookmarks:', 'newrec', this.settings.newBookmarksRec);
        this.simpleTrueFalse('Show Updated Bookmark:', 'showupdate', this.settings.showUpdatedBookmarks);
        this.simpleTrueFalse('Hide Mark for Later:', 'hidemark', this.settings.hideDefaultToreadBtn);
        this.menuHardRule(this.container);

        // Bookmark Tags
        this.container.append(Object.assign(document.createElement('h2'), {
            textContent: "Bookmark Tags",
        }));
        this.container.append(Object.assign(document.createElement('p'), {
            textContent: `Create, edit, and delete tags. Beware, changing a tag will not automatically update bookmarks that already have it. When adding a tag, you must give an ID that is only used for local storage. You may set the tag label after you provide an ID.`,
        }));

        this.container.append(document.createElement('hr'));
        this.databaseTags();
        this.menuHardRule(this.container);
        
        // Words Tags
        this.container.append(Object.assign(document.createElement('h2'), {
            textContent: "Word Count Tags",
        }));
        this.container.append(Object.assign(document.createElement('p'), {
            textContent: `These tags are added automatically to allow for sorting based on word count. Ranges can overlap! Provided are generally accepted word count ranges for categorizing books. 100,000 words is actually on the longer side of most novels!`,
        }));

        this.container.append(document.createElement('hr'));
        this.databaseWord();
        this.menuHardRule(this.container);

        // Information Tags
        this.container.append(Object.assign(document.createElement('h2'), {
            textContent: "Information Tags",
        }));
        this.container.append(Object.assign(document.createElement('p'), {
            textContent: `These tags are for creating bookmarks when doing an action with a work. These are ONLY triggered with the buttons provided by AO3. You will also have to remove these tags manually if you ever want to remove them.`,
        }));

        this.simpleTrueFalse('When Leaving Kudos:', 'kudosed', this.settings.databaseInfo[3].enabled)
        //this.simpleTrueFalse('When Commenting:', 'commented', this.settings.databaseInfo[2].enabled)
        this.simpleTrueFalse('When Subscribing:', 'subscribed', this.settings.databaseInfo[5].enabled)
        this.menuHardRule(this.container);

        // Actions Footer
        this.menuActionsMenu(this.container);
        document.getElementById('KT-SETM-optionssave').addEventListener("click", () => {
            this.saveSettings();
        });
    }

    databaseTags() {
        let container = this.menuOptionContainer(this.container);

        // Create the existing Tags
        let tagContainer = this.menuOptionContainer(container);
        console.info(`[Kat's Tweaks] Settings Manager - Tags: `, this.settings.databaseTags)
        this.settings.databaseTags.forEach(({keyID, tagLabel, posLabel, negLabel, btnHeader, btnFooter}) => {
            this.drawTag(tagContainer, this.id, keyID, tagLabel, posLabel, negLabel, btnHeader, btnFooter);
        });

        // Add Tags Input
        container.append(Object.assign(document.createElement('input'), {
            type: 'text',
            id: `${this.id}-addTag-text`,
            value: ""
        }));
        StyleManager.setInputFilter(document.getElementById(`${this.id}-addTag-text`), function(value) {
            return /^[a-zA-Z0-9\-\_]{0,12}$/.test(value);
        }, "Only letters, dashes(-), and underscores(_) up to 12 characters are allowed!");

        container.append(Object.assign(document.createElement('input'), {
            type: 'button',
            id: `${this.id}-addTag`,
            value: 'Add Tag ID',
        }));
        document.getElementById(`${this.id}-addTag`).addEventListener("click", () => {
            let textValue = document.getElementById(`${this.id}-addTag-text`).value;
            let alreadyUsed = false;
            this.settings.databaseTags.forEach(({keyID}) => {
                DEBUG && console.log(`[Kat's Tweaks] Testing id: `, keyID);
                if (keyID == `${textValue}`) {
                    alreadyUsed = true;
                }
            });

            if (`${textValue}` == "") {
                DEBUG && console.log(`[Kat's Tweaks] Tag ID is empty!`);
                let box = document.getElementById(`${this.id}-addTag-text`);
                box.classList.add("input-error");
                box.setCustomValidity("ID can't be empty!");
                box.reportValidity();
                return;
            }
            else if (alreadyUsed) {
                DEBUG && console.log(`[Kat's Tweaks] Tag ID already in use!`);
                let box = document.getElementById(`${this.id}-addTag-text`);
                box.classList.add("input-error");
                box.setCustomValidity("ID is already in use!");
                box.reportValidity();
                return;
            }
            else {
                this.settings.databaseTags.push({
                    keyID: `${textValue}`,
                    tagLabel: 'New Tag',
                    posLabel: 'Mark as New Tag',
                    negLabel: 'Remove from New Tag',
                    btnHeader: false,
                    btnFooter: false,
                });
                this.drawTag(tagContainer, this.id, `${textValue}`, 'New Tag', 'Mark as New Tag', 'Remove from New Tag', false, false);
                DEBUG && console.log(`[Kat's Tweaks] Tags: `, this.settings.databaseTags);
            }
        });
    }

    drawTag(tagContainer, moduleID, tagID, tagNames, posLabel, negLabel, btnHeader, btnFooter) {
        let newTag = this.menuOptionContainer(tagContainer, `${moduleID}-tagContainer-${tagID}`);
        tagContainer.append(newTag);
        newTag.append(Object.assign(document.createElement('h4'), {
            innerText: `${tagNames}`,
            align: 'left',
            id: `${moduleID}-tagLabel-${tagID}`,
        }));
        
        // Header
        newTag.append(Object.assign(document.createElement(`label`), {
            htmlFor: `${this.id}-tagHeader-${tagID}`,
            innerHTML: `<span class="optionlabel">Display Button At Top:</span>`,
        }));
        newTag.append(Object.assign(document.createElement(`input`), {
            id: `${moduleID}-tagHeader-${tagID}`,
            type: 'checkbox',
            checked: btnHeader,
        }));

        newTag.append(document.createElement('br'));

        // Footer
        newTag.append(Object.assign(document.createElement(`label`), {
            htmlFor: `${this.id}-tagFooter-${tagID}`,
            innerHTML: `<span class="optionlabel">Display Button At Bottom:</span>`,
        }));
        newTag.append(Object.assign(document.createElement(`input`), {
            id: `${moduleID}-tagFooter-${tagID}`,
            type: 'checkbox',
            checked: btnFooter,
        }));

        newTag.append(document.createElement('br'));

        // Add Tag Label
        newTag.append(Object.assign(document.createElement(`input`), {
            id: `${moduleID}-tagAddLabel-${tagID}`,
            type: 'text',
            value: posLabel,
        }));

        // Remove Tag Label
        newTag.append(Object.assign(document.createElement(`input`), {
            id: `${moduleID}-tagRemoveLabel-${tagID}`,
            type: 'text',
            value: negLabel,
        }));

        // Rename Label
        newTag.append(Object.assign(document.createElement('input'), {
            type: 'button',
            id: `${moduleID}-renameTag-${tagID}`,
            value: 'Rename',
        }));
        document.getElementById(`${moduleID}-renameTag-${tagID}`).addEventListener("click", () => {
            this.settings.databaseTags.forEach(({keyID}, index, array) => {
                if (keyID == tagID) {
                    let newName = prompt(`[Kat's Tweaks] Renaming ${tagNames} (${tagID})\nEnter New Name:`) || tagNames;
                    array[index].name = newName;
                    document.getElementById(`${moduleID}-tagLabel-${tagID}`).innerText = newName;
                }
            });
        });
        
        // Remove Tag
        newTag.append(Object.assign(document.createElement('input'), {
            type: 'button',
            id: `${moduleID}-removeTag-${tagID}`,
            className: 'removeTag',
            value: 'Remove',
        }));

        document.querySelectorAll(`#${moduleID}-removeTag-${tagID}`).forEach(button => {
            button.addEventListener('click', () => {
                this.settings.databaseTags.forEach(({keyID}, index, array) => {
                    if (keyID == tagID) {
                        array.splice(index, 1);
                        DEBUG && console.log(`[Kat's Tweaks] Tag Remove ${tagID} | New Tag List: `, this.settings.databaseTags);
                        document.querySelectorAll(`#${moduleID}-tagContainer-${tagID}`).forEach(function() {
                            document.getElementById(`${moduleID}-tagContainer-${tagID}`).remove();
                        })
                    }
                });
            })
        });

        newTag.append(document.createElement('hr'));
    }
    
    databaseWord() {
        let container = this.menuOptionContainer(this.container);

        // Create the existing Tags
        let tagContainer = this.menuOptionContainer(container);
        console.info(`[Kat's Tweaks] Settings Manager - Tags: `, this.settings.databaseWord)
        this.settings.databaseWord.forEach(({keyID, tagLabel, wordMin, wordMax}) => {
            this.drawWord(tagContainer, this.id, keyID, tagLabel, wordMin, wordMax);
        });

        // Add Tags Input
        container.append(Object.assign(document.createElement('input'), {
            type: 'text',
            id: `${this.id}-addWord-text`,
            value: ""
        }));
        StyleManager.setInputFilter(document.getElementById(`${this.id}-addWord-text`), function(value) {
            return /^[a-zA-Z0-9\-\_]{0,12}$/.test(value);
        }, "Only letters, dashes(-), and underscores(_) up to 12 characters are allowed!");
        
        container.append(Object.assign(document.createElement('input'), {
            type: 'button',
            id: `${this.id}-addWord`,
            value: 'Add Tag ID',
        }));
        document.getElementById(`${this.id}-addWord`).addEventListener("click", () => {
            let textValue = document.getElementById(`${this.id}-addWord-text`).value;
            let alreadyUsed = false;
            this.settings.databaseWord.forEach(({keyID}) => {
                DEBUG && console.log(`[Kat's Tweaks] Testing id: `, keyID);
                if (keyID == `${textValue}`) {
                    alreadyUsed = true;
                }
            });

            if (`${textValue}` == "") {
                DEBUG && console.log(`[Kat's Tweaks] Tag ID is empty!`);
                let box = document.getElementById(`${this.id}-addWord-text`);
                box.classList.add("input-error");
                box.setCustomValidity("ID can't be empty!");
                box.reportValidity();
                return;
            }
            else if (alreadyUsed) {
                DEBUG && console.log(`[Kat's Tweaks] Tag ID already in use!`);
                let box = document.getElementById(`${this.id}-addWord-text`);
                box.classList.add("input-error");
                box.setCustomValidity("ID is already in use!");
                box.reportValidity();
                return;
            }
            else {
                this.settings.databaseWord.push({
                    keyID: `${textValue}`,
                    tagLabel: 'New Word Range',
                    wordMin: 0,
                    wordMax: Infinity,
                });
                this.drawWord(tagContainer, this.id, `${textValue}`, 'New Word Range', 0, Infinity);
                DEBUG && console.log(`[Kat's Tweaks] Tags: `, this.settings.databaseWord);
            }
        });
    }
    
    drawWord(tagContainer, moduleID, tagID, tagNames, wordMin, wordMax) {
        let newTag = this.menuOptionContainer(tagContainer, `${moduleID}-tagContainer-${tagID}`);
        tagContainer.append(newTag);
        Object.assign(this.menuSpanInLine(newTag), {
            innerText: `${tagNames}`,
            id: `${moduleID}-wordsLabel-${tagID}`,
        });
        
        // Min Words
        newTag.append(Object.assign(document.createElement(`input`), {
            id: `${moduleID}-wordsMin-${tagID}`,
            type: 'text',
            value: wordMin,
        }));
        StyleManager.setInputFilter(document.getElementById(`${moduleID}-wordsMin-${tagID}`), function(value) {
            return /^\d*\.?\d*$/.test(value);
        }, "Only numbers are allowed!");

        // Max Words
        newTag.append(Object.assign(document.createElement(`input`), {
            id: `${moduleID}-wordsMax-${tagID}`,
            type: 'text',
            value: wordMax,
        }));
        StyleManager.setInputFilter(document.getElementById(`${moduleID}-wordsMax-${tagID}`), function(value) {
            return /^\d*\.?\d*$/.test(value);
        }, "Only numbers are allowed!");

        // Rename Label
        newTag.append(Object.assign(document.createElement('input'), {
            type: 'button',
            id: `${moduleID}-renameWords-${tagID}`,
            value: 'Rename',
        }));
        document.getElementById(`${moduleID}-renameWords-${tagID}`).addEventListener("click", () => {
            this.settings.databaseWord.forEach(({keyID}, index, array) => {
                if (keyID == tagID) {
                    let newName = prompt(`[Kat's Tweaks] Renaming ${tagNames} (${tagID})\nEnter New Name:`) || tagNames;
                    array[index].name = newName;
                    document.getElementById(`${moduleID}-wordsLabel-${tagID}`).innerText = newName;
                }
            });
        });
        
        // Remove Tag
        newTag.append(Object.assign(document.createElement('input'), {
            type: 'button',
            id: `${moduleID}-removeWords-${tagID}`,
            className: 'removeWords',
            value: 'Remove',
        }));

        document.querySelectorAll(`#${moduleID}-removeWords-${tagID}`).forEach(button => {
            button.addEventListener('click', () => {
                this.settings.databaseWord.forEach(({keyID}, index, array) => {
                    if (keyID == tagID) {
                        array.splice(index, 1);
                        DEBUG && console.log(`[Kat's Tweaks] Tag Remove ${tagID} | New Tag List: `, this.settings.databaseWord);
                        document.querySelectorAll(`#${moduleID}-tagContainer-${tagID}`).forEach(function() {
                            document.getElementById(`${moduleID}-tagContainer-${tagID}`).remove();
                        })
                    }
                });
            })
        });

        newTag.append(document.createElement('hr'));
    }

    saveSettings() {
        let confirmed = confirm('Sure you want to save these settings?');

        this.settings.enabled = document.getElementById(`${this.id}-enabled`).checked;
        this.settings.defaultNote = document.getElementById(`${this.id}-usernote`).value;
        this.settings.details = document.getElementById(`${this.id}-details`).value;
        this.settings.includeFandom = document.getElementById(`${this.id}-includeFandom`).checked;
        this.settings.newBookmarksPrivate = document.getElementById(`${this.id}-newprivate`).checked;
        this.settings.newBookmarksRec = document.getElementById(`${this.id}-newrec`).checked;
        this.settings.showUpdatedBookmarks = document.getElementById(`${this.id}-showupdate`).checked;
        this.settings.hideDefaultToreadBtn = document.getElementById(`${this.id}-hidemark`).checked;

        //this.settings.databaseInfo[2].enabled = document.getElementById(`${this.id}-commented`).checked;
        this.settings.databaseInfo[3].enabled = document.getElementById(`${this.id}-kudosed`).checked;
        this.settings.databaseInfo[5].enabled = document.getElementById(`${this.id}-subscribed`).checked;

        this.settings.databaseTags.forEach(({keyID}, index, array) => {
            array[index].tagLabel = document.getElementById(`${this.id}-tagLabel-${keyID}`).innerText;
            array[index].posLabel = document.getElementById(`${this.id}-tagAddLabel-${keyID}`).value;
            array[index].negLabel = document.getElementById(`${this.id}-tagRemoveLabel-${keyID}`).value;
            array[index].btnHeader = document.getElementById(`${this.id}-tagHeader-${keyID}`).checked;
            array[index].btnFooter = document.getElementById(`${this.id}-tagFooter-${keyID}`).checked;
        });

        this.settings.databaseWord.forEach(({keyID}, index, array) => {
            array[index].tagLabel = document.getElementById(`${this.id}-wordsLabel-${keyID}`).innerText;
            array[index].wordMin = document.getElementById(`${this.id}-wordsMin-${keyID}`).value || 0;
            array[index].wordMax = document.getElementById(`${this.id}-wordsMax-${keyID}`).value || Infinity;
        });

        if (confirmed) {
            LOADED_SETTINGS.bookmarking = this.settings;
            localStorage.setItem('KT-SavedSettings', JSON.stringify(LOADED_SETTINGS));
            DEBUG && console.log(`[Kat's Tweaks] Settings Saved:`, LOADED_SETTINGS);
            window.location.reload();
        }
    }

    moduleSettingValidation() {
        const setDefault = DEFAULT_SETTINGS.bookmarking;
        const setLoaded = LOADED_SETTINGS.bookmarking;
        let settings = setLoaded || setDefault;

        // Value Settings
        try { settings.dateFormat = setLoaded.dateFormat; }
        catch { settings.dateFormat = setDefault.dateFormat; }

        try { settings.defaultNote = setLoaded.defaultNote; }
        catch { settings.defaultNote = setDefault.defaultNote; }

        try { settings.details = setLoaded.details; }
        catch { settings.details = setDefault.details; }

        // Databases
        try { settings.databaseInfo = setLoaded.databaseInfo; }
        catch { settings.databaseInfo = setDefault.databaseInfo; }

        try { settings.databaseTags = setLoaded.databaseTags; }
        catch { settings.databaseTags = setDefault.databaseTags; }

        try { settings.databaseWord = setLoaded.databaseWord; }
        catch { settings.databaseWord = setDefault.databaseWord; }

        return settings;
    }
}

class SettingsTagColor extends SettingsMenu {
    constructor(container) {
        super();
        this.id = "KT-COLR";
        this.container = container;
        this.settings = this.moduleSettingValidation();
        
        let title = Object.assign(document.createElement('h1'), {
            textContent: "Tag Colors",
        });
        this.container.append(title);
        this.container.append(Object.assign(document.createElement('p'), {
            textContent: `Due to current bug you must save settings after creating a new group before tags will be saved within that group. Sorry for the inconvenience.`,
        }));

        this.simpleTrueFalse('Module Enabled', 'enabled', this.settings.enabled);
        this.menuHardRule(this.container);

        // Warnings Database
        this.menuSectionText(this.container, 'Content Warnings', 'Apply color coding the six content warning labels available.');
        this.databaseItems(this.settings.databaseWarn, 'WARN', true);
        this.menuHardRule(this.container);

        // Relationships Database
        this.menuSectionText(this.container, 'Relationships', 'This works by finding the containing phrase. It must match where the relationship symbol is or it can be left without a symbol to color code any relationships with a character. Such as all relationships with John can be selected with "John". For something like Jane Jones & John Jones, it must be selected with at least "Jones & John" however this might catch any other Jones except Jane. For best results, match the tag exactly. You can assign multiple tags to the same coloring group.');
        this.container.append(document.createElement('hr'));
        this.databaseItems(this.settings.databaseShip, 'SHIP');
        this.menuHardRule(this.container);

        // Character Database
        this.menuSectionText(this.container, 'Characters', 'This works by finding the containing phrase. To color any tags with Jane Jones, the tag added here can match "Jane" or "Jones". However this will match any Jane or Jones character. For best results, match the tag exactly. You can assign multiple tags to the same coloring group.');
        this.container.append(document.createElement('hr'));
        this.databaseItems(this.settings.databaseChar, 'CHAR');
        this.menuHardRule(this.container);

        // Freeform Database
        this.menuSectionText(this.container, 'Freeform', 'This works by finding the containing phrase. Tags like "no beta" can be caught as long as the case matches. For tags like "Angst", any other tags that include Angst will also be caught. For best results, match the tag exactly. You can assign multiple tags to the same coloring group. Priority can be used to make sure certain colors have higher priority over others. This might be used to make "No Angst" transparent to disable any coloring that might have been done.');
        this.container.append(document.createElement('hr'));
        this.databaseItems(this.settings.databaseFree, 'FREE');
        this.menuHardRule(this.container);
        
        // Actions Footer
        this.menuActionsMenu(this.container);
        document.getElementById('KT-SETM-optionssave').addEventListener("click", () => {
            this.saveSettings();
        });

    }

    databaseItems(database, listID, immutable) {
        let container = this.menuOptionContainer(this.container);

        // Create the existing groups
        let tagContainer = this.menuOptionContainer(container);
        console.info(`[Kat's Tweaks] Settings Manager - Tags: `, database)
        database.forEach(({keyID, keyName, priority, tagNames, color, css}) => {
            this.drawGroup(tagContainer, this.id, listID, keyID, keyName, tagNames, priority, color, css, database, immutable);
        });

        // Add Tag Group Input
        if (!immutable) { 
            this.createUniqueItemInput(container, `${this.id}-${listID}`);
            document.getElementById(`${this.id}-${listID}-addItem`).addEventListener("click", () => {
                let textValue = document.getElementById(`${this.id}-${listID}-addItem-text`).value;
                let alreadyUsed = false;
                database.forEach(({keyID}) => {
                    DEBUG && console.log(`[Kat's Tweaks] Testing KeyID: `, keyID);
                    if (keyID == `${textValue}`) {
                        alreadyUsed = true;
                    }
                });
    
                if (`${textValue}` == "") {
                    DEBUG && console.log(`[Kat's Tweaks] KeyID is empty!`);
                    let box = document.getElementById(`${this.id}-${listID}-addItem-text`);
                    box.classList.add("input-error");
                    box.setCustomValidity("Key ID can't be empty!");
                    box.reportValidity();
                    return;
                }
                else if (alreadyUsed) {
                    DEBUG && console.log(`[Kat's Tweaks] KeyID '${textValue}' already in use!`);
                    let box = document.getElementById(`${this.id}-${listID}-addItem-text`);
                    box.classList.add("input-error");
                    box.setCustomValidity("Key ID is already in use!");
                    box.reportValidity();
                    return;
                }
                else {
                    this.pushItem(tagContainer, this.id, listID, database, textValue);
                }
            });
        }
    }

    drawGroup(itemContainer, moduleID, listID, itemID, itemName, itemTags, itemPriority, itemColor, itemCSS, database, immutable) {
        jscolor.init();
        let newItem = this.menuOptionContainer(itemContainer, `${moduleID}-${listID}-itemContainer-${itemID}`);

        if (immutable) {
            let label = Object.assign(this.menuSpanInLine(newItem), {
                innerText: `${itemTags[0]}`,
                id: `${moduleID}-${listID}-tagList-${itemID}`,
            })
            label.style.backgroundColor = itemColor;
            this.createColorPick(newItem, `${moduleID}-${listID}`, `tagList`, itemID, itemColor);
            return;
        }

        itemContainer.append(newItem);
        newItem.append(Object.assign(document.createElement('h4'), {
            innerText: `${itemName}`,
            align: 'left',
            id: `${moduleID}-${listID}-itemKeyID-${itemID}`,
        }));

        // Priority
        newItem.append(Object.assign(this.menuSpanInLine(newItem), {
            innerText: `Priority: `,
        }));
        this.createNumberBox(newItem, `${moduleID}-${listID}`, 'priority', itemID, itemPriority)
        newItem.append(document.createElement('br'));
        
        // Create Color Picker
        newItem.append(Object.assign(this.menuSpanInLine(newItem), {
            innerText: `Color: `,
        }));
        this.createColorPick(newItem, `${moduleID}-${listID}`, `tagList`, itemID, itemColor);

        // Tag List
        let tagListContainer = Object.assign(document.createElement('p'), {
            id: `${moduleID}-${listID}-tagList-${itemID}`,
        });
        tagListContainer.style.backgroundColor = itemColor;
        newItem.append(tagListContainer);
        itemTags.forEach(tag => {
            let randID = Math.floor(Math.random() * 1000000);
            DEBUG && console.log(`[Kat's Tweaks] ID ${randID} given to '${tag}' for ${itemID}`)
            this.drawItem(tagListContainer, moduleID, listID, itemID, itemTags, tag, randID, immutable);
        });
        
        // Add Tag to List
        newItem.append(Object.assign(document.createElement('input'), {
            type: 'text',
            id: `${moduleID}-${listID}-addItemTag-${itemID}-text`,
            value: ""
        }));
        newItem.append(Object.assign(document.createElement('input'), {
            type: 'button',
            id: `${moduleID}-${listID}-addItemTag-${itemID}`,
            value: 'Add Tag',
        }));
        document.getElementById(`${moduleID}-${listID}-addItemTag-${itemID}`).addEventListener("click", () => {
            let textValue = document.getElementById(`${moduleID}-${listID}-addItemTag-${itemID}-text`).value;
            let randID = Math.floor(Math.random() * 1000000);
            itemTags.push(`${textValue}`);
            DEBUG && console.log(`[Kat's Tweaks] Item ${itemID} Tag Add ${textValue} | New Tag List: `, itemTags);
            DEBUG && console.log(`[Kat's Tweaks] ID ${randID} given to '${textValue}' for ${itemID}`);
            this.drawItem(tagListContainer, moduleID, listID, itemID, itemTags, textValue, randID);
        });
        
        // Remove Database Item
        this.createRemoveItem(newItem, `${moduleID}-${listID}`, 'itemContainer', itemID, database, 'Remove Group')

        // Rename Label
        newItem.append(Object.assign(document.createElement('input'), {
            type: 'button',
            id: `${moduleID}-${listID}-renameTag-${itemID}`,
            value: 'Rename Group',
        }));
        document.getElementById(`${moduleID}-${listID}-renameTag-${itemID}`).addEventListener("click", () => {
            database.forEach(({keyID}, index, array) => {
                if (keyID == itemID) {
                    let newName = prompt(`[Kat's Tweaks] Renaming ${itemName} (${itemID})\nEnter New Name:`) || itemName;
                    array[index].keyName = newName;
                    document.getElementById(`${moduleID}-${listID}-itemKeyID-${itemID}`).innerText = newName;
                }
            });
        });

        newItem.append(document.createElement('hr'));
    }

    drawItem(container, moduleID, listID, itemID, itemTags, tag, randID, immutable) {
        let tagLabel = Object.assign(this.menuSpanInLine(container), {
            innerText: `${tag}`,
            id: `${moduleID}-${listID}-tagLabel-${itemID}-${randID}`,
        });
        container.append(tagLabel);

        if (!immutable) {
            tagLabel.append(Object.assign(document.createElement('input'), {
                type: 'button',
                id: `${moduleID}-${listID}-removeItem-${itemID}-${randID}`,
                className: 'removeItemTag',
                value: 'X',
            }));
            document.querySelectorAll(`#${moduleID}-${listID}-removeItem-${itemID}-${randID}`).forEach(button => {
                button.addEventListener('click', () => {
                    document.querySelectorAll(`#${moduleID}-${listID}-tagLabel-${itemID}-${randID}`).forEach(function() {
                        document.getElementById(`${moduleID}-${listID}-tagLabel-${itemID}-${randID}`).remove();
                    });
                    document.querySelectorAll(`#${moduleID}-${listID}-removeItem-${itemID}-${randID}`).forEach(function() {
                        document.getElementById(`${moduleID}-${listID}-removeItem-${itemID}-${randID}`).remove();
                    });
                    let foundTag = false;
                    itemTags.forEach((arrTag, index, array) => {
                        if ((arrTag == tag) && (!foundTag)) {
                            array.splice(index, 1);
                            DEBUG && console.log(`[Kat's Tweaks] Item ${itemID} Tag Remove ${tag} | New Tag List: `, itemTags);
                            foundTag = true;
                        }
                    });
                })
            });
        }
    }

    pushItem(container, moduleID, listID, database, keyID) {
        database.push({
            keyID: `${keyID}`,
            keyName: 'New Tag Color Group',
            priority: 0,
            tagNames: ['No Tag Set'],
            color: '#80808080',
            css: `background-color: #80808080 !important;`,
        });
        this.drawGroup(container, moduleID, listID, keyID, 'New Tag Color Group', ['No Tag Set'], 0, '#80808080', `background-color: #80808080 !important;`, database);
        DEBUG && console.log(`[Kat's Tweaks] Created ${keyID} | Database Items: `, database);
    }

    saveSettings() {
        let confirmed = confirm('Sure you want to save these settings?');

        // Databases
        this.settings.databaseWarn.forEach(({keyID}, index, array) => {
            array[index].color = document.getElementById(`${this.id}-WARN-colorPick-${keyID}`).jscolor.toHEXAString();
        });
        this.settings.databaseShip.forEach(({keyID}, index, array) => {
            array[index].priority = document.getElementById(`${this.id}-SHIP-priority-${keyID}`).value;
            array[index].color = document.getElementById(`${this.id}-SHIP-colorPick-${keyID}`).jscolor.toHEXAString();
        });
        this.settings.databaseChar.forEach(({keyID}, index, array) => {
            array[index].priority = document.getElementById(`${this.id}-CHAR-priority-${keyID}`).value;
            array[index].color = document.getElementById(`${this.id}-CHAR-colorPick-${keyID}`).jscolor.toHEXAString();
        });
        this.settings.databaseFree.forEach(({keyID}, index, array) => {
            array[index].priority = document.getElementById(`${this.id}-FREE-priority-${keyID}`).value;
            array[index].color = document.getElementById(`${this.id}-FREE-colorPick-${keyID}`).jscolor.toHEXAString();
        });

        if (confirmed) {
            LOADED_SETTINGS.tagColor = this.settings;
            localStorage.setItem('KT-SavedSettings', JSON.stringify(LOADED_SETTINGS));
            DEBUG && console.log(`[Kat's Tweaks] Settings Saved:`, LOADED_SETTINGS);
            window.location.reload();
        }
    }

    moduleSettingValidation() {
        const setDefault = DEFAULT_SETTINGS.tagColor;
        const setLoaded = LOADED_SETTINGS.tagColor;
        let settings = setLoaded || setDefault;

        // Databases
        try { settings.databaseWarn = setLoaded.databaseWarn; }
        catch { settings.databaseWarn = setDefault.databaseWarn; }
        try { settings.databaseShip = setLoaded.databaseShip; }
        catch { settings.databaseShip = setDefault.databaseShip; }
        try { settings.databaseChar = setLoaded.databaseChar; }
        catch { settings.databaseChar = setDefault.databaseChar; }
        try { settings.databaseFree = setLoaded.databaseFree; }
        catch { settings.databaseFree = setDefault.databaseFree; }

        return settings;
    }
}

class StyleManager {
    static addStyle(debugID, css) {
        const customStyle = document.createElement('style');
        customStyle.id = 'KT';
        customStyle.innerHTML = css;
        document.head.appendChild(customStyle);
        DEBUG && console.info(`[Kat's Tweaks] Custom style '${debugID}' added successfully`);
    }

    // Restricts input for the given textbox to the given inputFilter function.
    // https://stackoverflow.com/questions/469357/html-text-input-allow-only-numeric-input
    static setInputFilter(textbox, inputFilter, errMsg) {
        [ "input", "keydown", "keyup", "mousedown", "mouseup", "select", "contextmenu", "drop", "focusout" ].forEach(function(event) {
            textbox.addEventListener(event, function(e) {
                if (inputFilter(this.value)) {
                    // Accepted value.
                    if ([ "keydown", "mousedown", "focusout" ].indexOf(e.type) >= 0){
                      this.classList.remove("input-error");
                      this.setCustomValidity("");
                    }

                    this.oldValue = this.value;
                    this.oldSelectionStart = this.selectionStart;
                    this.oldSelectionEnd = this.selectionEnd;
                }
                else if (this.hasOwnProperty("oldValue")) {
                    // Rejected value: restore the previous one.
                    this.classList.add("input-error");
                    this.setCustomValidity(errMsg);
                    this.reportValidity();
                    this.value = this.oldValue;
                    this.setSelectionRange(this.oldSelectionStart, this.oldSelectionEnd);
                }
                else {
                    // Rejected value: nothing to restore.
                    this.value = "";
                }
            });
        });
    }

    static SETM_SettingsContainer() {
        let background = document.createElement('div');
        background.id = 'KT-SETM-optionsbackground';
        background.style.background = 'rgba(0, 0, 0, 0.75)';
        background.style.position = 'fixed';
        background.style.width = '100%';
        background.style.height = '100%';

        let box = document.createElement('div');
        box.id = 'KT-SETM-optionsbox';

        document.body.append(background);
        document.querySelector('#main').append(box);
        
        return box;
    }
}

class Main {
    constructor() {
        this.settings = this.loadSettings();
        this.reversiCheck();
        this.initStyles();
        new SettingsManager();
    }

    reversiCheck() {
        let bgColor = window.getComputedStyle(document.body).backgroundColor;
        let reversi = document.querySelector('.wrapper').classList.contains('KT-reversi');
        if ((bgColor == 'rgb(51, 51, 51)' && !reversi) || this.settings.reversi) {
            document.querySelector('.wrapper').classList.add('KT-reversi');
            LOADED_SETTINGS.reversi = true;
            DEBUG && console.log(`[Kat's Tweaks] Reversi Detected!`)
        }
    }

    // Load settings from the storage or fallback to default ones
    loadSettings() {
        const startTime = performance.now();
        let savedSettings = localStorage.getItem('KT-SavedSettings');

        if (savedSettings) {
            try {
                LOADED_SETTINGS = JSON.parse(savedSettings);
                DEBUG && console.log(`[Kat's Tweaks] Settings loaded successfully:`, savedSettings);
            } catch (error) {
                DEBUG && console.error(`[Kat's Tweaks] Error parsing settings: ${error}`);
                LOADED_SETTINGS = DEFAULT_SETTINGS
            }
        } else {
            LOADED_SETTINGS = DEFAULT_SETTINGS;
            DEBUG && console.warn(`[Kat's Tweaks] No saved settings found, using default settings.`, LOADED_SETTINGS);
        }

        const endTime = performance.now();
        DEBUG && console.log(`[Kat's Tweaks] Settings loaded in ${endTime - startTime} ms`);
        return LOADED_SETTINGS;
    }

    initStyles() {
        StyleManager.addStyle('SETM Default Style', `#header .KT-SETM-menu-header { text-align: center !important; font-weight: bold; } #KT-SETM-optionsbox { position: fixed; top: 0px; bottom: 0px; left: 0px; right: 0px; height: min-content; width: 70%; max-height: 90%; max-width: 800px; margin: auto; overflow-y: auto; border: 10px solid #990000; box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, .2); padding: 0 20px; background-color: rgb(255, 255, 255); z-index: 999; } #KT-SETM-optionsbox hr.big-hr { border: 0; height: 1px; background-image: linear-gradient(to right, rgba(0, 0, 0, 0), #990000, rgba(0, 0, 0, 0)); } #KT-SETM-optionsbox p.actions { text-align: right } #KT-SETM-optionsbox p input[type="button"] { float: none; text-align: right; } #KT-SETM-optionsbox h1, #KT-SETM-optionsbox h2 { text-align: center; } #KT-SETM-optionsbox input[type="button"] { height: auto; cursor: pointer; } #KT-SETM-optionsbox .optionlabel { display: inline-block; min-width: 13.5em; } .input-error{ outline: 1px solid #990000 !important; }`);
        StyleManager.addStyle('SETM Reversi Overrides', `.KT-reversi #KT-SETM-optionsbox { border: 10px solid #5998D6; background-color: rgb(51, 51, 51); } .KT-reversi #KT-SETM-optionsbox hr.big-hr { background-image: linear-gradient(to right, rgba(0, 0, 0, 0), #5998D6, rgba(0, 0, 0, 0)); } .KT-reversi .input-error  { outline: 1px solid #5998D6 !important; }`)
    }
}

new Main();