diff --git a/renderer/src/modules/addonmanager.js b/renderer/src/modules/addonmanager.js
index 365445b27..1009babaa 100644
--- a/renderer/src/modules/addonmanager.js
+++ b/renderer/src/modules/addonmanager.js
@@ -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
+ */
- 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;
+ 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;
}
- 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, "-");
diff --git a/renderer/src/modules/thememanager.js b/renderer/src/modules/thememanager.js
index 008ed669f..a73769ae3 100644
--- a/renderer/src/modules/thememanager.js
+++ b/renderer/src/modules/thememanager.js
@@ -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,57 @@ 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 label = match[3].split(":");
+ const name = label[0].trim();
+ const note = label[1]?.trim();
+ const value = match[4];
+ if (type === "checkbox") variables.push({type: "switch", id: variable, name: name, note: note, value: parseInt(value) === 1});
+ if (type === "text") variables.push({type: "text", id: variable, name: name, note: note, value: value});
+ if (type === "color") variables.push({type: "color", id: variable, name: name, note: note, 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, note: note, 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, note: note, options: options, value: selected || options[0].value});
+ }
+ }
+ metaInfo.var = variables;
+ metaInfo.instance = {getSettingsPanel: this.getThemeSettingsPanel(metaInfo.name, metaInfo.var)};
+
+ 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 +130,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 +138,52 @@ 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 || !addon.var || !Array.isArray(addon.var)) 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 {`];
+ if (Array.isArray(addon.var)) {
+ 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");
+ }
};
\ No newline at end of file
diff --git a/renderer/src/styles/ui/colorpicker.css b/renderer/src/styles/ui/colorpicker.css
index 24f240c0e..68c9677c4 100644
--- a/renderer/src/styles/ui/colorpicker.css
+++ b/renderer/src/styles/ui/colorpicker.css
@@ -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 {
diff --git a/renderer/src/styles/ui/modal.css b/renderer/src/styles/ui/modal.css
index 5d05ee6ef..b853fcaff 100644
--- a/renderer/src/styles/ui/modal.css
+++ b/renderer/src/styles/ui/modal.css
@@ -85,6 +85,12 @@
max-height: 100%;
}
+.bd-modal-root > div[role="dialog"] {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
.bd-close-button {
height: 26px;
padding: 4px;
@@ -118,7 +124,7 @@
.bd-modal-medium {
width: 600px;
max-height: 800px;
- min-height: 400px;
+ min-height: 200px;
}
.bd-modal-large {
diff --git a/renderer/src/ui/modals/root.jsx b/renderer/src/ui/modals/root.jsx
index 8f25a0876..37f5ea780 100644
--- a/renderer/src/ui/modals/root.jsx
+++ b/renderer/src/ui/modals/root.jsx
@@ -19,6 +19,9 @@ export const Styles = Object.freeze({
});
+const FocusLock = WebpackModules.getModule(m => m?.render?.toString().includes("impressionProperties") && m?.render?.toString().includes(".Provider"), {searchExports: true}) ?? React.Fragment;
+
+
export default function ModalRoot({className, transitionState, children, size = Sizes.DYNAMIC, style = Styles.CUSTOM}) {
const visible = transitionState == 0 || transitionState == 1; // 300 ms
// const visible = transitionState;
@@ -36,7 +39,9 @@ export default function ModalRoot({className, transitionState, children, size =
className={Utilities.className("bd-modal-root", size, className, style)}
style={springStyles}
>
+