From 1f062fee8996d53446947322ac3fd83652693e32 Mon Sep 17 00:00:00 2001 From: creations Date: Fri, 26 Sep 2025 00:25:09 -0400 Subject: [PATCH] Plugin Cards (#415) --- src/components/Icons.tsx | 17 ++ .../settings/tabs/plugins/PluginCard.tsx | 6 +- .../settings/tabs/plugins/index.tsx | 36 ++-- src/equicordplugins/equicordHelper/README.md | 2 +- src/equicordplugins/equicordHelper/index.tsx | 67 +------- .../equicordHelper/pluginButtons.tsx | 71 ++++++++ .../equicordHelper/pluginCards.tsx | 155 ++++++++++++++++++ src/plugins/index.ts | 31 +++- src/plugins/messageLogger/index.tsx | 5 +- src/utils/constants.ts | 1 + 10 files changed, 301 insertions(+), 90 deletions(-) create mode 100644 src/equicordplugins/equicordHelper/pluginButtons.tsx create mode 100644 src/equicordplugins/equicordHelper/pluginCards.tsx diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index c38ba1a5c..76096ad84 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -128,6 +128,23 @@ export function InfoIcon(props: IconProps) { ); } +export function WarningIcon({ height = 32, width = 32, className }: IconProps) { + return ( + + + + ); +} + export function OwnerCrownIcon(props: IconProps) { return ( { +interface PluginCardProps { plugin: Plugin; - disabled: boolean; + disabled?: boolean; onRestartNeeded(name: string, key: string): void; isNew?: boolean; + onMouseEnter?: React.MouseEventHandler; + onMouseLeave?: React.MouseEventHandler; } export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) { diff --git a/src/components/settings/tabs/plugins/index.tsx b/src/components/settings/tabs/plugins/index.tsx index deef932ab..20a747505 100644 --- a/src/components/settings/tabs/plugins/index.tsx +++ b/src/components/settings/tabs/plugins/index.tsx @@ -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 ( {matchingExcludedPlugins.length @@ -160,19 +160,7 @@ export default function PluginSettings() { })); }, []); - const depMap = useMemo(() => { - const o = {} as Record; - 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)); + : settings.plugins[d].enabled)} />; requiredPlugins.push( @@ -419,7 +407,7 @@ export default function PluginSettings() { ); } -function makeDependencyList(deps: string[]) { +export function PluginDependencyList({ deps }: { deps: string[]; }) { return ( This plugin is required by: diff --git a/src/equicordplugins/equicordHelper/README.md b/src/equicordplugins/equicordHelper/README.md index 9c1dcab8a..d371ef512 100644 --- a/src/equicordplugins/equicordHelper/README.md +++ b/src/equicordplugins/equicordHelper/README.md @@ -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 diff --git a/src/equicordplugins/equicordHelper/index.tsx b/src/equicordplugins/equicordHelper/index.tsx index 0d068f6b3..0f33cd1a6 100644 --- a/src/equicordplugins/equicordHelper/index.tsx +++ b/src/equicordplugins/equicordHelper/index.tsx @@ -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( - + return ( + <> + + + ); } - - return buttons.length - ? {buttons} - : null; -} +}); diff --git a/src/equicordplugins/equicordHelper/pluginButtons.tsx b/src/equicordplugins/equicordHelper/pluginButtons.tsx new file mode 100644 index 000000000..a7c26e0d0 --- /dev/null +++ b/src/equicordplugins/equicordHelper/pluginButtons.tsx @@ -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( + + ); + } + + if (pluginButtons.length === 0) return null; + + return ( +
+ {pluginButtons} +
+ ); +}, { noop: true }); diff --git a/src/equicordplugins/equicordHelper/pluginCards.tsx b/src/equicordplugins/equicordHelper/pluginCards.tsx new file mode 100644 index 000000000..17af02327 --- /dev/null +++ b/src/equicordplugins/equicordHelper/pluginCards.tsx @@ -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 = ( + { }} + disabled={true} + infoButton={} + /> + ); + + return description + ? {card} + : 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." + : ; + + return ( + + {({ onMouseLeave, onMouseEnter }) => + + } + + ); + } + + return ( + + ); +} + +export const PluginCards = ErrorBoundary.wrap(function PluginCards({ message }: { message: Message; }) { + const seenPlugins = new Set(); + 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( + + ); + }); + + // 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( + + ); + } + } + } + + if (pluginCards.length === 0) return null; + + return ( +
+ {pluginCards} +
+ ); +}, { noop: true }); diff --git a/src/plugins/index.ts b/src/plugins/index.ts index eb2ba73ce..7b111d630 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -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(); 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 = {}; + 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; } diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index 7805175b8..081337afc 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -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) { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index b2e3e5544..629333e68 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -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";