Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lay the groundwork for potential theme settings #1664

Open
wants to merge 3 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
57 changes: 31 additions & 26 deletions renderer/src/modules/addonmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,46 +130,53 @@ 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 = "";
let accum = "";
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);
}
else {
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;
Expand All @@ -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, "-");
Expand Down
98 changes: 98 additions & 0 deletions renderer/src/modules/thememanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";}
Expand Down Expand Up @@ -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;
}
Expand All @@ -79,13 +130,60 @@ 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}));
}

removeTheme(idOrAddon) {
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");
}
};
2 changes: 1 addition & 1 deletion renderer/src/styles/ui/colorpicker.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 7 additions & 1 deletion renderer/src/styles/ui/modal.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -118,7 +124,7 @@
.bd-modal-medium {
width: 600px;
max-height: 800px;
min-height: 400px;
min-height: 200px;
}

.bd-modal-large {
Expand Down
5 changes: 5 additions & 0 deletions renderer/src/ui/modals/root.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,7 +39,9 @@ export default function ModalRoot({className, transitionState, children, size =
className={Utilities.className("bd-modal-root", size, className, style)}
style={springStyles}
>
<FocusLock disableTrack={true}>
{children}
</FocusLock>
</Spring.animated.div>;
// const [visible, setVisible] = React.useState(true);

Expand Down
9 changes: 9 additions & 0 deletions renderer/src/ui/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion renderer/src/ui/settings/components/item.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default function SettingItem({id, name, note, inline, children}) {
<label htmlFor={id} className={"bd-setting-title"}>{name}</label>
{inline && children}
</div>
<div className={"bd-setting-note"}>{note}</div>
{note && <div className={"bd-setting-note"}>{note}</div>}
{!inline && children}
<div className={"bd-setting-divider"} />
</div>;
Expand Down