Merge pull request #1566 from BetterDiscord/functional-components

Convert to functional react components
This commit is contained in:
Zerebos 2023-03-22 14:11:51 -04:00 committed by GitHub
commit 2aa71d87d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 1282 additions and 1641 deletions

View File

@ -21,6 +21,7 @@
"dotenv": "^16.0.3",
"eslint": "^8.23.0",
"eslint-plugin-react": "^7.31.6",
"eslint-plugin-react-hooks": "^4.6.0",
"mocha": "^10.0.0",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"

View File

@ -8,6 +8,7 @@ importers:
dotenv: ^16.0.3
eslint: ^8.23.0
eslint-plugin-react: ^7.31.6
eslint-plugin-react-hooks: ^4.6.0
mocha: ^10.0.0
webpack: ^5.74.0
webpack-cli: ^4.10.0
@ -16,6 +17,7 @@ importers:
dotenv: 16.0.3
eslint: 8.23.0
eslint-plugin-react: 7.31.6_eslint@8.23.0
eslint-plugin-react-hooks: 4.6.0_eslint@8.23.0
mocha: 10.0.0
webpack: 5.74.0_webpack-cli@4.10.0
webpack-cli: 4.10.0_webpack@5.74.0
@ -2343,6 +2345,15 @@ packages:
engines: {node: '>=10'}
dev: true
/eslint-plugin-react-hooks/4.6.0_eslint@8.23.0:
resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==}
engines: {node: '>=10'}
peerDependencies:
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0
dependencies:
eslint: 8.23.0
dev: true
/eslint-plugin-react/7.31.6_eslint@8.23.0:
resolution: {integrity: sha512-CXu4eu28sb8Sd2+cyUYsJVyDvpTlaXPG+bOzzpS9IzZKtye96AYX3ZmHQ6ayn/OAIQ/ufDJP8ElPWd63Pepn9w==}
engines: {node: '>=4'}

View File

