diff --git a/renderer/src/ui/customcss/csseditor.jsx b/renderer/src/ui/customcss/csseditor.jsx index c9ddc2cf..d9126b4b 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;} + }; + }, []); - componentDidMount() { - Events.on("customcss-updated", this.updateEditor); - } + useEffect(() => { + Events.on("customcss-updated", updateEditor); + return () => Events.off("customcss-updated", 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), []); + const popoutNative = useCallback(() => openNative?.(), []); + const popout = useCallback((event, currentCSS) => openDetached?.(currentCSS), []); - updateEditor(newCSS) { - if (!this.editor) return; - this.editor.value = newCSS; - } + const onChange = useCallback((newCSS) => { + notifyParent?.(newCSS); + setUnsaved(true); + }, []); - get value() {return this.editor.session.getValue();} - set value(newValue) { - this.editor.setValue(newValue); - } + const saveCss = useCallback((event, newCSS) => { + save?.(newCSS); + setUnsaved(false); + }, []); + - 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..9cf0b9a0 100644 --- a/renderer/src/ui/customcss/editor.jsx +++ b/renderer/src/ui/customcss/editor.jsx @@ -2,41 +2,76 @@ 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 [bindings, 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]); + 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]); - componentDidMount() { + useEffect(() => { + setBindings([...bindings, editor?.onDidChangeModelContent(onChange)]); + return () => { + for (const binding of bindings) binding?.dispose(); + setBindings([]); + }; + }, [editor]); + + useEffect(() => { 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 +84,53 @@ export default class CodeEditor extends React.Component { renderWhitespace: Settings.get("settings", "editor", "renderWhitespace") }); - this.bindings.push(this.editor.onDidChangeModelContent(this.onChange)); + 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; + textarea.onchange = (e) => onChange(e.target.value); + textarea.oninput = (e) => onChange(e.target.value); - this.editor = { + setEditor({ dispose: () => textarea.remove(), getValue: () => textarea.value, - setValue: (value) => textarea.value = value, + setValue: (val) => textarea.value = val, layout: () => {}, - }; + }); - document.getElementById(this.props.id).appendChild(textarea); + document.getElementById(id).appendChild(textarea); } - ThemeStore?.addChangeListener?.(this.onThemeChange); - window.addEventListener("resize", this.resize); - } + ThemeStore?.addChangeListener?.(onThemeChange); + window.addEventListener("resize", resize); - componentWillUnmount() { - window.removeEventListener("resize", this.resize); - ThemeStore?.removeChangeListener?.(this.onThemeChange); - for (const binding of this.bindings) binding.dispose(); - this.editor.dispose(); - } + return () => { + window.removeEventListener("resize", resize); + ThemeStore?.removeChangeListener?.(onThemeChange); + editor?.dispose(); + }; + }, []); - onThemeChange() { - const newTheme = ThemeStore?.theme === "light" ? "vs" : "vs-dark"; - if (newTheme === this.props.theme) return; - this.props.theme = newTheme; - if (window.monaco?.editor) window.monaco.editor.setTheme(this.props.theme); - } - get value() {return this.editor.getValue();} - set value(newValue) {this.editor.setValue(newValue);} + if (editor && editor.layout) editor.layout(); - onChange() { - if (this.props.onChange) this.props.onChange(this.value); - } + 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())); - showSettings() {return this.editor.keyBinding.$defaultHandler.commands.showSettingsMenu.exec(this.editor);} - resize() {this.editor.layout();} - - buildControl(control) { - if (control.type == "checkbox") return this.makeCheckbox(control); - return this.makeButton(control); - } - - makeCheckbox(checkbox) { - return ; - } - - 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/misc/addoneditor.jsx b/renderer/src/ui/misc/addoneditor.jsx index eeca49a0..0c5f042a 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;} + }; + }, []); - update() { - this.forceUpdate(); - } + const popoutNative = useCallback(() => openNative?.(), []); + const onChange = useCallback(() => setUnsaved(true), []); + const saveAddon = useCallback((event, newCSS) => { + save?.(newCSS); + setUnsaved(false); + }, []); - 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/settings/addonlist.jsx b/renderer/src/ui/settings/addonlist.jsx index 88221727..53e04dbb 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";