Plugin
Settings
Adds Settings UI and debug info
1
import { definePluginSettings } from "@api/Settings";2
import { BackupRestoreIcon, CloudIcon, MainSettingsIcon, PaintbrushIcon, PatchHelperIcon, PlaceholderIcon, PluginsIcon, UpdaterIcon, VesktopSettingsIcon } from "@components/Icons";3
import { BackupAndRestoreTab, CloudTab, PatchHelperTab, PluginsTab, ThemesTab, UpdaterTab, VencordTab } from "@components/settings/tabs";4
import { BRAND_NAME } from "@utils/branding";5
import { Devs } from "@utils/constants";6
import { isTruthy } from "@utils/guards";7
import definePlugin, { IconProps, OptionType } from "@utils/types";8
import { extractAndLoadChunksLazy, waitFor } from "@webpack";9
import { React } from "@webpack/common";10
import type { ComponentType, PropsWithChildren, ReactNode } from "react";11
12
import gitHash from "~git-hash";13
14
let LayoutTypes = {15
SECTION: 1,16
SIDEBAR_ITEM: 2,17
PANEL: 3,18
CATEGORY: 5,19
CUSTOM: 19,20
};21
waitFor(["SECTION", "SIDEBAR_ITEM", "PANEL", "CUSTOM"], v => LayoutTypes = v);22
23
const FallbackSectionTypes = {24
HEADER: "HEADER",25
DIVIDER: "DIVIDER",26
CUSTOM: "CUSTOM"27
};28
type SectionTypes = typeof FallbackSectionTypes;29
30
type SettingsLocation =31
| "top"32
| "aboveNitro"33
| "belowNitro"34
| "aboveActivity"35
| "belowActivity"36
| "bottom";37
38
interface SettingsLayoutNode {39
type: number;40
key?: string;41
legacySearchKey?: string;42
getLegacySearchKey?(): string;43
useLabel?(): string;44
useTitle?(): string;45
hideTitle?: boolean;46
buildLayout?(): SettingsLayoutNode[];47
icon?(): ReactNode;48
render?(): ReactNode;49
StronglyDiscouragedCustomComponent?(): ReactNode;50
}51
52
interface EntryOptions {53
key: string,54
title: string,55
panelTitle?: string,56
Component: ComponentType<{}>,57
Icon: ComponentType<IconProps>;58
}59
interface SettingsLayoutBuilder {60
key?: string;61
buildLayout(): SettingsLayoutNode[];62
}63
64
const settings = definePluginSettings({65
settingsLocation: {66
type: OptionType.SELECT,67
description: `Where to put the ${BRAND_NAME} settings section`,68
options: [69
{ label: "At the very top", value: "top" },70
{ label: "Above the Nitro section", value: "aboveNitro", default: true },71
{ label: "Below the Nitro section", value: "belowNitro" },72
{ label: "Above Activity Settings", value: "aboveActivity" },73
{ label: "Below Activity Settings", value: "belowActivity" },74
{ label: "At the very bottom", value: "bottom" },75
] as { label: string; value: SettingsLocation; default?: boolean; }[]76
},77
includeVencordInfoWhenCopying: {78
type: OptionType.BOOLEAN,79
description: `Also copy ${BRAND_NAME} info (${BRAND_NAME}, Electron, Chromium) when clicking the version info in the bottom left area of the Settings page`,80
default: true81
}82
});83
84
const preloadSettingsChunks = extractAndLoadChunksLazy([039;analyticsKey:"user_settings"039;]);85
86
export default definePlugin({87
name: "Settings",88
description: "Adds Settings UI and debug info",89
authors: [Devs.Ven, Devs.Megu],90
required: true,91
92
settings,93
94
start() {95
preloadSettingsChunks().catch(() => { });96
},97
98
patches: [99
{100
find: "#{intl::COPY_VERSION}",101
replacement: [102
{103
match: /"text-xxs\/normal".{0,300}?(?=null!=(\i)&&(.{0,20}\i\.\i.{0,200}?,children:).{0,15}?("span"),({className:\i\.\i,children:\["Build Override: ",\1\.id\]\})\)\}\))/,104
replace: (m, _buildOverride, makeRow, component, props) => {105
props = props.replace(/children:\[.+\]/, "");106
return `${m},$self.makeInfoElements(${component},${props}).map(e=>${makeRow}e})),`;107
}108
},109
{110
match: /copyValue:\i\.join\(" "\)/g,111
replace: "$& + $self.getInfoString()"112
}113
]114
},115
{116
// Canary 2026 settings footer (lazy chunk)117
find: 039;copyValue:d.join(" ")039;,118
replacement: {119
match: /copyValue:\i\.join\(" "\)/g,120
replace: "$& + $self.getInfoString()"121
}122
},123
{124
// Canary 2026 settings layout tree (lazy chunk 60813)125
find: 039;"buildLayout"in t&&"function"==typeof t.buildLayout039;,126
replacement: {127
match: /(\i)\.buildLayout\(\)(?=\.map)/,128
replace: "$self.buildLayout($1)"129
}130
},131
{132
find: ".buildLayout().map",133
replacement: {134
match: /(\i)\.buildLayout\(\)(?=\.map)/,135
replace: "$self.buildLayout($1)"136
}137
}138
],139
140
buildEntry(options: EntryOptions): SettingsLayoutNode {141
const { key, title, panelTitle = title, Component, Icon } = options;142
143
const panel: SettingsLayoutNode = {144
key: key + "_panel",145
type: LayoutTypes.PANEL,146
useTitle: () => panelTitle,147
buildLayout: () => [{148
type: LayoutTypes.CATEGORY,149
key: key + "_category",150
buildLayout: () => [{151
type: LayoutTypes.CUSTOM,152
key: key + "_custom",153
Component: Component,154
useSearchTerms: () => [title]155
}]156
}]157
};158
159
return ({160
key,161
type: LayoutTypes.SIDEBAR_ITEM,162
useTitle: () => title,163
icon: () => <Icon width={20} height={20} />,164
buildLayout: () => [panel]165
});166
},167
168
buildLayout(originalLayoutBuilder: SettingsLayoutBuilder) {169
const layout = originalLayoutBuilder.buildLayout();170
if (originalLayoutBuilder.key !== "$Root") return layout;171
if (!Array.isArray(layout)) return layout;172
173
if (layout.some(s => s?.key === "vencord_section")) return layout;174
175
const { buildEntry } = this;176
177
const vencordEntries: SettingsLayoutNode[] = [178
buildEntry({179
key: "vencord_main",180
title: BRAND_NAME,181
panelTitle: `${BRAND_NAME} Settings`,182
Component: VencordTab,183
Icon: MainSettingsIcon184
}),185
buildEntry({186
key: "vencord_plugins",187
title: "Plugins",188
Component: PluginsTab,189
Icon: PluginsIcon190
}),191
buildEntry({192
key: "vencord_themes",193
title: "Themes",194
Component: ThemesTab,195
Icon: PaintbrushIcon196
}),197
!IS_UPDATER_DISABLED && UpdaterTab && buildEntry({198
key: "vencord_updater",199
title: "Updater",200
panelTitle: `${BRAND_NAME} Updater`,201
Component: UpdaterTab,202
Icon: UpdaterIcon203
}),204
buildEntry({205
key: "vencord_cloud",206
title: "Cloud",207
panelTitle: `${BRAND_NAME} Cloud`,208
Component: CloudTab,209
Icon: CloudIcon210
}),211
buildEntry({212
key: "vencord_backup_restore",213
title: "Backup & Restore",214
Component: BackupAndRestoreTab,215
Icon: BackupRestoreIcon216
}),217
!IS_STANDALONE && PatchHelperTab && buildEntry({218
key: "vencord_patch_helper",219
title: "Patch Helper",220
Component: PatchHelperTab,221
Icon: PatchHelperIcon222
}),223
...this.customEntries.map(buildEntry),224
// TODO: Remove deprecated customSections in a future update225
...this.customSections.map((func, i) => {226
const { section, element, label } = func(FallbackSectionTypes);227
if (Object.values(FallbackSectionTypes).includes(section)) return null;228
229
return buildEntry({230
key: `vencord_deprecated_custom_${section}`,231
title: label,232
Component: element,233
Icon: section === "Vesktop" ? VesktopSettingsIcon : PlaceholderIcon234
});235
})236
].filter(isTruthy);237
238
const vencordSection: SettingsLayoutNode = {239
key: "vencord_section",240
type: LayoutTypes.SECTION,241
useTitle: () => `${BRAND_NAME} Settings`,242
hideTitle: true,243
buildLayout: () => vencordEntries244
};245
246
const { settingsLocation } = settings.store;247
248
const places: Record<SettingsLocation, string> = {249
top: "user_section",250
aboveNitro: "billing_section",251
belowNitro: "billing_section",252
aboveActivity: "games_and_apps_section",253
belowActivity: "games_and_apps_section",254
bottom: "utility_section"255
};256
257
const key = places[settingsLocation] ?? places.top;258
let idx = layout.findIndex(s => typeof s?.key === "string" && s.key === key);259
260
if (idx === -1 && settingsLocation.startsWith("above") && settingsLocation.includes("Activity")) {261
idx = layout.findIndex(s => s?.key === "activity_section");262
}263
if (idx === -1 && settingsLocation.startsWith("below") && settingsLocation.includes("Activity")) {264
idx = layout.findIndex(s => s?.key === "activity_section");265
}266
267
if (idx === -1) {268
idx = settingsLocation === "top" ? 1 : 2;269
} else if (settingsLocation.startsWith("below")) {270
idx += 1;271
}272
273
layout.splice(idx, 0, vencordSection);274
275
return layout;276
},277
278
/** @deprecated Use customEntries */279
customSections: [] as ((SectionTypes: SectionTypes) => any)[],280
customEntries: [] as EntryOptions[],281
282
get electronVersion() {283
return VencordNative.native.getVersions().electron || window.legcord?.electron || null;284
},285
286
get chromiumVersion() {287
try {288
return VencordNative.native.getVersions().chrome289
// @ts-expect-error Typescript will add userAgentData IMMEDIATELY290
|| navigator.userAgentData?.brands?.find(b => b.brand === "Chromium" || b.brand === "Google Chrome")?.version291
|| null;292
} catch { class="ts-cmt">// inb4 some stupid browser throws unsupported error for navigator.userAgentData, it039;s only in chromium293
return null;294
}295
},296
297
get additionalInfo() {298
if (IS_DEV) return " (Dev)";299
if (IS_WEB) return " (Web)";300
if (IS_VESKTOP) return ` (Vesktop v${VesktopNative.app.getVersion()})`;301
if (IS_STANDALONE) return " (Standalone)";302
return "";303
},304
305
getInfoRows() {306
const { electronVersion, chromiumVersion, additionalInfo } = this;307
308
const rows = [`${BRAND_NAME} ${gitHash}${additionalInfo}`];309
310
if (electronVersion) rows.push(`Electron ${electronVersion}`);311
if (chromiumVersion) rows.push(`Chromium ${chromiumVersion}`);312
313
return rows;314
},315
316
getInfoString() {317
if (!settings.store.includeVencordInfoWhenCopying) return "";318
return "\n" + this.getInfoRows().join("\n");319
},320
321
makeInfoElements(Component: ComponentType<PropsWithChildren>, props: PropsWithChildren) {322
return this.getInfoRows().map((text, i) =>323
<Component key={i} {...props}>{text}</Component>324
);325
}326
});327