diff --git a/src/plugins/betterSettings/PluginsSubmenu.tsx b/src/plugins/betterSettings/PluginsSubmenu.tsx
new file mode 100644
index 00000000..b22f82a6
--- /dev/null
+++ b/src/plugins/betterSettings/PluginsSubmenu.tsx
@@ -0,0 +1,68 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { openPluginModal } from "@components/PluginSettings/PluginModal";
+import { isObjectEmpty } from "@utils/misc";
+import { Alerts, i18n, 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]) => (
+ Vencord.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 6a3ded3c..f0dd89a7 100644
--- a/src/plugins/betterSettings/index.tsx
+++ b/src/plugins/betterSettings/index.tsx
@@ -13,6 +13,8 @@ import { waitFor } from "@webpack";
import { ComponentDispatch, FocusLock, i18n, 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("");
@@ -118,13 +120,21 @@ export default definePlugin({
},
{ // Settings cog context menu
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
- replacement: {
- match: /(EXPERIMENTS:.+?)(\(0,\i.\i\)\(\))(?=\.filter\(\i=>\{let\{section:\i\}=)/,
- replace: "$1$self.wrapMenu($2)"
- }
- }
+ replacement: [
+ {
+ match: /(EXPERIMENTS:.+?)(\(0,\i.\i\)\(\))(?=\.filter\(\i=>\{let\{section:\i\}=)/,
+ replace: "$1$self.wrapMenu($2)"
+ },
+ {
+ match: /case \i\.\i\.DEVELOPER_OPTIONS:return \i;/,
+ replace: "$&case 'VencordPlugins':return $self.PluginsSubmenu();"
+ }
+ ]
+ },
],
+ PluginsSubmenu,
+
// 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/webpack/common/types/menu.d.ts b/src/webpack/common/types/menu.d.ts
index 0b8ab5c6..5ae9062c 100644
--- a/src/webpack/common/types/menu.d.ts
+++ b/src/webpack/common/types/menu.d.ts
@@ -72,6 +72,11 @@ export interface Menu {
onChange(value: number): void,
renderValue?(value: number): string,
}>;
+ MenuSearchControl: RC<{
+ query: string
+ onChange(query: string): void;
+ placeholder?: string;
+ }>;
}
export interface ContextMenuApi {