Plugin

Settings

Adds Settings UI and debug info

index.tsx
Download

Source

src/plugins/_core/settings/index.tsx
1import { definePluginSettings } from "@api/Settings";
2import { BackupRestoreIcon, CloudIcon, MainSettingsIcon, PaintbrushIcon, PatchHelperIcon, PlaceholderIcon, PluginsIcon, UpdaterIcon, VesktopSettingsIcon } from "@components/Icons";
3import { BackupAndRestoreTab, CloudTab, PatchHelperTab, PluginsTab, ThemesTab, UpdaterTab, VencordTab } from "@components/settings/tabs";
4import { BRAND_NAME } from "@utils/branding";
5import { Devs } from "@utils/constants";
6import { isTruthy } from "@utils/guards";
7import definePlugin, { IconProps, OptionType } from "@utils/types";
8import { extractAndLoadChunksLazy, waitFor } from "@webpack";
9import { React } from "@webpack/common";
10import type { ComponentType, PropsWithChildren, ReactNode } from "react";
11
12import gitHash from "~git-hash";
13
14let LayoutTypes = {
15 SECTION: 1,
16 SIDEBAR_ITEM: 2,
17 PANEL: 3,
18 CATEGORY: 5,
19 CUSTOM: 19,
20};
21waitFor(["SECTION", "SIDEBAR_ITEM", "PANEL", "CUSTOM"], v => LayoutTypes = v);
22
23const FallbackSectionTypes = {
24 HEADER: "HEADER",
25 DIVIDER: "DIVIDER",
26 CUSTOM: "CUSTOM"
27};
28type SectionTypes = typeof FallbackSectionTypes;
29
30type SettingsLocation =
31 | "top"
32 | "aboveNitro"
33 | "belowNitro"
34 | "aboveActivity"
35 | "belowActivity"
36 | "bottom";
37
38interface 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
52interface EntryOptions {
53 key: string,
54 title: string,
55 panelTitle?: string,
56 Component: ComponentType<{}>,
57 Icon: ComponentType<IconProps>;
58}
59interface SettingsLayoutBuilder {
60 key?: string;
61 buildLayout(): SettingsLayoutNode[];
62}
63
64const 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: true
81 }
82});
83
84const preloadSettingsChunks = extractAndLoadChunksLazy([&#039;analyticsKey:"user_settings"&#039;]);
85
86export 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.buildLayout&#039;,
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: MainSettingsIcon
184 }),
185 buildEntry({
186 key: "vencord_plugins",
187 title: "Plugins",
188 Component: PluginsTab,
189 Icon: PluginsIcon
190 }),
191 buildEntry({
192 key: "vencord_themes",
193 title: "Themes",
194 Component: ThemesTab,
195 Icon: PaintbrushIcon
196 }),
197 !IS_UPDATER_DISABLED && UpdaterTab && buildEntry({
198 key: "vencord_updater",
199 title: "Updater",
200 panelTitle: `${BRAND_NAME} Updater`,
201 Component: UpdaterTab,
202 Icon: UpdaterIcon
203 }),
204 buildEntry({
205 key: "vencord_cloud",
206 title: "Cloud",
207 panelTitle: `${BRAND_NAME} Cloud`,
208 Component: CloudTab,
209 Icon: CloudIcon
210 }),
211 buildEntry({
212 key: "vencord_backup_restore",
213 title: "Backup & Restore",
214 Component: BackupAndRestoreTab,
215 Icon: BackupRestoreIcon
216 }),
217 !IS_STANDALONE && PatchHelperTab && buildEntry({
218 key: "vencord_patch_helper",
219 title: "Patch Helper",
220 Component: PatchHelperTab,
221 Icon: PatchHelperIcon
222 }),
223 ...this.customEntries.map(buildEntry),
224 // TODO: Remove deprecated customSections in a future update
225 ...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 : PlaceholderIcon
234 });
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: () => vencordEntries
244 };
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().chrome
289 // @ts-expect-error Typescript will add userAgentData IMMEDIATELY
290 || navigator.userAgentData?.brands?.find(b => b.brand === "Chromium" || b.brand === "Google Chrome")?.version
291 || null;
292 } catch { class="ts-cmt">// inb4 some stupid browser throws unsupported error for navigator.userAgentData, it&#039;s only in chromium
293 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