From ed1060b4008f45ce5cc98c545d3f0e3299e3ed06 Mon Sep 17 00:00:00 2001 From: V Date: Fri, 21 Nov 2025 13:04:09 +0100 Subject: [PATCH 1/4] improve Vencord Toolbox (#3787) --- packages/discord-types/src/menu.d.ts | 4 +- src/Vencord.ts | 6 +- src/components/ErrorBoundary.tsx | 21 +- src/components/settings/tabs/BaseTab.tsx | 21 +- src/components/settings/tabs/index.ts | 2 +- src/components/settings/tabs/styles.css | 4 +- .../settings/tabs/updater/index.tsx | 22 +- src/plugins/_core/supportHelper.tsx | 4 +- src/plugins/betterSettings/PluginsSubmenu.tsx | 70 ---- src/plugins/betterSettings/index.tsx | 8 +- .../{index.ts => index.tsx} | 20 +- .../fixSpotifyEmbeds.desktop/native.ts | 40 ++- src/plugins/gameActivityToggle/index.tsx | 90 +++-- .../shikiCodeblocks.desktop/settings.ts | 2 +- src/plugins/vencordToolbox/index.tsx | 86 ++--- src/plugins/vencordToolbox/menu.tsx | 317 ++++++++++++++++++ src/utils/types.ts | 11 +- 17 files changed, 510 insertions(+), 218 deletions(-) delete mode 100644 src/plugins/betterSettings/PluginsSubmenu.tsx rename src/plugins/fixSpotifyEmbeds.desktop/{index.ts => index.tsx} (63%) create mode 100644 src/plugins/vencordToolbox/menu.tsx diff --git a/packages/discord-types/src/menu.d.ts b/packages/discord-types/src/menu.d.ts index 0866e1c3e..cbedc713a 100644 --- a/packages/discord-types/src/menu.d.ts +++ b/packages/discord-types/src/menu.d.ts @@ -1,4 +1,4 @@ -import type { ComponentType, CSSProperties, MouseEvent, PropsWithChildren, ReactNode, UIEvent } from "react"; +import type { ComponentType, CSSProperties, ForwardRefRenderFunction, MouseEvent, PropsWithChildren, ReactNode, UIEvent } from "react"; type RC = ComponentType>>; @@ -46,6 +46,8 @@ export interface Menu { MenuControlItem: RC<{ id: string; interactive?: boolean; + label?: string; + control: ForwardRefRenderFunction; }>; MenuSliderControl: RC<{ minValue: number, diff --git a/src/Vencord.ts b/src/Vencord.ts index ebbeeb30c..841fd9576 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -29,7 +29,7 @@ export * as WebpackPatcher from "./webpack/patchWebpack"; export { PlainSettings, Settings }; import { addVencordUiStyles } from "@components/css"; -import { openUpdaterModal } from "@components/settings/tabs/updater"; +import { openSettingsTabModal, UpdaterTab } from "@components/settings"; import { debounce } from "@shared/debounce"; import { IS_WINDOWS } from "@utils/constants"; import { createAndAppendStyle } from "@utils/css"; @@ -105,6 +105,8 @@ async function syncSettings() { let notifiedForUpdatesThisSession = false; async function runUpdateCheck() { + if (IS_UPDATER_DISABLED) return; + const notify = (data: NotificationData) => { if (notifiedForUpdatesThisSession) return; notifiedForUpdatesThisSession = true; @@ -135,7 +137,7 @@ async function runUpdateCheck() { notify({ title: "A Vencord update is available!", body: "Click here to view the update", - onClick: openUpdaterModal! + onClick: () => openSettingsTabModal(UpdaterTab!) }); } catch (err) { UpdateLogger.error("Failed to check for updates", err); diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 7058b5fd4..16fc30b44 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -20,6 +20,7 @@ import { LazyComponent, LazyComponentWrapper } from "@utils/lazyReact"; import { Logger } from "@utils/Logger"; import { Margins } from "@utils/margins"; import type { React } from "@webpack/common"; +import { ComponentType } from "react"; import { ErrorCard } from "./ErrorCard"; @@ -115,13 +116,21 @@ const ErrorBoundary = LazyComponent(() => { }; }) as LazyComponentWrapper> & { - wrap(Component: React.ComponentType, errorBoundaryProps?: Omit, "wrappedProps">): React.FunctionComponent; + wrap(Component: React.ComponentType, errorBoundaryProps?: Omit, "wrappedProps"> & { displayName?: string; }): React.FunctionComponent; }>; -ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => ( - - - -); +ErrorBoundary.wrap = (Component, errorBoundaryProps) => { + const wrapper: ComponentType = props => ( + + + + ); + + if (errorBoundaryProps?.displayName) { + wrapper.displayName = errorBoundaryProps.displayName; + } + + return wrapper; +}; export default ErrorBoundary; diff --git a/src/components/settings/tabs/BaseTab.tsx b/src/components/settings/tabs/BaseTab.tsx index dd83b8cd9..5d9914c3d 100644 --- a/src/components/settings/tabs/BaseTab.tsx +++ b/src/components/settings/tabs/BaseTab.tsx @@ -20,6 +20,7 @@ import { BaseText } from "@components/BaseText"; import ErrorBoundary from "@components/ErrorBoundary"; import { handleComponentFailed } from "@components/handleComponentFailed"; import { Margins } from "@utils/margins"; +import { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { onlyOnce } from "@utils/onlyOnce"; import type { ComponentType, PropsWithChildren } from "react"; @@ -35,8 +36,26 @@ export function SettingsTab({ title, children }: PropsWithChildren<{ title: stri export const handleSettingsTabError = onlyOnce(handleComponentFailed); export function wrapTab(component: ComponentType, tab: string) { - return ErrorBoundary.wrap(component, { + const wrapped = ErrorBoundary.wrap(component, { + displayName: `${tab}SettingsTab`, message: `Failed to render the ${tab} tab. If this issue persists, try using the installer to reinstall!`, onError: handleSettingsTabError, }); + + return wrapped; +} + +export function openSettingsTabModal(Tab: ComponentType) { + try { + openModal(wrapTab((modalProps: ModalProps) => ( + + + + + + + ), Tab.displayName || "Settings Tab")); + } catch { + handleSettingsTabError(); + } } diff --git a/src/components/settings/tabs/index.ts b/src/components/settings/tabs/index.ts index 9b4296c39..0858bb098 100644 --- a/src/components/settings/tabs/index.ts +++ b/src/components/settings/tabs/index.ts @@ -14,5 +14,5 @@ export { openPluginModal } from "./plugins/PluginModal"; export { default as BackupAndRestoreTab } from "./sync/BackupAndRestoreTab"; export { default as CloudTab } from "./sync/CloudTab"; export { default as ThemesTab } from "./themes"; -export { openUpdaterModal, default as UpdaterTab } from "./updater"; +export { default as UpdaterTab } from "./updater"; export { default as VencordTab } from "./vencord"; diff --git a/src/components/settings/tabs/styles.css b/src/components/settings/tabs/styles.css index e9aa67769..91cb59e63 100644 --- a/src/components/settings/tabs/styles.css +++ b/src/components/settings/tabs/styles.css @@ -50,10 +50,10 @@ background-color: var(--button-danger-background); } -.vc-updater-modal { +.vc-settings-modal { padding: 1.5em !important; } -.vc-updater-modal-close-button { +.vc-settings-modal-close { float: right; } \ No newline at end of file diff --git a/src/components/settings/tabs/updater/index.tsx b/src/components/settings/tabs/updater/index.tsx index bdc13bd0b..935f66125 100644 --- a/src/components/settings/tabs/updater/index.tsx +++ b/src/components/settings/tabs/updater/index.tsx @@ -20,9 +20,8 @@ import { useSettings } from "@api/Settings"; import { Divider } from "@components/Divider"; import { FormSwitch } from "@components/FormSwitch"; import { Link } from "@components/Link"; -import { handleSettingsTabError, SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab"; +import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab"; import { Margins } from "@utils/margins"; -import { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { useAwaiter } from "@utils/react"; import { getRepo, isNewer, UpdateLogger } from "@utils/updater"; import { Forms, React } from "@webpack/common"; @@ -94,22 +93,3 @@ function Updater() { export default IS_UPDATER_DISABLED ? null : wrapTab(Updater, "Updater"); - -export const openUpdaterModal = IS_UPDATER_DISABLED - ? null - : function () { - const UpdaterTab = wrapTab(Updater, "Updater"); - - try { - openModal(wrapTab((modalProps: ModalProps) => ( - - - - - - - ), "UpdaterModal")); - } catch { - handleSettingsTabError(); - } - }; diff --git a/src/plugins/_core/supportHelper.tsx b/src/plugins/_core/supportHelper.tsx index d2580218e..5ebdbe0de 100644 --- a/src/plugins/_core/supportHelper.tsx +++ b/src/plugins/_core/supportHelper.tsx @@ -23,7 +23,7 @@ import { Card } from "@components/Card"; import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; import { Link } from "@components/Link"; -import { openUpdaterModal } from "@components/settings/tabs/updater"; +import { openSettingsTabModal, UpdaterTab } from "@components/settings"; import { CONTRIB_ROLE_ID, Devs, DONOR_ROLE_ID, KNOWN_ISSUES_CHANNEL_ID, REGULAR_ROLE_ID, SUPPORT_CATEGORY_ID, SUPPORT_CHANNEL_ID, VENBOT_USER_ID, VENCORD_GUILD_ID } from "@utils/constants"; import { sendMessage } from "@utils/discord"; import { Logger } from "@utils/Logger"; @@ -188,7 +188,7 @@ export default definePlugin({ Please first update before asking for support! , - onCancel: () => openUpdaterModal!(), + onCancel: () => openSettingsTabModal(UpdaterTab!), cancelText: "View Updates", confirmText: "Update & Restart Now", onConfirm: forceUpdate, diff --git a/src/plugins/betterSettings/PluginsSubmenu.tsx b/src/plugins/betterSettings/PluginsSubmenu.tsx deleted file mode 100644 index 14f835f99..000000000 --- a/src/plugins/betterSettings/PluginsSubmenu.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { isPluginEnabled } from "@api/PluginManager"; -import { openPluginModal } from "@components/settings/tabs"; -import { getIntlMessage } from "@utils/discord"; -import { isObjectEmpty } from "@utils/misc"; -import { Alerts, Menu, useMemo, useState } from "@webpack/common"; - -import Plugins from "~plugins"; - -function onRestartNeeded() { - Alerts.show({ - title: "Restart required", - body:

You have changed settings that require a restart.

, - confirmText: "Restart now", - cancelText: "Later!", - onConfirm: () => location.reload() - }); -} - -export default function PluginsSubmenu() { - const sortedPlugins = useMemo(() => Object.values(Plugins) - .sort((a, b) => a.name.localeCompare(b.name)), []); - const [query, setQuery] = useState(""); - - const search = query.toLowerCase(); - const include = (p: typeof Plugins[keyof typeof Plugins]) => ( - isPluginEnabled(p.name) - && p.options && !isObjectEmpty(p.options) - && ( - p.name.toLowerCase().includes(search) - || p.description.toLowerCase().includes(search) - || p.tags?.some(t => t.toLowerCase().includes(search)) - ) - ); - - const plugins = sortedPlugins.filter(include); - - return ( - <> - ( - - )} - /> - - {!!plugins.length && } - - {plugins.map(p => ( - openPluginModal(p, onRestartNeeded)} - /> - ))} - - ); -} diff --git a/src/plugins/betterSettings/index.tsx b/src/plugins/betterSettings/index.tsx index 4a359a0ca..be6b017dc 100644 --- a/src/plugins/betterSettings/index.tsx +++ b/src/plugins/betterSettings/index.tsx @@ -6,6 +6,7 @@ import { definePluginSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; +import { buildPluginMenuEntries, buildThemeMenuEntries } from "@plugins/vencordToolbox/menu"; import { Devs } from "@utils/constants"; import { getIntlMessage } from "@utils/discord"; import { Logger } from "@utils/Logger"; @@ -14,8 +15,6 @@ import { waitFor } from "@webpack"; import { ComponentDispatch, FocusLock, Menu, useEffect, useRef } from "@webpack/common"; import type { HTMLAttributes, ReactElement } from "react"; -import PluginsSubmenu from "./PluginsSubmenu"; - type SettingsEntry = { section: string, label: string; }; const cl = classNameFactory(""); @@ -131,13 +130,14 @@ export default definePlugin({ }, { match: /case \i\.\i\.DEVELOPER_OPTIONS:return \i;/, - replace: "$&case 'VencordPlugins':return $self.PluginsSubmenu();" + replace: "$&case 'VencordPlugins':return $self.buildPluginMenuEntries(true);$&case 'VencordThemes':return $self.buildThemeMenuEntries();" } ] }, ], - PluginsSubmenu, + buildPluginMenuEntries, + buildThemeMenuEntries, // This is the very outer layer of the entire ui, so we can't wrap this in an ErrorBoundary // without possibly also catching unrelated errors of children. diff --git a/src/plugins/fixSpotifyEmbeds.desktop/index.ts b/src/plugins/fixSpotifyEmbeds.desktop/index.tsx similarity index 63% rename from src/plugins/fixSpotifyEmbeds.desktop/index.ts rename to src/plugins/fixSpotifyEmbeds.desktop/index.tsx index 02a2845cf..c5b42742a 100644 --- a/src/plugins/fixSpotifyEmbeds.desktop/index.ts +++ b/src/plugins/fixSpotifyEmbeds.desktop/index.tsx @@ -8,18 +8,20 @@ import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { makeRange, OptionType } from "@utils/types"; +const settings = definePluginSettings({ + volume: { + type: OptionType.SLIDER, + description: "The volume % to set for spotify embeds. Anything above 10% is veeeery loud", + markers: makeRange(0, 100, 10), + stickToMarkers: false, + default: 10 + } +}); + // The entire code of this plugin can be found in ipcPlugins export default definePlugin({ name: "FixSpotifyEmbeds", description: "Fixes spotify embeds being incredibly loud by letting you customise the volume", authors: [Devs.Ven], - settings: definePluginSettings({ - volume: { - type: OptionType.SLIDER, - description: "The volume % to set for spotify embeds. Anything above 10% is veeeery loud", - markers: makeRange(0, 100, 10), - stickToMarkers: false, - default: 10 - } - }) + settings, }); diff --git a/src/plugins/fixSpotifyEmbeds.desktop/native.ts b/src/plugins/fixSpotifyEmbeds.desktop/native.ts index 79602609c..b730883c1 100644 --- a/src/plugins/fixSpotifyEmbeds.desktop/native.ts +++ b/src/plugins/fixSpotifyEmbeds.desktop/native.ts @@ -5,23 +5,59 @@ */ import { RendererSettings } from "@main/settings"; -import { app } from "electron"; +import { app, WebFrameMain, webFrameMain } from "electron"; + +// TODO: routingID is deprecated and should be replaced with frameToken, but it's too new +const ids = [] as Record<"routingId" | "processId", number>[]; + +function cleanUpAndGetSpotifyFrames() { + const spotifyFrames = [] as WebFrameMain[]; + for (let i = ids.length - 1; i >= 0; i--) { + const { processId, routingId } = ids[i]; + + const frame = webFrameMain.fromId(processId, routingId); + if (!frame) { + ids.splice(i, 1); + continue; + } + + spotifyFrames.push(frame); + } + + return spotifyFrames; +} app.on("browser-window-created", (_, win) => { win.webContents.on("frame-created", (_, { frame }) => { frame?.once("dom-ready", () => { if (frame.url.startsWith("https://open.spotify.com/embed/")) { + cleanUpAndGetSpotifyFrames(); // clean up stale frames + + const { routingId, processId } = frame; + ids.push({ routingId, processId }); + const settings = RendererSettings.store.plugins?.FixSpotifyEmbeds; if (!settings?.enabled) return; frame.executeJavaScript(` + globalThis._vcVolume = ${settings.volume / 100}; const original = Audio.prototype.play; Audio.prototype.play = function() { - this.volume = ${(settings.volume / 100) || 0.1}; + this.volume = _vcVolume; return original.apply(this, arguments); } `); } }); }); + + RendererSettings.addChangeListener("plugins.FixSpotifyEmbeds.volume", newVolume => { + try { + cleanUpAndGetSpotifyFrames().forEach(frame => + frame.executeJavaScript(`globalThis._vcVolume = ${newVolume / 100}`) + ); + } catch (e) { + console.error("FixSpotifyEmbeds: Failed to update volume", e); + } + }); }); diff --git a/src/plugins/gameActivityToggle/index.tsx b/src/plugins/gameActivityToggle/index.tsx index 8959acc29..0df692a06 100644 --- a/src/plugins/gameActivityToggle/index.tsx +++ b/src/plugins/gameActivityToggle/index.tsx @@ -16,12 +16,15 @@ * along with this program. If not, see . */ +import { isPluginEnabled } from "@api/PluginManager"; import { definePluginSettings } from "@api/Settings"; import { getUserSettingLazy } from "@api/UserSettings"; import ErrorBoundary from "@components/ErrorBoundary"; +import VencordToolboxPlugin from "@plugins/vencordToolbox"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { findComponentByCodeLazy } from "@webpack"; +import { Menu } from "@webpack/common"; import managedStyle from "./style.css?managed"; @@ -29,8 +32,29 @@ const Button = findComponentByCodeLazy(".NONE,disabled:", ".PANEL_BUTTON"); const ShowCurrentGame = getUserSettingLazy("status", "showCurrentGame")!; -function makeIcon(showCurrentGame?: boolean) { +const settings = definePluginSettings({ + oldIcon: { + type: OptionType.BOOLEAN, + description: "Use the old icon style before Discord icon redesign", + default: false + }, + location: { + type: OptionType.SELECT, + description: "Where to show the game activity toggle button", + options: [ + { label: "Next to Mute/Deafen", value: "PANEL", default: true }, + { label: "Vencord Toolbox", value: "TOOLBOX" } + ], + get hidden() { + return !isPluginEnabled(VencordToolboxPlugin.name); + } + } +}); + +function Icon() { const { oldIcon } = settings.use(["oldIcon"]); + const showCurrentGame = ShowCurrentGame.useSetting(); + const redLinePath = !oldIcon ? "M22.7 2.7a1 1 0 0 0-1.4-1.4l-20 20a1 1 0 1 0 1.4 1.4Z" @@ -40,33 +64,34 @@ function makeIcon(showCurrentGame?: boolean) { ? "M23.27 4.73 19.27 .73 -.27 20.27 3.73 24.27Z" : "M23.27 4.54 19.46.73 .73 19.46 4.54 23.27 23.27 4.54Z"; - return function () { - return ( - - - {!showCurrentGame && <> - - - - - - } - - ); - }; + return ( + + + {!showCurrentGame && <> + + + + + + } + + ); } function GameActivityToggleButton(props: { nameplate?: any; }) { + const { location } = settings.use(["location"]); const showCurrentGame = ShowCurrentGame.useSetting(); + if (location !== "PANEL" && isPluginEnabled(VencordToolboxPlugin.name)) return null; + return (