@ -1,7 +1,11 @@
{
"extends": ["plugin:react/recommended"],
"extends": [
"plugin:react/recommended",
"plugin:react-hooks/recommended"
],
"plugins": [
"react"
"react",
"react-hooks"
],
"settings": {
"react": {

View File

@ -66,10 +66,11 @@ const ReactUtils = {
constructor(props) {
super(props);
this.element = element;
this.state = {hasError: false};
}
componentDidCatch() {this.setState({hasError: true});}
componentDidMount() {this.refs.element.appendChild(this.element);}
render() {return DiscordModules.React.createElement("div", {className: "react-wrapper", ref: "element"});}
render() {return this.state.hasError ? null : DiscordModules.React.createElement("div", {className: "react-wrapper", ref: "element"});}
};
}

View File

@ -8,6 +8,7 @@ import ThemeManager from "./thememanager";
import Settings from "./settingsmanager";
import * as Builtins from "builtins";
import Modals from "../ui/modals";
import FloatingWindows from "../ui/floatingwindows";
import DataStore from "./datastore";
import DiscordModules from "./discordmodules";
import LoadingIcon from "../loadingicon";
@ -47,6 +48,7 @@ export default new class Core {
await Editor.initialize();
Modals.initialize();
FloatingWindows.initialize();
Logger.log("Startup", "Initializing Builtins");
for (const module in Builtins) {

View File

@ -44,7 +44,7 @@ export default new class PluginManager extends AddonManager {
this.setupFunctions();
Settings.registerPanel("plugins", Strings.Panels.plugins, {
order: 3,
element: () => SettingsRenderer.getAddonPanel(Strings.Panels.plugins, this.addonList, this.state, {
element: SettingsRenderer.getAddonPanel(Strings.Panels.plugins, this.addonList, this.state, {
type: this.prefix,
folder: this.addonFolder,
onChange: this.togglePlugin.bind(this),

View File

@ -23,7 +23,7 @@ export default new class ThemeManager extends AddonManager {
const errors = super.initialize();
Settings.registerPanel("themes", Strings.Panels.themes, {
order: 4,
element: () => SettingsRenderer.getAddonPanel(Strings.Panels.themes, this.addonList, this.state, {
element: SettingsRenderer.getAddonPanel(Strings.Panels.themes, this.addonList, this.state, {
type: this.prefix,
folder: this.addonFolder,
onChange: this.toggleTheme.bind(this),

View File

@ -3,24 +3,19 @@ import Extension from "./icons/extension";
import ThemeIcon from "./icons/theme";
import Divider from "./divider";
const Parser = Object(WebpackModules.getByProps("defaultRules", "parse")).defaultRules;
const {useState, useCallback, useMemo} = React;
const joinClassNames = (...classNames) => classNames.filter(e => e).join(" ");
class AddonError extends React.Component {
constructor(props) {
super(props);
function AddonError({err, index}) {
const [expanded, setExpanded] = useState(false);
const toggle = useCallback(() => setExpanded(!expanded), [expanded]);
this.state = {
expanded: false
};
}
toggle() {
this.setState({expanded: !this.state.expanded});
}
renderErrorBody(err) {
function renderErrorBody() {
const stack = err?.error?.stack ?? err.stack;
if (!this.state.expanded || !stack) return null;
if (!expanded || !stack) return null;
return <div className="bd-addon-error-body">
<Divider />
<div className="bd-addon-error-stack">
@ -28,10 +23,9 @@ class AddonError extends React.Component {
</div>
</div>;
}
render() {
const err = this.props.err;
return <div key={`${err.type}-${this.props.index}`} className={joinClassNames("bd-addon-error", (this.state.expanded) ? "expanded" : "collapsed")}>
<div className="bd-addon-error-header" onClick={() => {this.toggle();}} >
return <div key={`${err.type}-${index}`} className={joinClassNames("bd-addon-error", (expanded) ? "expanded" : "collapsed")}>
<div className="bd-addon-error-header" onClick={toggle} >
<div className="bd-addon-error-icon">
{err.type == "plugin" ? <Extension /> : <ThemeIcon />}
</div>
@ -48,65 +42,32 @@ class AddonError extends React.Component {
<path fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" d="M7 10L12 15 17 10" aria-hidden="true"></path>
</svg>
</div>
{this.renderErrorBody(err)}
{renderErrorBody(err)}
</div>;
}
function generateTab(id, errors) {
return {id, errors, name: Strings.Panels[id]};
}
export default class AddonErrorModal extends React.Component {
constructor(props) {
super(props);
const tabs = this.getTabs();
export default function AddonErrorModal({pluginErrors, themeErrors}) {
const tabs = useMemo(() => {
return [
pluginErrors.length && generateTab("plugins", pluginErrors),
themeErrors.length && generateTab("themes", themeErrors)
].filter(e => e);
}, [pluginErrors, themeErrors]);
this.state = {
selectedTab: tabs[0].id,
};
}
const [tabId, setTab] = useState(tabs[0].id);
const switchToTab = useCallback((id) => setTab(id), []);
const selectedTab = tabs.find(e => e.id === tabId);
mergeErrors(errors1 = [], errors2 = []) {
const list = [];
const allErrors = [...errors2, ...errors1];
for (const error of allErrors) {
if (list.find(e => e.file === error.file)) continue;
list.push(error);
}
return list;
}
refreshTabs(pluginErrors, themeErrors) {
this._tabs = null;
this.props.pluginErrors = this.mergeErrors(this.props.pluginErrors, pluginErrors);
this.props.themeErrors = this.mergeErrors(this.props.themeErrors, themeErrors);
this.forceUpdate();
}
generateTab(id, errors) {
return {
id: id,
name: Strings.Panels[id],
errors: errors
};
}
getTabs() {
return this._tabs || (this._tabs = [
this.props.pluginErrors.length && this.generateTab("plugins", this.props.pluginErrors),
this.props.themeErrors.length && this.generateTab("themes", this.props.themeErrors)
].filter(e => e));
}
switchToTab(id) {
this.setState({selectedTab: id});
}
render() {
const selectedTab = this.getTabs().find(e => this.state.selectedTab === e.id);
const tabs = this.getTabs();
return <>
<div className={`bd-error-modal-header ${DiscordClasses.Modal.header} ${DiscordClasses.Modal.separator}`}>
<h4 className={`${DiscordClasses.Titles.defaultColor} ${DiscordClasses.Text.size14} ${DiscordClasses.Titles.h4} ${DiscordClasses.Margins.marginBottom8}`}>{Strings.Modals.addonErrors}</h4>
<div className="bd-tab-bar">
{tabs.map(tab => <div onClick={() => {this.switchToTab(tab.id);}} className={joinClassNames("bd-tab-item", tab.id === selectedTab.id && "selected")}>{tab.name}</div>)}
{tabs.map(tab => <div onClick={() => {switchToTab(tab.id);}} className={joinClassNames("bd-tab-item", tab.id === selectedTab.id && "selected")}>{tab.name}</div>)}
</div>
</div>
<div className={`bd-error-modal-content ${DiscordClasses.Modal.content} ${DiscordClasses.Scrollers.thin}`}>
@ -116,4 +77,3 @@ export default class AddonErrorModal extends React.Component {
</div>
</>;
}
}

View File

@ -1,17 +1,15 @@
import {React, DiscordClasses} from "modules";
import SimpleMarkdown from "../../structs/markdown";
export default class EmptyImage extends React.Component {
render() {
return <div className={`bd-empty-image-container ${DiscordClasses.EmptyImage.emptyContainer}` + (this.props.className ? ` ${this.props.className}` : "")}>
export default function EmptyImage(props) {
return <div className={`bd-empty-image-container ${DiscordClasses.EmptyImage.emptyContainer}` + (props.className ? ` ${props.className}` : "")}>
<div className={`bd-empty-image ${DiscordClasses.EmptyImage.emptyImage}`}></div>
<div className={`bd-empty-image-header ${DiscordClasses.EmptyImage.emptyHeader}`}>
{this.props.title || "You don't have anything!"}
{props.title || "You don't have anything!"}
</div>
<div className={`bd-empty-image-message`}>
{SimpleMarkdown.parseToReact(this.props.message || "You should probably get something.")}
{SimpleMarkdown.parseToReact(props.message || "You should probably get something.")}
</div>
{this.props.children}
{props.children}
</div>;
}
}

View File

@ -1,13 +1,11 @@
import {React, DiscordModules} from "modules";
import MagnifyingGlass from "../icons/magnifyingglass";
export default class NoResults extends React.Component {
render() {
return <div className={"bd-empty-results" + (this.props.className ? ` ${this.props.className}` : "")}>
export default function NoResults(props) {
return <div className={"bd-empty-results" + (props.className ? ` ${props.className}` : "")}>
<MagnifyingGlass />
<div className="bd-empty-results-text">
{this.props.text || DiscordModules.Strings.SEARCH_NO_RESULTS || ""}
{props.text || DiscordModules.Strings.SEARCH_NO_RESULTS || ""}
</div>
</div>;
}
}

View File

@ -1,27 +1,23 @@
import {React} from "modules";
export default class Checkbox extends React.Component {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
this.state = {checked: this.props.checked || false};
}
const {useState, useCallback} = React;
export default function Checkbox({checked: initialState, text, onChange: notifyParent}) {
const [checked, setChecked] = useState(initialState);
const onClick = useCallback(() => {
notifyParent?.(!checked);
setChecked(!checked);
}, [notifyParent, checked]);
render() {
return <div className="checkbox-item">
<div className="checkbox-label label-JWQiNe da-label">{this.props.text}</div>
<div className="checkbox-wrapper checkbox-3kaeSU da-checkbox checkbox-3EVISJ da-checkbox" onClick={this.onClick}>
<div className="checkbox-label label-JWQiNe da-label">{text}</div>
<div className="checkbox-wrapper checkbox-3kaeSU da-checkbox checkbox-3EVISJ da-checkbox" onClick={onClick}>
<div className="checkbox-inner checkboxInner-3yjcPe da-checkboxInner">
<input className="checkbox checkboxElement-1qV33p da-checkboxElement" checked={this.state.checked} type="checkbox" />
<input className="checkbox checkboxElement-1qV33p da-checkboxElement" checked={checked} type="checkbox" />
<span></span>
</div>
<span></span>
</div>
</div>;
}
onClick() {
this.props.onChange(!this.state.checked);
this.setState({checked: !this.state.checked});
}
}

View File

@ -1,91 +1,65 @@
import {React, Settings, Events, Strings} from "modules";
import Editor from "./editor";
// import Checkbox from "./checkbox";
import Refresh from "../icons/reload";
import Save from "../icons/save";
import Edit from "../icons/edit";
import Detach from "../icons/detach";
export default class CssEditor extends React.Component {
const {useState, useCallback, useEffect, forwardRef, useImperativeHandle, useRef} = React;
constructor(props) {
super(props);
this.hasUnsavedChanges = false;
export default forwardRef(function CssEditor({css, openNative, update, save, onChange: notifyParent, readOnly = false, id = "bd-customcss-editor", openDetached = false}, ref) {
const editorRef = useRef(null);
const [hasUnsavedChanges, setUnsaved] = useState(false);
this.onChange = this.onChange.bind(this);
this.toggleLiveUpdate = this.toggleLiveUpdate.bind(this);
this.updateCss = this.updateCss.bind(this);
this.saveCss = this.saveCss.bind(this);
this.openDetached = this.props.openDetached ? this.openDetached.bind(this) : null;
this.openNative = this.openNative.bind(this);
this.updateEditor = this.updateEditor.bind(this);
const updateEditor = useCallback((newCSS) => {
editorRef.current.value = newCSS;
}, [editorRef]);
this.controls = [
{label: React.createElement(Refresh, {size: "18px"}), tooltip: Strings.CustomCSS.update, onClick: this.updateCss},
{label: React.createElement(Save, {size: "18px"}), tooltip: Strings.CustomCSS.save, onClick: this.saveCss},
{label: React.createElement(Edit, {size: "18px"}), tooltip: Strings.CustomCSS.openNative, onClick: this.openNative},
{label: Strings.Collections.settings.customcss.liveUpdate.name, type: "checkbox", onChange: this.toggleLiveUpdate, checked: Settings.get("settings", "customcss", "liveUpdate"), side: "right"}
];
if (this.openDetached) this.controls.push({label: React.createElement(Detach, {size: "18px"}), tooltip: Strings.CustomCSS.openDetached, onClick: this.openDetached, side: "right"});
}
useImperativeHandle(ref, () => {
return {
resize() {editorRef.current.resize();},
showSettings() {editorRef.current.showSettings();},
get value() {return editorRef.current.getValue();},
set value(newValue) {editorRef.current.setValue(newValue);},
get hasUnsavedChanges() {return hasUnsavedChanges;}
};
}, [hasUnsavedChanges]);
componentDidMount() {
Events.on("customcss-updated", this.updateEditor);
}
useEffect(() => {
Events.on("customcss-updated", updateEditor);
return () => Events.off("customcss-updated", updateEditor);
}, [updateEditor]);
componentWillUnmount() {
Events.off("customcss-updated", this.updateEditor);
}
const toggleLiveUpdate = useCallback((checked) => Settings.set("settings", "customcss", "liveUpdate", checked), []);
const updateCss = useCallback((event, newCSS) => update?.(newCSS), [update]);
const popoutNative = useCallback(() => openNative?.(), [openNative]);
const popout = useCallback((event, currentCSS) => openDetached?.(currentCSS), [openDetached]);
updateEditor(newCSS) {
if (!this.editor) return;
this.editor.value = newCSS;
}
const onChange = useCallback((newCSS) => {
notifyParent?.(newCSS);
setUnsaved(true);
}, [notifyParent]);
get value() {return this.editor.session.getValue();}
set value(newValue) {
this.editor.setValue(newValue);
}
const saveCss = useCallback((event, newCSS) => {
save?.(newCSS);
setUnsaved(false);
}, [save]);
showSettings() {return this.editor.keyBinding.$defaultHandler.commands.showSettingsMenu.exec(this.editor);}
resize() {return this.editor.resize();}
setEditorRef(editor) {
this.editor = editor;
if (this.props.editorRef && typeof(this.props.editorRef.current) !== "undefined") this.props.editorRef.current = editor;
else if (this.props.editorRef) this.props.editorRef = editor;
}
onChange() {
this.hasUnsavedChanges = true;
if (this.props.onChange) this.props.onChange(...arguments);
}
render() {
return <Editor ref={this.setEditorRef.bind(this)} readOnly={this.props.readOnly} id={this.props.id || "bd-customcss-editor"} onChange={this.onChange} controls={this.controls} value={this.props.css} />;
}
toggleLiveUpdate(checked) {
Settings.set("settings", "customcss", "liveUpdate", checked);
}
updateCss(event, newCss) {
if (this.props.update) this.props.update(newCss);
}
saveCss(event, newCss) {
this.hasUnsavedChanges = false;
if (this.props.save) this.props.save(newCss);
}
openDetached(event, currentCSS) {
if (!this.props.openDetached) return;
this.props.openDetached(currentCSS);
}
openNative() {
if (this.props.openNative) this.props.openNative();
}
}
return <Editor
ref={editorRef}
readOnly={readOnly}
id={id}
onChange={onChange}
controls={[
{label: <Refresh size="18px" />, tooltip: Strings.CustomCSS.update, onClick: updateCss},
{label: <Save size="18px" />, tooltip: Strings.CustomCSS.save, onClick: saveCss},
{label: <Edit size="18px" />, tooltip: Strings.CustomCSS.openNative, onClick: popoutNative},
{label: Strings.Collections.settings.customcss.liveUpdate.name, type: "checkbox", onChange: toggleLiveUpdate, checked: Settings.get("settings", "customcss", "liveUpdate"), side: "right"},
openDetached && {label: <Detach size="18px" />, tooltip: Strings.CustomCSS.openDetached, onClick: popout, side: "right"}
].filter(c => c)}
value={css}
/>;
});

View File

@ -2,41 +2,79 @@ import {React, DiscordModules, Settings} from "modules";
import Checkbox from "./checkbox";
const {useState, useCallback, useEffect, forwardRef, useMemo, useImperativeHandle} = React;
const ThemeStore = DiscordModules.ThemeStore;
const languages = ["abap", "abc", "actionscript", "ada", "apache_conf", "asciidoc", "assembly_x86", "autohotkey", "batchfile", "bro", "c_cpp", "c9search", "cirru", "clojure", "cobol", "coffee", "coldfusion", "csharp", "csound_document", "csound_orchestra", "csound_score", "css", "curly", "d", "dart", "diff", "dockerfile", "dot", "drools", "dummy", "dummysyntax", "eiffel", "ejs", "elixir", "elm", "erlang", "forth", "fortran", "ftl", "gcode", "gherkin", "gitignore", "glsl", "gobstones", "golang", "graphqlschema", "groovy", "haml", "handlebars", "haskell", "haskell_cabal", "haxe", "hjson", "html", "html_elixir", "html_ruby", "ini", "io", "jack", "jade", "java", "javascript", "json", "jsoniq", "jsp", "jssm", "jsx", "julia", "kotlin", "latex", "less", "liquid", "lisp", "livescript", "logiql", "lsl", "lua", "luapage", "lucene", "makefile", "markdown", "mask", "matlab", "maze", "mel", "mushcode", "mysql", "nix", "nsis", "objectivec", "ocaml", "pascal", "perl", "pgsql", "php", "pig", "powershell", "praat", "prolog", "properties", "protobuf", "python", "r", "razor", "rdoc", "red", "rhtml", "rst", "ruby", "rust", "sass", "scad", "scala", "scheme", "scss", "sh", "sjs", "smarty", "snippets", "soy_template", "space", "sql", "sqlserver", "stylus", "svg", "swift", "tcl", "tex", "text", "textile", "toml", "tsx", "twig", "typescript", "vala", "vbscript", "velocity", "verilog", "vhdl", "wollok", "xml", "xquery", "yaml", "django"];
export default class CodeEditor extends React.Component {
static get defaultId() {return "bd-editor";}
constructor(props) {
super(props);
this.props.theme = ThemeStore?.theme === "light" ? "vs" : "vs-dark";
this.props.language = this.props.language.toLowerCase().replace(/ /g, "_");
if (!languages.includes(this.props.language)) this.props.language = CodeEditor.defaultProps.language;
this.bindings = [];
this.resize = this.resize.bind(this);
this.onChange = this.onChange.bind(this);
this.onThemeChange = this.onThemeChange.bind(this);
function makeButton(button, value) {
return <DiscordModules.Tooltip color="primary" position="top" text={button.tooltip}>
{props => {
return <button {...props} className="btn btn-primary" onClick={(event) => {button.onClick(event, value?.());}}>{button.label}</button>;
}}
</DiscordModules.Tooltip>;
}
static get defaultProps() {
function makeCheckbox(checkbox) {
return <Checkbox text={checkbox.label} onChange={checkbox.onChange} checked={checkbox.checked} />;
}
function buildControl(value, control) {
if (control.type == "checkbox") return makeCheckbox(control);
return makeButton(control, value);
}
export default forwardRef(function CodeEditor({value, language: requestedLang = "css", id = "bd-editor", controls = [], onChange: notifyParent}, ref) {
const language = useMemo(() => {
const requested = requestedLang.toLowerCase().replace(/ /g, "_");
if (!languages.includes(requested)) return "css";
return requested;
}, [requestedLang]);
const [theme, setTheme] = useState(() => ThemeStore?.theme === "light" ? "vs" : "vs-dark");
const [editor, setEditor] = useState(null);
const [, setBindings] = useState([]);
const onThemeChange = useCallback(() => {
const newTheme = ThemeStore?.theme === "light" ? "vs" : "vs-dark";
if (newTheme === theme) return;
if (window.monaco?.editor) window.monaco.editor.setTheme(newTheme);
setTheme(newTheme);
}, [theme]);
const onChange = useCallback(() => {
notifyParent?.(editor?.getValue());
}, [editor, notifyParent]);
const resize = useCallback(() => editor.layout(), [editor]);
const showSettings = useCallback(() => editor.keyBinding.$defaultHandler.commands.showSettingsMenu.exec(editor), [editor]);
useImperativeHandle(ref, () => {
return {
controls: [],
language: "css",
id: this.defaultId
resize,
showSettings,
get value() {return editor.getValue();},
set value(newValue) {editor.setValue(newValue);}
};
}
}, [editor, resize, showSettings]);
componentDidMount() {
useEffect(() => {
setBindings(bins => [...bins, editor?.onDidChangeModelContent(onChange)]);
return () => {
setBindings(bins => {
for (const binding of bins) binding?.dispose();
return [];
});
};
}, [editor, onChange]);
useEffect(() => {
let toDispose = null;
if (window.monaco?.editor) {
this.editor = window.monaco.editor.create(document.getElementById(this.props.id), {
value: this.props.value,
language: this.props.language,
theme: ThemeStore?.theme == "light" ? "vs" : "vs-dark",
const monacoEditor = window.monaco.editor.create(document.getElementById(id), {
value: value,
language: language,
theme: theme,
fontSize: Settings.get("settings", "editor", "fontSize"),
lineNumbers: Settings.get("settings", "editor", "lineNumbers"),
minimap: {enabled: Settings.get("settings", "editor", "minimap")},
@ -49,78 +87,51 @@ export default class CodeEditor extends React.Component {
renderWhitespace: Settings.get("settings", "editor", "renderWhitespace")
});
this.bindings.push(this.editor.onDidChangeModelContent(this.onChange));
toDispose = monacoEditor;
setEditor(monacoEditor);
}
else {
const textarea = document.createElement("textarea");
textarea.className = "bd-fallback-editor";
textarea.value = this.props.value;
textarea.onchange = (e) => this.onChange(e.target.value);
textarea.oninput = (e) => this.onChange(e.target.value);
textarea.value = value;
this.editor = {
setEditor({
dispose: () => textarea.remove(),
getValue: () => textarea.value,
setValue: (value) => textarea.value = value,
setValue: (val) => textarea.value = val,
layout: () => {},
onDidChangeModelContent: (cb) => {
textarea.onchange = cb;
textarea.oninput = cb;
}
});
document.getElementById(id).appendChild(textarea);
}
return () => {
toDispose?.dispose?.();
};
}, [id, language, theme, value]);
document.getElementById(this.props.id).appendChild(textarea);
}
useEffect(() => {
ThemeStore?.addChangeListener?.(onThemeChange);
window.addEventListener("resize", resize);
ThemeStore?.addChangeListener?.(this.onThemeChange);
window.addEventListener("resize", this.resize);
}
return () => {
window.removeEventListener("resize", resize);
ThemeStore?.removeChangeListener?.(onThemeChange);
};
}, [onThemeChange, resize]);
componentWillUnmount() {
window.removeEventListener("resize", this.resize);
ThemeStore?.removeChangeListener?.(this.onThemeChange);
for (const binding of this.bindings) binding.dispose();
this.editor.dispose();
}
onThemeChange() {
const newTheme = ThemeStore?.theme === "light" ? "vs" : "vs-dark";
if (newTheme === this.props.theme) return;
this.props.theme = newTheme;
if (window.monaco?.editor) window.monaco.editor.setTheme(this.props.theme);
}
if (editor && editor.layout) editor.layout();
get value() {return this.editor.getValue();}
set value(newValue) {this.editor.setValue(newValue);}
const controlsLeft = controls.filter(c => c.side != "right").map(buildControl.bind(null, () => editor?.getValue()));
const controlsRight = controls.filter(c => c.side == "right").map(buildControl.bind(null, () => editor?.getValue()));
onChange() {
if (this.props.onChange) this.props.onChange(this.value);
}
showSettings() {return this.editor.keyBinding.$defaultHandler.commands.showSettingsMenu.exec(this.editor);}
resize() {this.editor.layout();}
buildControl(control) {
if (control.type == "checkbox") return this.makeCheckbox(control);
return this.makeButton(control);
}
makeCheckbox(checkbox) {
return <Checkbox text={checkbox.label} onChange={checkbox.onChange} checked={checkbox.checked} />;
}
makeButton(button) {
return <DiscordModules.Tooltip color="primary" position="top" text={button.tooltip}>
{props => {
return <button {...props} className="btn btn-primary" onClick={(event) => {button.onClick(event, this.value);}}>{button.label}</button>;
}}
</DiscordModules.Tooltip>;
}
render() {
if (this.editor && this.editor.layout) this.editor.layout();
const controlsLeft = this.props.controls.filter(c => c.side != "right").map(this.buildControl.bind(this));
const controlsRight = this.props.controls.filter(c => c.side == "right").map(this.buildControl.bind(this));
return <div id="bd-editor-panel" className={this.props.theme}>
return <div id="bd-editor-panel" className={theme}>
<div id="bd-editor-controls">
<div className="controls-section controls-left">
{controlsLeft}
@ -130,8 +141,7 @@ export default class CodeEditor extends React.Component {
</div>
</div>
<div className="editor-wrapper">
<div id={this.props.id} className={"editor " + this.props.theme}></div>
<div id={id} className={"editor " + theme}></div>
</div>
</div>;
}
}
});

View File

@ -1,54 +1,36 @@
import {React} from "modules";
import {React, Events} from "modules";
import FloatingWindow from "./window";
class FloatingWindowContainer extends React.Component {
const {useState, useCallback, useEffect} = React;
constructor(props) {
super(props);
this.state = {windows: []};
}
get minY() {
function minY() {
const appContainer = document.querySelector(`#app-mount > div[class*="app-"]`);
if (appContainer) return appContainer.offsetTop;
return 0;
}
render() {
return this.state.windows.map(window =>
<FloatingWindow {...window} close={this.close.bind(this, window.id)} minY={this.minY} key={window.id}>
export default function FloatingWindowContainer() {
const [windows, setWindows] = useState([]);
const open = useCallback(window => {
setWindows(wins => [...wins, window]);
}, []);
const close = useCallback(id => {
setWindows(windows.filter(w => {
if (w.id === id && w.onClose) w.onClose();
return w.id !== id;
}));
}, [windows]);
useEffect(() => {
Events.on("open-window", open);
return () => Events.off("open-window", open);
}, [open]);
return windows.map(window =>
<FloatingWindow {...window} close={() => close(window.id)} minY={minY()} key={window.id}>
{window.children}
</FloatingWindow>
);
}
open(window) {
this.setState(state => {
state.windows.push(window);
return {windows: state.windows};
});
}
close(id) {
this.setState(state => {
return {
windows: state.windows.filter(w => {
if (w.id == id && w.onClose) w.onClose();
return w.id != id;
})
};
});
}
static get id() {return "floating-windows";}
static get root() {
if (this._root) return this._root;
const container = document.createElement("div");
container.id = this.id;
document.body.append(container);
return this._root = container;
}
}
export default FloatingWindowContainer;

View File

@ -5,169 +5,12 @@ import CloseButton from "../icons/close";
import MaximizeIcon from "../icons/fullscreen";
import Modals from "../modals";
// const Draggable = WebpackModules.getByDisplayName("Draggable");
// {
// "dragAnywhere": true,
// "className": "pictureInPictureWindow-1B5qSe",
// "maxX": 1969,
// "maxY": this.maxY,
// "onDragStart": "ƒ () {}",
// "onDrag": "ƒ () {}",
// "onDragEnd": "ƒ () {}",
// "children": "<div />",
// "initialX": 0,
// "initialY": 0
// }
const {useState, useCallback, useEffect, useRef} = React;
export default class FloatingWindow extends React.Component {
constructor(props) {
super(props);
this.state = {modalOpen: false};
this.offX = 0;
this.offY = 0;
this.maxX = this.props.maxX || Screen.width;
this.maxY = this.props.maxY || Screen.height;
this.minX = this.props.minX || 0;
this.minY = this.props.minY || 0;
this.titlebar = React.createRef();
this.window = React.createRef();
this.close = this.close.bind(this);
this.maximize = this.maximize.bind(this);
this.onDrag = this.onDrag.bind(this);
this.onDragStart = this.onDragStart.bind(this);
this.onDragStop = this.onDragStop.bind(this);
this.onResizeStart = this.onResizeStart.bind(this);
}
componentDidMount() {
this.window.current.addEventListener("mousedown", this.onResizeStart, false);
this.titlebar.current.addEventListener("mousedown", this.onDragStart, false);
document.addEventListener("mouseup", this.onDragStop, false);
}
onResizeStart() {
this.currentWidth = this.window.current.offsetWidth;
this.currentHeight = this.window.current.offsetHeight;
}
onDragStop() {
document.removeEventListener("mousemove", this.onDrag, true);
const width = this.window.current.offsetWidth;
const height = this.window.current.offsetHeight;
if (width != this.currentWidth || height != this.currentHeight) {
if (this.props.onResize) this.props.onResize();
const left = parseInt(this.window.current.style.left);
const top = parseInt(this.window.current.style.top);
if (left + width >= this.maxX) this.window.current.style.width = (this.maxX - left) + "px";
if (top + height >= this.maxY) this.window.current.style.height = (this.maxY - top) + "px";
}
this.currentWidth = width;
this.currentHeight = height;
}
onDragStart(e) {
const div = this.window.current;
this.offY = e.clientY - parseInt(div.offsetTop);
this.offX = e.clientX - parseInt(div.offsetLeft);
document.addEventListener("mousemove", this.onDrag, true);
}
onDrag(e) {
const div = this.window.current;
let newTop = (e.clientY - this.offY);
if (newTop <= this.minY) newTop = this.minY;
if (newTop + this.currentHeight >= this.maxY) newTop = this.maxY - this.currentHeight;
let newLeft = (e.clientX - this.offX);
if (newLeft <= this.minX) newLeft = this.minX;
if (newLeft + this.currentWidth >= this.maxX) newLeft = this.maxX - this.currentWidth;
div.style.top = newTop + "px";
div.style.left = newLeft + "px";
}
componentWillUnmount() {
this.titlebar.current.removeEventListener("mousedown", this.onDragStart, false);
document.removeEventListener("mouseup", this.onDragStop, false);
}
render() {
const top = this.props.center ? (Screen.height / 2) - (this.props.height / 2) : this.props.top;
const left = this.props.center ? (Screen.width / 2) - (this.props.width / 2) : this.props.left;
// console.log(top, left);
const className = `floating-window${` ${this.props.className}` || ""}${this.props.resizable ? " resizable" : ""}${this.state.modalOpen ? " modal-open" : ""}`;
const styles = {height: this.props.height, width: this.props.width, left: left || 0, top: top || 0};
return <div id={this.props.id} className={className} ref={this.window} style={styles}>
<div className="floating-window-titlebar" ref={this.titlebar}>
<span className="title">{this.props.title}</span>
<div className="floating-window-buttons">
<div className="button maximize-button" onClick={this.maximize}>
<MaximizeIcon size="18px" />
</div>
<div className="button close-button" onClick={this.close}>
<CloseButton />
</div>
</div>
</div>
<div className="floating-window-content">
{this.props.children}
</div>
</div>;
}
maximize() {
this.window.current.style.width = "100%";
this.window.current.style.height = "100%";
if (this.props.onResize) this.props.onResize();
const width = this.window.current.offsetWidth;
const height = this.window.current.offsetHeight;
const left = parseInt(this.window.current.style.left);
const top = parseInt(this.window.current.style.top);
const right = left + width;
const bottom = top + height;
// Prevent expanding off the bottom and right and readjust position
if (bottom > this.maxY) this.window.current.style.top = (this.maxY - height) + "px";
if (right > this.maxX) this.window.current.style.left = (this.maxX - width) + "px";
const newLeft = parseInt(this.window.current.style.left);
const newTop = parseInt(this.window.current.style.top);
// For small screens it's possible this pushes us off the other direction... we need to readjust size
if (newTop < this.minY) {
const difference = this.minY - newTop;
this.window.current.style.top = this.minY + "px";
this.window.current.style.height = (height - difference) + "px";
}
if (newLeft < this.minX) {
const difference = this.minX - newLeft;
this.window.current.style.left = this.minX + "px";
this.window.current.style.height = (width - difference) + "px";
}
}
async close() {
let shouldClose = true;
const confirmClose = typeof(this.props.confirmClose) == "function" ? this.props.confirmClose() : this.props.confirmClose;
if (confirmClose) {
this.setState({modalOpen: true});
shouldClose = await this.confirmClose();
this.setState({modalOpen: false});
}
if (this.props.close && shouldClose) this.props.close();
}
confirmClose() {
function confirmClose(confirmationText) {
return new Promise(resolve => {
Modals.showConfirmationModal(Strings.Modals.confirmAction, this.props.confirmationText, {
Modals.showConfirmationModal(Strings.Modals.confirmAction, confirmationText, {
danger: true,
confirmText: Strings.Modals.close,
onConfirm: () => {resolve(true);},
@ -175,4 +18,140 @@ export default class FloatingWindow extends React.Component {
});
});
}
export default function FloatingWindow({id, title, resizable, children, className, center, top: initialTop, left: initialLeft, width: initialWidth, height: initialHeight, minX = 0, minY = 0, maxX = Screen.width, maxY = Screen.height, onResize, close: doClose, confirmClose: doConfirmClose, confirmationText}) {
const [modalOpen, setOpen] = useState(false);
const [isDragging, setDragging] = useState(false);
const [position, setPosition] = useState({x: center ? (Screen.width / 2) - (initialWidth / 2) : initialLeft, y: center ? (Screen.height / 2) - (initialHeight / 2) : initialTop});
const [offset, setOffset] = useState({x: 0, y: 0});
const [size, setSize] = useState({width: 0, height: 0});
const titlebar = useRef(null);
const window = useRef(null);
const onResizeStart = useCallback(() => {
setSize({width: window.current.offsetWidth, height: window.current.offsetHeight});
}, [window]);
const onDrag = useCallback((e) => {
if (!isDragging) return;
let newTop = (e.clientY - offset.y);
if (newTop <= minY) newTop = minY;
if (newTop + size.height >= maxY) newTop = maxY - size.height;
let newLeft = (e.clientX - offset.x);
if (newLeft <= minX) newLeft = minX;
if (newLeft + size.width >= maxX) newLeft = maxX - size.width;
setPosition({x: newLeft, y: newTop});
}, [offset, size, isDragging, minX, minY, maxX, maxY]);
const onDragStart = useCallback((e) => {
const div = window.current;
setOffset({x: e.clientX - parseInt(div.offsetLeft), y: e.clientY - parseInt(div.offsetTop)});
setDragging(true);
}, [window]);
const onDragStop = useCallback(() => {
setDragging(false);
const width = window.current.offsetWidth;
const height = window.current.offsetHeight;
if (width != size.width || height != size.height) {
if (onResize) onResize();
const left = parseInt(window.current.style.left);
const top = parseInt(window.current.style.top);
if (left + width >= maxX) window.current.style.width = (maxX - left) + "px";
if (top + height >= maxY) window.current.style.height = (maxY - top) + "px";
}
setSize({width, height});
}, [window, size, maxX, maxY, onResize]);
useEffect(() => {
const winRef = window.current;
const titleRef = titlebar.current;
winRef.addEventListener("mousedown", onResizeStart, false);
titleRef.addEventListener("mousedown", onDragStart, false);
document.addEventListener("mouseup", onDragStop, false);
document.addEventListener("mousemove", onDrag, true);
return () => {
document.removeEventListener("mouseup", onDragStop, false);
document.removeEventListener("mousemove", onDrag, true);
winRef.removeEventListener("mousedown", onResizeStart, false);
titleRef.removeEventListener("mousedown", onDragStart, false);
};
}, [titlebar, window, onDragStart, onDragStop, onDrag, onResizeStart]);
const maximize = useCallback(() => {
window.current.style.width = "100%";
window.current.style.height = "100%";
if (onResize) onResize();
const width = window.current.offsetWidth;
const height = window.current.offsetHeight;
const left = parseInt(window.current.style.left);
const top = parseInt(window.current.style.top);
const right = left + width;
const bottom = top + height;
// Prevent expanding off the bottom and right and readjust position
if (bottom > maxY) window.current.style.top = (maxY - height) + "px";
if (right > maxX) window.current.style.left = (maxX - width) + "px";
const newLeft = parseInt(window.current.style.left);
const newTop = parseInt(window.current.style.top);
// For small screens it's possible pushes us off the other direction... we need to readjust size
if (newTop < minY) {
const difference = minY - newTop;
window.current.style.top = minY + "px";
window.current.style.height = (height - difference) + "px";
}
if (newLeft < minX) {
const difference = minX - newLeft;
window.current.style.left = minX + "px";
window.current.style.height = (width - difference) + "px";
}
}, [window, minX, minY, maxX, maxY, onResize]);
const close = useCallback(async () => {
let shouldClose = true;
const didConfirmClose = typeof(doConfirmClose) == "function" ? doConfirmClose() : doConfirmClose;
if (didConfirmClose) {
setOpen(true);
shouldClose = await confirmClose(confirmationText);
setOpen(false);
}
if (doClose && shouldClose) doClose();
}, [confirmationText, doClose, doConfirmClose]);
const finalClassname = `floating-window${` ${className}` || ""}${resizable ? " resizable" : ""}${modalOpen ? " modal-open" : ""}`;
const styles = {height: initialHeight, width: initialWidth, left: position.x || 0, top: position.y || 0};
return <div id={id} className={finalClassname} ref={window} style={styles}>
<div className="floating-window-titlebar" ref={titlebar}>
<span className="title">{title}</span>
<div className="floating-window-buttons">
<div className="button maximize-button" onClick={maximize}>
<MaximizeIcon size="18px" />
</div>
<div className="button close-button" onClick={close}>
<CloseButton />
</div>
</div>
</div>
<div className="floating-window-content">
{children}
</div>
</div>;
}

View File

@ -1,25 +1,25 @@
import {WebpackModules, React, ReactDOM, DOMManager} from "modules";
import {WebpackModules, React, ReactDOM, DOMManager, Events} from "modules";
import FloatingWindowContainer from "./floating/container";
/* eslint-disable new-cap */
const AppLayerProvider = WebpackModules.getByDisplayName("AppLayerProvider");
let hasInitialized = false;
export default class FloatingWindows {
static initialize() {
const containerRef = React.createRef();
const container = <FloatingWindowContainer ref={containerRef} />;
const container = <FloatingWindowContainer />;
const wrapped = AppLayerProvider
? React.createElement(AppLayerProvider().props.layerContext.Provider, {value: [document.querySelector("#app-mount > .layerContainer-2v_Sit")]}, container) // eslint-disable-line new-cap
: container;
const div = DOMManager.parseHTML(`<div id="floating-windows-layer">`);
DOMManager.bdBody.append(div);
ReactDOM.render(wrapped, div);
this.ref = containerRef;
hasInitialized = true;
}
static open(window) {
if (!this.ref) this.initialize();
return this.ref.current.open(window);
if (!hasInitialized) this.initialize();
return Events.emit("open-window", window);
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class BDLogo extends React.Component {
render() {
return <svg className={"bd-logo " + this.props.className} height="100%" width={this.props.size || "16px"} viewBox="0 0 2000 2000">
export default function BDLogo(props) {
return <svg className={"bd-logo " + props.className} height="100%" width={props.size || "16px"} viewBox="0 0 2000 2000">
<g>
<path fill="#3E82E5" d="M1402.2,631.7c-9.7-353.4-286.2-496-642.6-496H68.4v714.1l442,398V490.7h257c274.5,0,274.5,344.9,0,344.9H597.6v329.5h169.8c274.5,0,274.5,344.8,0,344.8h-699v354.9h691.2c356.3,0,632.8-142.6,642.6-496c0-162.6-44.5-284.1-122.9-368.6C1357.7,915.8,1402.2,794.3,1402.2,631.7z"/><path fill="#FFFFFF" d="M1262.5,135.2L1262.5,135.2l-76.8,0c26.6,13.3,51.7,28.1,75,44.3c70.7,49.1,126.1,111.5,164.6,185.3c39.9,76.6,61.5,165.6,64.3,264.6l0,1.2v1.2c0,141.1,0,596.1,0,737.1v1.2l0,1.2c-2.7,99-24.3,188-64.3,264.6c-38.5,73.8-93.8,136.2-164.6,185.3c-22.6,15.7-46.9,30.1-72.6,43.1h72.5c346.2,1.9,671-171.2,671-567.9V716.7C1933.5,312.2,1608.7,135.2,1262.5,135.2z"/>
</g>
</svg>;
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Checkmark extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" className={this.props.className || ""} style={{width: size, height: size}} onClick={this.props.onClick}>
export default function Checkmark(props) {
const size = props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" className={props.className || ""} style={{width: size, height: size}} onClick={props.onClick}>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z" />
</svg>;
}
}

View File

@ -1,8 +1,7 @@
import {React} from "modules";
export default class CloseButton extends React.Component {
render() {
const size = this.props.size || "18px";
export default function Close(props) {
const size = props.size || "18px";
return <svg viewBox="0 0 12 12" style={{width: size, height: size}}>
<g className="background" fill="none" fillRule="evenodd">
<path d="M0 0h12v12H0" />
@ -10,4 +9,3 @@ export default class CloseButton extends React.Component {
</g>
</svg>;
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Cog extends React.Component {
render() {
const size = this.props.size || "20px";
export default function Cog(props) {
const size = props.size || "20px";
return <svg viewBox="0 0 20 20" style={{width: size, height: size}}>
<path fill="none" d="M0 0h20v20H0V0z" />
<path d="M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0 .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z" />
</svg>;
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Delete extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg className={this.props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}} onClick={this.props.onClick}>
export default function Delete(props) {
const size = props.size || "24px";
return <svg className={props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}} onClick={props.onClick}>
<path fill="none" d="M0 0h24v24H0V0z"/><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zm2.46-7.12l1.41-1.41L12 12.59l2.12-2.12 1.41 1.41L13.41 14l2.12 2.12-1.41 1.41L12 15.41l-2.12 2.12-1.41-1.41L10.59 14l-2.13-2.12zM15.5 4l-1-1h-5l-1 1H5v2h14V4z"/>
<path fill="none" d="M0 0h24v24H0z"/>
</svg>;
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Detach extends React.Component {
render() {
const size = this.props.size || "24px";
export default function Detach(props) {
const size = props.size || "24px";
return <svg viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z" />
</svg>;
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class DollarSign extends React.Component {
render() {
const size = this.props.size || "18px";
return <svg viewBox="2 2 20 20" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick}>
export default function DollarSign(props) {
const size = props.size || "18px";
return <svg viewBox="2 2 20 20" fill="#FFFFFF" style={{width: size, height: size}} onClick={props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z"/>
</svg>;
}
}

View File

@ -1,10 +1,8 @@
import {React} from "modules";
export default class DownArrow extends React.Component {
render() {
const size = this.props.size || "16px";
return <svg className={this.props.className || ""} fill="currentColor" viewBox="0 0 24 24" style={{width: size, height: size}}>
export default function DownArrow(props) {
const size = props.size || "16px";
return <svg className={props.className || ""} fill="currentColor" viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M8.12 9.29L12 13.17l3.88-3.88c.39-.39 1.02-.39 1.41 0 .39.39.39 1.02 0 1.41l-4.59 4.59c-.39.39-1.02.39-1.41 0L6.7 10.7c-.39-.39-.39-1.02 0-1.41.39-.38 1.03-.39 1.42 0z"/>
</svg>;
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Edit extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick}>
export default function Edit(props) {
const size = props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={props.onClick}>
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>;
}
}

View File

@ -1,12 +1,9 @@
import {React} from "modules";
export default class Error extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick} className={this.props.className}>
export default function Error(props) {
const size = props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={props.onClick} className={props.className}>
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/>
</svg>;
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Extension extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick} className={this.props.className}>
export default function Extension(props) {
const size = props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={props.onClick} className={props.className}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/>
</svg>;
}
}

View File

@ -1,9 +1,8 @@
import {React} from "modules";
export default class Favorite extends React.Component {
render() {
return <svg aria-hidden="false" width="16" height="16" viewBox="0 0 24 24">
export default function Favorite(props) {
const size = props.size || "24px";
return <svg aria-hidden="false" viewBox="0 0 24 24" style={{width: size, height: size}}>
<path fill="currentColor" d="M21.924 8.61789C21.77 8.24489 21.404 8.00089 21 8.00089H15.618L12.894 2.55389C12.555 1.87689 11.444 1.87689 11.105 2.55389L8.38199 8.00089H2.99999C2.59599 8.00089 2.22999 8.24489 2.07599 8.61789C1.92199 8.99089 2.00699 9.42289 2.29299 9.70789L6.87699 14.2919L5.03899 20.7269C4.92399 21.1299 5.07199 21.5619 5.40999 21.8089C5.74999 22.0569 6.20699 22.0659 6.55399 21.8329L12 18.2029L17.445 21.8329C17.613 21.9449 17.806 22.0009 18 22.0009C18.207 22.0009 18.414 21.9369 18.59 21.8089C18.928 21.5619 19.076 21.1299 18.961 20.7269L17.123 14.2919L21.707 9.70789C21.993 9.42289 22.078 8.99089 21.924 8.61789Z"></path>
</svg>;
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class FullScreen extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg className={this.props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}} onClick={this.props.onClick}>
export default function FullScreen(props) {
const size = props.size || "24px";
return <svg className={props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}} onClick={props.onClick}>
<path fill="none" d="M0 0h24v24H0V0z"/>
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
</svg>;
}
}

View File

@ -1,10 +1,8 @@
import {React} from "modules";
export default class GitHub extends React.Component {
render() {
const size = this.props.size || "18px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick}>
export default function GitHub(props) {
const size = props.size || "18px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={props.onClick}>
<path d="m12 .5c-6.63 0-12 5.28-12 11.792 0 5.211 3.438 9.63 8.205 11.188.6.111.82-.254.82-.567 0-.28-.01-1.022-.015-2.005-3.338.711-4.042-1.582-4.042-1.582-.546-1.361-1.335-1.725-1.335-1.725-1.087-.731.084-.716.084-.716 1.205.082 1.838 1.215 1.838 1.215 1.07 1.803 2.809 1.282 3.495.981.108-.763.417-1.282.76-1.577-2.665-.295-5.466-1.309-5.466-5.827 0-1.287.465-2.339 1.235-3.164-.135-.298-.54-1.497.105-3.121 0 0 1.005-.316 3.3 1.209.96-.262 1.98-.392 3-.398 1.02.006 2.04.136 3 .398 2.28-1.525 3.285-1.209 3.285-1.209.645 1.624.24 2.823.12 3.121.765.825 1.23 1.877 1.23 3.164 0 4.53-2.805 5.527-5.475 5.817.42.354.81 1.077.81 2.182 0 1.578-.015 2.846-.015 3.229 0 .309.21.678.825.56 4.801-1.548 8.236-5.97 8.236-11.173 0-6.512-5.373-11.792-12-11.792z" />
</svg>;
}
}

View File

@ -1,12 +1,10 @@
import {React} from "modules";
export default class Globe extends React.Component {
render() {
const size = this.props.size || "18px";
const color = this.props.color || "#FFFFFF";
return <svg viewBox="2 2 20 20" fill={color} style={{width: size, height: size}} onClick={this.props.onClick}>
export default function Globe(props) {
const size = props.size || "18px";
const color = props.color || "#FFFFFF";
return <svg viewBox="2 2 20 20" fill={color} style={{width: size, height: size}} onClick={props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm6.93 6h-2.95c-.32-1.25-.78-2.45-1.38-3.56 1.84.63 3.37 1.91 4.33 3.56zM12 4.04c.83 1.2 1.48 2.53 1.91 3.96h-3.82c.43-1.43 1.08-2.76 1.91-3.96zM4.26 14C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2 0 .68.06 1.34.14 2H4.26zm.82 2h2.95c.32 1.25.78 2.45 1.38 3.56-1.84-.63-3.37-1.9-4.33-3.56zm2.95-8H5.08c.96-1.66 2.49-2.93 4.33-3.56C8.81 5.55 8.35 6.75 8.03 8zM12 19.96c-.83-1.2-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08 2.76-1.91 3.96zM14.34 14H9.66c-.09-.66-.16-1.32-.16-2 0-.68.07-1.35.16-2h4.68c.09.65.16 1.32.16 2 0 .68-.07 1.34-.16 2zm.25 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95c-.96 1.65-2.49 2.93-4.33 3.56zM16.36 14c.08-.66.14-1.32.14-2 0-.68-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2h-3.38z"/>
</svg>;
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Grid extends React.Component {
render() {
const size = this.props.size || "20px";
export default function Grid(props) {
const size = props.size || "20px";
return <svg viewBox="2 2 20 20" fill="#FFFFFF" style={{width: size, height: size}}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M4 11h5V5H4v6zm0 7h5v-6H4v6zm6 0h5v-6h-5v6zm6 0h5v-6h-5v6zm-6-7h5V5h-5v6zm6-6v6h5V5h-5z"/>
</svg>;
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class History extends React.Component {
render() {
const size = this.props.size || "18px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" className={this.props.className || ""} style={{width: size, height: size}} onClick={this.props.onClick}>
export default function History(props) {
const size = props.size || "18px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" className={props.className || ""} style={{width: size, height: size}} onClick={props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"/>
</svg>;
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Keyboard extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg className={this.props.className} viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}}>
export default function Keyboard(props) {
const size = props.size || "24px";
return <svg className={props.className} viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}}>
<path d="M20 5H4c-1.1 0-1.99.9-1.99 2L2 17c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-9 3h2v2h-2V8zm0 3h2v2h-2v-2zM8 8h2v2H8V8zm0 3h2v2H8v-2zm-1 2H5v-2h2v2zm0-3H5V8h2v2zm9 7H8v-2h8v2zm0-4h-2v-2h2v2zm0-3h-2V8h2v2zm3 3h-2v-2h2v2zm0-3h-2V8h2v2z" />
<path fill="none" d="M0 0h24v24H0zm0 0h24v24H0z" />
</svg>;
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class List extends React.Component {
render() {
const size = this.props.size || "20px";
export default function List(props) {
const size = props.size || "20px";
return <svg viewBox="2 2 20 20" fill="#FFFFFF" style={{width: size, height: size}}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M4 18h17v-6H4v6zM4 5v6h17V5H4z"/>
</svg>;
}
}

View File

@ -1,8 +1,7 @@
import {React} from "modules";
export default class MagnifyingGlass extends React.Component {
render() {
const size = this.props.size || "160px";
export default function MagnifyingGlass(props) {
const size = props.size || "160px";
return <svg xmlns="http://www.w3.org/2000/svg" style={{width: size, height: size}} viewBox="0 0 160 160">
<g fill="none" fillRule="evenodd">
<g transform="translate(9 9)">
@ -20,4 +19,3 @@ export default class MagnifyingGlass extends React.Component {
</g>
</svg>;
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class ArrowRight extends React.Component {
render() {
const size = this.props.size || "24px";
export default function ArrowRight(props) {
const size = props.size || "24px";
return <svg viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M10 17l5-5-5-5v10z" />
<path d="M0 24V0h24v24H0z" fill="none" />
</svg>;
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Patreon extends React.Component {
render() {
const size = this.props.size || "18px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick}>
export default function Patreon(props) {
const size = props.size || "18px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={props.onClick}>
<path d="m0 .5h4.219v23h-4.219z"/>
<path d="m15.384.5c-4.767 0-8.644 3.873-8.644 8.633 0 4.75 3.877 8.61 8.644 8.61 4.754 0 8.616-3.865 8.616-8.61 0-4.759-3.863-8.633-8.616-8.633z"/>
</svg>;
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class ArrowLeft extends React.Component {
render() {
const size = this.props.size || "24px";
export default function ArrowLeft(props) {
const size = props.size || "24px";
return <svg viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M14 7l-5 5 5 5V7z" />
<path d="M24 0v24H0V0h24z" fill="none" />
</svg>;
}
}

View File

@ -1,12 +1,10 @@
import {React} from "modules";
export default class Radio extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg className={this.props.className} viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}}>
export default function Radio(props) {
const size = props.size || "24px";
return <svg className={props.className} viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}}>
<path fill="none" d="M0 0h24v24H0z" />
{this.props.checked && <path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" />}
{!this.props.checked && <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" />}
{props.checked && <path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" />}
{!props.checked && <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" />}
</svg>;
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class ReloadIcon extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg className={this.props.className || ""} onClick={this.props.onClick} fill="#dcddde" viewBox="0 0 24 24" style={{width: size, height: size}}>
export default function ReloadIcon(props) {
const size = props.size || "24px";
return <svg className={props.className || ""} onClick={props.onClick} fill="#dcddde" viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" />
<path fill="none" d="M0 0h24v24H0z" />
</svg>;
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Save extends React.Component {
render() {
const size = this.props.size || "24px";
export default function Save(props) {
const size = props.size || "24px";
return <svg viewBox="0 0 24 24" style={{width: size, height: size}}>
<path fill="none" d="M0 0h24v24H0V0z" />
<path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm2 16H5V5h11.17L19 7.83V19zm-7-7c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3zM6 6h9v4H6z" />
</svg>;
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Search extends React.Component {
render() {
const size = this.props.size || "16px";
return <svg className={this.props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}}>
export default function Search(props) {
const size = props.size || "16px";
return <svg className={props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}}>
<path fill="none" d="M0 0h24v24H0V0z"/>
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
</svg>;
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Support extends React.Component {
render() {
const size = this.props.size || "18px";
return <svg viewBox="2 2 20 20" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick}>
export default function Support(props) {
const size = props.size || "18px";
return <svg viewBox="2 2 20 20" fill="#FFFFFF" style={{width: size, height: size}} onClick={props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"/>
</svg>;
}
}

View File

@ -1,11 +1,9 @@
import {React} from "modules";
export default class Theme extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick} className={this.props.className}>
export default function Theme(props) {
const size = props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={props.onClick} className={props.className}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9c.83 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1.01-.23-.26-.38-.61-.38-.99 0-.83.67-1.5 1.5-1.5H16c2.76 0 5-2.24 5-5 0-4.42-4.03-8-9-8zm-5.5 9c-.83 0-1.5-.67-1.5-1.5S5.67 9 6.5 9 8 9.67 8 10.5 7.33 12 6.5 12zm3-4C8.67 8 8 7.33 8 6.5S8.67 5 9.5 5s1.5.67 1.5 1.5S10.33 8 9.5 8zm5 0c-.83 0-1.5-.67-1.5-1.5S13.67 5 14.5 5s1.5.67 1.5 1.5S15.33 8 14.5 8zm3 4c-.83 0-1.5-.67-1.5-1.5S16.67 9 17.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/>
</svg>;
}
}

View File

@ -1,9 +1,8 @@
import {React} from "modules";
export default class Twitch extends React.Component {
render() {
return <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
export default function Twitch(props) {
const size = props.size || "18px";
return <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" style={{width: size, height: size}}>
<path fill="currentColor" d="M40.1 32L10 108.9v314.3h107V480h60.2l56.8-56.8h87l117-117V32H40.1zm357.8 254.1L331 353H224l-56.8 56.8V353H76.9V72.1h321v214zM331 149v116.9h-40.1V149H331zm-107 0v116.9h-40.1V149H224z"/>
</svg>;
}
}

View File

@ -4,60 +4,39 @@ import Editor from "../customcss/editor";
import Save from "../icons/save";
import Edit from "../icons/edit";
export default class AddonEditor extends React.Component {
const {useState, useCallback, forwardRef, useImperativeHandle, useRef} = React;
constructor(props) {
super(props);
this.hasUnsavedChanges = false;
this.onChange = this.onChange.bind(this);
this.save = this.save.bind(this);
this.openNative = this.openNative.bind(this);
this.update = this.update.bind(this);
export default forwardRef(function AddonEditor({content, language, save, openNative, id = "bd-addon-editor"}, ref) {
const editorRef = useRef(null);
const [hasUnsavedChanges, setUnsaved] = useState(false);
this.controls = [
{label: React.createElement(Save, {size: "18px"}), tooltip: Strings.CustomCSS.save, onClick: this.save},
{label: React.createElement(Edit, {size: "18px"}), tooltip: Strings.CustomCSS.openNative, onClick: this.openNative}
];
}
useImperativeHandle(ref, () => {
return {
resize() {editorRef.current.resize();},
showSettings() {editorRef.current.showSettings();},
get value() {return editorRef.current.getValue();},
set value(newValue) {editorRef.current.setValue(newValue);},
get hasUnsavedChanges() {return hasUnsavedChanges;}
};
}, [hasUnsavedChanges]);
update() {
this.forceUpdate();
}
const popoutNative = useCallback(() => openNative?.(), [openNative]);
const onChange = useCallback(() => setUnsaved(true), []);
const saveAddon = useCallback((event, newCSS) => {
save?.(newCSS);
setUnsaved(false);
}, [save]);
updateEditor(newCSS) {
if (!this.editor) return;
this.editor.value = newCSS;
}
get value() {return this.editor.session.getValue();}
set value(newValue) {
this.editor.setValue(newValue);
}
showSettings() {return this.editor.keyBinding.$defaultHandler.commands.showSettingsMenu.exec(this.editor);}
resize() {return this.editor.resize();}
setEditorRef(editor) {
this.editor = editor;
if (this.props.editorRef && typeof(this.props.editorRef.current) !== "undefined") this.props.editorRef.current = editor;
else if (this.props.editorRef) this.props.editorRef = editor;
}
render() {
return <Editor ref={this.setEditorRef.bind(this)} language={this.props.language} id={this.props.id || "bd-addon-editor"} controls={this.controls} value={this.props.content} onChange={this.onChange} />;
}
onChange() {
this.hasUnsavedChanges = true;
}
save(event, content) {
this.hasUnsavedChanges = false;
if (this.props.save) this.props.save(content);
}
openNative() {
if (this.props.openNative) this.props.openNative();
}
}
return <Editor
ref={editorRef}
language={language}
id={id}
controls={[
{label: <Save size="18px" />, tooltip: Strings.CustomCSS.save, onClick: saveAddon},
{label: <Edit size="18px" />, tooltip: Strings.CustomCSS.openNative, onClick: popoutNative}
]}
value={content}
onChange={onChange}
/>;
});

View File

@ -293,6 +293,11 @@ export default class Modals {
super(props);
this.elementRef = React.createRef();
this.element = panel;
this.state = {hasError: false};
}
componentDidCatch() {
this.setState({hasError: true});
}
componentDidMount() {
@ -300,6 +305,7 @@ export default class Modals {
}
render() {
if (this.state.hasError) return null;
const props = {
className: "bd-addon-settings-wrap",
ref: this.elementRef

View File

@ -1,46 +1,50 @@
import {React, Strings} from "modules";
const badge = <div className="flowerStarContainer-3zDVtj verified-1eC5dy background-2uufRq guildBadge-RlDbED"
style={{width: "16px", height: "16px"}}>
<svg aria-label="Verified &amp; Partnered" className="flowerStar-1GeTsn"
aria-hidden="false" width="16" height="16" viewBox="0 0 16 15.2">
<path fill="currentColor" fillRule="evenodd"
d="m16 7.6c0 .79-1.28 1.38-1.52 2.09s.44 2 0 2.59-1.84.35-2.46.8-.79 1.84-1.54 2.09-1.67-.8-2.47-.8-1.75 1-2.47.8-.92-1.64-1.54-2.09-2-.18-2.46-.8.23-1.84 0-2.59-1.54-1.3-1.54-2.09 1.28-1.38 1.52-2.09-.44-2 0-2.59 1.85-.35 2.48-.8.78-1.84 1.53-2.12 1.67.83 2.47.83 1.75-1 2.47-.8.91 1.64 1.53 2.09 2 .18 2.46.8-.23 1.84 0 2.59 1.54 1.3 1.54 2.09z">
</path>
const {useState, useCallback, useMemo} = React;
const badge = <div className="flowerStarContainer-1QeD-L verified-1Jv_7P background-3Da2vZ rowIcon-2tDEcE" style={{width: "16px", height: "16px"}}>
<svg aria-label="Verified &amp; Partnered" className="flowerStar-2tNFCR" aria-hidden="false" width="16" height="16" viewBox="0 0 16 15.2">
<path fill="currentColor" fillRule="evenodd" d="m16 7.6c0 .79-1.28 1.38-1.52 2.09s.44 2 0 2.59-1.84.35-2.46.8-.79 1.84-1.54 2.09-1.67-.8-2.47-.8-1.75 1-2.47.8-.92-1.64-1.54-2.09-2-.18-2.46-.8.23-1.84 0-2.59-1.54-1.3-1.54-2.09 1.28-1.38 1.52-2.09-.44-2 0-2.59 1.85-.35 2.48-.8.78-1.84 1.53-2.12 1.67.83 2.47.83 1.75-1 2.47-.8.91 1.64 1.53 2.09 2 .18 2.46.8-.23 1.84 0 2.59 1.54 1.3 1.54 2.09z"></path>
</svg>
<div className="childContainer-1wxZNh">
<svg className="icon-1ihkOt" aria-hidden="false" width="16" height="16" viewBox="0 0 16 15.2">
<div className="childContainer-U_a6Yh">
<svg className="icon-3BYlXK" aria-hidden="false" width="16" height="16" viewBox="0 0 16 15.2">
<path d="M7.4,11.17,4,8.62,5,7.26l2,1.53L10.64,4l1.36,1Z" fill="currentColor"></path>
</svg>
</div>
</div>;
export default class ServerCard extends React.Component {
constructor(props) {
super(props);
if (!this.props.server.iconUrl) this.props.server.iconUrl = this.props.defaultAvatar();
this.state = {
joined: this.props.joined
};
this.join = this.join.bind(this);
this.handleError = this.handleError.bind(this);
}
render() {
const {server} = this.props;
export default function ServerCard({server, joined, join, navigateTo, defaultAvatar}) {
const [isError, setError] = useState(false);
const handleError = useCallback(() => {
setError(true);
}, []);
const [hasJoined, setJoined] = useState(joined);
const doJoin = useCallback(async () => {
if (hasJoined) return navigateTo(server.identifier);
setJoined("joining");
const didJoin = await join(server.identifier, server.nativeJoin);
setJoined(didJoin);
}, [hasJoined, join, navigateTo, server.identifier, server.nativeJoin]);
const defaultIcon = useMemo(() => defaultAvatar(), [defaultAvatar]);
const currentIcon = !server.iconUrl || isError ? defaultIcon : server.iconUrl;
const addedDate = new Date(server.insertDate * 1000); // Convert from unix timestamp
const buttonText = typeof(this.state.joined) == "string" ? `${Strings.PublicServers.joining}...` : this.state.joined ? Strings.PublicServers.joined : Strings.PublicServers.join;
const buttonText = typeof(hasJoined) == "string" ? `${Strings.PublicServers.joining}...` : hasJoined ? Strings.PublicServers.joined : Strings.PublicServers.join;
return <div className="bd-server-card" role="button" tabIndex="0" onClick={this.join}>
return <div className="bd-server-card" role="button" tabIndex="0" onClick={doJoin}>
<div className="bd-server-header">
<div className="bd-server-splash-container"><img src={server.iconUrl} onError={this.handleError} className="bd-server-splash" /></div>
<img src={server.iconUrl} onError={this.handleError} className="bd-server-icon" />
<div className="bd-server-splash-container"><img src={currentIcon} onError={handleError} className="bd-server-splash" /></div>
<img src={currentIcon} onError={handleError} className="bd-server-icon" />
</div>
<div className="bd-server-info">
<div className="bd-server-title">
{server.pinned && badge}
<div className="bd-server-name">{server.name}</div>
{this.state.joined && <div className="bd-server-tag">{buttonText}</div>}
{hasJoined && <div className="bd-server-tag">{buttonText}</div>}
</div>
<div className="bd-server-description">{server.description}</div>
<div className="bd-server-footer">
@ -56,15 +60,3 @@ export default class ServerCard extends React.Component {
</div>
</div>;
}
handleError() {
this.props.server.iconUrl = this.props.defaultAvatar();
}
async join() {
if (this.state.joined) return this.props.navigateTo(this.props.server.identifier);
this.setState({joined: "joining"});
const didJoin = await this.props.join(this.props.server.identifier, this.props.server.nativejoin);
this.setState({joined: didJoin});
}
}

View File

@ -54,7 +54,7 @@ export default new class SettingsRenderer {
}
getAddonPanel(title, addonList, addonState, options = {}) {
return React.createElement(AddonList, Object.assign({}, {
return () => React.createElement(AddonList, Object.assign({}, {
title: title,
addonList: addonList,
addonState: addonState

View File

@ -17,6 +17,9 @@ import ThemeIcon from "../icons/theme";
import Modals from "../modals";
import Toasts from "../toasts";
const {useState, useCallback, useMemo} = React;
const LinkIcons = {
website: WebIcon,
source: GitHubIcon,
@ -43,80 +46,27 @@ const LayerManager = {
});
}
};
const UserStore = WebpackModules.getByProps("getCurrentUser");
const ChannelStore = WebpackModules.getByProps("getDMFromUserId");
const PrivateChannelActions = WebpackModules.getByProps("openPrivateChannel");
const ChannelActions = WebpackModules.getByProps("selectPrivateChannel");
const getString = value => typeof value == "string" ? value : value.toString();
export default class AddonCard extends React.Component {
constructor(props) {
super(props);
this.settingsPanel = "";
this.panelRef = React.createRef();
this.onChange = this.onChange.bind(this);
this.showSettings = this.showSettings.bind(this);
this.messageAuthor = this.messageAuthor.bind(this);
function makeButton(title, children, action, {isControl = false, danger = false, disabled = false} = {}) {
const ButtonType = isControl ? "button" : "div";
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => {
return <ButtonType {...props} className={(isControl ? "bd-button bd-addon-button" : "bd-addon-button") + (danger ? " bd-button-danger" : "") + (disabled ? " bd-button-disabled" : "")} onClick={action}>{children}</ButtonType>;
}}
</DiscordModules.Tooltip>;
}
showSettings() {
if (!this.props.hasSettings || !this.props.enabled) return;
const name = this.getString(this.props.addon.name);
try {
Modals.showAddonSettingsModal(name, this.props.getSettingsPanel());
}
catch (err) {
Toasts.show(Strings.Addons.settingsError.format({name}), {type: "error"});
Logger.stacktrace("Addon Settings", "Unable to get settings panel for " + name + ".", err);
}
}
getString(value) {return typeof value == "string" ? value : value.toString();}
onChange() {
this.props.onChange && this.props.onChange(this.props.addon.id);
this.props.enabled = !this.props.enabled;
this.forceUpdate();
}
messageAuthor() {
if (!this.props.addon.authorId) return;
if (LayerManager) LayerManager.popLayer();
if (!UserStore || !ChannelActions || !ChannelStore || !PrivateChannelActions) return;
const selfId = UserStore.getCurrentUser().id;
if (selfId == this.props.addon.authorId) return;
const privateChannelId = ChannelStore.getDMFromUserId(this.props.addon.authorId);
if (privateChannelId) return ChannelActions.selectPrivateChannel(privateChannelId);
PrivateChannelActions.openPrivateChannel(selfId, this.props.addon.authorId);
}
buildTitle(name, version, author) {
const authorArray = Strings.Addons.byline.split(/({{[A-Za-z]+}})/);
const authorComponent = author.link || author.id
? <a className="bd-link bd-link-website" href={author.link || null} onClick={this.messageAuthor} target="_blank" rel="noopener noreferrer">{author.name}</a>
: <span className="bd-author">{author.name}</span>;
const authorIndex = authorArray.findIndex(s => s == "{{author}}");
if (authorIndex) authorArray[authorIndex] = authorComponent;
return [
React.createElement("div", {className: "bd-name"}, name),
React.createElement("div", {className: "bd-meta"},
React.createElement("span", {className: "bd-version"}, `v${version}`),
...authorArray
)
];
}
buildLink(which) {
const url = this.props.addon[which];
function buildLink(type, url) {
if (!url) return null;
const icon = React.createElement(LinkIcons[which]);
const icon = React.createElement(LinkIcons[type]);
const link = <a className="bd-link bd-link-website" href={url} target="_blank" rel="noopener noreferrer">{icon}</a>;
if (which == "invite") {
if (type == "invite") {
link.props.onClick = function(event) {
event.preventDefault();
event.stopPropagation();
@ -124,71 +74,84 @@ export default class AddonCard extends React.Component {
const tester = /\.gg\/(.*)$/;
if (tester.test(code)) code = code.match(tester)[1];
LayerManager.popLayer();
DiscordModules.InviteActions.acceptInviteAndTransitionToInviteChannel({inviteKey: code});
DiscordModules.InviteActions?.acceptInviteAndTransitionToInviteChannel({inviteKey: code});
};
}
return this.makeButton(Strings.Addons[which], link);
return makeButton(Strings.Addons[type], link);
}
get controls() { // {this.props.hasSettings && <button onClick={this.showSettings} className="bd-button bd-button-addon-settings" disabled={!this.props.enabled}>{Strings.Addons.addonSettings}</button>}
return <div className="bd-controls">
{this.props.hasSettings && this.makeControlButton(Strings.Addons.addonSettings, <CogIcon size={"20px"} />, this.showSettings, {disabled: !this.props.enabled})}
{this.props.editAddon && this.makeControlButton(Strings.Addons.editAddon, <EditIcon size={"20px"} />, this.props.editAddon)}
{this.props.deleteAddon && this.makeControlButton(Strings.Addons.deleteAddon, <DeleteIcon size={"20px"} />, this.props.deleteAddon, {danger: true})}
</div>;
}
export default function AddonCard({addon, type, disabled, enabled: initialValue, onChange: parentChange, hasSettings, editAddon, deleteAddon, getSettingsPanel}) {
const [isEnabled, setEnabled] = useState(initialValue);
const onChange = useCallback(() => {
setEnabled(!isEnabled);
if (parentChange) parentChange(addon.id);
}, [addon.id, parentChange, isEnabled]);
get footer() {
const links = ["website", "source", "invite", "donate", "patreon"];
const linkComponents = links.map(this.buildLink.bind(this)).filter(c => c);// linkComponents.map((comp, i) => i < linkComponents.length - 1 ? [comp, " | "] : comp).flat()
const showSettings = useCallback(() => {
if (!hasSettings || !isEnabled) return;
const name = getString(addon.name);
try {
Modals.showAddonSettingsModal(name, getSettingsPanel());
}
catch (err) {
Toasts.show(Strings.Addons.settingsError.format({name}), {type: "error"});
Logger.stacktrace("Addon Settings", "Unable to get settings panel for " + name + ".", err);
}
}, [hasSettings, isEnabled, addon.name, getSettingsPanel]);
const messageAuthor = useCallback(() => {
if (!addon.authorId) return;
if (LayerManager) LayerManager.popLayer();
if (!UserStore || !ChannelActions || !ChannelStore || !PrivateChannelActions) return;
const selfId = UserStore.getCurrentUser().id;
if (selfId == addon.authorId) return;
const privateChannelId = ChannelStore.getDMFromUserId(addon.authorId);
if (privateChannelId) return ChannelActions.selectPrivateChannel(privateChannelId);
PrivateChannelActions.openPrivateChannel(selfId, addon.authorId);
}, [addon.authorId]);
const title = useMemo(() => {
const authorArray = Strings.Addons.byline.split(/({{[A-Za-z]+}})/);
const authorComponent = addon.authorLink || addon.authorId
? <a className="bd-link bd-link-website" href={addon.authorLink || null} onClick={messageAuthor} target="_blank" rel="noopener noreferrer">{getString(addon.author)}</a>
: <span className="bd-author">{getString(addon.author)}</span>;
const authorIndex = authorArray.findIndex(s => s == "{{author}}");
if (authorIndex) authorArray[authorIndex] = authorComponent;
return [
<div className="bd-name">{getString(addon.name)}</div>,
<div className="bd-meta">
<span className="bd-version">v{getString(addon.version)}</span>
{authorArray}
</div>
];
}, [addon.name, addon.version, addon.authorLink, addon.authorId, addon.author, messageAuthor]);
const footer = useMemo(() => {
const links = Object.keys(LinkIcons);
const linkComponents = links.map(l => buildLink(l, addon[l])).filter(c => c);
return <div className="bd-footer">
<span className="bd-links">{linkComponents}</span>
{this.controls}
<div className="bd-controls">
{hasSettings && makeButton(Strings.Addons.addonSettings, <CogIcon size={"20px"} />, showSettings, {isControl: true, disabled: !isEnabled})}
{editAddon && makeButton(Strings.Addons.editAddon, <EditIcon size={"20px"} />, editAddon, {isControl: true})}
{deleteAddon && makeButton(Strings.Addons.deleteAddon, <DeleteIcon size={"20px"} />, deleteAddon, {isControl: true, danger: true})}
</div>
</div>;
}
}, [hasSettings, editAddon, deleteAddon, addon, isEnabled, showSettings]);
makeButton(title, children, action) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => {
return <div {...props} className="bd-addon-button" onClick={action}>{children}</div>;
}}
</DiscordModules.Tooltip>;
}
makeControlButton(title, children, action, {danger = false, disabled = false} = {}) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => {
return <button {...props} className={"bd-button bd-addon-button" + (danger ? " bd-button-danger" : "") + (disabled ? " bd-button-disabled" : "")} onClick={action}>{children}</button>;
}}
</DiscordModules.Tooltip>;
}
render() {
const addon = this.props.addon;
const name = this.getString(addon.name);
const author = this.getString(addon.author);
const description = this.getString(addon.description);
const version = this.getString(addon.version);
return <div id={`${addon.id}-card`} className={"bd-addon-card" + (this.props.disabled ? " bd-addon-card-disabled" : "")}>
return <div id={`${addon.id}-card`} className={"bd-addon-card" + (disabled ? " bd-addon-card-disabled" : "")}>
<div className="bd-addon-header">
{this.props.type === "plugin" ? <ExtIcon size="18px" className="bd-icon" /> : <ThemeIcon size="18px" className="bd-icon" />}
<div className="bd-title">{this.buildTitle(name, version, {name: author, id: this.props.addon.authorId, link: this.props.addon.authorLink})}</div>
<Switch disabled={this.props.disabled} checked={this.props.enabled} onChange={this.onChange} />
{type === "plugin" ? <ExtIcon size="18px" className="bd-icon" /> : <ThemeIcon size="18px" className="bd-icon" />}
<div className="bd-title">{title}</div>
<Switch disabled={disabled} checked={isEnabled} onChange={onChange} />
</div>
<div className="bd-description-wrap">
{this.props.disabled && <div className="banner banner-danger"><ErrorIcon className="bd-icon" />{`An error was encountered while trying to load this ${this.props.type}.`}</div>}
<div className="bd-description">{SimpleMarkdown.parseToReact(description)}</div>
{disabled && <div className="banner banner-danger"><ErrorIcon className="bd-icon" />{`An error was encountered while trying to load this ${type}.`}</div>}
<div className="bd-description">{SimpleMarkdown.parseToReact(getString(addon.description))}</div>
</div>
{this.footer}
{footer}
</div>;
}
}
const originalRender = AddonCard.prototype.render;
Object.defineProperty(AddonCard.prototype, "render", {
enumerable: false,
configurable: false,
set: function() {Logger.warn("AddonCard", "Addon policy for plugins #5 https://github.com/BetterDiscord/BetterDiscord/wiki/Addon-Policies#plugins");},
get: () => originalRender
});

View File

@ -1,4 +1,3 @@
import Logger from "common/logger";
import {React, Strings, Events, DataStore, DiscordModules} from "modules";
import Modals from "../modals";
@ -13,82 +12,9 @@ import GridIcon from "../icons/grid";
import NoResults from "../blankslates/noresults";
import EmptyImage from "../blankslates/emptyimage";
export default class AddonList extends React.Component {
const {useState, useCallback, useEffect, useReducer, useMemo} = React;
constructor(props) {
super(props);
this.state = {query: "", sort: this.getControlState("sort", "name"), ascending: this.getControlState("ascending", true), view: this.getControlState("view", "list")};
this.sort = this.sort.bind(this);
this.reverse = this.reverse.bind(this);
this.search = this.search.bind(this);
this.update = this.update.bind(this);
this.listView = this.listView.bind(this);
this.gridView = this.gridView.bind(this);
this.openFolder = this.openFolder.bind(this);
}
componentDidMount() {
Events.on(`${this.props.prefix}-loaded`, this.update);
Events.on(`${this.props.prefix}-unloaded`, this.update);
}
componentWillUnmount() {
Events.off(`${this.props.prefix}-loaded`, this.update);
Events.off(`${this.props.prefix}-unloaded`, this.update);
}
onControlChange(control, value) {
const addonlistControls = DataStore.getBDData("addonlistControls") || {};
if (!addonlistControls[this.props.type]) addonlistControls[this.props.type] = {};
addonlistControls[this.props.type][control] = value;
DataStore.setBDData("addonlistControls", addonlistControls);
}
getControlState(control, defaultValue) {
const addonlistControls = DataStore.getBDData("addonlistControls") || {};
if (!addonlistControls[this.props.type]) return defaultValue;
if (!addonlistControls[this.props.type].hasOwnProperty(control)) return defaultValue;
return addonlistControls[this.props.type][control];
}
update() {
this.forceUpdate();
}
reload() {
if (this.props.refreshList) this.props.refreshList();
this.forceUpdate();
}
listView() {this.changeView("list");}
gridView() {this.changeView("grid");}
changeView(view) {
this.onControlChange("view", view);
this.setState({view});
}
reverse(value) {
this.onControlChange("ascending", value);
this.setState({ascending: value});
}
sort(value) {
this.onControlChange("sort", value);
this.setState({sort: value});
}
search(event) {
this.setState({query: event.target.value.toLocaleLowerCase()});
}
openFolder() {
const shell = require("electron").shell;
const open = shell.openItem || shell.openPath;
open(this.props.folder);
}
get sortOptions() {
return [
const SORT_OPTIONS = [
{label: Strings.Addons.name, value: "name"},
{label: Strings.Addons.author, value: "author"},
{label: Strings.Addons.version, value: "version"},
@ -96,23 +22,27 @@ export default class AddonList extends React.Component {
{label: Strings.Addons.modified, value: "modified"},
{label: Strings.Addons.isEnabled, value: "isEnabled"}
];
}
get directions() {
return [
const DIRECTIONS = [
{label: Strings.Sorting.ascending, value: true},
{label: Strings.Sorting.descending, value: false}
];
function openFolder(folder) {
const shell = require("electron").shell;
const open = shell.openItem || shell.openPath;
open(folder);
}
get emptyImage() {
const message = Strings.Addons.blankSlateMessage.format({link: `https://betterdiscord.app/${this.props.type}s`, type: this.props.type}).toString();
return <EmptyImage title={Strings.Addons.blankSlateHeader.format({type: this.props.type})} message={message}>
<button className="bd-button" onClick={this.openFolder}>{Strings.Addons.openFolder.format({type: this.props.type})}</button>
function blankslate(type, onClick) {
const message = Strings.Addons.blankSlateMessage.format({link: `https://betterdiscord.app/${type}s`, type}).toString();
return <EmptyImage title={Strings.Addons.blankSlateHeader.format({type})} message={message}>
<button className="bd-button" onClick={onClick}>{Strings.Addons.openFolder.format({type})}</button>
</EmptyImage>;
}
makeControlButton(title, children, action, selected = false) {
function makeControlButton(title, children, action, selected = false) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => {
return <button {...props} className={"bd-button bd-view-button" + (selected ? " selected" : "")} onClick={action}>{children}</button>;
@ -120,80 +50,21 @@ export default class AddonList extends React.Component {
</DiscordModules.Tooltip>;
}
render() {
const {title, folder, addonList, addonState, onChange, reload} = this.props;
const button = folder ? {title: Strings.Addons.openFolder.format({type: title}), onClick: this.openFolder} : null;
let sortedAddons = addonList.sort((a, b) => {
const sortByEnabled = this.state.sort === "isEnabled";
const first = sortByEnabled ? addonState[a.id] : a[this.state.sort];
const second = sortByEnabled ? addonState[b.id] : b[this.state.sort];
const stringSort = (str1, str2) => str1.toLocaleLowerCase().localeCompare(str2.toLocaleLowerCase());
if (typeof(first) == "string") return stringSort(first, second);
if (typeof(first) == "boolean") return (first === second) ? stringSort(a.name, b.name) : first ? -1 : 1;
if (first > second) return 1;
if (second > first) return -1;
return 0;
});
if (!this.state.ascending) sortedAddons.reverse();
if (this.state.query) {
sortedAddons = sortedAddons.filter(addon => {
let matches = addon.name.toLocaleLowerCase().includes(this.state.query);
matches = matches || addon.author.toLocaleLowerCase().includes(this.state.query);
matches = matches || addon.description.toLocaleLowerCase().includes(this.state.query);
if (!matches) return false;
return true;
});
function getState(type, control, defaultValue) {
const addonlistControls = DataStore.getBDData("addonlistControls") || {};
if (!addonlistControls[type]) return defaultValue;
if (!addonlistControls[type].hasOwnProperty(control)) return defaultValue;
return addonlistControls[type][control];
}
const renderedCards = sortedAddons.map(addon => {
const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function";
const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance);
return <ErrorBoundary><AddonCard disabled={addon.partial} type={this.props.type} editAddon={this.editAddon.bind(this, addon.id)} deleteAddon={this.deleteAddon.bind(this, addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>;
});
const hasAddonsInstalled = this.props.addonList.length !== 0;
const isSearching = !!this.state.query;
const hasResults = sortedAddons.length !== 0;
return [
<SettingsTitle key="title" text={title} button={button} />,
<div className={"bd-controls bd-addon-controls"}>
<Search onChange={this.search} placeholder={`${Strings.Addons.search.format({type: this.props.title})}...`} />
<div className="bd-controls-advanced">
<div className="bd-addon-dropdowns">
<div className="bd-select-wrapper">
<label className="bd-label">{Strings.Sorting.sortBy}:</label>
<Dropdown options={this.sortOptions} value={this.state.sort} onChange={this.sort} style="transparent" />
</div>
<div className="bd-select-wrapper">
<label className="bd-label">{Strings.Sorting.order}:</label>
<Dropdown options={this.directions} value={this.state.ascending} onChange={this.reverse} style="transparent" />
</div>
</div>
<div className="bd-addon-views">
{this.makeControlButton("List View", <ListIcon />, this.listView, this.state.view === "list")}
{this.makeControlButton("Grid View", <GridIcon />, this.gridView, this.state.view === "grid")}
</div>
</div>
</div>,
!hasAddonsInstalled && this.emptyImage,
isSearching && !hasResults && hasAddonsInstalled && <NoResults />,
hasAddonsInstalled && <div key="addonList" className={"bd-addon-list" + (this.state.view == "grid" ? " bd-grid-view" : "")}>{renderedCards}</div>
];
function saveState(type, control, value) {
const addonlistControls = DataStore.getBDData("addonlistControls") || {};
if (!addonlistControls[type]) addonlistControls[type] = {};
addonlistControls[type][control] = value;
DataStore.setBDData("addonlistControls", addonlistControls);
}
editAddon(id) {
if (this.props.editAddon) this.props.editAddon(id);
}
async deleteAddon(id) {
const addon = this.props.addonList.find(a => a.id == id);
const shouldDelete = await this.confirmDelete(addon);
if (!shouldDelete) return;
if (this.props.deleteAddon) this.props.deleteAddon(addon);
}
confirmDelete(addon) {
function confirmDelete(addon) {
return new Promise(resolve => {
Modals.showConfirmationModal(Strings.Modals.confirmAction, Strings.Addons.confirmDelete.format({name: addon.name}), {
danger: true,
@ -203,12 +74,111 @@ export default class AddonList extends React.Component {
});
});
}
export default function AddonList({prefix, type, title, folder, addonList, addonState, onChange, reload, editAddon, deleteAddon}) {
const [query, setQuery] = useState("");
const [sort, setSort] = useState(getState.bind(null, type, "sort", "name"));
const [ascending, setAscending] = useState(getState.bind(null, type, "ascending", true));
const [view, setView] = useState(getState.bind(null, type, "view", "list"));
const [forced, forceUpdate] = useReducer(x => x + 1, 0);
useEffect(() => {
Events.on(`${prefix}-loaded`, forceUpdate);
Events.on(`${prefix}-unloaded`, forceUpdate);
return () => {
Events.off(`${prefix}-loaded`, forceUpdate);
Events.off(`${prefix}-unloaded`, forceUpdate);
};
}, [prefix]);
const changeView = useCallback((value) => {
saveState(type, "view", value);
setView(value);
}, [type]);
const listView = useCallback(() => changeView("list"), [changeView]);
const gridView = useCallback(() => changeView("grid"), [changeView]);
const changeDirection = useCallback((value) => {
saveState(type, "ascending", value);
setAscending(value);
}, [type]);
const changeSort = useCallback((value) => {
saveState(type, "sort", value);
setSort(value);
}, [type]);
const search = useCallback((e) => setQuery(e.target.value.toLocaleLowerCase()), []);
const triggerEdit = useCallback((id) => editAddon?.(id), [editAddon]);
const triggerDelete = useCallback(async (id) => {
const addon = addonList.find(a => a.id == id);
const shouldDelete = await confirmDelete(addon);
if (!shouldDelete) return;
if (deleteAddon) deleteAddon(addon);
}, [addonList, deleteAddon]);
const button = folder ? {title: Strings.Addons.openFolder.format({type: title}), onClick: openFolder.bind(null, folder)} : null;
const renderedCards = useMemo(() => {
let sorted = addonList.sort((a, b) => {
const sortByEnabled = sort === "isEnabled";
const first = sortByEnabled ? addonState[a.id] : a[sort];
const second = sortByEnabled ? addonState[b.id] : b[sort];
const stringSort = (str1, str2) => str1.toLocaleLowerCase().localeCompare(str2.toLocaleLowerCase());
if (typeof(first) == "string") return stringSort(first, second);
if (typeof(first) == "boolean") return (first === second) ? stringSort(a.name, b.name) : first ? -1 : 1;
if (first > second) return 1;
if (second > first) return -1;
return 0;
});
if (!ascending) sorted.reverse();
if (query) {
sorted = sorted.filter(addon => {
let matches = addon.name.toLocaleLowerCase().includes(query);
matches = matches || addon.author.toLocaleLowerCase().includes(query);
matches = matches || addon.description.toLocaleLowerCase().includes(query);
if (!matches) return false;
return true;
});
}
const originalRender = AddonList.prototype.render;
Object.defineProperty(AddonList.prototype, "render", {
enumerable: false,
configurable: false,
set: function() {Logger.warn("AddonList", "Addon policy for plugins #5 https://github.com/BetterDiscord/BetterDiscord/wiki/Addon-Policies#plugins");},
get: () => originalRender
return sorted.map(addon => {
const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function";
const getSettings = hasSettings && 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
const hasAddonsInstalled = addonList.length !== 0;
const isSearching = !!query;
const hasResults = renderedCards.length !== 0;
return [
<SettingsTitle key="title" text={title} button={button} />,
<div className={"bd-controls bd-addon-controls"}>
<Search onChange={search} placeholder={`${Strings.Addons.search.format({type: title})}...`} />
<div className="bd-controls-advanced">
<div className="bd-addon-dropdowns">
<div className="bd-select-wrapper">
<label className="bd-label">{Strings.Sorting.sortBy}:</label>
<Dropdown options={SORT_OPTIONS} value={sort} onChange={changeSort} style="transparent" />
</div>
<div className="bd-select-wrapper">
<label className="bd-label">{Strings.Sorting.order}:</label>
<Dropdown options={DIRECTIONS} value={ascending} onChange={changeDirection} style="transparent" />
</div>
</div>
<div className="bd-addon-views">
{makeControlButton("List View", <ListIcon />, listView, view === "list")}
{makeControlButton("Grid View", <GridIcon />, gridView, view === "grid")}
</div>
</div>
</div>,
!hasAddonsInstalled && blankslate(type, () => openFolder(folder)),
isSearching && !hasResults && hasAddonsInstalled && <NoResults />,
hasAddonsInstalled && <div key="addonList" className={"bd-addon-list" + (view == "grid" ? " bd-grid-view" : "")}>{renderedCards}</div>
];
}

View File

@ -1,5 +1,8 @@
import {DiscordModules, React} from "modules";
const {useState, useCallback} = React;
const Checkmark = React.memo((props) => (
<svg width="16" height="16" viewBox="0 0 24 24" {...props}>
<path fillRule="evenodd" clipRule="evenodd" fill={props.color ?? "#fff"} d="M8.99991 16.17L4.82991 12L3.40991 13.41L8.99991 19L20.9999 7.00003L19.5899 5.59003L8.99991 16.17Z" />
@ -28,7 +31,6 @@ const resolveColor = (color, hex = true) => {
}
};
const getRGB = (color) => {
let result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color);
if (result) return [parseInt(result[1]), parseInt(result[2]), parseInt(result[3])];
@ -52,28 +54,20 @@ const getContrastColor = (color) => {
return (luma(color) >= 165) ? "#000" : "#fff";
};
export default class Color extends React.Component {
constructor(props) {
super(props);
this.state = {value: this.props.value};
this.onChange = this.onChange.bind(this);
}
onChange(e) {
this.setState({value: e.target.value});
if (this.props.onChange) this.props.onChange(resolveColor(e.target.value));
}
render() {
const intValue = resolveColor(this.state.value, false);
const {colors = defaultColors, defaultValue} = this.props;
export default function Color({value: initialValue, onChange, colors = defaultColors, defaultValue}) {
const [value, setValue] = useState(initialValue);
const change = useCallback((e) => {
onChange?.(resolveColor(e.target.value));
setValue(e.target.value);
}, [onChange]);
const intValue = resolveColor(value, false);
return <div className="bd-color-picker-container">
<div className="bd-color-picker-controls">
<DiscordModules.Tooltip text="Default" position="bottom">
{props => (
<div {...props} className="bd-color-picker-default" style={{backgroundColor: resolveColor(defaultValue)}} onClick={() => this.onChange({target: {value: defaultValue}})}>
<div {...props} className="bd-color-picker-default" style={{backgroundColor: resolveColor(defaultValue)}} onClick={() => change({target: {value: defaultValue}})}>
{intValue === resolveColor(defaultValue, false)
? <Checkmark width="25" height="25" />
: null
@ -84,8 +78,8 @@ export default class Color extends React.Component {
<DiscordModules.Tooltip text="Custom Color" position="bottom">
{props => (
<div className="bd-color-picker-custom">
<Dropper color={getContrastColor(resolveColor(this.state.value, true))} />
<input {...props} style={{backgroundColor: resolveColor(this.state.value)}} type="color" className="bd-color-picker" value={resolveColor(this.state.value)} onChange={this.onChange} />
<Dropper color={getContrastColor(resolveColor(value, true))} />
<input {...props} style={{backgroundColor: resolveColor(value)}} type="color" className="bd-color-picker" value={resolveColor(value)} onChange={change} />
</div>
)}
</DiscordModules.Tooltip>
@ -93,9 +87,9 @@ export default class Color extends React.Component {
<div className="bd-color-picker-swatch">
{
colors.map((int, index) => (
<div key={index} className="bd-color-picker-swatch-item" style={{backgroundColor: resolveColor(int)}} onClick={() => this.onChange({target: {value: int}})}>
<div key={index} className="bd-color-picker-swatch-item" style={{backgroundColor: resolveColor(int)}} onClick={() => change({target: {value: int}})}>
{intValue === int
? <Checkmark color={getContrastColor(resolveColor(this.state.value, true))} />
? <Checkmark color={getContrastColor(resolveColor(value, true))} />
: null
}
</div>
@ -104,4 +98,3 @@ export default class Color extends React.Component {
</div>
</div>;
}
}

View File

@ -1,56 +1,47 @@
import {React} from "modules";
import Arrow from "../../icons/downarrow";
export default class Select extends React.Component {
constructor(props) {
super(props);
this.state = {open: false, value: this.props.hasOwnProperty("value") ? this.props.value : this.props.options[0].value};
this.dropdown = React.createRef();
this.onChange = this.onChange.bind(this);
this.showMenu = this.showMenu.bind(this);
this.hideMenu = this.hideMenu.bind(this);
}
const {useState, useCallback} = React;
showMenu(event) {
export default function Select({value: initialValue, options, style, onChange}) {
const [value, setValue] = useState(initialValue ?? options[0].value);
const change = useCallback((val) => {
onChange?.(val);
setValue(val);
}, [onChange]);
const hideMenu = useCallback(() => {
setOpen(false);
document.removeEventListener("click", hideMenu);
}, []);
const [open, setOpen] = useState(false);
const showMenu = useCallback((event) => {
event.preventDefault();
event.stopPropagation();
this.setState((state) => ({open: !state.open}), () => {
if (!this.state.open) return;
const next = !open;
setOpen(next);
if (!next) return;
document.addEventListener("click", hideMenu);
}, [hideMenu, open]);
document.addEventListener("click", this.hideMenu);
});
}
hideMenu() {
this.setState({open: false}, () => {
document.removeEventListener("click", this.hideMenu);
});
}
onChange(value) {
this.setState({value});
if (this.props.onChange) this.props.onChange(value);
}
get selected() {return this.props.options.find(o => o.value == this.state.value);}
get options() {
const selected = this.selected;
return <div className="bd-select-options">
{this.props.options.map(opt =>
<div className={`bd-select-option${selected.value == opt.value ? " selected" : ""}`} onClick={this.onChange.bind(this, opt.value)}>{opt.label}</div>
// ?? options[0] provides a double failsafe
const selected = options.find(o => o.value == value) ?? options[0];
const optionComponents = <div className="bd-select-options">
{options.map(opt =>
<div className={`bd-select-option${selected.value == opt.value ? " selected" : ""}`} onClick={() => change(opt.value)}>{opt.label}</div>
)}
</div>;
}
render() {
const style = this.props.style == "transparent" ? " bd-select-transparent" : "";
const isOpen = this.state.open ? " menu-open" : "";
return <div className={`bd-select${style}${isOpen}`} onClick={this.showMenu} ref={this.dropdown}>
<div className="bd-select-value">{this.selected.label}</div>
const styleClass = style == "transparent" ? " bd-select-transparent" : "";
const isOpen = open ? " menu-open" : "";
return <div className={`bd-select${styleClass}${isOpen}`} onClick={showMenu}>
<div className="bd-select-value">{selected.label}</div>
<Arrow className="bd-select-arrow" />
{this.state.open && this.options}
{open && optionComponents}
</div>;
}
}

View File

@ -1,15 +1,13 @@
import {React} from "modules";
export default class SettingItem extends React.Component {
render() {
return <div className={"bd-setting-item" + (this.props.inline ? " inline" : "")}>
export default function SettingItem({id, name, note, inline, children}) {
return <div className={"bd-setting-item" + (inline ? " inline" : "")}>
<div className={"bd-setting-header"}>
<label htmlFor={this.props.id} className={"bd-setting-title"}>{this.props.name}</label>
{this.props.inline && this.props.children}
<label htmlFor={id} className={"bd-setting-title"}>{name}</label>
{inline && children}
</div>
<div className={"bd-setting-note"}>{this.props.note}</div>
{!this.props.inline && this.props.children}
<div className={"bd-setting-note"}>{note}</div>
{!inline && children}
<div className={"bd-setting-divider"} />
</div>;
}
}

View File

@ -3,78 +3,50 @@ import {React} from "modules";
import Keyboard from "../../icons/keyboard";
import Close from "../../icons/close";
export default class Keybind extends React.Component {
constructor(props) {
super(props);
this.state = {value: this.props.value, isRecording: false};
this.onClick = this.onClick.bind(this);
this.keyHandler = this.keyHandler.bind(this);
this.clearKeybind = this.clearKeybind.bind(this);
this.accum = [];
this.max = this.props.max ?? 2;
}
const {useState, useCallback, useEffect} = React;
componentDidMount() {
window.addEventListener("keydown", this.keyHandler);
}
componentWillUnmount() {
window.removeEventListener("keydown", this.keyHandler);
}
export default function Keybind({value: initialValue, onChange, max = 2, clearable = true}) {
const [state, setState] = useState({value: initialValue, isRecording: false, accum: []});
/**
*
* @param {KeyboardEvent} event
*/
keyHandler(event) {
if (!this.state.isRecording) return;
useEffect(() => {
window.addEventListener("keydown", keyHandler, true);
return () => window.removeEventListener("keydown", keyHandler, true);
});
const keyHandler = useCallback((event) => {
if (!state.isRecording) return;
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
if (event.repeat || this.accum.includes(event.key)) return;
if (event.repeat || state.accum.includes(event.key)) return;
this.accum.push(event.key);
if (this.accum.length == this.max) {
if (this.props.onChange) this.props.onChange(this.accum);
this.setState({value: this.accum.slice(0), isRecording: false}, () => this.accum.splice(0, this.accum.length));
}
state.accum.push(event.key);
if (state.accum.length == max) {
if (onChange) onChange(state.accum);
setState({value: state.accum.slice(0), isRecording: false, accum: []});
}
}, [state, max, onChange]);
/**
*
* @param {MouseEvent} e
*/
onClick(e) {
if (e.target?.className?.includes?.("bd-keybind-clear") || e.target?.closest(".bd-button")?.className?.includes("bd-keybind-clear")) return this.clearKeybind(e);
this.setState({isRecording: !this.state.isRecording});
}
/**
*
* @param {MouseEvent} event
*/
clearKeybind(event) {
const clearKeybind = useCallback((event) => {
event.stopPropagation();
event.preventDefault();
this.accum.splice(0, this.accum.length);
if (this.props.onChange) this.props.onChange(this.accum);
this.setState({value: this.accum, isRecording: false});
}
if (onChange) onChange([]);
setState({...state, isRecording: false, value: [], accum: []});
}, [onChange, state]);
display() {
if (this.state.isRecording) return "Recording...";
if (!this.state.value.length) return "N/A";
return this.state.value.join(" + ");
}
const onClick = useCallback((e) => {
if (e.target?.className?.includes?.("bd-keybind-clear") || e.target?.closest(".bd-button")?.className?.includes("bd-keybind-clear")) return clearKeybind(e);
setState({...state, isRecording: !state.isRecording});
}, [state, clearKeybind]);
render() {
const {clearable = true} = this.props;
return <div className={"bd-keybind-wrap" + (this.state.isRecording ? " recording" : "")} onClick={this.onClick}>
<input readOnly={true} type="text" className="bd-keybind-input" value={this.display()} />
const displayValue = state.isRecording ? "Recording..." : !state.value.length ? "N/A" : state.value.join(" + ");
return <div className={"bd-keybind-wrap" + (state.isRecording ? " recording" : "")} onClick={onClick}>
<input readOnly={true} type="text" className="bd-keybind-input" value={displayValue} />
<div className="bd-keybind-controls">
<button className={"bd-button bd-keybind-record" + (this.state.isRecording ? " bd-button-danger" : "")}><Keyboard size="24px" /></button>
{clearable && <button onClick={this.clearKeybind} className="bd-button bd-keybind-clear"><Close size="24px" /></button>}
<button className={"bd-button bd-keybind-record" + (state.isRecording ? " bd-button-danger" : "")}><Keyboard size="24px" /></button>
{clearable && <button onClick={clearKeybind} className="bd-button bd-keybind-clear"><Close size="24px" /></button>}
</div>
</div>;
}
}

View File

@ -1,18 +1,14 @@
import {React} from "modules";
export default class Number extends React.Component {
constructor(props) {
super(props);
this.state = {value: this.props.value};
this.onChange = this.onChange.bind(this);
}
const {useState, useCallback} = React;
onChange(e) {
this.setState({value: e.target.value});
if (this.props.onChange) this.props.onChange(e.target.value);
}
render() {
return <input onChange={this.onChange} type="number" className="bd-number-input" min={this.props.min} max={this.props.max} step={this.props.step} value={this.state.value} />;
}
export default function Number({value: initialValue, min, max, step, onChange}) {
const [value, setValue] = useState(initialValue);
const change = useCallback((e) => {
onChange?.(e.target.value);
setValue(e.target.value);
}, [onChange]);
return <input onChange={change} type="number" className="bd-number-input" min={min} max={max} step={step} value={value} />;
}

View File

@ -2,25 +2,22 @@ import {React} from "modules";
import RadioIcon from "../../icons/radio";
export default class Radio extends React.Component {
constructor(props) {
super(props);
this.state = {value: this.props.options.findIndex(o => o.value === this.props.value)};
this.onChange = this.onChange.bind(this);
this.renderOption = this.renderOption.bind(this);
}
const {useState, useCallback} = React;
onChange(e) {
const index = parseInt(e.target.value);
const newValue = this.props.options[index].value;
this.setState({value: index});
if (this.props.onChange) this.props.onChange(newValue);
}
renderOption(opt, index) {
const isSelected = this.state.value === index;
export default function Radio({name, value, options, onChange}) {
const [index, setIndex] = useState(options.findIndex(o => o.value === value));
const change = useCallback((e) => {
const newIndex = parseInt(e.target.value);
const newValue = options[newIndex].value;
onChange?.(newValue);
setIndex(newIndex);
}, [options, onChange]);
function renderOption(opt, i) {
const isSelected = index === i;
return <label className={"bd-radio-option" + (isSelected ? " bd-radio-selected" : "")}>
<input onChange={this.onChange} type="radio" name={this.props.name} checked={isSelected} value={index} />
<input onChange={change} type="radio" name={name} checked={isSelected} value={i} />
{/* <span className="bd-radio-button"></span> */}
<RadioIcon className="bd-radio-icon" size="24" checked={isSelected} />
<div className="bd-radio-label-wrap">
@ -30,15 +27,5 @@ export default class Radio extends React.Component {
</label>;
}
render() {
return <div className="bd-radio-group">
{this.props.options.map(this.renderOption)}
</div>;
return <div className="bd-radio-group">{options.map(renderOption)}</div>;
}
}
/* <label class="container">
<input type="radio" name="test" checked="checked">
<span class="checkmark"></span>
<div class="test">One<div class="desc">Description</div></div>
</label> */

View File

@ -1,22 +1,20 @@
import {React} from "modules";
import SearchIcon from "../../icons/search";
export default class Search extends React.Component {
constructor(props) {
super(props);
this.state = {value: this.props.value};
this.onChange = this.onChange.bind(this);
}
const {useState, useCallback} = React;
onChange(e) {
this.setState({value: e.target.value});
if (this.props.onChange) this.props.onChange(e);
}
render() {
return <div className={"bd-search-wrapper" + (this.props.className ? ` ${this.props.className}` : "")}>
<input onChange={this.onChange} onKeyDown={this.props.onKeyDown} type="text" className="bd-search" placeholder={this.props.placeholder} maxLength="50" value={this.state.value} />
export default function Search({onChange, className, onKeyDown, placeholder}) {
const [value, setValue] = useState("");
const change = useCallback((e) => {
onChange?.(e);
setValue(e.target.value);
}, [onChange]);
return <div className={"bd-search-wrapper" + (className ? ` ${className}` : "")}>
<input onChange={change} onKeyDown={onKeyDown} type="text" className="bd-search" placeholder={placeholder} maxLength="50" value={value} />
<SearchIcon />
</div>;
}
}

View File

@ -1,21 +1,16 @@
import {React} from "modules";
export default class Slider extends React.Component {
constructor(props) {
super(props);
this.state = {value: this.props.value};
this.onChange = this.onChange.bind(this);
}
const {useState, useCallback} = React;
onChange(e) {
this.setState({value: e.target.value});
// e.target.style.backgroundSize = (e.target.value - this.props.min) * 100 / (this.props.max - this.props.min) + "% 100%";
if (this.props.onChange) this.props.onChange(e.target.value);
}
render() {
export default function Slider({value: initialValue, min, max, step, onChange}) {
const [value, setValue] = useState(initialValue);
const change = useCallback((e) => {
onChange?.(e.target.value);
setValue(e.target.value);
}, [onChange]);
return <div className="bd-slider-wrap">
<div className="bd-slider-label">{this.state.value}</div><input onChange={this.onChange} type="range" className="bd-slider-input" min={this.props.min} max={this.props.max} step={this.props.step} value={this.state.value} style={{backgroundSize: (this.state.value - this.props.min) * 100 / (this.props.max - this.props.min) + "% 100%"}} />
<div className="bd-slider-label">{value}</div><input onChange={change} type="range" className="bd-slider-input" min={min} max={max} step={step} value={value} style={{backgroundSize: (value - min) * 100 / (max - min) + "% 100%"}} />
</div>;
}
}

View File

@ -1,23 +1,19 @@
import {React} from "modules";
export default class Switch extends React.Component {
constructor(props) {
super(props);
this.state = {checked: this.props.checked};
this.onChange = this.onChange.bind(this);
}
const {useState, useCallback} = React;
onChange() {
if (this.props.disabled) return;
this.props.onChange(!this.state.checked);
this.setState({checked: !this.state.checked});
}
render() {
const enabledClass = this.props.disabled ? " bd-switch-disabled" : "";
const checkedClass = this.state.checked ? " bd-switch-checked" : "";
export default function Switch({id, checked: initialValue, disabled, onChange}) {
const [checked, setChecked] = useState(initialValue);
const change = useCallback(() => {
onChange?.(!checked);
setChecked(!checked);
}, [checked, onChange]);
const enabledClass = disabled ? " bd-switch-disabled" : "";
const checkedClass = checked ? " bd-switch-checked" : "";
return <div className={`bd-switch` + enabledClass + checkedClass}>
<input id={this.props.id} type="checkbox" disabled={this.props.disabled} checked={this.state.checked} onChange={this.onChange} />
<input id={id} type="checkbox" disabled={disabled} checked={checked} onChange={change} />
<div className="bd-switch-body">
<svg className="bd-switch-slider" viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet">
<rect className="bd-switch-handle" fill="white" x="4" y="0" height="20" width="20" rx="10"></rect>
@ -29,4 +25,3 @@ export default class Switch extends React.Component {
</div>
</div>;
}
}

View File

@ -1,18 +1,14 @@
import {React} from "modules";
export default class Textbox extends React.Component {
constructor(props) {
super(props);
this.state = {value: this.props.value};
this.onChange = this.onChange.bind(this);
}
const {useState, useCallback} = React;
onChange(e) {
this.setState({value: e.target.value});
if (this.props.onChange) this.props.onChange(e.target.value);
}
render() {
return <input onChange={this.onChange} onKeyDown={this.props.onKeyDown} type="text" className="bd-text-input" placeholder={this.props.placeholder} maxLength={this.props.maxLength} value={this.state.value} />;
}
export default function Textbox({value: initialValue, maxLength, placeholder, onKeyDown, onChange}) {
const [value, setValue] = useState(initialValue);
const change = useCallback((e) => {
onChange?.(e.target.value);
setValue(e.target.value);
}, [onChange]);
return <input onChange={change} onKeyDown={onKeyDown} type="text" className="bd-text-input" placeholder={placeholder} maxLength={maxLength} value={value} />;
}

View File

@ -2,52 +2,42 @@ import {React} from "modules";
import Title from "./title";
import Divider from "../divider";
const {useState, useCallback, useRef} = React;
const baseClassName = "bd-settings-group";
export default class Drawer extends React.Component {
constructor(props) {
super(props);
if (this.props.button && this.props.collapsible) {
const original = this.props.button.onClick;
this.props.button.onClick = (event) => {
export default function Drawer({name, collapsible, shown = true, showDivider, children, button, onDrawerToggle}) {
const container = useRef(null);
const [collapsed, setCollapsed] = useState(collapsible && !shown);
const toggleCollapse = useCallback(() => {
const drawer = container.current;
const timeout = collapsed ? 300 : 1;
drawer.style.setProperty("height", drawer.scrollHeight + "px");
drawer.classList.add("animating");
if (onDrawerToggle) onDrawerToggle(collapsed);
setCollapsed(!collapsed);
setTimeout(() => {
drawer.style.setProperty("height", "");
drawer.classList.remove("animating");
}, timeout);
}, [collapsed, onDrawerToggle]);
const onClick = useCallback((event) => {
event.stopPropagation();
original(...arguments);
};
}
button?.onClick(...arguments);
}, [button]);
if (!this.props.hasOwnProperty("shown")) this.props.shown = true;
this.container = React.createRef();
this.state = {
collapsed: this.props.collapsible && !this.props.shown
};
this.toggleCollapse = this.toggleCollapse.bind(this);
}
toggleCollapse() {
const container = this.container.current;
const timeout = this.state.collapsed ? 300 : 1;
container.style.setProperty("height", container.scrollHeight + "px");
container.classList.add("animating");
this.setState({collapsed: !this.state.collapsed}, () => setTimeout(() => {
container.style.setProperty("height", "");
container.classList.remove("animating");
}, timeout));
if (this.props.onDrawerToggle) this.props.onDrawerToggle(this.state.collapsed);
}
render() {
const collapseClass = this.props.collapsible ? `collapsible ${this.state.collapsed ? "collapsed" : "expanded"}` : "";
const collapseClass = collapsible ? `collapsible ${collapsed ? "collapsed" : "expanded"}` : "";
const groupClass = `${baseClassName} ${collapseClass}`;
return <div className={groupClass}>
<Title text={this.props.name} collapsible={this.props.collapsible} onClick={this.toggleCollapse} button={this.props.button} isGroup={true} />
<div className="bd-settings-container" ref={this.container}>
{this.props.children}
<Title text={name} collapsible={collapsible} onClick={toggleCollapse} button={button ? {...button, onClick} : null} isGroup={true} />
<div className="bd-settings-container" ref={container}>
{children}
</div>
{this.props.showDivider && <Divider />}
{showDivider && <Divider />}
</div>;
}
}

View File

@ -10,38 +10,29 @@ import Radio from "./components/radio";
import Keybind from "./components/keybind";
import Color from "./components/color";
const {useCallback} = React;
export default class Group extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
}
export default function Group({onChange, id, name, button, shown, onDrawerToggle, showDivider, collapsible, settings}) {
const change = useCallback((settingId, value) => {
if (id) onChange?.(id, settingId, value);
else onChange?.(settingId, value);
}, [id, onChange]);
onChange(id, value) {
if (!this.props.onChange) return;
if (this.props.id) this.props.onChange(this.props.id, id, value);
else this.props.onChange(id, value);
this.forceUpdate();
}
render() {
const {settings} = this.props;
return <Drawer collapsible={this.props.collapsible} name={this.props.name} button={this.props.button} shown={this.props.shown} onDrawerToggle={this.props.onDrawerToggle} showDivider={this.props.showDivider}>
return <Drawer collapsible={collapsible} name={name} button={button} shown={shown} onDrawerToggle={onDrawerToggle} showDivider={showDivider}>
{settings.filter(s => !s.hidden).map((setting) => {
let component = null;
if (setting.type == "dropdown") component = <Dropdown disabled={setting.disabled} id={setting.id} options={setting.options} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "number") component = <Number disabled={setting.disabled} id={setting.id} min={setting.min} max={setting.max} step={setting.step} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "switch") component = <Switch disabled={setting.disabled} id={setting.id} checked={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "text") component = <Textbox disabled={setting.disabled} id={setting.id} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "slider") component = <Slider disabled={setting.disabled} id={setting.id} min={setting.min} max={setting.max} step={setting.step} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "radio") component = <Radio disabled={setting.disabled} id={setting.id} name={setting.id} options={setting.options} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "keybind") component = <Keybind disabled={setting.disabled} id={setting.id} value={setting.value} max={setting.max} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "color") component = <Color disabled={setting.disabled} id={setting.id} value={setting.value} defaultValue={setting.defaultValue} colors={setting.colors} onChange={this.onChange.bind(this, setting.id)} />;
const callback = value => change(setting.id, value);
if (setting.type == "dropdown") component = <Dropdown disabled={setting.disabled} id={setting.id} options={setting.options} value={setting.value} onChange={callback} />;
if (setting.type == "number") component = <Number disabled={setting.disabled} id={setting.id} min={setting.min} max={setting.max} step={setting.step} value={setting.value} onChange={callback} />;
if (setting.type == "switch") component = <Switch disabled={setting.disabled} id={setting.id} checked={setting.value} onChange={callback} />;
if (setting.type == "text") component = <Textbox disabled={setting.disabled} id={setting.id} value={setting.value} onChange={callback} />;
if (setting.type == "slider") component = <Slider disabled={setting.disabled} id={setting.id} min={setting.min} max={setting.max} step={setting.step} value={setting.value} onChange={callback} />;
if (setting.type == "radio") component = <Radio disabled={setting.disabled} id={setting.id} name={setting.id} options={setting.options} value={setting.value} onChange={callback} />;
if (setting.type == "keybind") component = <Keybind disabled={setting.disabled} id={setting.id} value={setting.value} max={setting.max} onChange={callback} />;
if (setting.type == "color") component = <Color disabled={setting.disabled} id={setting.id} value={setting.value} defaultValue={setting.defaultValue} colors={setting.colors} onChange={callback} />;
if (!component) return null;
return <Item id={setting.id} inline={setting.type !== "radio"} key={setting.id} name={setting.name} note={setting.note}>{component}</Item>;
})}
</Drawer>;
}
}

View File

@ -4,14 +4,9 @@ import HistoryIcon from "../icons/history";
import Modals from "../modals";
export default class SettingsTitle extends React.Component {
renderHeader() {
return <h2 className="bd-sidebar-header-label">BetterDiscord</h2>;
}
render() {
export default function SettingsTitle() {
return <div className="bd-sidebar-header">
{this.renderHeader()}
<h2 className="bd-sidebar-header-label">BetterDiscord</h2>
<DiscordModules.Tooltip color="primary" position="top" text="Changelog">
{props =>
<div {...props} className="bd-changelog-button" onClick={() => Modals.showChangelogModal(Changelog)}>
@ -21,4 +16,3 @@ export default class SettingsTitle extends React.Component {
</DiscordModules.Tooltip>
</div>;
}
}

View File

@ -1,27 +1,25 @@
import {React} from "modules";
const className = "bd-settings-title";
const className2 = "bd-settings-title bd-settings-group-title";
const {useCallback} = React;
export default class SettingsTitle extends React.Component {
constructor(props) {
super(props);
this.buttonClick = this.buttonClick.bind(this);
}
buttonClick(event) {
const basicClass = "bd-settings-title";
const groupClass = "bd-settings-title bd-settings-group-title";
export default function SettingsTitle({isGroup, className, button, onClick, text, otherChildren}) {
const click = useCallback((event) => {
event.stopPropagation();
event.preventDefault();
this.props?.button?.onClick?.(event);
}
button?.onClick?.(event);
}, [button]);
render() {
const baseClass = this.props.isGroup ? className2 : className;
const titleClass = this.props.className ? `${baseClass} ${this.props.className}` : baseClass;
return <h2 className={titleClass} onClick={() => {this.props.onClick && this.props.onClick();}}>
{this.props.text}
{this.props.button && <button className="bd-button bd-button-title" onClick={this.buttonClick}>{this.props.button.title}</button>}
{this.props.otherChildren}
const baseClass = isGroup ? groupClass : basicClass;
const titleClass = className ? `${baseClass} ${className}` : baseClass;
return <h2 className={titleClass} onClick={() => {onClick?.();}}>
{text}
{button && <button className="bd-button bd-button-title" onClick={click}>{button.title}</button>}
{otherChildren}
</h2>;
}
}

View File

@ -7,124 +7,104 @@ import Toasts from "./toasts";
import Checkmark from "./icons/check";
class CoreUpdaterPanel extends React.Component {
render() {
const {useState, useCallback, useEffect} = React;
function CoreUpdaterPanel({hasUpdate, remoteVersion, update}) {
return <Drawer name="BetterDiscord" collapsible={true}>
<SettingItem name={`Core v${Config.version}`} note={this.props.hasUpdate ? Strings.Updater.versionAvailable.format({version: this.props.remoteVersion}) : Strings.Updater.noUpdatesAvailable} inline={true} id={"core-updater"}>
{!this.props.hasUpdate && <div className="bd-filled-checkmark"><Checkmark /></div>}
{this.props.hasUpdate && <button className="bd-button" onClick={this.props.update}>{Strings.Updater.updateButton}</button>}
<SettingItem name={`Core v${Config.version}`} note={hasUpdate ? Strings.Updater.versionAvailable.format({version: remoteVersion}) : Strings.Updater.noUpdatesAvailable} inline={true} id={"core-updater"}>
{!hasUpdate && <div className="bd-filled-checkmark"><Checkmark /></div>}
{hasUpdate && <button className="bd-button" onClick={update}>{Strings.Updater.updateButton}</button>}
</SettingItem>
</Drawer>;
}
}
class NoUpdates extends React.Component {
render() {
function NoUpdates({type}) {
return <div className="bd-empty-updates">
<Checkmark size="48px" />
{Strings.Updater.upToDateBlankslate.format({type: this.props.type})}
{Strings.Updater.upToDateBlankslate.format({type: type})}
</div>;
}
}
class AddonUpdaterPanel extends React.Component {
render() {
const filenames = this.props.pending;
return <Drawer name={Strings.Panels[this.props.type]} collapsible={true} button={filenames.length ? {title: Strings.Updater.updateAll, onClick: () => this.props.updateAll(this.props.type)} : null}>
{!filenames.length && <NoUpdates type={this.props.type} />}
function AddonUpdaterPanel({pending, type, updater, update, updateAll}) {
const filenames = pending;
return <Drawer name={Strings.Panels[type]} collapsible={true} button={filenames.length ? {title: Strings.Updater.updateAll, onClick: () => updateAll(type)} : null}>
{!filenames.length && <NoUpdates type={type} />}
{filenames.map(f => {
const info = this.props.updater.cache[f];
const addon = this.props.updater.manager.addonList.find(a => a.filename === f);
const info = updater.cache[f];
const addon = updater.manager.addonList.find(a => a.filename === f);
return <SettingItem name={`${addon.name} v${addon.version}`} note={Strings.Updater.versionAvailable.format({version: info.version})} inline={true} id={addon.name}>
<button className="bd-button" onClick={() => this.props.update(this.props.type, f)}>{Strings.Updater.updateButton}</button>
<button className="bd-button" onClick={() => update(type, f)}>{Strings.Updater.updateButton}</button>
</SettingItem>;
})}
</Drawer>;
}
}
export default class UpdaterPanel extends React.Component {
constructor(props) {
super(props);
export default function UpdaterPanel({coreUpdater, pluginUpdater, themeUpdater}) {
const [hasCoreUpdate, setCoreUpdate] = useState(coreUpdater.hasUpdate);
const [updates, setUpdates] = useState({plugins: pluginUpdater.pending.slice(0), themes: themeUpdater.pending.slice(0)});
this.state = {
hasCoreUpdate: this.props.coreUpdater.hasUpdate,
plugins: this.props.pluginUpdater.pending.slice(0),
themes: this.props.themeUpdater.pending.slice(0)
};
this.checkForUpdates = this.checkForUpdates.bind(this);
this.updateAddon = this.updateAddon.bind(this);
this.updateCore = this.updateCore.bind(this);
this.updateAllAddons = this.updateAllAddons.bind(this);
this.update = this.update.bind(this);
}
update() {
this.checkAddons("plugins");
this.checkAddons("themes");
}
componentDidMount() {
Events.on(`plugin-loaded`, this.update);
Events.on(`plugin-unloaded`, this.update);
Events.on(`theme-loaded`, this.update);
Events.on(`theme-unloaded`, this.update);
}
componentWillUnmount() {
Events.off(`plugin-loaded`, this.update);
Events.off(`plugin-unloaded`, this.update);
Events.off(`theme-loaded`, this.update);
Events.off(`theme-unloaded`, this.update);
}
async checkForUpdates() {
Toasts.info(Strings.Updater.checking);
await this.checkCoreUpdate();
await this.checkAddons("plugins");
await this.checkAddons("themes");
Toasts.info(Strings.Updater.finishedChecking);
}
async checkCoreUpdate() {
await this.props.coreUpdater.checkForUpdate(false);
this.setState({hasCoreUpdate: this.props.coreUpdater.hasUpdate});
}
async updateCore() {
await this.props.coreUpdater.update();
this.setState({hasCoreUpdate: false});
}
async checkAddons(type) {
const updater = type === "plugins" ? this.props.pluginUpdater : this.props.themeUpdater;
const checkAddons = useCallback(async (type) => {
const updater = type === "plugins" ? pluginUpdater : themeUpdater;
await updater.checkAll(false);
this.setState({[type]: updater.pending.slice(0)});
}
setUpdates({...updates, [type]: updater.pending.slice(0)});
}, [updates, pluginUpdater, themeUpdater]);
async updateAddon(type, filename) {
const updater = type === "plugins" ? this.props.pluginUpdater : this.props.themeUpdater;
const update = useCallback(() => {
checkAddons("plugins");
checkAddons("themes");
}, [checkAddons]);
useEffect(() => {
Events.on(`plugin-loaded`, update);
Events.on(`plugin-unloaded`, update);
Events.on(`theme-loaded`, update);
Events.on(`theme-unloaded`, update);
return () => {
Events.off(`plugin-loaded`, update);
Events.off(`plugin-unloaded`, update);
Events.off(`theme-loaded`, update);
Events.off(`theme-unloaded`, update);
};
}, [update]);
const checkCoreUpdate = useCallback(async () => {
await coreUpdater.checkForUpdate(false);
setCoreUpdate(coreUpdater.hasUpdate);
}, [coreUpdater]);
const checkForUpdates = useCallback(async () => {
Toasts.info(Strings.Updater.checking);
await checkCoreUpdate();
await checkAddons("plugins");
await checkAddons("themes");
Toasts.info(Strings.Updater.finishedChecking);
}, [checkAddons, checkCoreUpdate]);
const updateCore = useCallback(async () => {
await coreUpdater.update();
setCoreUpdate(false);
}, [coreUpdater]);
const updateAddon = useCallback(async (type, filename) => {
const updater = type === "plugins" ? pluginUpdater : themeUpdater;
await updater.updateAddon(filename);
this.setState(prev => {
setUpdates(prev => {
prev[type].splice(prev[type].indexOf(filename), 1);
return prev;
});
}
}, [pluginUpdater, themeUpdater]);
async updateAllAddons(type) {
const toUpdate = this.state[type].slice(0);
const updateAllAddons = useCallback(async (type) => {
const toUpdate = updates[type].slice(0);
for (const filename of toUpdate) {
await this.updateAddon(type, filename);
}
await updateAddon(type, filename);
}
}, [updateAddon, updates]);
render() {
return [
<SettingsTitle text={Strings.Panels.updates} button={{title: Strings.Updater.checkForUpdates, onClick: this.checkForUpdates}} />,
<CoreUpdaterPanel remoteVersion={this.props.coreUpdater.remoteVersion} hasUpdate={this.state.hasCoreUpdate} update={this.updateCore} />,
<AddonUpdaterPanel type="plugins" pending={this.state.plugins} update={this.updateAddon} updateAll={this.updateAllAddons} updater={this.props.pluginUpdater} />,
<AddonUpdaterPanel type="themes" pending={this.state.themes} update={this.updateAddon} updateAll={this.updateAllAddons} updater={this.props.themeUpdater} />,
<SettingsTitle text={Strings.Panels.updates} button={{title: Strings.Updater.checkForUpdates, onClick: checkForUpdates}} />,
<CoreUpdaterPanel remoteVersion={coreUpdater.remoteVersion} hasUpdate={hasCoreUpdate} update={updateCore} />,
<AddonUpdaterPanel type="plugins" pending={updates.plugins} update={updateAddon} updateAll={updateAllAddons} updater={pluginUpdater} />,
<AddonUpdaterPanel type="themes" pending={updates.themes} update={updateAddon} updateAll={updateAllAddons} updater={themeUpdater} />,
];
}
}