Enable UserCSS style theme settings
This commit is contained in:
parent
e5dc449130
commit
e0787a8816
|
@ -130,29 +130,23 @@ export default class AddonManager {
|
|||
|
||||
extractMeta(fileContent, filename) {
|
||||
const firstLine = fileContent.split("\n")[0];
|
||||
const hasOldMeta = firstLine.includes("//META") && firstLine.includes("*//");
|
||||
if (hasOldMeta) return this.parseOldMeta(fileContent, filename);
|
||||
const hasNewMeta = firstLine.includes("/**");
|
||||
if (hasNewMeta) return this.parseNewMeta(fileContent);
|
||||
throw new AddonError(filename, filename, Strings.Addons.metaNotFound, {message: "", stack: fileContent}, this.prefix);
|
||||
const hasMetaComment = firstLine.includes("/**");
|
||||
if (!hasMetaComment) throw new AddonError(filename, filename, Strings.Addons.metaNotFound, {message: "", stack: fileContent}, this.prefix);
|
||||
const metaInfo = this.parseJSDoc(fileContent);
|
||||
|
||||
/**
|
||||
* Okay we have a meta JSDoc, let's validate it
|
||||
* and do some extra parsing for advanced options
|
||||
*/
|
||||
|
||||
if (!metaInfo.author || typeof(metaInfo.author) !== "string") metaInfo.author = Strings.Addons.unknownAuthor;
|
||||
if (!metaInfo.version || typeof(metaInfo.version) !== "string") metaInfo.version = "???";
|
||||
if (!metaInfo.description || typeof(metaInfo.description) !== "string") metaInfo.description = Strings.Addons.noDescription;
|
||||
|
||||
return metaInfo;
|
||||
}
|
||||
|
||||
parseOldMeta(fileContent, filename) {
|
||||
const meta = fileContent.split("\n")[0];
|
||||
const metaData = meta.substring(meta.lastIndexOf("//META") + 6, meta.lastIndexOf("*//"));
|
||||
let parsed = null;
|
||||
try {
|
||||
parsed = JSON.parse(metaData);
|
||||
}
|
||||
catch (err) {
|
||||
throw new AddonError(filename, filename, Strings.Addons.metaError, err, this.prefix);
|
||||
}
|
||||
if (!parsed || !parsed.name) throw new AddonError(filename, filename, Strings.Addons.missingNameData, {message: "", stack: meta}, this.prefix);
|
||||
parsed.format = "json";
|
||||
return parsed;
|
||||
}
|
||||
|
||||
parseNewMeta(fileContent) {
|
||||
parseJSDoc(fileContent) {
|
||||
const block = fileContent.split("/**", 2)[1].split("*/", 1)[0];
|
||||
const out = {};
|
||||
let field = "";
|
||||
|
@ -160,8 +154,15 @@ export default class AddonManager {
|
|||
for (const line of block.split(splitRegex)) {
|
||||
if (line.length === 0) continue;
|
||||
if (line.charAt(0) === "@" && line.charAt(1) !== " ") {
|
||||
out[field] = accum.trim();
|
||||
const l = line.indexOf(" ");
|
||||
if (!out[field]) {
|
||||
out[field] = accum.trim();
|
||||
}
|
||||
else {
|
||||
if (!Array.isArray(out[field])) out[field] = [out[field]];
|
||||
out[field].push(accum.trim());
|
||||
}
|
||||
let l = line.indexOf(" ");
|
||||
if (l < 0) l = line.length;
|
||||
field = line.substring(1, l);
|
||||
accum = line.substring(l + 1);
|
||||
}
|
||||
|
@ -169,7 +170,13 @@ export default class AddonManager {
|
|||
accum += " " + line.replace("\\n", "\n").replace(escapedAtRegex, "@");
|
||||
}
|
||||
}
|
||||
out[field] = accum.trim();
|
||||
if (!out[field]) {
|
||||
out[field] = accum.trim();
|
||||
}
|
||||
else {
|
||||
if (!Array.isArray(out[field])) out[field] = [out[field]];
|
||||
out[field].push(accum.trim());
|
||||
}
|
||||
delete out[""];
|
||||
out.format = "jsdoc";
|
||||
return out;
|
||||
|
@ -181,9 +188,7 @@ export default class AddonManager {
|
|||
fileContent = stripBOM(fileContent);
|
||||
const stats = fs.statSync(filename);
|
||||
const addon = this.extractMeta(fileContent, path.basename(filename));
|
||||
if (!addon.author) addon.author = Strings.Addons.unknownAuthor;
|
||||
if (!addon.version) addon.version = "???";
|
||||
if (!addon.description) addon.description = Strings.Addons.noDescription;
|
||||
|
||||
// if (!addon.name || !addon.author || !addon.description || !addon.version) return new AddonError(addon.name || path.basename(filename), filename, "Addon is missing name, author, description, or version", {message: "Addon must provide name, author, description, and version.", stack: ""}, this.prefix);
|
||||
addon.id = addon.name || path.basename(filename);
|
||||
addon.slug = path.basename(filename).replace(this.extension, "").replace(/ /g, "-");
|
||||
|
|
|
@ -8,12 +8,16 @@ import AddonManager from "./addonmanager";
|
|||
import Settings from "./settingsmanager";
|
||||
import DOMManager from "./dommanager";
|
||||
import Strings from "./strings";
|
||||
import DataStore from "./datastore";
|
||||
import Utilities from "./utilities";
|
||||
|
||||
import Toasts from "@ui/toasts";
|
||||
import Modals from "@ui/modals";
|
||||
import SettingsRenderer from "@ui/settings";
|
||||
|
||||
|
||||
const varRegex = /^(checkbox|text|color|select|number|range)\s+([A-Za-z0-9-_]+)\s+"([^"]+)"\s+(.*)$/;
|
||||
|
||||
export default new class ThemeManager extends AddonManager {
|
||||
get name() {return "ThemeManager";}
|
||||
get extension() {return ".theme.css";}
|
||||
|
@ -64,10 +68,54 @@ export default new class ThemeManager extends AddonManager {
|
|||
if (!addon.name || !addon.author || !addon.description || !addon.version) return new AddonError(addon.name || addon.filename, addon.filename, "Addon is missing name, author, description, or version", {message: "Addon must provide name, author, description, and version.", stack: ""}, this.prefix);
|
||||
}
|
||||
|
||||
extractMeta(fileContent, filename) {
|
||||
const metaInfo = super.extractMeta(fileContent, filename);
|
||||
if (!metaInfo.var) return metaInfo;
|
||||
|
||||
if (!Array.isArray(metaInfo.var)) metaInfo.var = [metaInfo.var];
|
||||
|
||||
const variables = [];
|
||||
for (const v of metaInfo.var) {
|
||||
const match = v.match(varRegex);
|
||||
if (!match || match.length !== 5) continue;
|
||||
const type = match[1];
|
||||
const variable = match[2];
|
||||
const name = match[3];
|
||||
const value = match[4];
|
||||
if (type === "checkbox") variables.push({type: "switch", id: variable, name: name, value: parseInt(value) === 1});
|
||||
if (type === "text") variables.push({type: "text", id: variable, name: name, value: value});
|
||||
if (type === "color") variables.push({type: "color", id: variable, name: name, value: value, defaultValue: value});
|
||||
|
||||
if (type === "number" || type === "range") {
|
||||
// [default, min, max, step, units]
|
||||
const parsed = JSON.parse(value);
|
||||
variables.push({type: type === "number" ? type : "slider", id: variable, name: name, value: parsed[0], min: parsed[1], max: parsed[2], step: parsed[3]});
|
||||
}
|
||||
if (type === "select") {
|
||||
const parsed = JSON.parse(value);
|
||||
let selected, options;
|
||||
if (Array.isArray(parsed)) {
|
||||
selected = parsed.find(o => o.endsWith("*")).replace("*", "");
|
||||
options = parsed.map(o => ({label: o.replace("*", ""), value: o.replace("*", "")}));
|
||||
}
|
||||
else {
|
||||
selected = Object.keys(parsed).find(k => k.endsWith("*"));
|
||||
selected = parsed[selected];
|
||||
options = Object.entries(parsed).map(a => ({label: a[0].replace("*", ""), value: a[1]}));
|
||||
}
|
||||
variables.push({type: "dropdown", id: variable, name: name, options: options, value: selected || options[0].value});
|
||||
}
|
||||
}
|
||||
metaInfo.var = variables;
|
||||
|
||||
return metaInfo;
|
||||
}
|
||||
|
||||
requireAddon(filename) {
|
||||
const addon = super.requireAddon(filename);
|
||||
addon.css = addon.fileContent;
|
||||
delete addon.fileContent;
|
||||
this.loadThemeSettings(addon);
|
||||
if (addon.format == "json") addon.css = addon.css.split("\n").slice(1).join("\n");
|
||||
return addon;
|
||||
}
|
||||
|
@ -79,6 +127,7 @@ export default new class ThemeManager extends AddonManager {
|
|||
const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : idOrAddon;
|
||||
if (!addon) return;
|
||||
DOMManager.injectTheme(addon.slug + "-theme-container", addon.css);
|
||||
DOMManager.injectTheme(addon.slug + "-theme-settings", this.buildCSSVars(addon));
|
||||
Toasts.show(Strings.Addons.enabled.format({name: addon.name, version: addon.version}));
|
||||
}
|
||||
|
||||
|
@ -86,6 +135,50 @@ export default new class ThemeManager extends AddonManager {
|
|||
const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : idOrAddon;
|
||||
if (!addon) return;
|
||||
DOMManager.removeTheme(addon.slug + "-theme-container");
|
||||
DOMManager.removeTheme(addon.slug + "-theme-settings");
|
||||
Toasts.show(Strings.Addons.disabled.format({name: addon.name, version: addon.version}));
|
||||
}
|
||||
|
||||
getThemeSettingsPanel(themeId, vars) {
|
||||
return SettingsRenderer.getSettingsGroup(vars, Utilities.debounce((id, value) => this.updateThemeSettings(themeId, id, value), 100));
|
||||
}
|
||||
|
||||
loadThemeSettings(addon) {
|
||||
const all = DataStore.getData("theme_settings") || {};
|
||||
const stored = all?.[addon.id];
|
||||
if (!stored) return;
|
||||
for (const v of addon.var) {
|
||||
if (v.id in stored) v.value = stored[v.id];
|
||||
}
|
||||
}
|
||||
|
||||
updateThemeSettings(themeId, id, value) {
|
||||
const addon = this.addonList.find(p => p.id == themeId);
|
||||
const varToUpdate = addon.var.find(v => v.id === id);
|
||||
varToUpdate.value = value;
|
||||
DOMManager.injectTheme(addon.slug + "-theme-settings", this.buildCSSVars(addon));
|
||||
this.saveThemeSettings(themeId);
|
||||
}
|
||||
|
||||
saveThemeSettings(themeId) {
|
||||
const all = DataStore.getData("theme_settings") || {};
|
||||
const addon = this.addonList.find(p => p.id == themeId);
|
||||
const data = {};
|
||||
for (const v of addon.var) {
|
||||
data[v.id] = v.value;
|
||||
}
|
||||
all[themeId] = data;
|
||||
DataStore.setData("theme_settings", all);
|
||||
}
|
||||
|
||||
buildCSSVars(idOrAddon) {
|
||||
const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : idOrAddon;
|
||||
const lines = [`:root {`];
|
||||
for (const v of addon.var) {
|
||||
const value = typeof(v.value) === "boolean" ? v.value ? 1 : 0 : v.value;
|
||||
lines.push(` --${v.id}: ${value};`);
|
||||
}
|
||||
lines.push(`}`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
};
|
|
@ -50,7 +50,7 @@
|
|||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
margin-left: 5px !important;
|
||||
max-width: 340px;
|
||||
max-width: 310px;
|
||||
}
|
||||
|
||||
.bd-color-picker-swatch-item {
|
||||
|
|
|
@ -40,6 +40,10 @@ export default class Modals {
|
|||
}
|
||||
|
||||
static get ModalQueue() {return this._ModalQueue ??= [];}
|
||||
static get ConfirmationModal() {return this._ConfirmationModal ??= WebpackModules.getModule(m => m?.toString?.()?.includes(".confirmButtonColor"), {searchExports: true}) ?? ConfirmationModal;}
|
||||
static get ModalRoot() {return this._ModalRoot ??= WebpackModules.getModule(m => m?.toString?.()?.includes("ENTERING") && m?.toString?.()?.includes("headerId"), {searchExports: true}) ?? ModalRoot;}
|
||||
static get ModalComponents() {return this._ModalComponents ??= WebpackModules.getByProps("Header", "Footer");}
|
||||
static get Buttons() {return this._Buttons ??= WebpackModules.getModule(m => m.BorderColors, {searchExports: true});}
|
||||
|
||||
static async initialize() {
|
||||
const names = ["ModalActions"];
|
||||
|
@ -194,9 +198,10 @@ export default class Modals {
|
|||
].filter(Boolean));
|
||||
});
|
||||
}
|
||||
}, React.createElement(ConfirmationModal, Object.assign({
|
||||
}, React.createElement(this.ConfirmationModal, Object.assign({
|
||||
header: title,
|
||||
danger: danger,
|
||||
danger: danger ? this.Buttons.Colors.RED : this.Buttons.Colors.BRAND,
|
||||
confirmButtonColor: danger ? this.Buttons.Colors.RED : this.Buttons.Colors.BRAND,
|
||||
confirmText: confirmText,
|
||||
cancelText: cancelText,
|
||||
onConfirm: onConfirm,
|
||||
|
@ -263,15 +268,16 @@ export default class Modals {
|
|||
if (typeof(child) === "function") child = React.createElement(child);
|
||||
|
||||
const options = {
|
||||
className: "bd-addon-modal",
|
||||
size: ModalRoot.Sizes.MEDIUM,
|
||||
className: "bd-addon-modal " + this.ModalComponents.Sizes.MEDIUM ?? this.ModalRoot.Sizes.MEDIUM,
|
||||
size: this.ModalComponents.Sizes.MEDIUM ?? this.ModalRoot.Sizes.MEDIUM,
|
||||
header: `${name} Settings`,
|
||||
cancelText: null,
|
||||
confirmText: Strings.Modals.done
|
||||
confirmText: Strings.Modals.done,
|
||||
confirmButtonColor: this.Buttons.Colors.BRAND
|
||||
};
|
||||
|
||||
return this.openModal(props => {
|
||||
return React.createElement(ErrorBoundary, null, React.createElement(ConfirmationModal, Object.assign(options, props), child));
|
||||
return React.createElement(ErrorBoundary, null, React.createElement(this.ConfirmationModal, Object.assign(options, props), child));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -58,6 +58,15 @@ export default new class SettingsRenderer {
|
|||
})];
|
||||
}
|
||||
|
||||
getSettingsGroup(settings, onChange) {
|
||||
return () => React.createElement(SettingsGroup, {
|
||||
onChange: onChange,
|
||||
shown: true,
|
||||
collapsible: false,
|
||||
settings: settings
|
||||
});
|
||||
}
|
||||
|
||||
getAddonPanel(title, addonList, addonState, options = {}) {
|
||||
return () => React.createElement(AddonList, Object.assign({}, {
|
||||
title: title,
|
||||
|
|
|
@ -3,6 +3,7 @@ import Strings from "@modules/strings";
|
|||
import Events from "@modules/emitter";
|
||||
import DataStore from "@modules/datastore";
|
||||
import DiscordModules from "@modules/discordmodules";
|
||||
import ThemeManager from "@modules/thememanager";
|
||||
|
||||
import Button from "../base/button";
|
||||
import SettingsTitle from "./title";
|
||||
|
@ -154,8 +155,8 @@ export default function AddonList({prefix, type, title, folder, addonList, addon
|
|||
}
|
||||
|
||||
return sorted.map(addon => {
|
||||
const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function";
|
||||
const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance);
|
||||
const hasSettings = (addon.var && addon.var.length) || (addon.instance && typeof(addon.instance.getSettingsPanel) === "function");
|
||||
const getSettings = hasSettings && (addon.var ? ThemeManager.getThemeSettingsPanel(addon.id, addon.var) : addon.instance.getSettingsPanel.bind(addon.instance));
|
||||
return <ErrorBoundary><AddonCard disabled={addon.partial} type={type} editAddon={() => triggerEdit(addon.id)} deleteAddon={() => triggerDelete(addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>;
|
||||
});
|
||||
}, [addonList, addonState, onChange, reload, triggerDelete, triggerEdit, type, sort, ascending, query, forced]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
|
Loading…
Reference in New Issue