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 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
|
||||
});
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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>;
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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>;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue