fix(PluginsTab): performance issues (#556)

* fix(PluginsTab): performance issues

* fix(PluginModal): setting description color
This commit is contained in:
prism 2025-12-12 18:46:16 +01:00 • committed by GitHub
parent fc53a87533
commit 525d06d488
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 78 additions and 66 deletions

View file

@ -25,8 +25,6 @@ import { Switch } from "@components/Switch";
import { Tooltip, useRef } from "@webpack/common";
import type { MouseEventHandler, ReactNode } from "react";
import { useTruncatedText } from "./tabs/plugins/components/truncateText";
const cl = classNameFactory("vc-addon-");
interface Props {
@ -49,7 +47,6 @@ interface Props {
export function AddonCard({ disabled, isNew, sourceBadge, tooltip, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) {
const titleRef = useRef<HTMLDivElement>(null);
const titleContainerRef = useRef<HTMLDivElement>(null);
const { truncated, containerRef } = useTruncatedText(description ? description.toString() : "");
return (
<div
@ -107,12 +104,11 @@ export function AddonCard({ disabled, isNew, sourceBadge, tooltip, name, infoBut
</div>
<div
ref={containerRef}
className={cl("note")}
style={{ lineHeight: "1.25em", fontSize: "small" }}
title={description ? description.toString() : ""}
>
{truncated}
{description}
</div>
{footer && <div className={cl("footer")}>{footer}</div>}

View file

@ -147,7 +147,7 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
onClick={() => openPluginModal(plugin, onRestartNeeded)}
className={cl("info-button")}
>
{plugin.options && !isObjectEmpty(plugin.options)
{plugin.settings?.def && !isObjectEmpty(plugin.settings.def)
? <CogWheel className={cl("info-icon")} />
: <InfoIcon className={cl("info-icon")} />
}

View file

@ -27,9 +27,9 @@
}
.vc-plugins-setting-description {
color: var(--text-default);
color: var(--text-subtle);
}
.vc-plugins-setting-error {
color: var(--text-feedback-critical, var(--text-danger, #FF5C5C));
}
}

View file

@ -22,6 +22,7 @@ import * as DataStore from "@api/DataStore";
import { isPluginEnabled, stopPlugin } from "@api/PluginManager";
import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Button } from "@components/Button";
import { Card } from "@components/Card";
import { Divider } from "@components/Divider";
import ErrorBoundary from "@components/ErrorBoundary";
@ -35,7 +36,7 @@ import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { useAwaiter, useIntersection } from "@utils/react";
import { Alerts, Button, lodash, Parser, React, Select, TextInput, Toasts, Tooltip, useMemo, useState } from "@webpack/common";
import { Alerts, lodash, Parser, React, Select, TextInput, Toasts, Tooltip, useCallback, useMemo, useState } from "@webpack/common";
import { JSX } from "react";
import Plugins, { ExcludedPlugins, PluginMeta } from "~plugins";
@ -68,7 +69,7 @@ function ReloadRequiredCard({ required, enabledPlugins, openWarningModal, resetC
<Paragraph className={cl("dep-text")}>
Restart now to apply new plugins and their settings
</Paragraph>
<Button className={cl("restart-button")} onClick={() => location.reload()}>
<Button variant="primary" className={cl("restart-button")} onClick={() => location.reload()}>
Restart
</Button>
</>
@ -81,7 +82,8 @@ function ReloadRequiredCard({ required, enabledPlugins, openWarningModal, resetC
)}
{enabledPlugins.length > 0 && !required && (
<Button
size={Button.Sizes.SMALL}
variant="secondary"
size="small"
className={"vc-plugins-disable-warning vc-modal-align-reset"}
onClick={() => {
return openWarningModal(null, null, null, false, enabledPlugins.length, resetCheckAndDo);
@ -192,16 +194,23 @@ export default function PluginSettings() {
const hasUserPlugins = useMemo(() => !IS_STANDALONE && Object.values(PluginMeta).some(m => m.userPlugin), []);
const [searchValue, setSearchValue] = useState({ value: "", status: SearchStatus.ALL });
const [searchInput, setSearchInput] = useState("");
const debouncedSetSearch = useMemo(
() => debounce((query: string) => setSearchValue(prev => ({ ...prev, value: query })), 150),
[]
);
const search = searchValue.value.toLowerCase();
const onSearch = (query: string) => {
setSearchValue(prev => ({ ...prev, value: query }));
};
const onStatusChange = (status: SearchStatus) => {
const onSearch = useCallback((query: string) => {
setSearchInput(query);
debouncedSetSearch(query);
}, [debouncedSetSearch]);
const onStatusChange = useCallback((status: SearchStatus) => {
setSearchValue(prev => ({ ...prev, status }));
};
}, []);
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
const pluginFilter = useCallback((plugin: typeof Plugins[keyof typeof Plugins], newPluginsSet: Set<string> | null) => {
const { status } = searchValue;
const enabled = isPluginEnabled(plugin.name);
@ -219,7 +228,7 @@ export default function PluginSettings() {
if (!PluginMeta[plugin.name].folderName.startsWith("src/plugins/")) return false;
break;
case SearchStatus.NEW:
if (!newPlugins?.includes(plugin.name)) return false;
if (!newPluginsSet?.has(plugin.name)) return false;
break;
case SearchStatus.USER_PLUGINS:
if (!PluginMeta[plugin.name]?.userPlugin) return false;
@ -236,9 +245,9 @@ export default function PluginSettings() {
plugin.description.toLowerCase().includes(search) ||
plugin.tags?.some(t => t.toLowerCase().includes(search))
);
};
}, [searchValue, search]);
const [newPlugins] = useAwaiter(() => DataStore.get("Vencord_existingPlugins").then((cachedPlugins: Record<string, number> | undefined) => {
const [newPluginsSet] = useAwaiter(() => DataStore.get("Vencord_existingPlugins").then((cachedPlugins: Record<string, number> | undefined) => {
const now = Date.now() / 1000;
const existingTimestamps: Record<string, number> = {};
const sortedPluginNames = Object.values(sortedPlugins).map(plugin => plugin.name);
@ -252,52 +261,56 @@ export default function PluginSettings() {
}
DataStore.set("Vencord_existingPlugins", existingTimestamps);
return lodash.isEqual(newPlugins, sortedPluginNames) ? [] : newPlugins;
return lodash.isEqual(newPlugins, sortedPluginNames) ? null : new Set(newPlugins);
}));
const plugins = [] as JSX.Element[];
const requiredPlugins = [] as JSX.Element[];
const handleRestartNeeded = useCallback((name: string) => changes.handleChange(name), [changes]);
const showApi = searchValue.status === SearchStatus.API_PLUGINS;
for (const p of sortedPlugins) {
if (p.hidden || (!p.options && p.name.endsWith("API") && !showApi))
continue;
const { plugins, requiredPlugins } = useMemo(() => {
const plugins = [] as JSX.Element[];
const requiredPlugins = [] as JSX.Element[];
if (!pluginFilter(p)) continue;
const showApi = searchValue.status === SearchStatus.API_PLUGINS;
for (const p of sortedPlugins) {
if (p.hidden || (!p.settings?.def && p.name.endsWith("API") && !showApi))
continue;
const isRequired = p.required || p.isDependency || depMap[p.name]?.some(d => settings.plugins[d].enabled);
if (!pluginFilter(p, newPluginsSet)) continue;
if (isRequired) {
const tooltipText = p.required || !depMap[p.name]
? "This plugin is required for Equicord to function."
: <PluginDependencyList deps={depMap[p.name]?.filter(d => settings.plugins[d].enabled)} />;
const isRequired = p.required || p.isDependency || depMap[p.name]?.some(d => settings.plugins[d].enabled);
requiredPlugins.push(
<Tooltip text={tooltipText} key={p.name}>
{({ onMouseLeave, onMouseEnter }) => (
<PluginCard
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
onRestartNeeded={name => changes.handleChange(name)}
disabled={true}
plugin={p}
key={p.name}
/>
)}
</Tooltip>
);
} else {
plugins.push(
<PluginCard
onRestartNeeded={name => changes.handleChange(name)}
disabled={false}
plugin={p}
isNew={newPlugins?.includes(p.name)}
key={p.name}
/>
);
if (isRequired) {
const tooltipText = p.required || !depMap[p.name]
? "This plugin is required for Equicord to function."
: <PluginDependencyList deps={depMap[p.name]?.filter(d => settings.plugins[d].enabled)} />;
requiredPlugins.push(
<Tooltip text={tooltipText} key={p.name}>
{({ onMouseLeave, onMouseEnter }) => (
<PluginCard
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
onRestartNeeded={handleRestartNeeded}
disabled={true}
plugin={p}
/>
)}
</Tooltip>
);
} else {
plugins.push(
<PluginCard
onRestartNeeded={handleRestartNeeded}
disabled={false}
plugin={p}
isNew={newPluginsSet?.has(p.name)}
key={p.name}
/>
);
}
}
}
return { plugins, requiredPlugins };
}, [sortedPlugins, searchValue, newPluginsSet, depMap, settings.plugins, pluginFilter, handleRestartNeeded]);
function resetCheckAndDo() {
let restartNeeded = false;
@ -341,15 +354,18 @@ export default function PluginSettings() {
// Code directly taken from supportHelper.tsx
const isApiPlugin = (plugin: string) => plugin.endsWith("API") || Plugins[plugin].required;
const { totalStockPlugins, totalUserPlugins, enabledStockPlugins, enabledUserPlugins, enabledPlugins } = useMemo(() => {
const isApiPlugin = (plugin: string) => plugin.endsWith("API") || Plugins[plugin].required;
const totalPlugins = Object.keys(Plugins).filter(p => !isApiPlugin(p));
const enabledPlugins = Object.keys(Plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && !isApiPlugin(p));
const totalPlugins = Object.keys(Plugins).filter(p => !isApiPlugin(p));
const enabledPlugins = Object.keys(Plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && !isApiPlugin(p));
const totalStockPlugins = totalPlugins.filter(p => !PluginMeta[p].userPlugin && !Plugins[p].hidden).length;
const totalUserPlugins = totalPlugins.filter(p => PluginMeta[p].userPlugin).length;
const enabledStockPlugins = enabledPlugins.filter(p => !PluginMeta[p].userPlugin).length;
const enabledUserPlugins = enabledPlugins.filter(p => PluginMeta[p].userPlugin).length;
const totalStockPlugins = totalPlugins.filter(p => !PluginMeta[p].userPlugin && !Plugins[p].hidden).length;
const totalUserPlugins = totalPlugins.filter(p => PluginMeta[p].userPlugin).length;
const enabledStockPlugins = enabledPlugins.filter(p => !PluginMeta[p].userPlugin).length;
const enabledUserPlugins = enabledPlugins.filter(p => PluginMeta[p].userPlugin).length;
return { totalStockPlugins, totalUserPlugins, enabledStockPlugins, enabledUserPlugins, enabledPlugins };
}, [settings.plugins]);
const pluginsToLoad = Math.min(36, plugins.length);
const [visibleCount, setVisibleCount] = React.useState(pluginsToLoad);
const loadMore = React.useCallback(() => {
@ -392,7 +408,7 @@ export default function PluginSettings() {
<div className={classes(Margins.bottom20, cl("filter-controls"))}>
<ErrorBoundary noop>
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} />
<TextInput autoFocus value={searchInput} placeholder="Search for a plugin..." onChange={onSearch} />
</ErrorBoundary>
<div>
<ErrorBoundary noop>