export default class ConfigMenu { /** * @typedef {Object} DribbblishConfigItem * @property {"checkbox" | "select" | "button" | "slider" | "number" | "text" | "time" | "color"} type * @property {String|DribbblishConfigArea} [area={name: "Main Settings", order: 0}] * @property {any} [data={}] * @property {Number} [order=0] order < 0 = Higher up | order > 0 = Lower Down * @property {String} key * @property {String} name * @property {String} [description=""] * @property {any} [defaultValue] * @property {Boolean} [hidden=false] * @property {Boolean} [insertOnTop=false] * @property {Boolean} [fireInitialChange=true] * @property {showChildren} [showChildren] * @property {onAppended} [onAppended] * @property {onChange} [onChange] * @property {DribbblishConfigItem[]} [children=[]] * @property {String} [childOf=null] key of parent (set automatically) */ /** * @typedef DribbblishConfigArea * @property {String} name * @property {Number} [order=0] order < 0 = Higher up | order > 0 = Lower Down */ /** * @callback showChildren * @this {DribbblishConfigItem} * @param {any} value * @returns {Boolean | String[]} */ /** * @callback onAppended * @this {DribbblishConfigItem} * @returns {void} */ /** * @callback onChange * @this {DribbblishConfigItem} * @param {any} value * @returns {void} */ /** @type {Object.} */ #config; constructor() { this.#config = {}; this.configButton = new Spicetify.Menu.Item("Dribbblish Settings", false, () => this.open()); this.configButton.register(); const container = document.createElement("div"); container.id = "dribbblish-config"; container.innerHTML = /* html */ `

Dribbblish Settings

`; document.body.appendChild(container); document.querySelector(".dribbblish-config-close").addEventListener("click", () => this.close()); document.querySelector(".dribbblish-config-backdrop").addEventListener("click", () => this.close()); } open() { document.getElementById("dribbblish-config").setAttribute("active", ""); } close() { document.getElementById("dribbblish-config").removeAttribute("active"); } /** * @private * @param {DribbblishConfigItem} options */ addInputHTML(options) { this.registerArea(options.area); const parent = document.querySelector(`.dribbblish-config-area[name="${options.area.name}"] .dribbblish-config-area-items`); const elem = document.createElement("div"); elem.style.order = options.order; elem.classList.add("dribbblish-config-item"); elem.setAttribute("key", options.key); elem.setAttribute("type", options.type); elem.setAttribute("hidden", options.hidden); if (options.childOf) elem.setAttribute("parent", options.childOf); elem.innerHTML = /* html */ `

${options.name}

`; if (options.insertOnTop && parent.children.length > 0) { parent.insertBefore(elem, parent.children[0]); } else { parent.appendChild(elem); } } /** * @param {DribbblishConfigItem} options */ register(options) { /** @type {DribbblishConfigItem} */ const defaultOptions = { hidden: false, area: "Main Settings", order: 0, data: {}, name: "", description: "", insertOnTop: false, fireInitialChange: true, showChildren: () => true, onAppended: () => {}, onChange: () => {}, children: [], childOf: null }; // Set Defaults options = { ...defaultOptions, ...options }; if (typeof options.area == "string") options.area = { name: options.area, order: 0 }; options.description = options.description .split("\n") .filter((line) => line.trim() != "") .map((line) => line.trim()) .join("\n"); options._onChange = options.onChange; options.onChange = (val) => { options._onChange.call(options, val); const show = options.showChildren.call(options, val); options.children.forEach((child) => this.setHidden(child.key, Array.isArray(show) ? !show.includes(child.key) : !show)); }; options.children = options.children.map((child) => { return { ...child, area: options.area, childOf: options.key }; }); this.#config[options.key] = options; this.#config[options.key].value = localStorage.getItem(`dribbblish:config:${options.key}`) ?? JSON.stringify(options.defaultValue); if (options.type == "checkbox") { const input = /* html */ ` `; this.addInputHTML({ ...options, input }); document.getElementById(`dribbblish-config-input-${options.key}`).addEventListener("change", (e) => { this.set(options.key, e.target.checked); options.onChange(this.get(options.key)); }); } else if (options.type == "select") { // Validate const val = this.get(options.key); if (val < 0 || val > options.data.length - 1) this.set(options.key); const input = /* html */ ` `; this.addInputHTML({ ...options, input }); document.getElementById(`dribbblish-config-input-${options.key}`).addEventListener("change", (e) => { this.set(options.key, Number(e.target.value)); options.onChange(this.get(options.key)); }); } else if (options.type == "button") { options.fireInitialChange = false; if (typeof options.data != "string") options.data = options.name; const input = /* html */ ` `; this.addInputHTML({ ...options, input }); document.getElementById(`dribbblish-config-input-${options.key}`).addEventListener("click", (e) => { options.onChange(true); }); } else if (options.type == "number") { // Validate if (options.defaultValue == null) options.defaultValue = 0; const val = this.get(options.key); if (options.data.min != null && val < options.data.min) this.set(options.key, options.data.min); if (options.data.max != null && val > options.data.max) this.set(options.key, options.data.max); const input = /* html */ ` `; this.addInputHTML({ ...options, input }); // Prevent inputting +, - and e. Why is it even possible in the first place? document.getElementById(`dribbblish-config-input-${options.key}`).addEventListener("keypress", (e) => { if (["+", "-", "e"].includes(e.key)) e.preventDefault(); }); document.getElementById(`dribbblish-config-input-${options.key}`).addEventListener("input", (e) => { if (options.data.min != null && e.target.value < options.data.min) e.target.value = options.data.min; if (options.data.max != null && e.target.value > options.data.max) e.target.value = options.data.max; this.set(options.key, Number(e.target.value)); options.onChange(this.get(options.key)); }); } else if (options.type == "text") { if (options.defaultValue == null) options.defaultValue = ""; const input = /* html */ ` `; this.addInputHTML({ ...options, input }); document.getElementById(`dribbblish-config-input-${options.key}`).addEventListener("input", (e) => { // TODO: maybe add an validation function via `data.validate` this.set(options.key, e.target.value); options.onChange(this.get(options.key)); }); } else if (options.type == "slider") { // Validate if (options.defaultValue == null) options.defaultValue = 0; const val = this.get(options.key); if (options.data.min != null && val < options.data.min) this.set(options.key, options.data.min); if (options.data.max != null && val > options.data.max) this.set(options.key, options.data.max); const input = /* html */ ` `; this.addInputHTML({ ...options, input }); document.getElementById(`dribbblish-config-input-${options.key}`).addEventListener("input", (e) => { document.getElementById(`dribbblish-config-input-${options.key}`).setAttribute("tooltip", `${e.target.value}${options.data?.suffix ?? ""}`); document.getElementById(`dribbblish-config-input-${options.key}`).setAttribute("value", e.target.value); this.set(options.key, Number(e.target.value)); options.onChange(this.get(options.key)); }); } else if (options.type == "time") { // Validate if (options.defaultValue == null) options.defaultValue = "00:00"; const input = /* html */ ` `; this.addInputHTML({ ...options, input }); document.getElementById(`dribbblish-config-input-${options.key}`).addEventListener("input", (e) => { document.getElementById(`dribbblish-config-input-${options.key}`).setAttribute("value", e.target.value); this.set(options.key, e.target.value); options.onChange(this.get(options.key)); }); } else if (options.type == "color") { // Validate if (options.defaultValue == null) options.defaultValue = "#000000"; const input = /* html */ ` `; this.addInputHTML({ ...options, input }); document.getElementById(`dribbblish-config-input-${options.key}`).addEventListener("input", (e) => { this.set(options.key, e.target.value); options.onChange(this.get(options.key)); }); } else { throw new Error(`Config Type "${options.type}" invalid`); } options.children.forEach((child) => this.register(child)); options.onAppended.call(options); if (options.fireInitialChange) options.onChange(this.get(options.key)); } /** * @param {DribbblishConfigArea} area */ registerArea(area) { if (!document.querySelector(`.dribbblish-config-area[name="${area.name}"]`)) { const areaElem = document.createElement("div"); areaElem.classList.add("dribbblish-config-area"); areaElem.style.order = area.order; const uncollapsedAreas = JSON.parse(localStorage.getItem("dribbblish:config-areas:uncollapsed") ?? "[]"); if (!uncollapsedAreas.includes(area.name)) areaElem.toggleAttribute("collapsed"); areaElem.setAttribute("name", area.name); areaElem.innerHTML = /* html */ `

${area.name}

`; document.querySelector(".dribbblish-config-areas").appendChild(areaElem); areaElem.querySelector("h2").addEventListener("click", () => { areaElem.toggleAttribute("collapsed"); let uncollapsedAreas = JSON.parse(localStorage.getItem("dribbblish:config-areas:uncollapsed") ?? "[]"); if (areaElem.hasAttribute("collapsed")) { uncollapsedAreas = uncollapsedAreas.filter((areaName) => areaName != area.name); } else { uncollapsedAreas.push(area.name); } localStorage.setItem("dribbblish:config-areas:uncollapsed", JSON.stringify(uncollapsedAreas)); }); } } /** * * @param {String} key * @param {any} defaultValueOverride * @returns {any} */ get(key, defaultValueOverride) { const val = JSON.parse(this.#config[key].value ?? null); // Turn undefined into null because `JSON.parse()` dosen't like undefined if (val == null) return defaultValueOverride ?? this.#config[key].defaultValue; return val; } /** * * @param {String} key * @param {any} val */ set(key, val) { this.#config[key].value = JSON.stringify(val); localStorage.setItem(`dribbblish:config:${key}`, JSON.stringify(val)); } /** * * @param {String} key * @param {Boolean} hidden */ setHidden(key, hidden) { this.#config[key].hidden = hidden; document.querySelector(`.dribbblish-config-item[key="${key}"]`).setAttribute("hidden", hidden); } getOptions(key) { return this.#config[key]; } }