refactor config & add ability for config items to have children wich can be hidden depending on parent's value

This commit is contained in:
Send_Nukez 2021-10-12 06:36:50 +02:00
parent 25e69123de
commit 47a5a15d8e
2 changed files with 120 additions and 65 deletions

View file

@ -4,19 +4,43 @@ class ConfigMenu {
/**
* @typedef {Object} DribbblishConfigOptions
* @property {"checkbox" | "select" | "button" | "slider" | "number" | "text" | "time"} type
* @property {String?} area
* @property {any?} data
* @property {String?} key
* @property {String?} name
* @property {String?} description
* @property {any?} defaultValue
* @property {Boolean?} insertOnTop
* @property {Function?} onAppended
* @property {Function?} onChange
* @property {String} [area="Main Settings"]
* @property {any} [data={}]
* @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 {DribbblishConfigOptions[]} [children=[]]
*/
/**
* @callback showChildren
* @param {any} value
* @returns {Boolean | String[]}
*/
/**
* @callback onAppended
* @returns {void}
*/
/**
* @callback onChange
* @param {any} value
* @returns {void}
*/
/** @type {Object.<string, DribbblishConfigOptions>} */
#config;
constructor() {
this.config = {};
this.#config = {};
this.configButton = new Spicetify.Menu.Item("Dribbblish config", false, () => DribbblishShared.config.open());
this.configButton.register();
@ -62,8 +86,10 @@ class ConfigMenu {
const elem = document.createElement("div");
elem.classList.add("dribbblish-config-item");
elem.setAttribute("key", `dribbblish:config:${options.key}`);
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 */ `
<h2 class="x-settings-title main-type-cello${!options.description ? " no-desc" : ""}" as="h2">${options.name}</h2>
<label class="main-type-mesto" as="label" for="dribbblish-config-input-${options.key}">${options.description}</label>
@ -83,24 +109,37 @@ class ConfigMenu {
* @param {DribbblishConfigOptions} options
*/
register(options) {
options = {
...{
area: "Main Settings",
data: {},
key: cyrb53Hash(options.name ?? ""),
name: "",
description: "",
insertOnTop: false,
onAppended: () => {},
onChange: () => {}
},
...options
const defaultOptions = {
hidden: false,
area: "Main Settings",
data: {},
name: "",
description: "",
insertOnTop: false,
fireInitialChange: true,
showChildren: () => true,
onAppended: () => {},
onChange: () => {},
children: []
};
var fireChange = true;
// Set Defaults
options = { ...defaultOptions, ...options };
options._onChange = options.onChange;
options.onChange = (val) => {
options._onChange(val);
const show = options.showChildren(val);
options.children.forEach((child) => this.setHidden(child.key, !show));
};
options.children = options.children.map((child) => {
return { ...defaultOptions, ...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 */ `
<input id="dribbblish-config-input-${options.key}" class="x-toggle-input" type="checkbox"${this.get(options.key, options.defaultValue) ? " checked" : ""}>
<input id="dribbblish-config-input-${options.key}" class="x-toggle-input" type="checkbox"${this.get(options.key) ? " checked" : ""}>
<span class="x-toggle-indicatorWrapper">
<span class="x-toggle-indicator"></span>
</span>
@ -113,21 +152,23 @@ class ConfigMenu {
});
} else if (options.type == "select") {
// Validate
const val = this.get(options.key, options.defaultValue);
if (val < 0 || val > options.data.length - 1) this.set(options.key, options.defaultValue);
const val = this.get(options.key);
if (val < 0 || val > options.data.length - 1) this.set(options.key);
const input = /* html */ `
<select class="main-dropDown-dropDown" id="dribbblish-config-input-${options.key}">
${options.data.map((option, i) => `<option value="${i}"${this.get(options.key, options.defaultValue) == i ? " selected" : ""}>${option}</option>`).join("")}
${options.data.map((option, i) => `<option value="${i}"${this.get(options.key) == i ? " selected" : ""}>${option}</option>`).join("")}
</select>
`;
this.addInputHTML({ ...options, input });
document.getElementById(`dribbblish-config-input-${options.key}`).addEventListener("change", (e) => {
this.set(options.key, e.target.value);
this.set(options.key, Number(e.target.value));
options.onChange(this.get(options.key));
});
} else if (options.type == "button") {
options.fireInitialChange = false;
const input = /* html */ `
<button class="main-buttons-button main-button-primary" type="button" id="dribbblish-config-input-${options.key}">
<div class="x-settings-buttonContainer">
@ -140,16 +181,15 @@ class ConfigMenu {
document.getElementById(`dribbblish-config-input-${options.key}`).addEventListener("click", (e) => {
options.onChange(true);
});
fireChange = false;
} else if (options.type == "number") {
// Validate
if (options.defaultValue == null) options.defaultValue = 0;
const val = this.get(options.key, options.defaultValue);
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 */ `
<input type="number" id="dribbblish-config-input-${options.key}" value="${this.get(options.key, options.defaultValue)}">
<input type="number" id="dribbblish-config-input-${options.key}" value="${this.get(options.key)}">
`;
this.addInputHTML({ ...options, input });
@ -162,14 +202,14 @@ class ConfigMenu {
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, e.target.value);
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 */ `
<input type="text" id="dribbblish-config-input-${options.key}" value="${this.get(options.key, options.defaultValue)}">
<input type="text" id="dribbblish-config-input-${options.key}" value="${this.get(options.key)}">
`;
this.addInputHTML({ ...options, input });
@ -181,7 +221,7 @@ class ConfigMenu {
} else if (options.type == "slider") {
// Validate
if (options.defaultValue == null) options.defaultValue = 0;
const val = this.get(options.key, options.defaultValue);
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);
@ -193,8 +233,8 @@ class ConfigMenu {
min="${options.data?.min ?? "0"}"
max="${options.data?.max ?? "100"}"
step="${options.data?.step ?? "1"}"
value="${this.get(options.key, options.defaultValue)}"
tooltip="${this.get(options.key, options.defaultValue)}${options.data?.suffix ?? ""}"
value="${this.get(options.key)}"
tooltip="${this.get(options.key)}${options.data?.suffix ?? ""}"
>
`;
this.addInputHTML({ ...options, input });
@ -202,43 +242,62 @@ class ConfigMenu {
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, e.target.value);
options.onChange(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 */ `
<input type="time" id="dribbblish-config-input-${options.key}" name="${options.name}" value="${this.get(options.key, options.defaultValue)}">
<input type="time" id="dribbblish-config-input-${options.key}" name="${options.name}" value="${this.get(options.key)}">
`;
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(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();
if (fireChange) options.onChange(this.get(options.key, options.defaultValue));
if (options.fireInitialChange) options.onChange(this.get(options.key));
}
get(key, defaultValue) {
const val = localStorage.getItem(`dribbblish:config:${key}`);
if (val == null) return defaultValue;
if (val == "true" || val == "false") return val == "true"; // Boolean
if (!isNaN(val) && /\d+\.\d+/.test(val) && !isNaN(parseFloat(val))) return parseFloat(val); // Float
if (!isNaN(val) && /\d+/.test(val) && !isNaN(parseInt(val))) return parseInt(val); // Int
return val; // String
/**
*
* @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) {
localStorage.setItem(`dribbblish:config:${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);
}
}
@ -366,19 +425,6 @@ function waitForElement(els, func, timeout = 100) {
}
}
function cyrb53Hash(str, seed = 0) {
let h1 = 0xdeadbeef ^ seed,
h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
}
waitForElement([`.main-rootlist-rootlistPlaylistsScrollNode ul[tabindex="0"]`, `.main-rootlist-rootlistPlaylistsScrollNode ul[tabindex="0"] li`], ([root, firstItem]) => {
const listElem = firstItem.parentElement;
root.classList.add("dribs-playlist-list");

View file

@ -851,6 +851,7 @@ li.GlueDropTarget {
#dribbblish-config .dribbblish-config-areas {
display: flex;
width: 100%;
flex-direction: column;
gap: 16px;
max-height: 60vh;
@ -864,6 +865,10 @@ li.GlueDropTarget {
gap: 16px;
}
#dribbblish-config .dribbblish-config-area:empty {
display: none
}
.dribbblish-config-area > h2 {
text-align: center;
height: 0.8em;
@ -882,6 +887,10 @@ li.GlueDropTarget {
"description input";
}
#dribbblish-config .dribbblish-config-item[hidden=true] {
display: none;
}
#dribbblish-config .dribbblish-config-item > .x-settings-title {
grid-area: header;
margin: 0px;