diff --git a/package.json b/package.json index 18eabfca..ffcffd48 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58419660..8338201c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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'} diff --git a/renderer/.eslintrc b/renderer/.eslintrc index 9c8e7075..de1cf8bf 100644 --- a/renderer/.eslintrc +++ b/renderer/.eslintrc @@ -1,7 +1,11 @@ { - "extends": ["plugin:react/recommended"], + "extends": [ + "plugin:react/recommended", + "plugin:react-hooks/recommended" + ], "plugins": [ - "react" + "react", + "react-hooks" ], "settings": { "react": { diff --git a/renderer/src/modules/api/reactutils.js b/renderer/src/modules/api/reactutils.js index b9edfc43..c5b3e28e 100644 --- a/renderer/src/modules/api/reactutils.js +++ b/renderer/src/modules/api/reactutils.js @@ -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"});} }; } diff --git a/renderer/src/modules/core.js b/renderer/src/modules/core.js index 411e028d..94dde01c 100644 --- a/renderer/src/modules/core.js +++ b/renderer/src/modules/core.js @@ -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) { diff --git a/renderer/src/modules/pluginmanager.js b/renderer/src/modules/pluginmanager.js index 38e74e5f..c315fd0c 100644 --- a/renderer/src/modules/pluginmanager.js +++ b/renderer/src/modules/pluginmanager.js @@ -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), diff --git a/renderer/src/modules/thememanager.js b/renderer/src/modules/thememanager.js index 5240eb85..dccfb3ab 100644 --- a/renderer/src/modules/thememanager.js +++ b/renderer/src/modules/thememanager.js @@ -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), diff --git a/renderer/src/ui/addonerrormodal.jsx b/renderer/src/ui/addonerrormodal.jsx index 4e0fbe04..5ac9bfaa 100644 --- a/renderer/src/ui/addonerrormodal.jsx +++ b/renderer/src/ui/addonerrormodal.jsx @@ -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
@@ -28,92 +23,57 @@ class AddonError extends React.Component {
; } - render() { - const err = this.props.err; - return
-
{this.toggle();}} > -
- {err.type == "plugin" ? : } -
-
-

{err.name}

-
- - - -
{err.message}
-
-
- - - + + return
+
+
+ {err.type == "plugin" ? : }
- {this.renderErrorBody(err)} -
; - } +
+

{err.name}

+
+ + + +
{err.message}
+
+
+ + + +
+ {renderErrorBody(err)} +
; } -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 <> -
-

{Strings.Modals.addonErrors}

-
- {tabs.map(tab =>
{this.switchToTab(tab.id);}} className={joinClassNames("bd-tab-item", tab.id === selectedTab.id && "selected")}>{tab.name}
)} -
+ return <> +
+

{Strings.Modals.addonErrors}

