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 Toasts from "../toasts";
const {useState, useCallback, useMemo} = React;
const LinkIcons = {
website: WebIcon,
source: GitHubIcon,
@ -27,168 +30,128 @@ const LinkIcons = {
const LayerManager = {
pushLayer(component) {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_PUSH",
component
});
DiscordModules.Dispatcher.dispatch({
type: "LAYER_PUSH",
component
});
},
popLayer() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP"
});
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP"
});
},
popAllLayers() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP_ALL"
});
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP_ALL"
});
}
};
};
const UserStore = WebpackModules.getByProps("getCurrentUser");
const ChannelStore = WebpackModules.getByProps("getDMFromUserId");
const PrivateChannelActions = WebpackModules.getByProps("openPrivateChannel");
const ChannelActions = WebpackModules.getByProps("selectPrivateChannel");
const getString = value => typeof value == "string" ? value : value.toString();
export default class AddonCard extends React.Component {
function makeButton(title, children, action, {isControl = false, danger = false, disabled = false} = {}) {
const ButtonType = isControl ? "button" : "div";
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => {
return <ButtonType {...props} className={(isControl ? "bd-button bd-addon-button" : "bd-addon-button") + (danger ? " bd-button-danger" : "") + (disabled ? " bd-button-disabled" : "")} onClick={action}>{children}</ButtonType>;
}}
</DiscordModules.Tooltip>;
}
constructor(props) {
super(props);
this.settingsPanel = "";
this.panelRef = React.createRef();
this.onChange = this.onChange.bind(this);
this.showSettings = this.showSettings.bind(this);
this.messageAuthor = this.messageAuthor.bind(this);
function buildLink(type, url) {
if (!url) return null;
const icon = React.createElement(LinkIcons[type]);
const link = <a className="bd-link bd-link-website" href={url} target="_blank" rel="noopener noreferrer">{icon}</a>;
if (type == "invite") {
link.props.onClick = function(event) {
event.preventDefault();
event.stopPropagation();
let code = url;
const tester = /\.gg\/(.*)$/;
if (tester.test(code)) code = code.match(tester)[1];
LayerManager.popLayer();
DiscordModules.InviteActions?.acceptInviteAndTransitionToInviteChannel({inviteKey: code});
};
}
return makeButton(Strings.Addons[type], link);
}
showSettings() {
if (!this.props.hasSettings || !this.props.enabled) return;
const name = this.getString(this.props.addon.name);
export default function AddonCard({addon, type, disabled, enabled, 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
? <a className="bd-link bd-link-website" href={author.link || null} onClick={this.messageAuthor} target="_blank" rel="noopener noreferrer">{author.name}</a>
: <span className="bd-author">{author.name}</span>;
const authorComponent = addon.authorLink || addon.authorId
? <a className="bd-link bd-link-website" href={addon.authorLink || null} onClick={messageAuthor} target="_blank" rel="noopener noreferrer">{getString(addon.author)}</a>
: <span className="bd-author">{getString(addon.author)}</span>;
const authorIndex = authorArray.findIndex(s => s == "{{author}}");
if (authorIndex) authorArray[authorIndex] = authorComponent;
return [
React.createElement("div", {className: "bd-name"}, name),
React.createElement("div", {className: "bd-meta"},
React.createElement("span", {className: "bd-version"}, `v${version}`),
...authorArray
)
<div className="bd-name">{getString(addon.name)}</div>,
<div className="bd-meta">
<span className="bd-version">v{getString(addon.version)}</span>
{authorArray}
</div>
];
}
}, []);
buildLink(which) {
const url = this.props.addon[which];
if (!url) return null;
const icon = React.createElement(LinkIcons[which]);
const link = <a className="bd-link bd-link-website" href={url} target="_blank" rel="noopener noreferrer">{icon}</a>;
if (which == "invite") {
link.props.onClick = function(event) {
event.preventDefault();
event.stopPropagation();
let code = url;
const tester = /\.gg\/(.*)$/;
if (tester.test(code)) code = code.match(tester)[1];
LayerManager.popLayer();
DiscordModules.InviteActions.acceptInviteAndTransitionToInviteChannel({inviteKey: code});
};
}
return this.makeButton(Strings.Addons[which], link);
}
get controls() { // {this.props.hasSettings && <button onClick={this.showSettings} className="bd-button bd-button-addon-settings" disabled={!this.props.enabled}>{Strings.Addons.addonSettings}</button>}
return <div className="bd-controls">
{this.props.hasSettings && this.makeControlButton(Strings.Addons.addonSettings, <CogIcon size={"20px"} />, this.showSettings, {disabled: !this.props.enabled})}
{this.props.editAddon && this.makeControlButton(Strings.Addons.editAddon, <EditIcon size={"20px"} />, this.props.editAddon)}
{this.props.deleteAddon && this.makeControlButton(Strings.Addons.deleteAddon, <DeleteIcon size={"20px"} />, this.props.deleteAddon, {danger: true})}
</div>;
}
get footer() {
const links = ["website", "source", "invite", "donate", "patreon"];
const linkComponents = links.map(this.buildLink.bind(this)).filter(c => c);// linkComponents.map((comp, i) => i < linkComponents.length - 1 ? [comp, " | "] : comp).flat()
const footer = useMemo(() => {
const links = Object.keys(LinkIcons);
const linkComponents = links.map(l => buildLink(l, addon[l])).filter(c => c);
return <div className="bd-footer">
<span className="bd-links">{linkComponents}</span>
{this.controls}
</div>;
}
makeButton(title, children, action) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => {
return <div {...props} className="bd-addon-button" onClick={action}>{children}</div>;
}}
</DiscordModules.Tooltip>;
}
makeControlButton(title, children, action, {danger = false, disabled = false} = {}) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => {
return <button {...props} className={"bd-button bd-addon-button" + (danger ? " bd-button-danger" : "") + (disabled ? " bd-button-disabled" : "")} onClick={action}>{children}</button>;
}}
</DiscordModules.Tooltip>;
}
render() {
const addon = this.props.addon;
const name = this.getString(addon.name);
const author = this.getString(addon.author);
const description = this.getString(addon.description);
const version = this.getString(addon.version);
return <div id={`${addon.id}-card`} className={"bd-addon-card" + (this.props.disabled ? " bd-addon-card-disabled" : "")}>
<div className="bd-addon-header">
{this.props.type === "plugin" ? <ExtIcon size="18px" className="bd-icon" /> : <ThemeIcon size="18px" className="bd-icon" />}
<div className="bd-title">{this.buildTitle(name, version, {name: author, id: this.props.addon.authorId, link: this.props.addon.authorLink})}</div>
<Switch disabled={this.props.disabled} checked={this.props.enabled} onChange={this.onChange} />
<div className="bd-controls">
{hasSettings && makeButton(Strings.Addons.addonSettings, <CogIcon size={"20px"} />, showSettings, {isControl: true, disabled: !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})}
</div>
<div className="bd-description-wrap">
{this.props.disabled && <div className="banner banner-danger"><ErrorIcon className="bd-icon" />{`An error was encountered while trying to load this ${this.props.type}.`}</div>}
<div className="bd-description">{SimpleMarkdown.parseToReact(description)}</div>
</div>
{this.footer}
</div>;
}
}, [hasSettings, editAddon, deleteAddon]);
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 EmptyImage from "../blankslates/emptyimage";
export default class AddonList extends React.Component {
const {useState, useCallback, useEffect, useReducer, useMemo} = React;
constructor(props) {
super(props);
this.state = {query: "", sort: this.getControlState("sort", "name"), ascending: this.getControlState("ascending", true), view: this.getControlState("view", "list")};
this.sort = this.sort.bind(this);
this.reverse = this.reverse.bind(this);
this.search = this.search.bind(this);
this.update = this.update.bind(this);
this.listView = this.listView.bind(this);
this.gridView = this.gridView.bind(this);
this.openFolder = this.openFolder.bind(this);
}
const SORT_OPTIONS = [
{label: Strings.Addons.name, value: "name"},
{label: Strings.Addons.author, value: "author"},
{label: Strings.Addons.version, value: "version"},
{label: Strings.Addons.added, value: "added"},
{label: Strings.Addons.modified, value: "modified"},
{label: Strings.Addons.isEnabled, value: "isEnabled"}
];
componentDidMount() {
Events.on(`${this.props.prefix}-loaded`, this.update);
Events.on(`${this.props.prefix}-unloaded`, this.update);
}
const DIRECTIONS = [
{label: Strings.Sorting.ascending, value: true},
{label: Strings.Sorting.descending, value: false}
];
componentWillUnmount() {
Events.off(`${this.props.prefix}-loaded`, this.update);
Events.off(`${this.props.prefix}-unloaded`, this.update);
}
onControlChange(control, value) {
const addonlistControls = DataStore.getBDData("addonlistControls") || {};
if (!addonlistControls[this.props.type]) addonlistControls[this.props.type] = {};
addonlistControls[this.props.type][control] = value;
DataStore.setBDData("addonlistControls", addonlistControls);
}
function openFolder(folder) {
const shell = require("electron").shell;
const open = shell.openItem || shell.openPath;
open(folder);
}
getControlState(control, defaultValue) {
const addonlistControls = DataStore.getBDData("addonlistControls") || {};
if (!addonlistControls[this.props.type]) return defaultValue;
if (!addonlistControls[this.props.type].hasOwnProperty(control)) return defaultValue;
return addonlistControls[this.props.type][control];
}
function blankslate(type, onClick) {
const message = Strings.Addons.blankSlateMessage.format({link: `https://betterdiscord.app/${type}s`, type}).toString();
return <EmptyImage title={Strings.Addons.blankSlateHeader.format({type})} message={message}>
<button className="bd-button" onClick={onClick}>{Strings.Addons.openFolder.format({type})}</button>
</EmptyImage>;
}
update() {
this.forceUpdate();
}
function makeControlButton(title, children, action, selected = false) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => {
return <button {...props} className={"bd-button bd-view-button" + (selected ? " selected" : "")} onClick={action}>{children}</button>;
}}
</DiscordModules.Tooltip>;
}
reload() {
if (this.props.refreshList) this.props.refreshList();
this.forceUpdate();
}
function getState(type, control, defaultValue) {
const addonlistControls = DataStore.getBDData("addonlistControls") || {};
if (!addonlistControls[type]) return defaultValue;
if (!addonlistControls[type].hasOwnProperty(control)) return defaultValue;
return addonlistControls[type][control];
}
listView() {this.changeView("list");}
gridView() {this.changeView("grid");}
changeView(view) {
this.onControlChange("view", view);
this.setState({view});
}
function saveState(type, control, value) {
const addonlistControls = DataStore.getBDData("addonlistControls") || {};
if (!addonlistControls[type]) addonlistControls[type] = {};
addonlistControls[type][control] = value;
DataStore.setBDData("addonlistControls", addonlistControls);
}
reverse(value) {
this.onControlChange("ascending", value);
this.setState({ascending: value});
}
function confirmDelete(addon) {
return new Promise(resolve => {
Modals.showConfirmationModal(Strings.Modals.confirmAction, Strings.Addons.confirmDelete.format({name: addon.name}), {
danger: true,
confirmText: Strings.Addons.deleteAddon,
onConfirm: () => {resolve(true);},
onCancel: () => {resolve(false);}
});
});
}
sort(value) {
this.onControlChange("sort", value);
this.setState({sort: value});
}
search(event) {
this.setState({query: event.target.value.toLocaleLowerCase()});
}
export default function AddonList({prefix, type, title, folder, addonList, addonState, onChange, reload, editAddon, deleteAddon}) {
const [query, setQuery] = useState("");
const [sort, setSort] = useState(getState.bind(null, type, "sort", "name"));
const [ascending, setAscending] = useState(getState.bind(null, type, "ascending", "true"));
const [view, setView] = useState(getState.bind(null, type, "view", "list"));
const [, 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 <EmptyImage title={Strings.Addons.blankSlateHeader.format({type: this.props.type})} message={message}>
<button className="bd-button" onClick={this.openFolder}>{Strings.Addons.openFolder.format({type: this.props.type})}</button>
</EmptyImage>;
}
const changeDirection = useCallback((value) => {
saveState(type, "ascending", value);
setAscending(value);
}, []);
makeControlButton(title, children, action, selected = false) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => {
return <button {...props} className={"bd-button bd-view-button" + (selected ? " selected" : "")} onClick={action}>{children}</button>;
}}
</DiscordModules.Tooltip>;
}
const changeSort = useCallback((value) => {
saveState(type, "sort", value);
setSort(value);
}, []);
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 <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 isSearching = !!this.state.query;
const hasResults = sortedAddons.length !== 0;
const hasAddonsInstalled = addonList.length !== 0;
const isSearching = !!query;
const hasResults = renderedCards.length !== 0;
return [
<SettingsTitle key="title" text={title} button={button} />,
<div className={"bd-controls bd-addon-controls"}>
<Search onChange={this.search} placeholder={`${Strings.Addons.search.format({type: this.props.title})}...`} />
<div className="bd-controls-advanced">
<div className="bd-addon-dropdowns">
<div className="bd-select-wrapper">
<label className="bd-label">{Strings.Sorting.sortBy}:</label>
<Dropdown options={this.sortOptions} value={this.state.sort} onChange={this.sort} style="transparent" />
</div>
<div className="bd-select-wrapper">
<label className="bd-label">{Strings.Sorting.order}:</label>
<Dropdown options={this.directions} value={this.state.ascending} onChange={this.reverse} style="transparent" />
</div>
return [
<SettingsTitle key="title" text={title} button={button} />,
<div className={"bd-controls bd-addon-controls"}>
<Search onChange={search} placeholder={`${Strings.Addons.search.format({type: title})}...`} />
<div className="bd-controls-advanced">
<div className="bd-addon-dropdowns">
<div className="bd-select-wrapper">
<label className="bd-label">{Strings.Sorting.sortBy}:</label>
<Dropdown options={SORT_OPTIONS} value={sort} onChange={changeSort} style="transparent" />
</div>
<div className="bd-addon-views">
{this.makeControlButton("List View", <ListIcon />, this.listView, this.state.view === "list")}
{this.makeControlButton("Grid View", <GridIcon />, this.gridView, this.state.view === "grid")}
<div className="bd-select-wrapper">
<label className="bd-label">{Strings.Sorting.order}:</label>
<Dropdown options={DIRECTIONS} value={ascending} onChange={changeDirection} style="transparent" />
</div>
</div>
</div>,
!hasAddonsInstalled && this.emptyImage,
isSearching && !hasResults && hasAddonsInstalled && <NoResults />,
hasAddonsInstalled && <div key="addonList" className={"bd-addon-list" + (this.state.view == "grid" ? " bd-grid-view" : "")}>{renderedCards}</div>
];
}
editAddon(id) {
if (this.props.editAddon) this.props.editAddon(id);
}
async deleteAddon(id) {
const addon = this.props.addonList.find(a => a.id == id);
const shouldDelete = await this.confirmDelete(addon);
if (!shouldDelete) return;
if (this.props.deleteAddon) this.props.deleteAddon(addon);
}
confirmDelete(addon) {
return new Promise(resolve => {
Modals.showConfirmationModal(Strings.Modals.confirmAction, Strings.Addons.confirmDelete.format({name: addon.name}), {
danger: true,
confirmText: Strings.Addons.deleteAddon,
onConfirm: () => {resolve(true);},
onCancel: () => {resolve(false);}
});
});
}
<div className="bd-addon-views">
{makeControlButton("List View", <ListIcon />, listView, view === "list")}
{makeControlButton("Grid View", <GridIcon />, gridView, view === "grid")}
</div>
</div>
</div>,
!hasAddonsInstalled && blankslate(type, () => openFolder(folder)),
isSearching && !hasResults && hasAddonsInstalled && <NoResults />,
hasAddonsInstalled && <div key="addonList" className={"bd-addon-list" + (view == "grid" ? " bd-grid-view" : "")}>{renderedCards}</div>
];
}
const originalRender = AddonList.prototype.render;
Object.defineProperty(AddonList.prototype, "render", {
enumerable: false,
configurable: false,
set: function() {Logger.warn("AddonList", "Addon policy for plugins #5 https://github.com/BetterDiscord/BetterDiscord/wiki/Addon-Policies#plugins");},
get: () => originalRender
});

