Convert settings components
This commit is contained in:
parent
99aa7ac1a8
commit
717c9026f4
|
@ -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
|
|
||||||
});
|
|
||||||
|
|
|
@ -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
|
|
||||||
});
|
|
||||||
|
|
|
@ -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>;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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>;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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>;
|
|
||||||
}
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue