diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx
index 40cb15c9d..eede9559e 100644
--- a/src/components/Icons.tsx
+++ b/src/components/Icons.tsx
@@ -444,3 +444,101 @@ export function PlaceholderIcon(props: IconProps) {
);
}
+
+export function MainSettingsIcon(props: IconProps) {
+ return (
+
+
+
+ );
+}
+
+export function PluginsIcon(props: IconProps) {
+ return (
+
+
+
+ );
+}
+
+export function CloudIcon(props: IconProps) {
+ return (
+
+
+
+ );
+}
+
+export function BackupRestoreIcon(props: IconProps) {
+ return (
+
+
+
+ );
+}
+
+export function UpdaterIcon(props: IconProps) {
+ return (
+
+
+
+ );
+}
+
+export function PatchHelperIcon(props: IconProps) {
+ return (
+
+
+
+ );
+}
+
+export function VesktopSettingsIcon(props: IconProps) {
+ return (
+
+
+
+ );
+}
diff --git a/src/components/settings/tabs/BaseTab.tsx b/src/components/settings/tabs/BaseTab.tsx
index 5d9914c3d..b4c0ebb53 100644
--- a/src/components/settings/tabs/BaseTab.tsx
+++ b/src/components/settings/tabs/BaseTab.tsx
@@ -16,20 +16,15 @@
* along with this program. If not, see .
*/
-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";
-export function SettingsTab({ title, children }: PropsWithChildren<{ title: string; }>) {
+export function SettingsTab({ children }: PropsWithChildren) {
return (
-
+
);
}
diff --git a/src/components/settings/tabs/patchHelper/index.tsx b/src/components/settings/tabs/patchHelper/index.tsx
index 2d33a0293..bd99f5c8b 100644
--- a/src/components/settings/tabs/patchHelper/index.tsx
+++ b/src/components/settings/tabs/patchHelper/index.tsx
@@ -104,7 +104,7 @@ function PatchHelper() {
}
return (
-
+
Full patch
+
diff --git a/src/components/settings/tabs/sync/BackupAndRestoreTab.tsx b/src/components/settings/tabs/sync/BackupAndRestoreTab.tsx
index e4e8b001f..447c1f442 100644
--- a/src/components/settings/tabs/sync/BackupAndRestoreTab.tsx
+++ b/src/components/settings/tabs/sync/BackupAndRestoreTab.tsx
@@ -27,7 +27,7 @@ import { Button, Text } from "@webpack/common";
function BackupAndRestoreTab() {
return (
-
+
Warning
diff --git a/src/components/settings/tabs/sync/CloudTab.tsx b/src/components/settings/tabs/sync/CloudTab.tsx
index 73526bcdb..bab67a7fd 100644
--- a/src/components/settings/tabs/sync/CloudTab.tsx
+++ b/src/components/settings/tabs/sync/CloudTab.tsx
@@ -96,10 +96,8 @@ function CloudTab() {
const settings = useSettings(["cloud.authenticated", "cloud.url"]);
return (
-
+
- Cloud Settings
-
Vencord comes with a cloud integration that adds goodies like settings sync across devices.
It respects your privacy, and
@@ -118,7 +116,7 @@ function CloudTab() {
settings.cloud.authenticated = v;
}}
/>
- Backend URL
+ Backend URL
Which backend to use when using cloud integrations.
diff --git a/src/components/settings/tabs/themes/index.tsx b/src/components/settings/tabs/themes/index.tsx
index 7d2234459..2bc5e81eb 100644
--- a/src/components/settings/tabs/themes/index.tsx
+++ b/src/components/settings/tabs/themes/index.tsx
@@ -37,7 +37,7 @@ function ThemesTab() {
const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL);
return (
-
+
+
Themes are not supported on the Userscript!
diff --git a/src/components/settings/tabs/updater/index.tsx b/src/components/settings/tabs/updater/index.tsx
index 935f66125..c238d0772 100644
--- a/src/components/settings/tabs/updater/index.tsx
+++ b/src/components/settings/tabs/updater/index.tsx
@@ -44,9 +44,7 @@ function Updater() {
};
return (
-
- Updater Settings
-
+
+
{isDonor(user?.id)
? (
.
*/
-import { Settings } from "@api/Settings";
+import { definePluginSettings } from "@api/Settings";
+import { BackupRestoreIcon, CloudIcon, MainSettingsIcon, PaintbrushIcon, PatchHelperIcon, PlaceholderIcon, PluginsIcon, UpdaterIcon, VesktopSettingsIcon } from "@components/Icons";
import { BackupAndRestoreTab, CloudTab, PatchHelperTab, PluginsTab, ThemesTab, UpdaterTab, VencordTab } from "@components/settings/tabs";
import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord";
-import definePlugin, { OptionType } from "@utils/types";
+import { isTruthy } from "@utils/guards";
+import definePlugin, { IconProps, OptionType } from "@utils/types";
+import { waitFor } from "@webpack";
import { React } from "@webpack/common";
+import type { ComponentType, PropsWithChildren, ReactNode } from "react";
import gitHash from "~git-hash";
-type SectionType = "HEADER" | "DIVIDER" | "CUSTOM";
-type SectionTypes = Record;
+let LayoutTypes = {
+ SECTION: 1,
+ SIDEBAR_ITEM: 2,
+ PANEL: 3,
+ PANE: 4
+};
+waitFor(["SECTION", "SIDEBAR_ITEM", "PANEL", "PANE"], v => LayoutTypes = v);
+
+const FallbackSectionTypes = {
+ HEADER: "HEADER",
+ DIVIDER: "DIVIDER",
+ CUSTOM: "CUSTOM"
+};
+type SectionTypes = typeof FallbackSectionTypes;
+
+type SettingsLocation =
+ | "top"
+ | "aboveNitro"
+ | "belowNitro"
+ | "aboveActivity"
+ | "belowActivity"
+ | "bottom";
+
+interface SettingsLayoutNode {
+ type: number;
+ key?: string;
+ legacySearchKey?: string;
+ useLabel?(): string;
+ useTitle?(): string;
+ buildLayout?(): SettingsLayoutNode[];
+ icon?(): ReactNode;
+ render?(): ReactNode;
+}
+
+interface EntryOptions {
+ key: string,
+ title: string,
+ panelTitle?: string,
+ Component: ComponentType<{}>,
+ Icon: ComponentType;
+}
+interface SettingsLayoutBuilder {
+ key?: string;
+ buildLayout(): SettingsLayoutNode[];
+}
+
+const settings = definePluginSettings({
+ settingsLocation: {
+ type: OptionType.SELECT,
+ description: "Where to put the Vencord settings section",
+ options: [
+ { label: "At the very top", value: "top" },
+ { label: "Above the Nitro section", value: "aboveNitro", default: true },
+ { label: "Below the Nitro section", value: "belowNitro" },
+ { label: "Above Activity Settings", value: "aboveActivity" },
+ { label: "Below Activity Settings", value: "belowActivity" },
+ { label: "At the very bottom", value: "bottom" },
+ ] as { label: string; value: SettingsLocation; default?: boolean; }[]
+ }
+});
export default definePlugin({
name: "Settings",
@@ -34,19 +96,28 @@ export default definePlugin({
authors: [Devs.Ven, Devs.Megu],
required: true,
+ settings,
+
patches: [
{
find: ".versionHash",
replacement: [
{
- match: /\.info.+?\[\(0,\i\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/,
- replace: (m, component, props) => {
+ match: /\.compactInfo.+?(?=null!=(\i)&&(.{0,20}\i\.Text.{0,200}?,children:).{0,15}?("span"),({className:\i\.versionHash,children:\["Build Override: ",\1\.id\]\})\)\}\))/,
+ replace: (m, _buildOverride, makeRow, component, props) => {
props = props.replace(/children:\[.+\]/, "");
- return `${m},$self.makeInfoElements(${component}, ${props})`;
+ return `${m},$self.makeInfoElements(${component},${props}).map(e=>${makeRow}e})),`;
}
},
{
- match: /copyValue:\i\.join\(" "\)/,
+ match: /\.info.+?\[\(0,\i\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/,
+ replace: (m, component, props) => {
+ props = props.replace(/children:\[.+\]/, "");
+ return `${m},$self.makeInfoElements(${component},${props})`;
+ }
+ },
+ {
+ match: /copyValue:\i\.join\(" "\)/g,
replace: "$& + $self.getInfoString()"
}
]
@@ -73,15 +144,147 @@ export default definePlugin({
}
},
{
- find: "2025-09-user-settings-redesign-1",
+ find: ".buildLayout().map",
replacement: {
- match: /enabled:![01],showLegacyOpen:/g,
- replace: "enabled:false,showLegacyOpen:"
+ match: /(\i)\.buildLayout\(\)(?=\.map)/,
+ replace: "$self.buildLayout($1)"
}
}
],
+ buildEntry(options: EntryOptions): SettingsLayoutNode {
+ const { key, title, panelTitle = title, Component, Icon } = options;
+
+ return ({
+ key,
+ type: LayoutTypes.SIDEBAR_ITEM,
+ legacySearchKey: title.toUpperCase(),
+ useTitle: () => title,
+ icon: () => ,
+ buildLayout: () => [
+ {
+ key: key + "_panel",
+ type: LayoutTypes.PANEL,
+ useTitle: () => panelTitle,
+ buildLayout: () => [
+ {
+ key: key + "_pane",
+ type: LayoutTypes.PANE,
+ buildLayout: () => [],
+ render: () => ,
+ useTitle: () => panelTitle
+ }
+ ]
+ }
+ ]
+ });
+ },
+
+ buildLayout(originalLayoutBuilder: SettingsLayoutBuilder) {
+ const layout = originalLayoutBuilder.buildLayout();
+ if (originalLayoutBuilder.key !== "$Root") return layout;
+ if (!Array.isArray(layout)) return layout;
+
+ if (layout.some(s => s?.key === "vencord_section")) return layout;
+
+ const { buildEntry } = this;
+
+ const vencordEntries: SettingsLayoutNode[] = [
+ buildEntry({
+ key: "vencord_main",
+ title: "Vencord",
+ panelTitle: "Vencord Settings",
+ Component: VencordTab,
+ Icon: MainSettingsIcon
+ }),
+ buildEntry({
+ key: "vencord_plugins",
+ title: "Plugins",
+ Component: PluginsTab,
+ Icon: PluginsIcon
+ }),
+ buildEntry({
+ key: "vencord_themes",
+ title: "Themes",
+ Component: ThemesTab,
+ Icon: PaintbrushIcon
+ }),
+ !IS_UPDATER_DISABLED && UpdaterTab && buildEntry({
+ key: "vencord_updater",
+ title: "Updater",
+ panelTitle: "Vencord Updater",
+ Component: UpdaterTab,
+ Icon: UpdaterIcon
+ }),
+ buildEntry({
+ key: "vencord_cloud",
+ title: "Cloud",
+ panelTitle: "Vencord Cloud",
+ Component: CloudTab,
+ Icon: CloudIcon
+ }),
+ buildEntry({
+ key: "vencord_backup_restore",
+ title: "Backup & Restore",
+ Component: BackupAndRestoreTab,
+ Icon: BackupRestoreIcon
+ }),
+ IS_DEV && PatchHelperTab && buildEntry({
+ key: "vencord_patch_helper",
+ title: "Patch Helper",
+ Component: PatchHelperTab,
+ Icon: PatchHelperIcon
+ }),
+ ...this.customEntries.map(buildEntry),
+ // TODO: Remove deprecated customSections in a future update
+ ...this.customSections.map((func, i) => {
+ const { section, element, label } = func(FallbackSectionTypes);
+ if (Object.values(FallbackSectionTypes).includes(section)) return null;
+
+ return buildEntry({
+ key: `vencord_deprecated_custom_${section}`,
+ title: label,
+ Component: element,
+ Icon: section === "Vesktop" ? VesktopSettingsIcon : PlaceholderIcon
+ });
+ })
+ ].filter(isTruthy);
+
+ const vencordSection: SettingsLayoutNode = {
+ key: "vencord_section",
+ type: LayoutTypes.SECTION,
+ useLabel: () => "Vencord",
+ buildLayout: () => vencordEntries
+ };
+
+ const { settingsLocation } = settings.store;
+
+ const places: Record = {
+ top: "user_section",
+ aboveNitro: "billing_section",
+ belowNitro: "billing_section",
+ aboveActivity: "activity_section",
+ belowActivity: "activity_section",
+ bottom: "logout_section"
+ };
+
+ const key = places[settingsLocation] ?? places.top;
+ let idx = layout.findIndex(s => typeof s?.key === "string" && s.key === key);
+
+ if (idx === -1) {
+ idx = 2;
+ } else if (settingsLocation.startsWith("below")) {
+ idx += 1;
+ }
+
+ layout.splice(idx, 0, vencordSection);
+
+ return layout;
+ },
+
+ /** @deprecated Use customEntries */
customSections: [] as ((SectionTypes: SectionTypes) => any)[],
+ customEntries: [] as EntryOptions[],
makeSettingsCategories(SectionTypes: SectionTypes) {
return [
@@ -139,12 +342,12 @@ export default definePlugin({
].filter(Boolean);
},
- isRightSpot({ header, settings }: { header?: string; settings?: string[]; }) {
- const firstChild = settings?.[0];
+ isRightSpot({ header, settings: s }: { header?: string; settings?: string[]; }) {
+ const firstChild = s?.[0];
// lowest two elements... sanity backup
if (firstChild === "LOGOUT" || firstChild === "SOCIAL_LINKS") return true;
- const { settingsLocation } = Settings.plugins.Settings;
+ const { settingsLocation } = settings.store;
if (settingsLocation === "bottom") return firstChild === "LOGOUT";
if (settingsLocation === "belowActivity") return firstChild === "CHANGELOG";
@@ -152,7 +355,7 @@ export default definePlugin({
if (!header) return;
try {
- const names = {
+ const names: Record, string> = {
top: getIntlMessage("USER_SETTINGS"),
aboveNitro: getIntlMessage("BILLING_SETTINGS"),
belowNitro: getIntlMessage("APP_SETTINGS"),
@@ -182,31 +385,12 @@ export default definePlugin({
return (...args: any[]) => {
const elements = originalHook(...args);
if (!this.patchedSettings.has(elements))
- elements.unshift(...this.makeSettingsCategories({
- HEADER: "HEADER",
- DIVIDER: "DIVIDER",
- CUSTOM: "CUSTOM"
- }));
+ elements.unshift(...this.makeSettingsCategories(FallbackSectionTypes));
return elements;
};
},
- options: {
- settingsLocation: {
- type: OptionType.SELECT,
- description: "Where to put the Vencord settings section",
- options: [
- { label: "At the very top", value: "top" },
- { label: "Above the Nitro section", value: "aboveNitro", default: true },
- { label: "Below the Nitro section", value: "belowNitro" },
- { label: "Above Activity Settings", value: "aboveActivity" },
- { label: "Below Activity Settings", value: "belowActivity" },
- { label: "At the very bottom", value: "bottom" },
- ]
- },
- },
-
get electronVersion() {
return VencordNative.native.getVersions().electron || window.legcord?.electron || null;
},
@@ -245,7 +429,7 @@ export default definePlugin({
return "\n" + this.getInfoRows().join("\n");
},
- makeInfoElements(Component: React.ComponentType, props: React.PropsWithChildren) {
+ makeInfoElements(Component: ComponentType, props: PropsWithChildren) {
return this.getInfoRows().map((text, i) =>
{text}
);
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 737c5eb9a..e864c651e 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -612,6 +612,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
alfred: {
name: "alfred",
id: 1038466644353232967n
+ },
+ vv: {
+ name: "VV",
+ id: 254866377087778816n
}
} satisfies Record);