From d3639d12ae8097ee094e2c6760fc5c177d2f9c68 Mon Sep 17 00:00:00 2001 From: Zerebos Date: Thu, 22 Feb 2024 00:06:30 -0500 Subject: [PATCH] Revamp list ui + add enable/disable all Fixes #723 --- assets/locales/en-us.json | 6 ++- renderer/src/builtins/developer/debuglogs.js | 1 + renderer/src/modules/addonmanager.js | 37 +++++++++++++-- renderer/src/modules/pluginmanager.js | 3 ++ renderer/src/modules/thememanager.js | 2 + renderer/src/styles/ui/addonlist.css | 24 ++++++---- renderer/src/styles/ui/bdsettings.css | 2 + renderer/src/ui/icons/folder.jsx | 9 ++++ renderer/src/ui/settings/addoncard.jsx | 26 ++++++++-- renderer/src/ui/settings/addonlist.jsx | 47 ++++++++++++++++--- .../src/ui/settings/components/switch.jsx | 7 +-- renderer/src/ui/settings/title.jsx | 4 +- 12 files changed, 138 insertions(+), 30 deletions(-) create mode 100644 renderer/src/ui/icons/folder.jsx diff --git a/assets/locales/en-us.json b/assets/locales/en-us.json index deacfe15..fb2d4b13 100644 --- a/assets/locales/en-us.json +++ b/assets/locales/en-us.json @@ -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.", diff --git a/renderer/src/builtins/developer/debuglogs.js b/renderer/src/builtins/developer/debuglogs.js index 8a7c68f7..cd1b8f3b 100644 --- a/renderer/src/builtins/developer/debuglogs.js +++ b/renderer/src/builtins/developer/debuglogs.js @@ -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"; diff --git a/renderer/src/modules/addonmanager.js b/renderer/src/modules/addonmanager.js index a88a5262..fec5711b 100644 --- a/renderer/src/modules/addonmanager.js +++ b/renderer/src/modules/addonmanager.js @@ -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) { diff --git a/renderer/src/modules/pluginmanager.js b/renderer/src/modules/pluginmanager.js index 28c2a77f..07e37968 100644 --- a/renderer/src/modules/pluginmanager.js +++ b/renderer/src/modules/pluginmanager.js @@ -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); diff --git a/renderer/src/modules/thememanager.js b/renderer/src/modules/thememanager.js index 008ed669..c6237472 100644 --- a/renderer/src/modules/thememanager.js +++ b/renderer/src/modules/thememanager.js @@ -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 }) }); diff --git a/renderer/src/styles/ui/addonlist.css b/renderer/src/styles/ui/addonlist.css index 2ad67c29..fa74266d 100644 --- a/renderer/src/styles/ui/addonlist.css +++ b/renderer/src/styles/ui/addonlist.css @@ -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 { diff --git a/renderer/src/styles/ui/bdsettings.css b/renderer/src/styles/ui/bdsettings.css index abc9c357..74ffce71 100644 --- a/renderer/src/styles/ui/bdsettings.css +++ b/renderer/src/styles/ui/bdsettings.css @@ -165,6 +165,8 @@ } .bd-settings-title { + display: flex; + justify-content: space-between; color: var(--header-primary, #FFFFFF); font-weight: 600; cursor: default; diff --git a/renderer/src/ui/icons/folder.jsx b/renderer/src/ui/icons/folder.jsx new file mode 100644 index 00000000..32d56a8d --- /dev/null +++ b/renderer/src/ui/icons/folder.jsx @@ -0,0 +1,9 @@ +import React from "@modules/react"; + +export default function FullScreen(props) { + const size = props.size || "20px"; + return + + + ; +} diff --git a/renderer/src/ui/settings/addoncard.jsx b/renderer/src/ui/settings/addoncard.jsx index ba6d7436..6cdd7a15 100644 --- a/renderer/src/ui/settings/addoncard.jsx +++ b/renderer/src/ui/settings/addoncard.jsx @@ -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,
{type === "plugin" ? : }
{title}
- +
{disabled &&
{`An error was encountered while trying to load this ${type}.`}
} diff --git a/renderer/src/ui/settings/addonlist.jsx b/renderer/src/ui/settings/addonlist.jsx index 3b28d427..505c64b7 100644 --- a/renderer/src/ui/settings/addonlist.jsx +++ b/renderer/src/ui/settings/addonlist.jsx @@ -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) { ; } +function makeBasicButton(title, children, action) { + return + {(props) => } + ; +} + function makeControlButton(title, children, action, selected = false) { return {(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 triggerEdit(addon.id)} deleteAddon={() => triggerDelete(addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} />; + return triggerEdit(addon.id)} deleteAddon={() => triggerDelete(addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} />; }); - }, [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 [ - , + + + ,
- + {/* */} +
+ {makeBasicButton(Strings.Addons.openFolder.format({type: title}), , openFolder.bind(null, folder))} + {makeBasicButton(Strings.Addons.enableAll, , confirmEnable(enableAll, title))} + {makeBasicButton(Strings.Addons.disableAll, , disableAll)} +
diff --git a/renderer/src/ui/settings/components/switch.jsx b/renderer/src/ui/settings/components/switch.jsx index d72099a0..398a6ae9 100644 --- a/renderer/src/ui/settings/components/switch.jsx +++ b/renderer/src/ui/settings/components/switch.jsx @@ -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
- +
diff --git a/renderer/src/ui/settings/title.jsx b/renderer/src/ui/settings/title.jsx index e42dba86..50f46186 100644 --- a/renderer/src/ui/settings/title.jsx +++ b/renderer/src/ui/settings/title.jsx @@ -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

{onClick?.();}}> {text} {button && } - {otherChildren} + {children}

; } \ No newline at end of file