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,92 +23,57 @@ 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();}} >
<div className="bd-addon-error-icon">
{err.type == "plugin" ? <Extension /> : <ThemeIcon />}
</div>
<div className="bd-addon-error-header-inner">
<h3 className={`bd-addon-error-file ${DiscordClasses.Text.colorHeaderPrimary} ${DiscordClasses.Integrations.secondaryHeader} ${DiscordClasses.Text.size16}`}>{err.name}</h3>
<div className={`bd-addon-error-details ${DiscordClasses.Integrations.detailsWrapper}`}>
<svg className={DiscordClasses.Integrations.detailsIcon} aria-hidden="false" width="16" height="16" viewBox="0 0 12 12">
<path fill="currentColor" d="M6 1C3.243 1 1 3.244 1 6c0 2.758 2.243 5 5 5s5-2.242 5-5c0-2.756-2.243-5-5-5zm0 2.376a.625.625 0 110 1.25.625.625 0 010-1.25zM7.5 8.5h-3v-1h1V6H5V5h1a.5.5 0 01.5.5v2h1v1z"></path>
</svg>
<div className={`${DiscordClasses.Text.colorHeaderSecondary} ${DiscordClasses.Text.size12}`}>{err.message}</div>
</div>
</div>
<svg className="bd-addon-error-expander" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" d="M7 10L12 15 17 10" aria-hidden="true"></path>
</svg>
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>
{this.renderErrorBody(err)}
</div>;
}
<div className="bd-addon-error-header-inner">
<h3 className={`bd-addon-error-file ${DiscordClasses.Text.colorHeaderPrimary} ${DiscordClasses.Integrations.secondaryHeader} ${DiscordClasses.Text.size16}`}>{err.name}</h3>
<div className={`bd-addon-error-details ${DiscordClasses.Integrations.detailsWrapper}`}>
<svg className={DiscordClasses.Integrations.detailsIcon} aria-hidden="false" width="16" height="16" viewBox="0 0 12 12">
<path fill="currentColor" d="M6 1C3.243 1 1 3.244 1 6c0 2.758 2.243 5 5 5s5-2.242 5-5c0-2.756-2.243-5-5-5zm0 2.376a.625.625 0 110 1.25.625.625 0 010-1.25zM7.5 8.5h-3v-1h1V6H5V5h1a.5.5 0 01.5.5v2h1v1z"></path>
</svg>
<div className={`${DiscordClasses.Text.colorHeaderSecondary} ${DiscordClasses.Text.size12}`}>{err.message}</div>
</div>
</div>
<svg className="bd-addon-error-expander" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" d="M7 10L12 15 17 10" aria-hidden="true"></path>
</svg>
</div>
{renderErrorBody(err)}
</div>;
}
export default class AddonErrorModal extends React.Component {
constructor(props) {
super(props);
const tabs = this.getTabs();
this.state = {
selectedTab: tabs[0].id,
};
}
function generateTab(id, errors) {
return {id, errors, name: Strings.Panels[id]};
}
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;
}
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]);
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();
}
const [tabId, setTab] = useState(tabs[0].id);
const switchToTab = useCallback((id) => setTab(id), []);
const selectedTab = tabs.find(e => e.id === tabId);
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>)}
</div>
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={() => {switchToTab(tab.id);}} className={joinClassNames("bd-tab-item", tab.id === selectedTab.id && "selected")}>{tab.name}</div>)}
</div>
<div className={`bd-error-modal-content ${DiscordClasses.Modal.content} ${DiscordClasses.Scrollers.thin}`}>
<div className="bd-addon-errors">
{selectedTab.errors.map((error, index) => <AddonError index={index} err={error} />)}
</div>
</div>
<div className={`bd-error-modal-content ${DiscordClasses.Modal.content} ${DiscordClasses.Scrollers.thin}`}>
<div className="bd-addon-errors">
{selectedTab.errors.map((error, index) => <AddonError index={index} err={error} />)}
</div>
</>;
}
</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}` : "")}>
<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!"}
</div>
<div className={`bd-empty-image-message`}>
{SimpleMarkdown.parseToReact(this.props.message || "You should probably get something.")}
</div>
{this.props.children}
</div>;
}
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}`}>
{props.title || "You don't have anything!"}
</div>
<div className={`bd-empty-image-message`}>
{SimpleMarkdown.parseToReact(props.message || "You should probably get something.")}
</div>
{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}` : "")}>
<MagnifyingGlass />
<div className="bd-empty-results-text">
{this.props.text || DiscordModules.Strings.SEARCH_NO_RESULTS || ""}
</div>
</div>;
}
export default function NoResults(props) {
return <div className={"bd-empty-results" + (props.className ? ` ${props.className}` : "")}>
<MagnifyingGlass />
<div className="bd-empty-results-text">
{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;
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}>
export default function Checkbox({checked: initialState, text, onChange: notifyParent}) {
const [checked, setChecked] = useState(initialState);
const onClick = useCallback(() => {
notifyParent?.(!checked);
setChecked(!checked);
}, [notifyParent, checked]);
return <div className="checkbox-item">
<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";}
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>;
}
constructor(props) {
super(props);
function makeCheckbox(checkbox) {
return <Checkbox text={checkbox.label} onChange={checkbox.onChange} checked={checkbox.checked} />;
}
this.props.theme = ThemeStore?.theme === "light" ? "vs" : "vs-dark";
function buildControl(value, control) {
if (control.type == "checkbox") return makeCheckbox(control);
return makeButton(control, value);
}
this.props.language = this.props.language.toLowerCase().replace(/ /g, "_");
if (!languages.includes(this.props.language)) this.props.language = CodeEditor.defaultProps.language;
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]);
this.bindings = [];
this.resize = this.resize.bind(this);
this.onChange = this.onChange.bind(this);
this.onThemeChange = this.onThemeChange.bind(this);
}
const [theme, setTheme] = useState(() => ThemeStore?.theme === "light" ? "vs" : "vs-dark");
const [editor, setEditor] = useState(null);
const [, setBindings] = useState([]);
static get defaultProps() {
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,89 +87,61 @@ 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(this.props.id).appendChild(textarea);
document.getElementById(id).appendChild(textarea);
}
ThemeStore?.addChangeListener?.(this.onThemeChange);
window.addEventListener("resize", this.resize);
}
return () => {
toDispose?.dispose?.();
};
}, [id, language, theme, value]);
componentWillUnmount() {
window.removeEventListener("resize", this.resize);
ThemeStore?.removeChangeListener?.(this.onThemeChange);
for (const binding of this.bindings) binding.dispose();
this.editor.dispose();
}
useEffect(() => {
ThemeStore?.addChangeListener?.(onThemeChange);
window.addEventListener("resize", resize);
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);
}
return () => {
window.removeEventListener("resize", resize);
ThemeStore?.removeChangeListener?.(onThemeChange);
};
}, [onThemeChange, resize]);
get value() {return this.editor.getValue();}
set value(newValue) {this.editor.setValue(newValue);}
onChange() {
if (this.props.onChange) this.props.onChange(this.value);
}
if (editor && editor.layout) editor.layout();
showSettings() {return this.editor.keyBinding.$defaultHandler.commands.showSettingsMenu.exec(this.editor);}
resize() {this.editor.layout();}
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()));
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}>
<div id="bd-editor-controls">
<div className="controls-section controls-left">
{controlsLeft}
</div>
<div className="controls-section controls-right">
{controlsRight}
</div>
return <div id="bd-editor-panel" className={theme}>
<div id="bd-editor-controls">
<div className="controls-section controls-left">
{controlsLeft}
</div>
<div className="editor-wrapper">
<div id={this.props.id} className={"editor " + this.props.theme}></div>
<div className="controls-section controls-right">
{controlsRight}
</div>
</div>;
}
}
</div>
<div className="editor-wrapper">
<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() {
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}>
{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;
}
function minY() {
const appContainer = document.querySelector(`#app-mount > div[class*="app-"]`);
if (appContainer) return appContainer.offsetTop;
return 0;
}
export default FloatingWindowContainer;
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>
);
}