+
+ {tabs.map(tab =>
{switchToTab(tab.id);}} className={joinClassNames("bd-tab-item", tab.id === selectedTab.id && "selected")}>{tab.name}
)}
-
-
- {selectedTab.errors.map((error, index) => )} -
+
+
+
+ {selectedTab.errors.map((error, index) => )}
- ; - } +
+ ; } \ No newline at end of file diff --git a/renderer/src/ui/blankslates/emptyimage.jsx b/renderer/src/ui/blankslates/emptyimage.jsx index fa1d5891..7fd77004 100644 --- a/renderer/src/ui/blankslates/emptyimage.jsx +++ b/renderer/src/ui/blankslates/emptyimage.jsx @@ -1,17 +1,15 @@ import {React, DiscordClasses} from "modules"; import SimpleMarkdown from "../../structs/markdown"; -export default class EmptyImage extends React.Component { - render() { - return
-
-
- {this.props.title || "You don't have anything!"} -
-
- {SimpleMarkdown.parseToReact(this.props.message || "You should probably get something.")} -
- {this.props.children} -
; - } +export default function EmptyImage(props) { + return
+
+
+ {props.title || "You don't have anything!"} +
+
+ {SimpleMarkdown.parseToReact(props.message || "You should probably get something.")} +
+ {props.children} +
; } \ No newline at end of file diff --git a/renderer/src/ui/blankslates/noresults.jsx b/renderer/src/ui/blankslates/noresults.jsx index e9d796eb..943f7102 100644 --- a/renderer/src/ui/blankslates/noresults.jsx +++ b/renderer/src/ui/blankslates/noresults.jsx @@ -1,13 +1,11 @@ import {React, DiscordModules} from "modules"; import MagnifyingGlass from "../icons/magnifyingglass"; -export default class NoResults extends React.Component { - render() { - return
- -
- {this.props.text || DiscordModules.Strings.SEARCH_NO_RESULTS || ""} -
-
; - } +export default function NoResults(props) { + return
+ +
+ {props.text || DiscordModules.Strings.SEARCH_NO_RESULTS || ""} +
+
; } \ No newline at end of file diff --git a/renderer/src/ui/customcss/checkbox.jsx b/renderer/src/ui/customcss/checkbox.jsx index 79f069e6..390f00f4 100644 --- a/renderer/src/ui/customcss/checkbox.jsx +++ b/renderer/src/ui/customcss/checkbox.jsx @@ -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
-
{this.props.text}
-
+ +export default function Checkbox({checked: initialState, text, onChange: notifyParent}) { + const [checked, setChecked] = useState(initialState); + const onClick = useCallback(() => { + notifyParent?.(!checked); + setChecked(!checked); + }, [notifyParent, checked]); + + return
+
{text}
+
- +
; - } - - onClick() { - this.props.onChange(!this.state.checked); - this.setState({checked: !this.state.checked}); - } } \ No newline at end of file diff --git a/renderer/src/ui/customcss/csseditor.jsx b/renderer/src/ui/customcss/csseditor.jsx index c9ddc2cf..3c7bd776 100644 --- a/renderer/src/ui/customcss/csseditor.jsx +++ b/renderer/src/ui/customcss/csseditor.jsx @@ -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 ; - } - - 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(); - } -} \ No newline at end of file + return , tooltip: Strings.CustomCSS.update, onClick: updateCss}, + {label: , tooltip: Strings.CustomCSS.save, onClick: saveCss}, + {label: , 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: , tooltip: Strings.CustomCSS.openDetached, onClick: popout, side: "right"} + ].filter(c => c)} + value={css} + />; +}); \ No newline at end of file diff --git a/renderer/src/ui/customcss/editor.jsx b/renderer/src/ui/customcss/editor.jsx index d73f8a86..bc79399a 100644 --- a/renderer/src/ui/customcss/editor.jsx +++ b/renderer/src/ui/customcss/editor.jsx @@ -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 + {props => { + return ; + }} + ; +} - constructor(props) { - super(props); +function makeCheckbox(checkbox) { + return ; +} - 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 ; - } - - makeButton(button) { - return - {props => { - return ; - }} - ; - } - - 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
-
-
- {controlsLeft} -
-
- {controlsRight} -
+ return
+
+
+ {controlsLeft}
-
-
+
+ {controlsRight}
-
; - } -} +
+
+
+
+
; +}); \ No newline at end of file diff --git a/renderer/src/ui/floating/container.jsx b/renderer/src/ui/floating/container.jsx index 17536df3..50008ad3 100644 --- a/renderer/src/ui/floating/container.jsx +++ b/renderer/src/ui/floating/container.jsx @@ -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 => - - {window.children} - - ); - } - - 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; \ No newline at end of file +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 => + close(window.id)} minY={minY()} key={window.id}> + {window.children} + + ); +} \ No newline at end of file diff --git a/renderer/src/ui/floating/window.jsx b/renderer/src/ui/floating/window.jsx index 33c2a18e..220b9844 100644 --- a/renderer/src/ui/floating/window.jsx +++ b/renderer/src/ui/floating/window.jsx @@ -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": "
", -// "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
-
- {this.props.title} -
-
- -
-
- -
-
-
-
- {this.props.children} -
-
; - } + 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
+
+ {title} +
+
+ +
+
+ +
+
+
+
+ {children} +
+
; } \ No newline at end of file diff --git a/renderer/src/ui/floatingwindows.js b/renderer/src/ui/floatingwindows.js index 7ec2dd39..c6d1d1cd 100644 --- a/renderer/src/ui/floatingwindows.js +++ b/renderer/src/ui/floatingwindows.js @@ -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 = ; + const container = ; 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(`
`); 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); } } \ No newline at end of file diff --git a/renderer/src/ui/icons/bdlogo.jsx b/renderer/src/ui/icons/bdlogo.jsx index e10c56dd..d6dc74d1 100644 --- a/renderer/src/ui/icons/bdlogo.jsx +++ b/renderer/src/ui/icons/bdlogo.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class BDLogo extends React.Component { - render() { - return - - - - ; - } -} \ No newline at end of file +export default function BDLogo(props) { + return + + + + ; +} diff --git a/renderer/src/ui/icons/check.jsx b/renderer/src/ui/icons/check.jsx index c84fd0fc..d994ea89 100644 --- a/renderer/src/ui/icons/check.jsx +++ b/renderer/src/ui/icons/check.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class Checkmark extends React.Component { - render() { - const size = this.props.size || "24px"; - return - - - ; - } +export default function Checkmark(props) { + const size = props.size || "24px"; + return + + + ; } \ No newline at end of file diff --git a/renderer/src/ui/icons/close.jsx b/renderer/src/ui/icons/close.jsx index b93cba8e..55a45c0a 100644 --- a/renderer/src/ui/icons/close.jsx +++ b/renderer/src/ui/icons/close.jsx @@ -1,13 +1,11 @@ import {React} from "modules"; -export default class CloseButton extends React.Component { - render() { - const size = this.props.size || "18px"; - return - - - - - ; - } +export default function Close(props) { + const size = props.size || "18px"; + return + + + + + ; } \ No newline at end of file diff --git a/renderer/src/ui/icons/cog.jsx b/renderer/src/ui/icons/cog.jsx index 4d76b92a..9a929bf5 100644 --- a/renderer/src/ui/icons/cog.jsx +++ b/renderer/src/ui/icons/cog.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class Cog extends React.Component { - render() { - const size = this.props.size || "20px"; - return - - - ; - } +export default function Cog(props) { + const size = props.size || "20px"; + return + + + ; } \ No newline at end of file diff --git a/renderer/src/ui/icons/delete.jsx b/renderer/src/ui/icons/delete.jsx index 4dcc5d16..92f4d79d 100644 --- a/renderer/src/ui/icons/delete.jsx +++ b/renderer/src/ui/icons/delete.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class Delete extends React.Component { - render() { - const size = this.props.size || "24px"; - return - - - ; - } +export default function Delete(props) { + const size = props.size || "24px"; + return + + + ; } \ No newline at end of file diff --git a/renderer/src/ui/icons/detach.jsx b/renderer/src/ui/icons/detach.jsx index e12449d9..8e5ade9f 100644 --- a/renderer/src/ui/icons/detach.jsx +++ b/renderer/src/ui/icons/detach.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class Detach extends React.Component { - render() { - const size = this.props.size || "24px"; - return - - - ; - } +export default function Detach(props) { + const size = props.size || "24px"; + return + + + ; } \ No newline at end of file diff --git a/renderer/src/ui/icons/dollarsign.jsx b/renderer/src/ui/icons/dollarsign.jsx index 3352af7a..5634e440 100644 --- a/renderer/src/ui/icons/dollarsign.jsx +++ b/renderer/src/ui/icons/dollarsign.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class DollarSign extends React.Component { - render() { - const size = this.props.size || "18px"; - return - - +export default function DollarSign(props) { + const size = props.size || "18px"; + return + + ; - } } \ No newline at end of file diff --git a/renderer/src/ui/icons/downarrow.jsx b/renderer/src/ui/icons/downarrow.jsx index 556bd932..96d40694 100644 --- a/renderer/src/ui/icons/downarrow.jsx +++ b/renderer/src/ui/icons/downarrow.jsx @@ -1,10 +1,8 @@ import {React} from "modules"; -export default class DownArrow extends React.Component { - render() { - const size = this.props.size || "16px"; - return - - ; - } +export default function DownArrow(props) { + const size = props.size || "16px"; + return + + ; } \ No newline at end of file diff --git a/renderer/src/ui/icons/edit.jsx b/renderer/src/ui/icons/edit.jsx index 17e407d6..e7cf3109 100644 --- a/renderer/src/ui/icons/edit.jsx +++ b/renderer/src/ui/icons/edit.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class Edit extends React.Component { - render() { - const size = this.props.size || "24px"; - return - - - ; - } +export default function Edit(props) { + const size = props.size || "24px"; + return + + + ; } \ No newline at end of file diff --git a/renderer/src/ui/icons/error.jsx b/renderer/src/ui/icons/error.jsx index eb1bf767..5c6a7e5f 100644 --- a/renderer/src/ui/icons/error.jsx +++ b/renderer/src/ui/icons/error.jsx @@ -1,12 +1,9 @@ import {React} from "modules"; -export default class Error extends React.Component { - render() { - const size = this.props.size || "24px"; - return - - - ; - } +export default function Error(props) { + const size = props.size || "24px"; + return + + + ; } - diff --git a/renderer/src/ui/icons/extension.jsx b/renderer/src/ui/icons/extension.jsx index 0f396de0..d908cd2c 100644 --- a/renderer/src/ui/icons/extension.jsx +++ b/renderer/src/ui/icons/extension.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class Extension extends React.Component { - render() { - const size = this.props.size || "24px"; - return - - - ; - } -} \ No newline at end of file +export default function Extension(props) { + const size = props.size || "24px"; + return + + + ; +} diff --git a/renderer/src/ui/icons/favorite.jsx b/renderer/src/ui/icons/favorite.jsx index 1d85e40b..929bb316 100644 --- a/renderer/src/ui/icons/favorite.jsx +++ b/renderer/src/ui/icons/favorite.jsx @@ -1,9 +1,8 @@ import {React} from "modules"; -export default class Favorite extends React.Component { - render() { - return - +export default function Favorite(props) { + const size = props.size || "24px"; + return + ; - } -} \ No newline at end of file +} diff --git a/renderer/src/ui/icons/fullscreen.jsx b/renderer/src/ui/icons/fullscreen.jsx index 06ffc02d..41ee8d71 100644 --- a/renderer/src/ui/icons/fullscreen.jsx +++ b/renderer/src/ui/icons/fullscreen.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class FullScreen extends React.Component { - render() { - const size = this.props.size || "24px"; - return - - - ; - } -} \ No newline at end of file +export default function FullScreen(props) { + const size = props.size || "24px"; + return + + + ; +} diff --git a/renderer/src/ui/icons/github.jsx b/renderer/src/ui/icons/github.jsx index 180ff1ff..851bc2cd 100644 --- a/renderer/src/ui/icons/github.jsx +++ b/renderer/src/ui/icons/github.jsx @@ -1,10 +1,8 @@ import {React} from "modules"; -export default class GitHub extends React.Component { - render() { - const size = this.props.size || "18px"; - return - - ; - } -} \ No newline at end of file +export default function GitHub(props) { + const size = props.size || "18px"; + return + + ; +} diff --git a/renderer/src/ui/icons/globe.jsx b/renderer/src/ui/icons/globe.jsx index c543802e..b89a3d53 100644 --- a/renderer/src/ui/icons/globe.jsx +++ b/renderer/src/ui/icons/globe.jsx @@ -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 - - - ; - } -} \ No newline at end of file +export default function Globe(props) { + const size = props.size || "18px"; + const color = props.color || "#FFFFFF"; + return + + + ; +} diff --git a/renderer/src/ui/icons/grid.jsx b/renderer/src/ui/icons/grid.jsx index 89f9940b..1e926576 100644 --- a/renderer/src/ui/icons/grid.jsx +++ b/renderer/src/ui/icons/grid.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class Grid extends React.Component { - render() { - const size = this.props.size || "20px"; - return - - - ; - } -} \ No newline at end of file +export default function Grid(props) { + const size = props.size || "20px"; + return + + + ; +} diff --git a/renderer/src/ui/icons/history.jsx b/renderer/src/ui/icons/history.jsx index 19fc43be..92999de0 100644 --- a/renderer/src/ui/icons/history.jsx +++ b/renderer/src/ui/icons/history.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class History extends React.Component { - render() { - const size = this.props.size || "18px"; - return - - - ; - } -} \ No newline at end of file +export default function History(props) { + const size = props.size || "18px"; + return + + + ; +} diff --git a/renderer/src/ui/icons/keyboard.jsx b/renderer/src/ui/icons/keyboard.jsx index 5472f2a3..52161f60 100644 --- a/renderer/src/ui/icons/keyboard.jsx +++ b/renderer/src/ui/icons/keyboard.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class Keyboard extends React.Component { - render() { - const size = this.props.size || "24px"; - return - - - ; - } -} \ No newline at end of file +export default function Keyboard(props) { + const size = props.size || "24px"; + return + + + ; +} diff --git a/renderer/src/ui/icons/list.jsx b/renderer/src/ui/icons/list.jsx index 9e6f3167..b6011e1a 100644 --- a/renderer/src/ui/icons/list.jsx +++ b/renderer/src/ui/icons/list.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class List extends React.Component { - render() { - const size = this.props.size || "20px"; - return - - - ; - } -} \ No newline at end of file +export default function List(props) { + const size = props.size || "20px"; + return + + + ; +} diff --git a/renderer/src/ui/icons/magnifyingglass.jsx b/renderer/src/ui/icons/magnifyingglass.jsx index e5c61dc1..1d838d50 100644 --- a/renderer/src/ui/icons/magnifyingglass.jsx +++ b/renderer/src/ui/icons/magnifyingglass.jsx @@ -1,11 +1,10 @@ import {React} from "modules"; -export default class MagnifyingGlass extends React.Component { - render() { - const size = this.props.size || "160px"; - return - - +export default function MagnifyingGlass(props) { + const size = props.size || "160px"; + return + + @@ -15,9 +14,8 @@ export default class MagnifyingGlass extends React.Component { - - - - ; - } -} \ No newline at end of file + + + + ; +} diff --git a/renderer/src/ui/icons/next.jsx b/renderer/src/ui/icons/next.jsx index 50b1b993..c2e01393 100644 --- a/renderer/src/ui/icons/next.jsx +++ b/renderer/src/ui/icons/next.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class ArrowRight extends React.Component { - render() { - const size = this.props.size || "24px"; - return - - +export default function ArrowRight(props) { + const size = props.size || "24px"; + return + + ; - } -} \ No newline at end of file +} diff --git a/renderer/src/ui/icons/patreon.jsx b/renderer/src/ui/icons/patreon.jsx index f408a7e3..573d8c2a 100644 --- a/renderer/src/ui/icons/patreon.jsx +++ b/renderer/src/ui/icons/patreon.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class Patreon extends React.Component { - render() { - const size = this.props.size || "18px"; - return - - - ; - } -} \ No newline at end of file +export default function Patreon(props) { + const size = props.size || "18px"; + return + + + ; +} diff --git a/renderer/src/ui/icons/previous.jsx b/renderer/src/ui/icons/previous.jsx index 913705df..36407172 100644 --- a/renderer/src/ui/icons/previous.jsx +++ b/renderer/src/ui/icons/previous.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class ArrowLeft extends React.Component { - render() { - const size = this.props.size || "24px"; - return - - +export default function ArrowLeft(props) { + const size = props.size || "24px"; + return + + ; - } -} \ No newline at end of file +} diff --git a/renderer/src/ui/icons/radio.jsx b/renderer/src/ui/icons/radio.jsx index d56d9c84..b08f2445 100644 --- a/renderer/src/ui/icons/radio.jsx +++ b/renderer/src/ui/icons/radio.jsx @@ -1,12 +1,10 @@ import {React} from "modules"; -export default class Radio extends React.Component { - render() { - const size = this.props.size || "24px"; - return - - {this.props.checked && } - {!this.props.checked && } - ; - } -} \ No newline at end of file + export default function Radio(props) { + const size = props.size || "24px"; + return + + {props.checked && } + {!props.checked && } + ; +} diff --git a/renderer/src/ui/icons/reload.jsx b/renderer/src/ui/icons/reload.jsx index 385a6f26..4ff81a98 100644 --- a/renderer/src/ui/icons/reload.jsx +++ b/renderer/src/ui/icons/reload.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class ReloadIcon extends React.Component { - render() { - const size = this.props.size || "24px"; - return - - +export default function ReloadIcon(props) { + const size = props.size || "24px"; + return + + ; - } -} \ No newline at end of file +} diff --git a/renderer/src/ui/icons/save.jsx b/renderer/src/ui/icons/save.jsx index 89b023a9..5a02f66c 100644 --- a/renderer/src/ui/icons/save.jsx +++ b/renderer/src/ui/icons/save.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class Save extends React.Component { - render() { - const size = this.props.size || "24px"; - return - - - ; - } -} \ No newline at end of file +export default function Save(props) { + const size = props.size || "24px"; + return + + + ; +} diff --git a/renderer/src/ui/icons/search.jsx b/renderer/src/ui/icons/search.jsx index e9d7a2be..3a936e90 100644 --- a/renderer/src/ui/icons/search.jsx +++ b/renderer/src/ui/icons/search.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class Search extends React.Component { - render() { - const size = this.props.size || "16px"; - return - - - ; - } -} \ No newline at end of file + export default function Search(props) { + const size = props.size || "16px"; + return + + + ; +} diff --git a/renderer/src/ui/icons/support.jsx b/renderer/src/ui/icons/support.jsx index fbe7413b..732c6409 100644 --- a/renderer/src/ui/icons/support.jsx +++ b/renderer/src/ui/icons/support.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class Support extends React.Component { - render() { - const size = this.props.size || "18px"; - return - - - ; - } -} \ No newline at end of file +export default function Support(props) { + const size = props.size || "18px"; + return + + + ; +} diff --git a/renderer/src/ui/icons/theme.jsx b/renderer/src/ui/icons/theme.jsx index f2e9906a..cccae69b 100644 --- a/renderer/src/ui/icons/theme.jsx +++ b/renderer/src/ui/icons/theme.jsx @@ -1,11 +1,9 @@ import {React} from "modules"; -export default class Theme extends React.Component { - render() { - const size = this.props.size || "24px"; - return - - - ; - } -} \ No newline at end of file +export default function Theme(props) { + const size = props.size || "24px"; + return + + + ; +} diff --git a/renderer/src/ui/icons/twitch.jsx b/renderer/src/ui/icons/twitch.jsx index 1c7182c3..04a313d4 100644 --- a/renderer/src/ui/icons/twitch.jsx +++ b/renderer/src/ui/icons/twitch.jsx @@ -1,9 +1,8 @@ import {React} from "modules"; -export default class Twitch extends React.Component { - render() { - return - +export default function Twitch(props) { + const size = props.size || "18px"; + return + ; - } } \ No newline at end of file diff --git a/renderer/src/ui/misc/addoneditor.jsx b/renderer/src/ui/misc/addoneditor.jsx index eeca49a0..43a1048a 100644 --- a/renderer/src/ui/misc/addoneditor.jsx +++ b/renderer/src/ui/misc/addoneditor.jsx @@ -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 ; - } - - 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(); - } -} \ No newline at end of file + return , tooltip: Strings.CustomCSS.save, onClick: saveAddon}, + {label: , tooltip: Strings.CustomCSS.openNative, onClick: popoutNative} + ]} + value={content} + onChange={onChange} + />; +}); \ No newline at end of file diff --git a/renderer/src/ui/modals.js b/renderer/src/ui/modals.js index d03e5a76..da72056d 100644 --- a/renderer/src/ui/modals.js +++ b/renderer/src/ui/modals.js @@ -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 diff --git a/renderer/src/ui/publicservers/card.jsx b/renderer/src/ui/publicservers/card.jsx index 824b83ae..d55ed630 100644 --- a/renderer/src/ui/publicservers/card.jsx +++ b/renderer/src/ui/publicservers/card.jsx @@ -1,70 +1,62 @@ import {React, Strings} from "modules"; -const badge =
- - - - -
- - - -
-
; +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
-
-
- -
-
-
- {server.pinned && badge} -
{server.name}
- {this.state.joined &&
{buttonText}
} -
-
{server.description}
-
-
-
-
{server.members.toLocaleString()} Members
-
-
-
-
Added {addedDate.toLocaleDateString()}
-
-
+const badge =
+ + + +
+ + +
; - } - 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
+
+
+ +
+
+
+ {server.pinned && badge} +
{server.name}
+ {hasJoined &&
{buttonText}
} +
+
{server.description}
+
+
+
+
{server.members.toLocaleString()} Members
+
+
+
+
Added {addedDate.toLocaleDateString()}
+
+
+
+
; } \ No newline at end of file diff --git a/renderer/src/ui/settings.js b/renderer/src/ui/settings.js index 8b56da87..3d6c9f4d 100644 --- a/renderer/src/ui/settings.js +++ b/renderer/src/ui/settings.js @@ -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 diff --git a/renderer/src/ui/settings/addoncard.jsx b/renderer/src/ui/settings/addoncard.jsx index 8604944b..927877cc 100644 --- a/renderer/src/ui/settings/addoncard.jsx +++ b/renderer/src/ui/settings/addoncard.jsx @@ -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 + {(props) => { + return {children}; + }} + ; +} - 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 = {icon}; + 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 - ? {author.name} - : {author.name}; + const authorComponent = addon.authorLink || addon.authorId + ? {getString(addon.author)} + : {getString(addon.author)}; 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 - ) +
{getString(addon.name)}
, +
+ v{getString(addon.version)} + {authorArray} +
]; - - } + }, [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 = {icon}; - 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 && } - return
- {this.props.hasSettings && this.makeControlButton(Strings.Addons.addonSettings, , this.showSettings, {disabled: !this.props.enabled})} - {this.props.editAddon && this.makeControlButton(Strings.Addons.editAddon, , this.props.editAddon)} - {this.props.deleteAddon && this.makeControlButton(Strings.Addons.deleteAddon, , this.props.deleteAddon, {danger: true})} -
; - } - - 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
{linkComponents} - {this.controls} -
; - } - - makeButton(title, children, action) { - return - {(props) => { - return
{children}
; - }} -
; - } - - makeControlButton(title, children, action, {danger = false, disabled = false} = {}) { - return - {(props) => { - return ; - }} - ; - } - - 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
-
- {this.props.type === "plugin" ? : } -
{this.buildTitle(name, version, {name: author, id: this.props.addon.authorId, link: this.props.addon.authorLink})}
- +
+ {hasSettings && makeButton(Strings.Addons.addonSettings, , showSettings, {isControl: true, disabled: !isEnabled})} + {editAddon && makeButton(Strings.Addons.editAddon, , editAddon, {isControl: true})} + {deleteAddon && makeButton(Strings.Addons.deleteAddon, , deleteAddon, {isControl: true, danger: true})}
-
- {this.props.disabled &&
{`An error was encountered while trying to load this ${this.props.type}.`}
} -
{SimpleMarkdown.parseToReact(description)}
-
- {this.footer}
; - } + }, [hasSettings, editAddon, deleteAddon, addon, isEnabled, showSettings]); + + return
+
+ {type === "plugin" ? : } +
{title}
+ +
+
+ {disabled &&
{`An error was encountered while trying to load this ${type}.`}
} +
{SimpleMarkdown.parseToReact(getString(addon.description))}
+
+ {footer} +
; } - -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 -}); diff --git a/renderer/src/ui/settings/addonlist.jsx b/renderer/src/ui/settings/addonlist.jsx index eba3cb02..2b7b9084 100644 --- a/renderer/src/ui/settings/addonlist.jsx +++ b/renderer/src/ui/settings/addonlist.jsx @@ -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 + + ; +} - update() { - this.forceUpdate(); - } +function makeControlButton(title, children, action, selected = false) { + return + {(props) => { + return ; + }} + ; +} - 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 - - ; - } + const changeDirection = useCallback((value) => { + saveState(type, "ascending", value); + setAscending(value); + }, [type]); - makeControlButton(title, children, action, selected = false) { - return - {(props) => { - return ; - }} - ; - } + 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 ; + return triggerEdit(addon.id)} deleteAddon={() => triggerDelete(addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} />; }); + }, [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 [ - , -
- -
-
-
- - -
-
- - -
+ return [ + , +
+ +
+
+
+ +
-
- {this.makeControlButton("List View", , this.listView, this.state.view === "list")} - {this.makeControlButton("Grid View", , this.gridView, this.state.view === "grid")} +
+ +
-
, - !hasAddonsInstalled && this.emptyImage, - isSearching && !hasResults && hasAddonsInstalled && , - hasAddonsInstalled &&
{renderedCards}
- ]; - } - - 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);} - }); - }); - } +
+ {makeControlButton("List View", , listView, view === "list")} + {makeControlButton("Grid View", , gridView, view === "grid")} +
+
+
, + !hasAddonsInstalled && blankslate(type, () => openFolder(folder)), + isSearching && !hasResults && hasAddonsInstalled && , + hasAddonsInstalled &&
{renderedCards}
+ ]; } - -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 -}); diff --git a/renderer/src/ui/settings/components/color.jsx b/renderer/src/ui/settings/components/color.jsx index 22774b0b..86040ca5 100644 --- a/renderer/src/ui/settings/components/color.jsx +++ b/renderer/src/ui/settings/components/color.jsx @@ -1,5 +1,8 @@ import {DiscordModules, React} from "modules"; +const {useState, useCallback} = React; + + const Checkmark = React.memo((props) => ( @@ -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
-
- - {props => ( -
this.onChange({target: {value: defaultValue}})}> - {intValue === resolveColor(defaultValue, false) - ? - : null - } -
- )} -
- - {props => ( -
- - -
- )} -
-
-
- { - colors.map((int, index) => ( -
this.onChange({target: {value: int}})}> - {intValue === int - ? - : null - } -
- )) - } -
-
; - } -} + const intValue = resolveColor(value, false); + return
+
+ + {props => ( +
change({target: {value: defaultValue}})}> + {intValue === resolveColor(defaultValue, false) + ? + : null + } +
+ )} +
+ + {props => ( +
+ + +
+ )} +
+
+
+ { + colors.map((int, index) => ( +
change({target: {value: int}})}> + {intValue === int + ? + : null + } +
+ )) + } +
+
; +} \ No newline at end of file diff --git a/renderer/src/ui/settings/components/dropdown.jsx b/renderer/src/ui/settings/components/dropdown.jsx index 241aa003..d4d52946 100644 --- a/renderer/src/ui/settings/components/dropdown.jsx +++ b/renderer/src/ui/settings/components/dropdown.jsx @@ -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
- {this.props.options.map(opt => -
{opt.label}
+ // ?? options[0] provides a double failsafe + const selected = options.find(o => o.value == value) ?? options[0]; + const optionComponents =
+ {options.map(opt => +
change(opt.value)}>{opt.label}
)}
; - } - render() { - const style = this.props.style == "transparent" ? " bd-select-transparent" : ""; - const isOpen = this.state.open ? " menu-open" : ""; - return
-
{this.selected.label}
- - {this.state.open && this.options} -
; - } + const styleClass = style == "transparent" ? " bd-select-transparent" : ""; + const isOpen = open ? " menu-open" : ""; + return
+
{selected.label}
+ + {open && optionComponents} +
; } \ No newline at end of file diff --git a/renderer/src/ui/settings/components/item.jsx b/renderer/src/ui/settings/components/item.jsx index e7fc2075..0fdb40e0 100644 --- a/renderer/src/ui/settings/components/item.jsx +++ b/renderer/src/ui/settings/components/item.jsx @@ -1,15 +1,13 @@ import {React} from "modules"; -export default class SettingItem extends React.Component { - render() { - return
-
- - {this.props.inline && this.props.children} -
-
{this.props.note}
- {!this.props.inline && this.props.children} -
-
; - } +export default function SettingItem({id, name, note, inline, children}) { + return
+
+ + {inline && children} +
+
{note}
+ {!inline && children} +
+
; } \ No newline at end of file diff --git a/renderer/src/ui/settings/components/keybind.jsx b/renderer/src/ui/settings/components/keybind.jsx index dd028bfc..6db46c86 100644 --- a/renderer/src/ui/settings/components/keybind.jsx +++ b/renderer/src/ui/settings/components/keybind.jsx @@ -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
- -
- - {clearable && } -
-
; - } + + const displayValue = state.isRecording ? "Recording..." : !state.value.length ? "N/A" : state.value.join(" + "); + return
+ +
+ + {clearable && } +
+
; } \ No newline at end of file diff --git a/renderer/src/ui/settings/components/number.jsx b/renderer/src/ui/settings/components/number.jsx index 51e9189b..f6e31db0 100644 --- a/renderer/src/ui/settings/components/number.jsx +++ b/renderer/src/ui/settings/components/number.jsx @@ -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 ; - } +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 ; } \ No newline at end of file diff --git a/renderer/src/ui/settings/components/radio.jsx b/renderer/src/ui/settings/components/radio.jsx index fbacc3fa..02d5da89 100644 --- a/renderer/src/ui/settings/components/radio.jsx +++ b/renderer/src/ui/settings/components/radio.jsx @@ -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