View File

@ -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={groupClass}>
<Title text={this.props.name} collapsible={this.props.collapsible} onClick={this.toggleCollapse} button={this.props.button} isGroup={true} />
<div className="bd-settings-container" ref={this.container}>
{this.props.children}
</div>
{this.props.showDivider && <Divider />}
</div>;
}
return <div className={groupClass}>
<Title text={name} collapsible={collapsible} onClick={toggleCollapse} button={button ? {...button, onClick} : null} isGroup={true} />
<div className="bd-settings-container" ref={container}>
{children}
</div>
{showDivider && <Divider />}
</div>;
}

View File

@ -10,38 +10,29 @@ import Radio from "./components/radio";
import Keybind from "./components/keybind";
import Color from "./components/color";
const {useCallback} = React;
export default class Group extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
}
export default function Group({onChange, id, name, button, shown, onDrawerToggle, showDivider, collapsible, settings}) {
const change = useCallback((settingId, value) => {
if (id) onChange?.(id, settingId, value);
else onChange?.(settingId, value);
}, [id]);
onChange(id, value) {
if (!this.props.onChange) return;
if (this.props.id) this.props.onChange(this.props.id, id, value);
else this.props.onChange(id, value);
this.forceUpdate();
}
render() {
const {settings} = this.props;
return <Drawer collapsible={this.props.collapsible} name={this.props.name} button={this.props.button} shown={this.props.shown} onDrawerToggle={this.props.onDrawerToggle} showDivider={this.props.showDivider}>
{settings.filter(s => !s.hidden).map((setting) => {
let component = null;
if (setting.type == "dropdown") component = <Dropdown disabled={setting.disabled} id={setting.id} options={setting.options} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "number") component = <Number disabled={setting.disabled} id={setting.id} min={setting.min} max={setting.max} step={setting.step} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "switch") component = <Switch disabled={setting.disabled} id={setting.id} checked={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "text") component = <Textbox disabled={setting.disabled} id={setting.id} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "slider") component = <Slider disabled={setting.disabled} id={setting.id} min={setting.min} max={setting.max} step={setting.step} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "radio") component = <Radio disabled={setting.disabled} id={setting.id} name={setting.id} options={setting.options} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "keybind") component = <Keybind disabled={setting.disabled} id={setting.id} value={setting.value} max={setting.max} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "color") component = <Color disabled={setting.disabled} id={setting.id} value={setting.value} defaultValue={setting.defaultValue} colors={setting.colors} onChange={this.onChange.bind(this, setting.id)} />;
if (!component) return null;
return <Item id={setting.id} inline={setting.type !== "radio"} key={setting.id} name={setting.name} note={setting.note}>{component}</Item>;
})}
</Drawer>;
}
return <Drawer collapsible={collapsible} name={name} button={button} shown={shown} onDrawerToggle={onDrawerToggle} showDivider={showDivider}>
{settings.filter(s => !s.hidden).map((setting) => {
let component = null;
const callback = value => change(setting.id, value);
if (setting.type == "dropdown") component = <Dropdown disabled={setting.disabled} id={setting.id} options={setting.options} value={setting.value} onChange={callback} />;
if (setting.type == "number") component = <Number disabled={setting.disabled} id={setting.id} min={setting.min} max={setting.max} step={setting.step} value={setting.value} onChange={callback} />;
if (setting.type == "switch") component = <Switch disabled={setting.disabled} id={setting.id} checked={setting.value} onChange={callback} />;
if (setting.type == "text") component = <Textbox disabled={setting.disabled} id={setting.id} value={setting.value} onChange={callback} />;
if (setting.type == "slider") component = <Slider disabled={setting.disabled} id={setting.id} min={setting.min} max={setting.max} step={setting.step} value={setting.value} onChange={callback} />;
if (setting.type == "radio") component = <Radio disabled={setting.disabled} id={setting.id} name={setting.id} options={setting.options} value={setting.value} onChange={callback} />;
if (setting.type == "keybind") component = <Keybind disabled={setting.disabled} id={setting.id} value={setting.value} max={setting.max} onChange={callback} />;
if (setting.type == "color") component = <Color disabled={setting.disabled} id={setting.id} value={setting.value} defaultValue={setting.defaultValue} colors={setting.colors} onChange={callback} />;
if (!component) return null;
return <Item id={setting.id} inline={setting.type !== "radio"} key={setting.id} name={setting.name} note={setting.note}>{component}</Item>;
})}
</Drawer>;
}

View File

@ -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>;
}
}