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} > + {children} + ; // const [visible, setVisible] = React.useState(true); diff --git a/renderer/src/ui/settings.js b/renderer/src/ui/settings.js index c54d3c073..a4803d1bc 100644 --- a/renderer/src/ui/settings.js +++ b/renderer/src/ui/settings.js @@ -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, diff --git a/renderer/src/ui/settings/components/item.jsx b/renderer/src/ui/settings/components/item.jsx index 4fd212432..fcf63ba68 100644 --- a/renderer/src/ui/settings/components/item.jsx +++ b/renderer/src/ui/settings/components/item.jsx @@ -7,7 +7,7 @@ export default function SettingItem({id, name, note, inline, children}) { {inline && children} -
{note}
+ {note &&
{note}
} {!inline && children}
;