Convert settings components

This commit is contained in:
Zack Rauen 2023-03-08 20:08:48 -05:00
parent 99aa7ac1a8
commit 717c9026f4
5 changed files with 292 additions and 379 deletions

View File

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

View File

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

View File

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

View File

@ -10,38 +10,29 @@ import Radio from "./components/radio";
import Keybind from "./components/keybind"; import Keybind from "./components/keybind";
import Color from "./components/color"; 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) { return <Drawer collapsible={collapsible} name={name} button={button} shown={shown} onDrawerToggle={onDrawerToggle} showDivider={showDivider}>
if (!this.props.onChange) return; {settings.filter(s => !s.hidden).map((setting) => {
if (this.props.id) this.props.onChange(this.props.id, id, value); let component = null;
else this.props.onChange(id, value); const callback = value => change(setting.id, value);
this.forceUpdate(); 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} />;
render() { if (setting.type == "text") component = <Textbox disabled={setting.disabled} id={setting.id} value={setting.value} onChange={callback} />;
const {settings} = this.props; 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} />;
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}> if (setting.type == "keybind") component = <Keybind disabled={setting.disabled} id={setting.id} value={setting.value} max={setting.max} onChange={callback} />;
{settings.filter(s => !s.hidden).map((setting) => { if (setting.type == "color") component = <Color disabled={setting.disabled} id={setting.id} value={setting.value} defaultValue={setting.defaultValue} colors={setting.colors} onChange={callback} />;
let component = null; if (!component) return 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)} />; return <Item id={setting.id} inline={setting.type !== "radio"} key={setting.id} name={setting.name} note={setting.note}>{component}</Item>;
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)} />; </Drawer>;
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>;
}
} }

View File

@ -1,27 +1,25 @@
import {React} from "modules"; import {React} from "modules";
const className = "bd-settings-title"; const {useCallback} = React;
const className2 = "bd-settings-title bd-settings-group-title";
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.stopPropagation();
event.preventDefault(); 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>;
}
} }