Revamp list ui + add enable/disable all

Fixes #723
This commit is contained in:
Zerebos 2024-02-22 00:06:30 -05:00
parent 1015a60041
commit d3639d12ae
12 changed files with 138 additions and 30 deletions

View File

@ -185,7 +185,11 @@
"isEnabled": "Enabled",
"wasLoaded": "{{name}} v{{version}} was loaded.",
"listView": "List View",
"gridView": "Grid View"
"gridView": "Grid View",
"enableAll": "Enable All",
"disableAll": "Disable All",
"results": "{{count}} Results",
"enableAllWarning": "Enabling all {{type}} can cause temporary lag and unexpected errors.\n\n(Hold shift while clicking to skip this prompt!)"
},
"CustomCSS": {
"confirmationText": "You have unsaved changes to your Custom CSS. Closing this window will lose all those changes.",

View File

@ -3,6 +3,7 @@ import path from "path";
import Builtin from "@structs/builtin";
import DataStore from "@modules/datastore";
import Strings from "@modules/strings";
import Modals from "@ui/modals";

View File

@ -1,6 +1,5 @@
import path from "path";
import fs from "fs";
import {shell} from "electron";
import Logger from "@common/logger";
@ -18,6 +17,8 @@ import FloatingWindows from "@ui/floatingwindows";
import Toasts from "@ui/toasts";
// const SWITCH_ANIMATION_TIME = 250;
const openItem = ipc.openPath;
const splitRegex = /[^\S\r\n]*?\r?(?:\r\n|\n)[^\S\r\n]*?\*[^\S\r\n]?/;
@ -271,8 +272,21 @@ export default class AddonManager {
if (!addon || addon.partial) return;
if (this.state[addon.id]) return;
this.state[addon.id] = true;
this.startAddon(addon);
this.saveState();
this.emit("enabled", addon);
// setTimeout(() => {
this.startAddon(addon);
this.saveState();
// }, SWITCH_ANIMATION_TIME);
}
enableAllAddons() {
const originalSetting = Settings.get("settings", "general", "showToasts", false);
Settings.set("settings", "general", "showToasts", false);
for (let a = 0; a < this.addonList.length; a++) {
this.enableAddon(this.addonList[a]);
}
Settings.set("settings", "general", "showToasts", originalSetting);
this.emit("batch");
}
disableAddon(idOrAddon) {
@ -280,8 +294,21 @@ export default class AddonManager {
if (!addon || addon.partial) return;
if (!this.state[addon.id]) return;
this.state[addon.id] = false;
this.stopAddon(addon);
this.saveState();
this.emit("disabled", addon);
// setTimeout(() => {
this.stopAddon(addon);
this.saveState();
// }, SWITCH_ANIMATION_TIME);
}
disableAllAddons() {
const originalSetting = Settings.get("settings", "general", "showToasts", false);
Settings.set("settings", "general", "showToasts", false);
for (let a = 0; a < this.addonList.length; a++) {
this.disableAddon(this.addonList[a]);
}
Settings.set("settings", "general", "showToasts", originalSetting);
this.emit("batch");
}
toggleAddon(id) {

View File

@ -57,6 +57,8 @@ export default new class PluginManager extends AddonManager {
saveAddon: this.saveAddon.bind(this),
editAddon: this.editAddon.bind(this),
deleteAddon: this.deleteAddon.bind(this),
enableAll: this.enableAllAddons.bind(this),
disableAll: this.disableAllAddons.bind(this),
prefix: this.prefix
})
});
@ -151,6 +153,7 @@ export default new class PluginManager extends AddonManager {
}
catch (err) {
this.state[addon.id] = false;
this.emit("disabled", addon);
Toasts.error(Strings.Addons.couldNotStart.format({name: addon.name, version: addon.version}));
Logger.stacktrace(this.name, `${addon.name} v${addon.version} could not be started.`, err);
return new AddonError(addon.name, addon.filename, Strings.Addons.enabled.format({method: "start()"}), {message: err.message, stack: err.stack}, this.prefix);

View File

@ -35,6 +35,8 @@ export default new class ThemeManager extends AddonManager {
saveAddon: this.saveAddon.bind(this),
editAddon: this.editAddon.bind(this),
deleteAddon: this.deleteAddon.bind(this),
enableAll: this.enableAllAddons.bind(this),
disableAll: this.disableAllAddons.bind(this),
prefix: this.prefix
})
});

View File

