const settingType = { COLOR: [0, "#ff0000"], TEXT: [1, ""], NUMBER: [2, 0], BOOLEAN: [3, false], SELECT: [4, null] } class Setting { constructor (name, storageName, type, disabled = false, defaultValue = null, description = "", customValidator = () => true) { this.tabName = null; this.name = name; this.storageName = storageName; this.type = type[0]; this.disabled = disabled; this.defaultValue = defaultValue ?? type[1]; this.description = description; this.validate = customValidator; this.listeners = []; } set(value) { if (!this.validate(value)) return false; this.value = value; const settings = JSON.parse(localStorage.getItem(`${this.tabName}/settings`)) ?? {}; settings[this.name] = value; localStorage.setItem(`${this.tabName}/settings`, JSON.stringify(settings)); for (const listener of this.listeners) { listener(value); } } update() { this.value = (JSON.parse(localStorage.getItem(`${this.tabName}/settings`)) ?? {})[this.name] ?? this.defaultValue; } get() { this.update(); return this.value; } enable() { this.disabled = false; } disable() { this.disabled = true; } onUpdate(callback) { this.listeners.push(callback); } #parseColor(colorString) { if (colorString instanceof Array) return parseColor(colorString[0]); if (typeof colorString != "string") return "#ffffff"; if (colorString.startsWith("rgb(")) { const color = colorString.replace("rgb(", "").replace(")", ""); return `#${color.split(",").map(a => parseInt(a).toString(16)).join("")}`; } else { if (colorString.startsWith("#")) { const color = colorString.slice(1); if (color.length == 3) return `#${color.repeat(2)}`; else if (color.length == 2) return `#${color.repeat(3)}`; else if (color.length >= 6) return `#${color.slice(0, 6)}`; else return `#${color}`; } } } build() { const value = this.get(); const id = "betterSettings/" + this.modName + "/" + this.storageName; const span = document.createElement("span"); span.className = "setting-span"; span.title = 'Default: "' + this.defaultValue + '"' + (this.disabled ? ". This setting is disabled" : "") + (this.description ? `. ${this.description}` : ""); span.innerText = this.name + " "; const element = document.createElement("input"); switch (this.type) { case 0: { element.type = "color"; element.disabled = this.disabled; element.id = id; element.value = value; element.onchange = (ev) => { this.set(this.#parseColor(ev.target.value)); } break; } case 1: { element.type = "text"; element.disabled = this.disabled; element.id = id; element.value = value; element.onchange = (ev) => { this.set(ev.target.value); } break; } case 2: { element.type = "number"; element.disabled = this.disabled; element.id = id; element.value = value; element.onchange = (ev) => { this.set(parseFloat(ev.target.value)); } break; } case 3: { element.type = "input"; element.className = "toggleInput"; element.disabled = this.disabled; element.id = id; element.value = value ? "ON" : "OFF"; element.setAttribute("state", value ? "1" : "0"); element.onclick = (ev) => { ev.target.value = ev.target.value == "ON" ? "OFF" : "ON"; ev.target.setAttribute("state", ev.target.getAttribute("state") == "1" ? "0" : "1"); this.set(ev.target.value == "ON"); } break; } } span.appendChild(element); return span; } } class SelectSetting extends Setting { constructor (name, storageName, values, disabled = false, defaultValue = null) { super(name, storageName, settingType.SELECT, disabled, defaultValue ?? values[0][1]); this.values = values; } build() { const value = this.get(); const id = "betterSettings/" + this.modName + "/" + this.storageName; let selected = false; const span = document.createElement("span"); span.className = "setting-span"; span.title = "Default: " + this.defaultValue; span.innerText = this.name; const element = document.createElement("select"); element.id = id; for (const val of this.values) { const option = document.createElement("option"); option.value = val[0]; option.innerText = val[1]; if (val[0] == value && !selected) { option.selected = true; selected = true; } element.appendChild(option); } element.onchange = (ev) => { this.set(ev.target.value); } span.appendChild(element); return span; } } class SettingsTab { constructor (tabName) { this.categories = new Map(); this.registry = new Map(); this.tabName = tabName; } registerSetting(setting, category = "General") { setting.tabName = this.tabName.toLowerCase().replace(/ /, "_"); setting.update(); if (this.categories.has(category)) this.categories.get(category).push(setting); else this.categories.set(category, [setting]); this.registry.set(setting.storageName, setting); return this; } registerSettings(category = "General", ...settings) { for (const setting of settings) { this.registerSetting(setting, category); } return this; } set(name, value) { this.registry.get(name)?.set(value); } get(name) { return this.registry.get(name)?.get(); } build() { const result = document.createElement("div"); for (const key of this.categories.keys()) { const category = document.createElement("div"); const title = document.createElement("span"); title.innerText = key; title.className = "betterSettings-categoryTitle"; category.appendChild(title); for (const setting of this.categories.get(key)) { if (setting instanceof Setting) category.appendChild(setting.build()); } result.append(category, document.createElement("br")); } return result; } } class SettingsManager { constructor () { this.settings = new Map(); } registerTab(settingsTab) { this.settings.set(settingsTab.tabName, settingsTab); } getSettings() { return this.settings; } } const settingsManager = new SettingsManager(); { const injectCss = () => { const css = `.modSelectSettingsButton { padding: 10px; cursor: pointer; } .modSelectSettingsButton[current=true] { background-color: rgb(71, 71, 71); } .modSelectSettingsButton:hover { background-color: rgb(51, 51, 51); } #modSelectControls { margin-bottom: 10px; position: relative; display: flex; overflow-x: scroll; } .betterSettings-categoryTitle { font-size: 1.25em; }`; const style = document.createElement("style"); style.innerHTML = css; document.head.appendChild(style); } const inject = () => { const settingsMenu = document.getElementById("settingsMenu"); const menuText = settingsMenu.querySelector(".menuText"); const menuTextChildren = menuText.children; const generalDiv = document.createElement("div"); generalDiv.id = "betterSettings/div/general"; while (menuTextChildren.length > 0) { generalDiv.appendChild(menuTextChildren[0]); } menuText.appendChild(generalDiv); const controls = document.createElement("div"); controls.id = "modSelectControls"; const generalButton = document.createElement("button"); generalButton.setAttribute("current", true); generalButton.id = "betterSettings/button/general"; generalButton.className = "modSelectSettingsButton"; generalButton.innerText = "General"; generalButton.onclick = (ev) => { for (const element of controls.children) { element.setAttribute("current", false); document.getElementById(element.id.replace("button", "div")).style.display = "none"; } ev.target.setAttribute("current", true); document.getElementById("betterSettings/div/general").style.display = ""; } controls.appendChild(generalButton); const wrapper = document.createElement("div"); wrapper.appendChild(generalDiv); for (const mod of settingsManager.getSettings().keys()) { const modButton = document.createElement("button"); modButton.setAttribute("current", false); modButton.id = "betterSettings/button/" + mod; modButton.className = "modSelectSettingsButton"; modButton.innerText = mod; modButton.onclick = (ev) => { for (const element of controls.children) { element.setAttribute("current", false); document.getElementById(element.id.replace("button", "div")).style.display = "none"; } ev.target.setAttribute("current", true); document.getElementById("betterSettings/div/" + mod).style.display = ""; } controls.appendChild(modButton); const modDiv = document.createElement("div"); modDiv.style.display = "none"; modDiv.id = "betterSettings/div/" + mod; modDiv.appendChild(settingsManager.getSettings().get(mod).build()); wrapper.appendChild(modDiv); } menuText.append(controls, wrapper); } runAfterLoadList.push(inject, injectCss); }