Plugin Cards (#415)

This commit is contained in:
creations 2025-09-26 00:25:09 -04:00 • committed by GitHub
parent 6dd595b4fd
commit 1f062fee89
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 301 additions and 90 deletions

View file

@ -128,6 +128,23 @@ export function InfoIcon(props: IconProps) {
);
}
export function WarningIcon({ height = 32, width = 32, className }: IconProps) {
return (
<Icon
height={height}
width={width}
className={classes(className, "vc-warning-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
fillRule="evenodd"
d="M10 3.1a2.37 2.37 0 0 1 4 0l8.71 14.75c.84 1.41-.26 3.15-2 3.15H3.29c-1.74 0-2.84-1.74-2-3.15L9.99 3.1Zm3.25 14.65a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0ZM13.06 14l.37-5.94a1 1 0 0 0-1-1.06h-.87a1 1 0 0 0-1 1.06l.38 5.94a1.06 1.06 0 0 0 2.12 0Z"
/>
</Icon>
);
}
export function OwnerCrownIcon(props: IconProps) {
return (
<Icon

View file

@ -28,11 +28,13 @@ const { startDependenciesRecursive, startPlugin, stopPlugin, isPluginEnabled } =
export const ButtonClasses = findByPropsLazy("button", "disabled", "enabled");
interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
interface PluginCardProps {
plugin: Plugin;
disabled: boolean;
disabled?: boolean;
onRestartNeeded(name: string, key: string): void;
isNew?: boolean;
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
onMouseLeave?: React.MouseEventHandler<HTMLDivElement>;
}
export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {

View file

@ -104,19 +104,19 @@ const enum SearchStatus {
NEW,
}
export const ExcludedReasons: Record<"web" | "discordDesktop" | "vesktop" | "equibop" | "desktop" | "dev", string> = {
desktop: "Discord Desktop app or Vesktop",
discordDesktop: "Discord Desktop app",
vesktop: "Vesktop & Equibop apps",
equibop: "Vesktop & Equibop apps",
web: "Vesktop & Equibop apps as well as the Web version of Discord",
dev: "Developer version of Equicord"
};
function ExcludedPluginsList({ search }: { search: string; }) {
const matchingExcludedPlugins = Object.entries(ExcludedPlugins)
.filter(([name]) => name.toLowerCase().includes(search));
const ExcludedReasons: Record<"web" | "discordDesktop" | "vesktop" | "equibop" | "desktop" | "dev", string> = {
desktop: "Discord Desktop app or Vesktop",
discordDesktop: "Discord Desktop app",
vesktop: "Vesktop & Equibop apps",
equibop: "Vesktop & Equibop apps",
web: "Vesktop & Equibop apps as well as the Web version of Discord",
dev: "Developer version of Equicord"
};
return (
<Text variant="text-md/normal" className={Margins.top16}>
{matchingExcludedPlugins.length
@ -160,19 +160,7 @@ export default function PluginSettings() {
}));
}, []);
const depMap = useMemo(() => {
const o = {} as Record<string, string[]>;
for (const plugin in Plugins) {
const deps = Plugins[plugin].dependencies;
if (deps) {
for (const dep of deps) {
o[dep] ??= [];
o[dep].push(plugin);
}
}
}
return o;
}, []);
const depMap = Vencord.Plugins.calculatePluginDependencyMap();
const sortedPlugins = useMemo(() => Object.values(Plugins)
.sort((a, b) => a.name.localeCompare(b.name)), []);
@ -241,7 +229,7 @@ export default function PluginSettings() {
if (isRequired) {
const tooltipText = p.required || !depMap[p.name]
? "This plugin is required for Equicord to function."
: makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled));
: <PluginDependencyList deps={depMap[p.name]?.filter(d => settings.plugins[d].enabled)} />;
requiredPlugins.push(
<Tooltip text={tooltipText} key={p.name}>
@ -419,7 +407,7 @@ export default function PluginSettings() {
);
}
function makeDependencyList(deps: string[]) {
export function PluginDependencyList({ deps }: { deps: string[]; }) {
return (
<React.Fragment>
<Forms.FormText>This plugin is required by:</Forms.FormText>

View file

@ -1,3 +1,3 @@
# EquicordHelper
Fixes various issues for crashing and adds other items that might help with support
Fixes various issues for crashing and adds other items such as plugin cards and buttons for managing plugins quickly in support

View file

@ -8,13 +8,10 @@ import "@equicordplugins/_misc/styles.css";
import { definePluginSettings } from "@api/Settings";
import { Devs, EquicordDevs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { isEquicordGuild, isEquicordSupport } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
import { Button, Flex, showToast, Toasts } from "@webpack/common";
import { JSX } from "react";
import { toggleEnabled } from "./utils";
import { PluginButtons } from "./pluginButtons";
import { PluginCards } from "./pluginCards";
const settings = definePluginSettings({
disableCreateDMButton: {
@ -70,59 +67,11 @@ export default definePlugin({
},
],
renderMessageAccessory(props) {
return pluginToggleButtons(props);
}
});
function pluginToggleButtons(props) {
const buttons = [] as JSX.Element[];
const msg = props.message.content?.toLowerCase() ?? "";
const contentWords = (msg.match(/`\w+`/g) ?? []).map(e => e.slice(1, -1));
const matchedPlugins = Object.keys(Vencord.Plugins.plugins).filter(name => contentWords.includes(name.toLowerCase()));
const matchedPlugin = matchedPlugins.sort((a, b) => b.length - a.length)[0];
const pluginData = matchedPlugin ? Vencord.Plugins.plugins[matchedPlugin] : null;
const isEquicord = isEquicordGuild(props.channel.id) && isEquicordSupport(props.message.author.id);
const startsWithEnabled = msg.startsWith("enable");
const startsWithDisabled = msg.startsWith("disable");
const shouldAddPluginButtons = pluginData && isEquicord && (startsWithEnabled || startsWithDisabled);
if (shouldAddPluginButtons) {
if (pluginData.required || pluginData.name.endsWith("API")) return;
const isEnabled = Vencord.Plugins.isPluginEnabled(matchedPlugin);
let label = `${matchedPlugin} is already ${isEnabled ? "enabled" : "disabled"}`;
let disabled = true;
if ((startsWithDisabled && isEnabled) || (startsWithEnabled && !isEnabled)) {
label = `${isEnabled ? "Disable" : "Enable"} ${matchedPlugin}`;
disabled = false;
}
buttons.push(
<Button
key="vc-plugin-toggle"
color={disabled ? Button.Colors.PRIMARY : (isEnabled ? Button.Colors.RED : Button.Colors.GREEN)}
disabled={disabled}
size={Button.Sizes.SMALL}
onClick={async () => {
try {
const success = await toggleEnabled(matchedPlugin);
if (success) showToast(`${label}`, Toasts.Type.SUCCESS);
} catch (e) {
new Logger("EquicordHelper").error("Error while toggling:", e);
showToast(`Failed to ${label.toLowerCase()}`, Toasts.Type.FAILURE);
}
}}
>
{label}
</Button>
return (
<>
<PluginButtons message={props.message} />
<PluginCards message={props.message} />
</>
);
}
return buttons.length
? <Flex>{buttons}</Flex>
: null;
}
});

View file

@ -0,0 +1,71 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Logger } from "@utils/Logger";
import { isEquicordGuild, isEquicordSupport } from "@utils/misc";
import { Message } from "@vencord/discord-types";
import { Button, showToast, Toasts } from "@webpack/common";
import { JSX } from "react";
import { toggleEnabled } from "./utils";
export const PluginButtons = ErrorBoundary.wrap(function PluginCards({ message }: { message: Message; }) {
const pluginButtons = [] as JSX.Element[];
const msg = message.content?.toLowerCase() ?? "";
const contentWords = (msg.match(/`\w+`/g) ?? []).map(e => e.slice(1, -1));
const matchedPlugins = Object.keys(Vencord.Plugins.plugins).filter(name => contentWords.includes(name.toLowerCase()));
const matchedPlugin = matchedPlugins.sort((a, b) => b.length - a.length)[0];
const pluginData = matchedPlugin ? Vencord.Plugins.plugins[matchedPlugin] : null;
const isEquicord = isEquicordGuild(message.channel_id) && isEquicordSupport(message.author.id);
const startsWithEnabled = msg.startsWith("enable");
const startsWithDisabled = msg.startsWith("disable");
const shouldAddPluginButtons = pluginData && isEquicord && (startsWithEnabled || startsWithDisabled);
if (shouldAddPluginButtons) {
if (pluginData.required || pluginData.name.endsWith("API")) return;
const isEnabled = Vencord.Plugins.isPluginEnabled(matchedPlugin);
let label = `${matchedPlugin} is already ${isEnabled ? "enabled" : "disabled"}`;
let disabled = true;
if ((startsWithDisabled && isEnabled) || (startsWithEnabled && !isEnabled)) {
label = `${isEnabled ? "Disable" : "Enable"} ${matchedPlugin}`;
disabled = false;
}
pluginButtons.push(
<Button
key="vc-plugin-toggle"
color={disabled ? Button.Colors.PRIMARY : (isEnabled ? Button.Colors.RED : Button.Colors.GREEN)}
disabled={disabled}
size={Button.Sizes.SMALL}
onClick={async () => {
try {
const success = await toggleEnabled(matchedPlugin);
if (success) showToast(`${label}`, Toasts.Type.SUCCESS);
} catch (e) {
new Logger("EquicordHelper").error("Error while toggling:", e);
showToast(`Failed to ${label.toLowerCase()}`, Toasts.Type.FAILURE);
}
}}
>
{label}
</Button>
);
}
if (pluginButtons.length === 0) return null;
return (
<div className="vc-plugins-action-buttons" style={{ marginTop: "0px" }}>
{pluginButtons}
</div>
);
}, { noop: true });

View file

@ -0,0 +1,155 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { useSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { WarningIcon } from "@components/Icons";
import { AddonCard } from "@components/settings";
import { ExcludedReasons, PluginDependencyList } from "@components/settings/tabs/plugins";
import { PluginCard } from "@components/settings/tabs/plugins/PluginCard";
import { EQUIBOT_USER_ID } from "@utils/constants";
import { isEquicordGuild, isEquicordSupport } from "@utils/misc";
import { Message } from "@vencord/discord-types";
import { showToast, Tooltip, TooltipContainer } from "@webpack/common";
import { JSX } from "react";
import plugins, { ExcludedPlugins } from "~plugins";
export function ChatPluginCard({ url, description }: { url: string, description: string; }) {
const pluginNameFromUrl = new URL(url).pathname.split("/")[2];
const actualPluginName = Object.keys(plugins).find(name =>
name.toLowerCase() === pluginNameFromUrl?.toLowerCase()
);
const pluginName = actualPluginName || pluginNameFromUrl;
useSettings([`plugins.${pluginName ?? ""}.enabled`]);
if (!pluginName) return null;
const p = plugins[pluginName];
const excludedPlugin = ExcludedPlugins[pluginName];
if (excludedPlugin || !p) {
const toolTipText = !p
? `${pluginName} is only available on the ${ExcludedReasons[ExcludedPlugins[pluginName]]}`
: "This plugin is not on this version of Equicord. Try updating!";
const card = (
<AddonCard
name={pluginName}
description={description || toolTipText}
enabled={false}
setEnabled={() => { }}
disabled={true}
infoButton={<WarningIcon />}
/>
);
return description
? <TooltipContainer text={toolTipText}>{card}</TooltipContainer>
: card;
}
const onRestartNeeded = () => showToast("A restart is required for the change to take effect!");
const required = Vencord.Plugins.isPluginRequired(pluginName);
const dependents = Vencord.Plugins.calculatePluginDependencyMap()[p.name]?.filter(d => Vencord.Plugins.isPluginEnabled(d));
if (required) {
const tooltipText = p.required || !dependents.length
? "This plugin is required for Equicord to function."
: <PluginDependencyList deps={dependents} />;
return (
<Tooltip text={tooltipText} key={p.name}>
{({ onMouseLeave, onMouseEnter }) =>
<PluginCard
key={p.name}
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
onRestartNeeded={onRestartNeeded}
plugin={p}
disabled
/>
}
</Tooltip>
);
}
return (
<PluginCard
key={p.name}
onRestartNeeded={onRestartNeeded}
plugin={p}
/>
);
}
export const PluginCards = ErrorBoundary.wrap(function PluginCards({ message }: { message: Message; }) {
const seenPlugins = new Set<string>();
const pluginCards: JSX.Element[] = [];
// Process embeds
message.embeds?.forEach(embed => {
if (!embed.url?.startsWith("https://equicord.org/plugins/") && !embed.url?.startsWith("https://vencord.dev/plugins/")) return;
const isEquicord = isEquicordGuild(message.channel_id) && isEquicordSupport(message.author.id);
if (!isEquicord) return;
const pluginNameFromUrl = new URL(embed.url).pathname.split("/")[2];
const actualPluginName = Object.keys(plugins).find(name =>
name.toLowerCase() === pluginNameFromUrl?.toLowerCase()
);
const pluginName = actualPluginName || pluginNameFromUrl;
if (!pluginName || seenPlugins.has(pluginName)) return;
seenPlugins.add(pluginName);
pluginCards.push(
<ChatPluginCard
key={embed.url}
url={embed.url}
description={embed.rawDescription}
/>
);
});
// Process components
const components = (message.components?.[0] as any)?.components;
if (message.author.id === EQUIBOT_USER_ID && components?.length >= 4) {
const description = components[1]?.content;
const pluginUrl = components.find((c: any) => c?.components)?.components[0]?.url;
if (pluginUrl?.startsWith("https://equicord.org/plugins/") || pluginUrl?.startsWith("https://vencord.dev/plugins/")) {
const pluginNameFromUrl = new URL(pluginUrl).pathname.split("/")[2];
const actualPluginName = Object.keys(plugins).find(name =>
name.toLowerCase() === pluginNameFromUrl?.toLowerCase()
);
const pluginName = actualPluginName || pluginNameFromUrl;
if (pluginName && !seenPlugins.has(pluginName)) {
seenPlugins.add(pluginName);
pluginCards.push(
<ChatPluginCard
key={pluginUrl}
url={pluginUrl}
description={description}
/>
);
}
}
}
if (pluginCards.length === 0) return null;
return (
<div className="vc-plugins-management-cards vc-plugins-grid" style={{ marginTop: "0px" }}>
{pluginCards}
</div>
);
}, { noop: true });

View file

@ -28,6 +28,7 @@ import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/Messag
import { addNicknameIcon, removeNicknameIcon } from "@api/NicknameIcons";
import { Settings, SettingsStore } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import { makeLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
import { canonicalizeFind, canonicalizeReplacement } from "@utils/patches";
import { Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types";
@ -52,10 +53,36 @@ const subscribedFluxEventsPlugins = new Set<string>();
const pluginsValues = Object.values(Plugins);
const settings = Settings.plugins;
/**
* Whether a plugin is required (or dependency of another enabled plugin)
*/
export function isPluginRequired(name: string) {
const p = Plugins[name];
if (!p) return false;
return p.required || p.isDependency;
}
/**
* A map of plugin names to the plugins that depend on them
*/
export const calculatePluginDependencyMap = makeLazy(() => {
const dependencies: Record<string, string[]> = {};
for (const plugin in Plugins) {
const deps = Plugins[plugin].dependencies;
if (deps) {
for (const dep of deps) {
dependencies[dep] ??= [];
dependencies[dep].push(plugin);
}
}
}
return dependencies;
});
export function isPluginEnabled(p: string) {
return (
Plugins[p]?.required ||
Plugins[p]?.isDependency ||
isPluginRequired(p) ||
settings[p]?.enabled
) ?? false;
}

View file

@ -14,7 +14,7 @@ import { updateMessage } from "@api/MessageUpdater";
import { definePluginSettings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs, VC_SUPPORT_CATEGORY_ID, VENBOT_USER_ID } from "@utils/constants";
import { Devs, EQUIBOT_USER_ID, SUPPORT_CHANNEL_ID, VC_SUPPORT_CATEGORY_ID, VENBOT_USER_ID } from "@utils/constants";
import { getIntlMessage } from "@utils/discord";
import { Logger } from "@utils/Logger";
import { classes } from "@utils/misc";
@ -409,7 +409,8 @@ export default definePlugin({
(isEdit ? !logEdits : !logDeletes) ||
ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id) ||
// Ignore Venbot in the support channels (love you venbot!!!)
(message.author?.id === VENBOT_USER_ID && ChannelStore.getChannel(message.channel_id)?.parent_id === VC_SUPPORT_CATEGORY_ID));
(message.author?.id === VENBOT_USER_ID && ChannelStore.getChannel(message.channel_id)?.parent_id === VC_SUPPORT_CATEGORY_ID) ||
(message.author?.id === EQUIBOT_USER_ID && ChannelStore.getChannel(message.channel_id)?.id === SUPPORT_CHANNEL_ID));
},
EditMarker({ message, className, children, ...props }: any) {

View file

@ -27,6 +27,7 @@ export const EQUICORD_TEAM = "1173520023239786538";
export const EQUICORD_HELPERS = "1326406112144265257";
export const EQUIBOP_CONTRIB_ROLE_ID = "1287079931645263968";
export const VENCORD_CONTRIB_ROLE_ID = "1173343399470964856";
export const EQUIBOT_USER_ID = "1243063117852835941";
// Vencord
export const VC_SUPPORT_CHANNEL_ID = "1026515880080842772";