From 717c9026f454119f63b2a536c6956778fbfbbe85 Mon Sep 17 00:00:00 2001 From: Zack Rauen Date: Wed, 8 Mar 2023 20:08:48 -0500 Subject: [PATCH] Convert settings components --- renderer/src/ui/settings/addoncard.jsx | 217 ++++++++---------- renderer/src/ui/settings/addonlist.jsx | 293 +++++++++++-------------- renderer/src/ui/settings/drawer.jsx | 72 +++--- renderer/src/ui/settings/group.jsx | 53 ++--- renderer/src/ui/settings/title.jsx | 36 ++- 5 files changed, 292 insertions(+), 379 deletions(-) diff --git a/renderer/src/ui/settings/addoncard.jsx b/renderer/src/ui/settings/addoncard.jsx index 8604944b..990b36dd 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, onChange: parentChange, hasSettings, editAddon, deleteAddon, getSettingsPanel}) { + const [isEnabled, setEnabled] = useState(enabled); + const onChange = useCallback(() => { + setEnabled(!isEnabled); + if (parentChange) parentChange(addon.id); + }, []); + + const showSettings = useCallback(() => { + if (!hasSettings || !enabled) 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, enabled]); - 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} +
]; - - } + }, []); - 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: !enabled})} + {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]); + + 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..88221727 100644 --- a/renderer/src/ui/settings/addonlist.jsx +++ b/renderer/src/ui/settings/addonlist.jsx @@ -13,120 +13,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 [, 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); + }; + }, []); - 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); + }, []); - get directions() { - return [ - {label: Strings.Sorting.ascending, value: true}, - {label: Strings.Sorting.descending, value: false} - ]; - } + const listView = useCallback(() => changeView("list"), []); + const gridView = useCallback(() => changeView("grid"), []); - 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); + }, []); - makeControlButton(title, children, action, selected = false) { - return - {(props) => { - return ; - }} - ; - } + const changeSort = useCallback((value) => { + saveState(type, "sort", value); + setSort(value); + }, []); - 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), []); + 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); + }, []); + + const button = folder ? {title: Strings.Addons.openFolder.format({type: title}), onClick: openFolder} : 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 +133,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, sort, ascending, query]); - 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/drawer.jsx b/renderer/src/ui/settings/drawer.jsx index ac2857fb..692b0114 100644 --- a/renderer/src/ui/settings/drawer.jsx +++ b/renderer/src/ui/settings/drawer.jsx @@ -2,52 +2,42 @@ import {React} from "modules"; import Title from "./title"; import Divider from "../divider"; +const {useState, useCallback, useRef} = React; + const baseClassName = "bd-settings-group"; -export default class Drawer extends React.Component { - constructor(props) { - super(props); - if (this.props.button && this.props.collapsible) { - const original = this.props.button.onClick; - this.props.button.onClick = (event) => { - event.stopPropagation(); - original(...arguments); - }; - } +export default function Drawer({name, collapsible, shown = true, showDivider, children, button, onDrawerToggle}) { + const container = useRef(null); + const [collapsed, setCollapsed] = useState(collapsible && !shown); + const toggleCollapse = useCallback(() => { + const drawer = container.current; + const timeout = collapsed ? 300 : 1; + drawer.style.setProperty("height", drawer.scrollHeight + "px"); + drawer.classList.add("animating"); + if (onDrawerToggle) onDrawerToggle(collapsed); + setCollapsed(!collapsed); + setTimeout(() => { + drawer.style.setProperty("height", ""); + drawer.classList.remove("animating"); + }, timeout); + + }, [collapsed]); - if (!this.props.hasOwnProperty("shown")) this.props.shown = true; - this.container = React.createRef(); - this.state = { - collapsed: this.props.collapsible && !this.props.shown - }; + const onClick = useCallback((event) => { + event.stopPropagation(); + button?.onClick(...arguments); + }, [button]); - this.toggleCollapse = this.toggleCollapse.bind(this); - } + const collapseClass = collapsible ? `collapsible ${collapsed ? "collapsed" : "expanded"}` : ""; + const groupClass = `${baseClassName} ${collapseClass}`; - toggleCollapse() { - const container = this.container.current; - const timeout = this.state.collapsed ? 300 : 1; - container.style.setProperty("height", container.scrollHeight + "px"); - container.classList.add("animating"); - this.setState({collapsed: !this.state.collapsed}, () => setTimeout(() => { - container.style.setProperty("height", ""); - container.classList.remove("animating"); - }, timeout)); - if (this.props.onDrawerToggle) this.props.onDrawerToggle(this.state.collapsed); - } - - render() { - const collapseClass = this.props.collapsible ? `collapsible ${this.state.collapsed ? "collapsed" : "expanded"}` : ""; - const groupClass = `${baseClassName} ${collapseClass}`; - - return
- - <div className="bd-settings-container" ref={this.container}> - {this.props.children} - </div> - {this.props.showDivider && <Divider />} - </div>; - } + return <div className={groupClass}> + <Title text={name} collapsible={collapsible} onClick={toggleCollapse} button={button ? {...button, onClick} : null} isGroup={true} /> + <div className="bd-settings-container" ref={container}> + {children} + </div> + {showDivider && <Divider />} + </div>; } \ No newline at end of file diff --git a/renderer/src/ui/settings/group.jsx b/renderer/src/ui/settings/group.jsx index 4911135e..2c96ac7d 100644 --- a/renderer/src/ui/settings/group.jsx +++ b/renderer/src/ui/settings/group.jsx @@ -10,38 +10,29 @@ import Radio from "./components/radio"; import Keybind from "./components/keybind"; import Color from "./components/color"; +const {useCallback} = React; -export default class Group extends React.Component { - constructor(props) { - super(props); - this.onChange = this.onChange.bind(this); - } +export default function Group({onChange, id, name, button, shown, onDrawerToggle, showDivider, collapsible, settings}) { + const change = useCallback((settingId, value) => { + if (id) onChange?.(id, settingId, value); + else onChange?.(settingId, value); + }, [id]); - onChange(id, value) { - if (!this.props.onChange) return; - if (this.props.id) this.props.onChange(this.props.id, id, value); - else this.props.onChange(id, value); - this.forceUpdate(); - } - - render() { - const {settings} = this.props; - - return <Drawer collapsible={this.props.collapsible} name={this.props.name} button={this.props.button} shown={this.props.shown} onDrawerToggle={this.props.onDrawerToggle} showDivider={this.props.showDivider}> - {settings.filter(s => !s.hidden).map((setting) => { - let component = null; - if (setting.type == "dropdown") component = <Dropdown disabled={setting.disabled} id={setting.id} options={setting.options} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />; - if (setting.type == "number") component = <Number disabled={setting.disabled} id={setting.id} min={setting.min} max={setting.max} step={setting.step} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />; - if (setting.type == "switch") component = <Switch disabled={setting.disabled} id={setting.id} checked={setting.value} onChange={this.onChange.bind(this, setting.id)} />; - if (setting.type == "text") component = <Textbox disabled={setting.disabled} id={setting.id} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />; - if (setting.type == "slider") component = <Slider disabled={setting.disabled} id={setting.id} min={setting.min} max={setting.max} step={setting.step} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />; - if (setting.type == "radio") component = <Radio disabled={setting.disabled} id={setting.id} name={setting.id} options={setting.options} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />; - if (setting.type == "keybind") component = <Keybind disabled={setting.disabled} id={setting.id} value={setting.value} max={setting.max} onChange={this.onChange.bind(this, setting.id)} />; - if (setting.type == "color") component = <Color disabled={setting.disabled} id={setting.id} value={setting.value} defaultValue={setting.defaultValue} colors={setting.colors} onChange={this.onChange.bind(this, setting.id)} />; - if (!component) return null; - return <Item id={setting.id} inline={setting.type !== "radio"} key={setting.id} name={setting.name} note={setting.note}>{component}</Item>; - })} - </Drawer>; - } + return <Drawer collapsible={collapsible} name={name} button={button} shown={shown} onDrawerToggle={onDrawerToggle} showDivider={showDivider}> + {settings.filter(s => !s.hidden).map((setting) => { + let component = null; + const callback = value => change(setting.id, value); + if (setting.type == "dropdown") component = <Dropdown disabled={setting.disabled} id={setting.id} options={setting.options} value={setting.value} onChange={callback} />; + if (setting.type == "number") component = <Number disabled={setting.disabled} id={setting.id} min={setting.min} max={setting.max} step={setting.step} value={setting.value} onChange={callback} />; + if (setting.type == "switch") component = <Switch disabled={setting.disabled} id={setting.id} checked={setting.value} onChange={callback} />; + if (setting.type == "text") component = <Textbox disabled={setting.disabled} id={setting.id} value={setting.value} onChange={callback} />; + if (setting.type == "slider") component = <Slider disabled={setting.disabled} id={setting.id} min={setting.min} max={setting.max} step={setting.step} value={setting.value} onChange={callback} />; + if (setting.type == "radio") component = <Radio disabled={setting.disabled} id={setting.id} name={setting.id} options={setting.options} value={setting.value} onChange={callback} />; + if (setting.type == "keybind") component = <Keybind disabled={setting.disabled} id={setting.id} value={setting.value} max={setting.max} onChange={callback} />; + if (setting.type == "color") component = <Color disabled={setting.disabled} id={setting.id} value={setting.value} defaultValue={setting.defaultValue} colors={setting.colors} onChange={callback} />; + if (!component) return null; + return <Item id={setting.id} inline={setting.type !== "radio"} key={setting.id} name={setting.name} note={setting.note}>{component}</Item>; + })} + </Drawer>; } \ No newline at end of file diff --git a/renderer/src/ui/settings/title.jsx b/renderer/src/ui/settings/title.jsx index b318631c..8cdc9ff2 100644 --- a/renderer/src/ui/settings/title.jsx +++ b/renderer/src/ui/settings/title.jsx @@ -1,27 +1,25 @@ import {React} from "modules"; -const className = "bd-settings-title"; -const className2 = "bd-settings-title bd-settings-group-title"; +const {useCallback} = React; -export default class SettingsTitle extends React.Component { - constructor(props) { - super(props); - this.buttonClick = this.buttonClick.bind(this); - } - buttonClick(event) { +const basicClass = "bd-settings-title"; +const groupClass = "bd-settings-title bd-settings-group-title"; + +export default function SettingsTitle({isGroup, className, button, onClick, text, otherChildren}) { + const click = useCallback((event) => { event.stopPropagation(); event.preventDefault(); - this.props?.button?.onClick?.(event); - } + button?.onClick?.(event); + }, []); + + + const baseClass = isGroup ? groupClass : basicClass; + const titleClass = className ? `${baseClass} ${className}` : baseClass; + return <h2 className={titleClass} onClick={() => {onClick?.();}}> + {text} + {button && <button className="bd-button bd-button-title" onClick={click}>{button.title}</button>} + {otherChildren} + </h2>; - render() { - const baseClass = this.props.isGroup ? className2 : className; - const titleClass = this.props.className ? `${baseClass} ${this.props.className}` : baseClass; - return <h2 className={titleClass} onClick={() => {this.props.onClick && this.props.onClick();}}> - {this.props.text} - {this.props.button && <button className="bd-button bd-button-title" onClick={this.buttonClick}>{this.props.button.title}</button>} - {this.props.otherChildren} - </h2>; - } } \ No newline at end of file