@ -212,7 +212,7 @@
flex-wrap: wrap;
}
.bd-addon-controls .bd-search {
.bd-settings-title .bd-search {
font-size: 13px;
margin: 0;
width: 200px;
@ -261,35 +261,43 @@
margin-left: 10px;
}
.bd-addon-views .bd-view-button {
.bd-addon-controls .bd-button {
background-color: transparent;
padding: 3px 4px;
}
.bd-addon-views .bd-view-button svg {
.bd-addon-controls .bd-button svg {
fill: var(--interactive-normal);
}
.bd-addon-views .bd-view-button.selected svg {
.bd-addon-controls .bd-button.selected svg {
fill: #FFFFFF;
}
.bd-addon-views .bd-view-button:hover {
.bd-addon-controls .bd-button:hover {
background-color: var(--background-modifier-selected);
}
.bd-addon-views .bd-view-button:active {
.bd-addon-controls .bd-button:active {
background-color: var(--background-modifier-accent);
}
.bd-addon-views .bd-view-button.selected {
.bd-addon-controls .bd-button.selected {
background-color: #3E82E5;
}
.bd-addon-views .bd-view-button + .bd-view-button {
.bd-addon-controls .bd-button + .bd-button {
margin-left: 5px;
}
.bd-controls-basic .bd-button:active svg {
fill: #FFFFFF;
}
.bd-controls-basic .bd-button:active {
background-color: #3E82E5;
}
.bd-addon-list .bd-footer .bd-links,
.bd-addon-list .bd-footer .bd-links a,
.bd-addon-list .bd-footer .bd-addon-button {

View File

@ -165,6 +165,8 @@
}
.bd-settings-title {
display: flex;
justify-content: space-between;
color: var(--header-primary, #FFFFFF);
font-weight: 600;
cursor: default;

View File

@ -0,0 +1,9 @@
import React from "@modules/react";
export default function FullScreen(props) {
const size = props.size || "20px";
return <svg className={props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}} onClick={props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
</svg>;
}

View File

@ -3,6 +3,7 @@ import Logger from "@common/logger";
import SimpleMarkdown from "@structs/markdown";
import React from "@modules/react";
import Events from "@modules/emitter";
import Strings from "@modules/strings";
import WebpackModules from "@modules/webpackmodules";
import DiscordModules from "@modules/discordmodules";
@ -25,7 +26,7 @@ import ExtIcon from "@ui/icons/extension";
import ErrorIcon from "@ui/icons/error";
import ThemeIcon from "@ui/icons/theme";
const {useState, useCallback, useMemo} = React;
const {useState, useCallback, useMemo, useEffect} = React;
const LinkIcons = {
@ -88,12 +89,27 @@ function buildLink(type, url) {
return makeButton(Strings.Addons[type], link);
}
export default function AddonCard({addon, type, disabled, enabled: initialValue, onChange: parentChange, hasSettings, editAddon, deleteAddon, getSettingsPanel}) {
export default function AddonCard({addon, prefix, type, disabled, enabled: initialValue, onChange: parentChange, hasSettings, editAddon, deleteAddon, getSettingsPanel}) {
const [isEnabled, setEnabled] = useState(initialValue);
useEffect(() => {
const onEnabled = updated => {
if (addon.id === updated.id) setEnabled(true);
};
const onDisabled = updated => {
if (addon.id === updated.id) setEnabled(false);
};
Events.on(`${prefix}-enabled`, onEnabled);
Events.on(`${prefix}-disabled`, onDisabled);
return () => {
Events.off(`${prefix}-enabled`, onEnabled);
Events.off(`${prefix}-disabled`, onDisabled);
};
}, [prefix, addon]);
const onChange = useCallback(() => {
setEnabled(!isEnabled);
if (parentChange) parentChange(addon.id);
}, [addon.id, parentChange, isEnabled]);
}, [addon.id, parentChange]);
const showSettings = useCallback(() => {
if (!hasSettings || !isEnabled) return;
@ -154,7 +170,7 @@ export default function AddonCard({addon, type, disabled, enabled: initialValue,
<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={isEnabled} onChange={onChange} />
<Switch internalState={false} disabled={disabled} checked={isEnabled} 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>}

View File

@ -15,6 +15,9 @@ import ErrorBoundary from "@ui/errorboundary";
import ListIcon from "@ui/icons/list";
import GridIcon from "@ui/icons/grid";
import FolderIcon from "@ui/icons/folder";
import CheckIcon from "@ui/icons/check";
import CloseIcon from "@ui/icons/close";
import NoResults from "@ui/blankslates/noresults";
import EmptyImage from "@ui/blankslates/emptyimage";
@ -48,6 +51,12 @@ function blankslate(type, onClick) {
</EmptyImage>;
}
function makeBasicButton(title, children, action) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => <button {...props} className="bd-button" onClick={action}>{children}</button>}
</DiscordModules.Tooltip>;
}
function makeControlButton(title, children, action, selected = false) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => {
@ -81,8 +90,28 @@ function confirmDelete(addon) {
});
}
/**
* @param {function} action
* @param {string} type
* @returns
*/
function confirmEnable(action, type) {
/**
* @param {MouseEvent} event
*/
return function(event) {
if (event.shiftKey) return action();
Modals.showConfirmationModal(Strings.Modals.confirmAction, Strings.Addons.enableAllWarning.format({type: type.toLocaleLowerCase()}), {
confirmText: Strings.Modals.okay,
cancelText: Strings.Modals.cancel,
danger: true,
onConfirm: action,
});
};
}
export default function AddonList({prefix, type, title, folder, addonList, addonState, onChange, reload, editAddon, deleteAddon}) {
export default function AddonList({prefix, type, title, folder, addonList, addonState, onChange, reload, editAddon, deleteAddon, enableAll, disableAll}) {
const [query, setQuery] = useState("");
const [sort, setSort] = useState(getState.bind(null, type, "sort", "name"));
const [ascending, setAscending] = useState(getState.bind(null, type, "ascending", true));
@ -125,7 +154,6 @@ export default function AddonList({prefix, type, title, folder, addonList, addon
if (deleteAddon) deleteAddon(addon);
}, [addonList, deleteAddon]);
const button = folder ? {title: Strings.Addons.openFolder.format({type: title}), onClick: openFolder.bind(null, folder)} : null;
const renderedCards = useMemo(() => {
let sorted = addonList.sort((a, b) => {
const sortByEnabled = sort === "isEnabled";
@ -154,18 +182,25 @@ export default function AddonList({prefix, type, title, folder, addonList, 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={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>;
return <ErrorBoundary><AddonCard disabled={addon.partial} type={type} prefix={prefix} 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, onChange, reload, triggerDelete, triggerEdit, type, sort, ascending, query, forced]); // eslint-disable-line react-hooks/exhaustive-deps
}, [addonList, addonState, onChange, reload, triggerDelete, triggerEdit, type, prefix, sort, ascending, query, forced]); // eslint-disable-line react-hooks/exhaustive-deps
const hasAddonsInstalled = addonList.length !== 0;
const isSearching = !!query;
const hasResults = renderedCards.length !== 0;
return [
<SettingsTitle key="title" text={title} button={button} />,
<SettingsTitle key="title" text={isSearching ? `${title} - ${Strings.Addons.results.format({count: `${renderedCards.length}`})}` : title}>
<Search onChange={search} placeholder={`${Strings.Addons.search.format({type: `${renderedCards.length} ${title}`})}...`} />
</SettingsTitle>,
<div className={"bd-controls bd-addon-controls"}>
<Search onChange={search} placeholder={`${Strings.Addons.search.format({type: title})}...`} />
{/* <Search onChange={search} placeholder={`${Strings.Addons.search.format({type: title})}...`} /> */}
<div className="bd-controls-basic">
{makeBasicButton(Strings.Addons.openFolder.format({type: title}), <FolderIcon />, openFolder.bind(null, folder))}
{makeBasicButton(Strings.Addons.enableAll, <CheckIcon size="20px" />, confirmEnable(enableAll, title))}
{makeBasicButton(Strings.Addons.disableAll, <CloseIcon size="20px" />, disableAll)}
</div>
<div className="bd-controls-advanced">
<div className="bd-addon-dropdowns">
<div className="bd-select-wrapper">

View File

@ -3,17 +3,18 @@ import React from "@modules/react";
const {useState, useCallback} = React;
export default function Switch({id, checked: initialValue, disabled, onChange}) {
export default function Switch({id, checked: initialValue, disabled, onChange, internalState = true}) {
const [checked, setChecked] = useState(initialValue);
const change = useCallback(() => {
onChange?.(!checked);
setChecked(!checked);
}, [checked, onChange]);
const isChecked = internalState ? checked : initialValue;
const enabledClass = disabled ? " bd-switch-disabled" : "";
const checkedClass = checked ? " bd-switch-checked" : "";
const checkedClass = isChecked ? " bd-switch-checked" : "";
return <div className={`bd-switch` + enabledClass + checkedClass}>
<input id={id} type="checkbox" disabled={disabled} checked={checked} onChange={change} />
<input id={id} type="checkbox" disabled={disabled} checked={isChecked} onChange={change} />
<div className="bd-switch-body">
<svg className="bd-switch-slider" viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet">
<rect className="bd-switch-handle" fill="white" x="4" y="0" height="20" width="20" rx="10"></rect>

View File

@ -6,7 +6,7 @@ const {useCallback} = React;
const basicClass = "bd-settings-title";
const groupClass = "bd-settings-title bd-settings-group-title";
export default function SettingsTitle({isGroup, className, button, onClick, text, otherChildren}) {
export default function SettingsTitle({isGroup, className, button, onClick, text, children}) {
const click = useCallback((event) => {
event.stopPropagation();
event.preventDefault();
@ -19,7 +19,7 @@ export default function SettingsTitle({isGroup, className, button, onClick, text
return <h2 className={titleClass} onClick={() => {onClick?.();}}>
{text}
{button && <button className="bd-button bd-button-title" onClick={click}>{button.title}</button>}
{otherChildren}
{children}
</h2>;
}