View File

@ -5,174 +5,153 @@ 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);
function confirmClose(confirmationText) {
return new Promise(resolve => {
Modals.showConfirmationModal(Strings.Modals.confirmAction, confirmationText, {
danger: true,
confirmText: Strings.Modals.close,
onConfirm: () => {resolve(true);},
onCancel: () => {resolve(false);}
});
});
}
this.state = {modalOpen: false};
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});
this.offX = 0;
this.offY = 0;
const titlebar = useRef(null);
const window = useRef(null);
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();
const onResizeStart = useCallback(() => {
setSize({width: window.current.offsetWidth, height: window.current.offsetHeight});
}, [window]);
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);
}
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;
onResizeStart() {
this.currentWidth = this.window.current.offsetWidth;
this.currentHeight = this.window.current.offsetHeight;
}
let newLeft = (e.clientX - offset.x);
if (newLeft <= minX) newLeft = minX;
if (newLeft + size.width >= maxX) newLeft = maxX - size.width;
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";
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";
}
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);
}
setSize({width, height});
}, [window, size, maxX, maxY, onResize]);
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;
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);
div.style.top = newTop + "px";
div.style.left = newLeft + "px";
}
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]);
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>;
}
const maximize = useCallback(() => {
window.current.style.width = "100%";
window.current.style.height = "100%";
if (onResize) onResize();
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 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 > this.maxY) this.window.current.style.top = (this.maxY - height) + "px";
if (right > this.maxX) this.window.current.style.left = (this.maxX - width) + "px";
if (bottom > maxY) window.current.style.top = (maxY - height) + "px";
if (right > maxX) window.current.style.left = (maxX - width) + "px";
const newLeft = parseInt(this.window.current.style.left);
const newTop = parseInt(this.window.current.style.top);
const newLeft = parseInt(window.current.style.left);
const newTop = parseInt(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";
// 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 < this.minX) {
const difference = this.minX - newLeft;
this.window.current.style.left = this.minX + "px";
this.window.current.style.height = (width - 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]);
async close() {
const close = useCallback(async () => {
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});
const didConfirmClose = typeof(doConfirmClose) == "function" ? doConfirmClose() : doConfirmClose;
if (didConfirmClose) {
setOpen(true);
shouldClose = await confirmClose(confirmationText);
setOpen(false);
}
if (this.props.close && shouldClose) this.props.close();
}
if (doClose && shouldClose) doClose();
}, [confirmationText, doClose, doConfirmClose]);
confirmClose() {
return new Promise(resolve => {
Modals.showConfirmationModal(Strings.Modals.confirmAction, this.props.confirmationText, {
danger: true,
confirmText: Strings.Modals.close,
onConfirm: () => {resolve(true);},
onCancel: () => {resolve(false);}
});
});
}
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">
<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>;
}
}
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}>
<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>;
}
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,13 +1,11 @@
import {React} from "modules";
export default class CloseButton extends React.Component {
render() {
const size = this.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" />
<path className="fill" fill="#dcddde" d="M9.5 3.205L8.795 2.5 6 5.295 3.205 2.5l-.705.705L5.295 6 2.5 8.795l.705.705L6 6.705 8.795 9.5l.705-.705L6.705 6" />
</g>
</svg>;
}
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" />
<path className="fill" fill="#dcddde" d="M9.5 3.205L8.795 2.5 6 5.295 3.205 2.5l-.705.705L5.295 6 2.5 8.795l.705.705L6 6.705 8.795 9.5l.705-.705L6.705 6" />
</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";
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>;
}
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}>
<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>;
}
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";
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>;
}
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}>
<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"/>
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}}>
<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>;
}
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}>
<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>;
}
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}>
<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>;
}
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}>
<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>;
}
}
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">
<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>
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}>
<path fill="none" d="M0 0h24v24H0V0z"/>
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
</svg>;
}
}
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}>
<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>;
}
}
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}>
<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>;
}
}
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";
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>;
}
}
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}>
<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>;
}
}
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}}>
<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>;
}
}
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";
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>;
}
}
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,11 +1,10 @@
import {React} from "modules";
export default class MagnifyingGlass extends React.Component {
render() {
const size = this.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)">
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)">
<path fill="rgba(0,0,0,0.1)" d="M42.1262,100.7598 C25.1382,83.7718 25.1382,56.2288 42.1262,39.2408 C59.1142,22.2538 86.6572,22.2538 103.6452,39.2408 C120.6322,56.2288 120.6322,83.7718 103.6452,100.7598 C86.6572,117.7478 59.1142,117.7478 42.1262,100.7598"/>
<path stroke="#1E2126" strokeWidth="2" d="M121.8938,119.4976 C94.5578,146.8346 50.2358,146.8346 22.8988,119.4976 C-4.4382,92.1616 -4.4382,47.8396 22.8988,20.5026 C50.2358,-6.8334 94.5578,-6.8344 121.8938,20.5026 C149.2308,47.8396 149.2308,92.1616 121.8938,119.4976 Z" strokeLinecap="round" strokeLinejoin="round" strokeDasharray="4 5"/>
<path fill="#C9D2F0" d="M1.8313,140.566 L1.8313,140.566 C-0.6097,138.125 -0.6097,134.166 1.8313,131.725 L38.6023,94.954 L47.4433,103.795 L10.6723,140.566 C8.2303,143.007 4.2723,143.007 1.8313,140.566"/>
@ -15,9 +14,8 @@ export default class MagnifyingGlass extends React.Component {
<path fill="#F3F9FF" d="M44.112,98.2847 C28.491,82.6637 28.491,57.3377 44.112,41.7167 C59.733,26.0957 85.06,26.0957 100.681,41.7157 C116.302,57.3367 116.302,82.6637 100.681,98.2847 C85.06,113.9057 59.733,113.9057 44.112,98.2847 M108.007,34.3897 C88.34,14.7227 56.453,14.7227 36.786,34.3897 C17.119,54.0567 17.119,85.9437 36.786,105.6107 C56.453,125.2777 88.34,125.2777 108.007,105.6107 C127.674,85.9437 127.674,54.0567 108.007,34.3897"/>
<path stroke="#1E2126" strokeWidth="2" d="M116.386 94.545C115.853 95.498 115.287 96.438 114.688 97.362M108.0071 105.6109C88.3401 125.2779 56.4531 125.2779 36.7861 105.6109 17.1191 85.9439 17.1191 54.0569 36.7861 34.3899 56.4531 14.7229 88.3401 14.7229 108.0071 34.3899 122.7701 49.1529 126.4511 70.7999 119.0511 88.9969" strokeLinecap="round" strokeLinejoin="round"/>
<path stroke="#1E2126" strokeWidth="2" d="M44.112,98.2847 C28.491,82.6637 28.491,57.3377 44.112,41.7167 C59.733,26.0957 85.06,26.0957 100.681,41.7157 C116.302,57.3367 116.302,82.6637 100.681,98.2847 C85.06,113.9057 59.733,113.9057 44.112,98.2847 Z" strokeLinecap="round" strokeLinejoin="round"/>
</g>
<rect width="160" height="160" y="-1"/>
</g>
</svg>;
}
}
</g>
<rect width="160" height="160" y="-1"/>
</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";
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" />
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}>
<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>;
}
}
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";
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" />
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}}>
<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" />}
</svg>;
}
}
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" />
{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}}>
<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" />
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";
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>;
}
}
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}}>
<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>;
}
}
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}>
<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>;
}
}
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}>
<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>;
}
}
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">
<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"/>
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,70 +1,62 @@
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>
</svg>
<div className="childContainer-1wxZNh">
<svg className="icon-1ihkOt" 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>;
const {useState, useCallback, useMemo} = React;
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;
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;
return <div className="bd-server-card" role="button" tabIndex="0" onClick={this.join}>
<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>
<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>}
</div>
<div className="bd-server-description">{server.description}</div>
<div className="bd-server-footer">
<div className="bd-server-count">
<div className="bd-server-count-dot"></div>
<div className="bd-server-count-text">{server.members.toLocaleString()} Members</div>
</div>
<div className="bd-server-count">
<div className="bd-server-count-dot"></div>
<div className="bd-server-count-text">Added {addedDate.toLocaleDateString()}</div>
</div>
</div>
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-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>;
}
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});
}
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(hasJoined) == "string" ? `${Strings.PublicServers.joining}...` : hasJoined ? Strings.PublicServers.joined : Strings.PublicServers.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={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>
{hasJoined && <div className="bd-server-tag">{buttonText}</div>}
</div>
<div className="bd-server-description">{server.description}</div>
<div className="bd-server-footer">
<div className="bd-server-count">
<div className="bd-server-count-dot"></div>
<div className="bd-server-count-text">{server.members.toLocaleString()} Members</div>
</div>
<div className="bd-server-count">
<div className="bd-server-count-dot"></div>
<div className="bd-server-count-text">Added {addedDate.toLocaleDateString()}</div>
</div>
</div>
</div>
</div>;
}

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,
@ -27,168 +30,128 @@ const LinkIcons = {
const LayerManager = {
pushLayer(component) {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_PUSH",
component
});
DiscordModules.Dispatcher.dispatch({
type: "LAYER_PUSH",
component
});
},
popLayer() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP"
});
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP"
});
},
popAllLayers() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP_ALL"
});
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP_ALL"
});
}
};
};
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 {
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>;
}
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 buildLink(type, url) {
if (!url) return null;
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 (type == "invite") {
link.props.onClick = function(event) {
event.preventDefault();
event.stopPropagation();
let code = url;
const tester = /\.gg\/(.*)$/;
if (tester.test(code)) code = code.match(tester)[1];
LayerManager.popLayer();
DiscordModules.InviteActions?.acceptInviteAndTransitionToInviteChannel({inviteKey: code});
};
}
return makeButton(Strings.Addons[type], link);
}
showSettings() {
if (!this.props.hasSettings || !this.props.enabled) return;
const name = this.getString(this.props.addon.name);
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]);
const showSettings = useCallback(() => {
if (!hasSettings || !isEnabled) return;
const name = getString(addon.name);
try {
Modals.showAddonSettingsModal(name, this.props.getSettingsPanel());
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]);
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;
const messageAuthor = useCallback(() => {
if (!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 (selfId == addon.authorId) return;
const privateChannelId = ChannelStore.getDMFromUserId(addon.authorId);
if (privateChannelId) return ChannelActions.selectPrivateChannel(privateChannelId);
PrivateChannelActions.openPrivateChannel(selfId, this.props.addon.authorId);
}
PrivateChannelActions.openPrivateChannel(selfId, addon.authorId);
}, [addon.authorId]);
buildTitle(name, version, author) {
const title = useMemo(() => {
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 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 [
React.createElement("div", {className: "bd-name"}, name),
React.createElement("div", {className: "bd-meta"},
React.createElement("span", {className: "bd-version"}, `v${version}`),
...authorArray
)
<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]);
buildLink(which) {
const url = this.props.addon[which];
if (!url) return null;
const icon = React.createElement(LinkIcons[which]);
const link = <a className="bd-link bd-link-website" href={url} target="_blank" rel="noopener noreferrer">{icon}</a>;
if (which == "invite") {
link.props.onClick = function(event) {
event.preventDefault();
event.stopPropagation();
let code = url;
const tester = /\.gg\/(.*)$/;
if (tester.test(code)) code = code.match(tester)[1];
LayerManager.popLayer();
DiscordModules.InviteActions.acceptInviteAndTransitionToInviteChannel({inviteKey: code});
};
}
return this.makeButton(Strings.Addons[which], 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>;
}
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 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>;
}
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" : "")}>
<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} />
<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 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>
</div>
{this.footer}
</div>;
}
}, [hasSettings, editAddon, deleteAddon, addon, isEnabled, showSettings]);
return <div id={`${addon.id}-card`} className={"bd-addon-card" + (disabled ? " bd-addon-card-disabled" : "")}>
<div className="bd-addon-header">
{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">
{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>
{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,120 +12,119 @@ 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);
}
const SORT_OPTIONS = [
{label: Strings.Addons.name, value: "name"},
{label: Strings.Addons.author, value: "author"},
{label: Strings.Addons.version, value: "version"},
{label: Strings.Addons.added, value: "added"},
{label: Strings.Addons.modified, value: "modified"},
{label: Strings.Addons.isEnabled, value: "isEnabled"}
];
componentDidMount() {
Events.on(`${this.props.prefix}-loaded`, this.update);
Events.on(`${this.props.prefix}-unloaded`, this.update);
}
const DIRECTIONS = [
{label: Strings.Sorting.ascending, value: true},
{label: Strings.Sorting.descending, value: false}
];
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);
}
function openFolder(folder) {
const shell = require("electron").shell;
const open = shell.openItem || shell.openPath;
open(folder);
}
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];
}
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>;
}
update() {
this.forceUpdate();
}
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>;
}}
</DiscordModules.Tooltip>;
}
reload() {
if (this.props.refreshList) this.props.refreshList();
this.forceUpdate();
}
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];
}
listView() {this.changeView("list");}
gridView() {this.changeView("grid");}
changeView(view) {
this.onControlChange("view", view);
this.setState({view});
}
function saveState(type, control, value) {
const addonlistControls = DataStore.getBDData("addonlistControls") || {};
if (!addonlistControls[type]) addonlistControls[type] = {};
addonlistControls[type][control] = value;
DataStore.setBDData("addonlistControls", addonlistControls);
}
reverse(value) {
this.onControlChange("ascending", value);
this.setState({ascending: value});
}
function confirmDelete(addon) {
return new Promise(resolve => {
Modals.showConfirmationModal(Strings.Modals.confirmAction, Strings.Addons.confirmDelete.format({name: addon.name}), {
danger: true,
confirmText: Strings.Addons.deleteAddon,
onConfirm: () => {resolve(true);},
onCancel: () => {resolve(false);}
});
});
}
sort(value) {
this.onControlChange("sort", value);
this.setState({sort: value});
}
search(event) {
this.setState({query: event.target.value.toLocaleLowerCase()});
}
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);
openFolder() {
const shell = require("electron").shell;
const open = shell.openItem || shell.openPath;
open(this.props.folder);
}
useEffect(() => {
Events.on(`${prefix}-loaded`, forceUpdate);
Events.on(`${prefix}-unloaded`, forceUpdate);
return () => {
Events.off(`${prefix}-loaded`, forceUpdate);
Events.off(`${prefix}-unloaded`, forceUpdate);
};
}, [prefix]);
get sortOptions() {
return [
{label: Strings.Addons.name, value: "name"},
{label: Strings.Addons.author, value: "author"},
{label: Strings.Addons.version, value: "version"},
{label: Strings.Addons.added, value: "added"},
{label: Strings.Addons.modified, value: "modified"},
{label: Strings.Addons.isEnabled, value: "isEnabled"}
];
}
const changeView = useCallback((value) => {
saveState(type, "view", value);
setView(value);
}, [type]);
get directions() {
return [
{label: Strings.Sorting.ascending, value: true},
{label: Strings.Sorting.descending, value: false}
];
}
const listView = useCallback(() => changeView("list"), [changeView]);
const gridView = useCallback(() => changeView("grid"), [changeView]);
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>
</EmptyImage>;
}
const changeDirection = useCallback((value) => {
saveState(type, "ascending", value);
setAscending(value);
}, [type]);
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>;
}}
</DiscordModules.Tooltip>;
}
const changeSort = useCallback((value) => {
saveState(type, "sort", value);
setSort(value);
}, [type]);
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 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;
@ -134,81 +132,53 @@ export default class AddonList extends React.Component {
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 (!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 renderedCards = sortedAddons.map(addon => {
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={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>;
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 = this.props.addonList.length !== 0;
const isSearching = !!this.state.query;
const hasResults = sortedAddons.length !== 0;
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={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>
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-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 className="bd-select-wrapper">
<label className="bd-label">{Strings.Sorting.order}:</label>
<Dropdown options={DIRECTIONS} value={ascending} onChange={changeDirection} style="transparent" />
</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>
];
}
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) {
return new Promise(resolve => {
Modals.showConfirmationModal(Strings.Modals.confirmAction, Strings.Addons.confirmDelete.format({name: addon.name}), {
danger: true,
confirmText: Strings.Addons.deleteAddon,
onConfirm: () => {resolve(true);},
onCancel: () => {resolve(false);}
});
});
}
<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>
];
}
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
});

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,56 +54,47 @@ 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));
}
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]);
render() {
const intValue = resolveColor(this.state.value, false);
const {colors = defaultColors, defaultValue} = this.props;
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}})}>
{intValue === resolveColor(defaultValue, false)
? <Checkmark width="25" height="25" />
: null
}
</div>
)}
</DiscordModules.Tooltip>
<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} />
</div>
)}
</DiscordModules.Tooltip>
</div>
<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}})}>
{intValue === int
? <Checkmark color={getContrastColor(resolveColor(this.state.value, true))} />
: null
}
</div>
))
}
</div>
</div>;
}
}
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={() => change({target: {value: defaultValue}})}>
{intValue === resolveColor(defaultValue, false)
? <Checkmark width="25" height="25" />
: null
}
</div>
)}
</DiscordModules.Tooltip>
<DiscordModules.Tooltip text="Custom Color" position="bottom">
{props => (
<div className="bd-color-picker-custom">
<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>
</div>
<div className="bd-color-picker-swatch">
{
colors.map((int, index) => (
<div key={index} className="bd-color-picker-swatch-item" style={{backgroundColor: resolveColor(int)}} onClick={() => change({target: {value: int}})}>
{intValue === int
? <Checkmark color={getContrastColor(resolveColor(value, true))} />
: null
}
</div>
))
}
</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>
<Arrow className="bd-select-arrow" />
{this.state.open && this.options}
</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" />
{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" : "")}>
<div className={"bd-setting-header"}>
<label htmlFor={this.props.id} className={"bd-setting-title"}>{this.props.name}</label>
{this.props.inline && this.props.children}
</div>
<div className={"bd-setting-note"}>{this.props.note}</div>
{!this.props.inline && this.props.children}
<div className={"bd-setting-divider"} />
</div>;
}
export default function SettingItem({id, name, note, inline, children}) {
return <div className={"bd-setting-item" + (inline ? " inline" : "")}>
<div className={"bd-setting-header"}>
<label htmlFor={id} className={"bd-setting-title"}>{name}</label>
{inline && children}
</div>
<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()} />
<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>}
</div>
</div>;
}
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" + (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>;
}
}
/* <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> */
return <div className="bd-radio-group">{options.map(renderOption)}</div>;
}

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} />
<SearchIcon />
</div>;
}
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() {
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>;
}
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">{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,32 +1,27 @@
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" : "";
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} />
<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>
<svg className="bd-switch-symbol" viewBox="0 0 20 20" fill="none">
<path></path>
<path></path>
</svg>
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={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>
<svg className="bd-switch-symbol" viewBox="0 0 20 20" fill="none">
<path></path>
<path></path>
</svg>
</div>
</div>;
}
</svg>
</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) => {
event.stopPropagation();
original(...arguments);
};
}
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]);
if (!this.props.hasOwnProperty("shown")) this.props.shown = true;
this.container = React.createRef();
this.state = {
collapsed: this.props.collapsible && !this.props.shown
};
const onClick = useCallback((event) => {
event.stopPropagation();
button?.onClick(...arguments);
}, [button]);
this.toggleCollapse = this.toggleCollapse.bind(this);
}
const collapseClass = collapsible ? `collapsible ${collapsed ? "collapsed" : "expanded"}` : "";
const groupClass = `${baseClassName} ${collapseClass}`;
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 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}
</div>
{this.props.showDivider && <Divider />}
</div>;
}
return <div className={groupClass}>
<Title text={name} collapsible={collapsible} onClick={toggleCollapse} button={button ? {...button, onClick} : null} isGroup={true} />
<div className="bd-settings-container" ref={container}>
{children}
</div>
{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}>
{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)} />;
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>;
}
return <Drawer collapsible={collapsible} name={name} button={button} shown={shown} onDrawerToggle={onDrawerToggle} showDivider={showDivider}>
{settings.filter(s => !s.hidden).map((setting) => {
let component = null;
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,21 +4,15 @@ 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() {
return <div className="bd-sidebar-header">
{this.renderHeader()}
<DiscordModules.Tooltip color="primary" position="top" text="Changelog">
{props =>
<div {...props} className="bd-changelog-button" onClick={() => Modals.showChangelogModal(Changelog)}>
<HistoryIcon className="bd-icon" size="16px" />
</div>
}
</DiscordModules.Tooltip>
</div>;
}
}
export default function SettingsTitle() {
return <div className="bd-sidebar-header">
<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)}>
<HistoryIcon className="bd-icon" size="16px" />
</div>
}
</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]);
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>;
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}
</h2>;
}
}

View File

@ -7,124 +7,104 @@ import Toasts from "./toasts";
import Checkmark from "./icons/check";
class CoreUpdaterPanel extends React.Component {
render() {
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>
</Drawer>;
}
const {useState, useCallback, useEffect} = React;
function CoreUpdaterPanel({hasUpdate, remoteVersion, update}) {
return <Drawer name="BetterDiscord" collapsible={true}>
<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() {
return <div className="bd-empty-updates">
<Checkmark size="48px" />
{Strings.Updater.upToDateBlankslate.format({type: this.props.type})}
</div>;
}
function NoUpdates({type}) {
return <div className="bd-empty-updates">
<Checkmark size="48px" />
{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} />}
{filenames.map(f => {
const info = this.props.updater.cache[f];
const addon = this.props.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>
</SettingItem>;
})}
</Drawer>;
}
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 = 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={() => 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} />,
];
}
return [
<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} />,
];
}