Plugin

Settings

Adds Settings UI and debug info

index.tsx
Download

Source

src/plugins/_core/settings/index.tsx
1import { BRAND_NAME } from "@utils/branding";
2import { definePluginSettings } from "@api/Settings";
3import { BackupRestoreIcon, CloudIcon, MainSettingsIcon, PaintbrushIcon, PatchHelperIcon, PlaceholderIcon, PluginsIcon, UpdaterIcon, VesktopSettingsIcon } from "@components/Icons";
4import { BackupAndRestoreTab, CloudTab, PatchHelperTab, PluginsTab, ThemesTab, UpdaterTab, VencordTab } from "@components/settings/tabs";
5import { Devs } from "@utils/constants";
6import { isTruthy } from "@utils/guards";
7import definePlugin, { IconProps, OptionType } from "@utils/types";
8import { 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 buildLayout?(): SettingsLayoutNode[];
46 icon?(): ReactNode;
47 render?(): ReactNode;
48 StronglyDiscouragedCustomComponent?(): ReactNode;
49}
50
51interface EntryOptions {
52 key: string,
53 title: string,
54 panelTitle?: string,
55 Component: ComponentType<{}>,
56 Icon: ComponentType<IconProps>;
57}
58interface SettingsLayoutBuilder {
59 key?: string;
60 buildLayout(): SettingsLayoutNode[];
61}
62
63const settings = definePluginSettings({
64 settingsLocation: {
65 type: OptionType.SELECT,
66 description: `Where to put the ${BRAND_NAME} settings section`,
67 options: [
68 { label: "At the very top", value: "top" },
69 { label: "Above the Nitro section", value: "aboveNitro", default: true },
70 { label: "Below the Nitro section", value: "belowNitro" },
71 { label: "Above Activity Settings", value: "aboveActivity" },
72 { label: "Below Activity Settings", value: "belowActivity" },
73 { label: "At the very bottom", value: "bottom" },
74 ] as { label: string; value: SettingsLocation; default?: boolean; }[]
75 },
76 includeVencordInfoWhenCopying: {
77 type: OptionType.BOOLEAN,
78 description: "Also copy Vencord info (Vencord, Electron, Chromium) when clicking the version info in the bottom left area of the Settings page",
79 default: true
80 }
81});
82
83export default definePlugin({
84 name: "Settings",
85 description: "Adds Settings UI and debug info",
86 authors: [Devs.Ven, Devs.Megu],
87 required: true,
88
89 settings,
90
91 patches: [
92 {
93 find: "#{intl::COPY_VERSION}",
94 replacement: [
95 {
96 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\]\})\)\}\))/,
97 replace: (m, _buildOverride, makeRow, component, props) => {
98 props = props.replace(/children:\[.+\]/, "");
99 return `${m},$self.makeInfoElements(${component},${props}).map(e=>${makeRow}e})),`;
100 }
101 },
102 {
103 match: /copyValue:\i\.join\(" "\)/g,
104 replace: "$& + $self.getInfoString()"
105 }
106 ]
107 },
108 {
109 find: ".buildLayout().map",
110 replacement: {
111 match: /(\i)\.buildLayout\(\)(?=\.map)/,
112 replace: "$self.buildLayout($1)"
113 }
114 }
115 ],
116
117 buildEntry(options: EntryOptions): SettingsLayoutNode {
118 const { key, title, panelTitle = title, Component, Icon } = options;
119
120 const panel: SettingsLayoutNode = {
121 key: key + "_panel",
122 type: LayoutTypes.PANEL,
123 useTitle: () => panelTitle,
124 buildLayout: () => [{
125 type: LayoutTypes.CATEGORY,
126 key: key + "_category",
127 buildLayout: () => [{
128 type: LayoutTypes.CUSTOM,
129 key: key + "_custom",
130 Component: Component,
131 useSearchTerms: () => [title]
132 }]
133 }]
134 };
135
136 return ({
137 key,
138 type: LayoutTypes.SIDEBAR_ITEM,
139 useTitle: () => title,
140 icon: () => <Icon width={20} height={20} />,
141 buildLayout: () => [panel]
142 });
143 },
144
145 buildLayout(originalLayoutBuilder: SettingsLayoutBuilder) {
146 const layout = originalLayoutBuilder.buildLayout();
147 if (originalLayoutBuilder.key !== "$Root") return layout;
148 if (!Array.isArray(layout)) return layout;
149
150 if (layout.some(s => s?.key === "vencord_section")) return layout;
151
152 const { buildEntry } = this;
153
154 const vencordEntries: SettingsLayoutNode[] = [
155 buildEntry({
156 key: "vencord_main",
157 title: BRAND_NAME,
158 panelTitle: `${BRAND_NAME} Settings`,
159 Component: VencordTab,
160 Icon: MainSettingsIcon
161 }),
162 buildEntry({
163 key: "vencord_plugins",
164 title: "Plugins",
165 Component: PluginsTab,
166 Icon: PluginsIcon
167 }),
168 buildEntry({
169 key: "vencord_themes",
170 title: "Themes",
171 Component: ThemesTab,
172 Icon: PaintbrushIcon
173 }),
174 !IS_UPDATER_DISABLED && UpdaterTab && buildEntry({
175 key: "vencord_updater",
176 title: "Updater",
177 panelTitle: `${BRAND_NAME} Updater`,
178 Component: UpdaterTab,
179 Icon: UpdaterIcon
180 }),
181 buildEntry({
182 key: "vencord_cloud",
183 title: "Cloud",
184 panelTitle: `${BRAND_NAME} Cloud`,
185 Component: CloudTab,
186 Icon: CloudIcon
187 }),
188 buildEntry({
189 key: "vencord_backup_restore",
190 title: "Backup & Restore",
191 Component: BackupAndRestoreTab,
192 Icon: BackupRestoreIcon
193 }),
194 !IS_STANDALONE && PatchHelperTab && buildEntry({
195 key: "vencord_patch_helper",
196 title: "Patch Helper",
197 Component: PatchHelperTab,
198 Icon: PatchHelperIcon
199 }),
200 ...this.customEntries.map(buildEntry),
201 // TODO: Remove deprecated customSections in a future update
202 ...this.customSections.map((func, i) => {
203 const { section, element, label } = func(FallbackSectionTypes);
204 if (Object.values(FallbackSectionTypes).includes(section)) return null;
205
206 return buildEntry({
207 key: `vencord_deprecated_custom_${section}`,
208 title: label,
209 Component: element,
210 Icon: section === "Vesktop" ? VesktopSettingsIcon : PlaceholderIcon
211 });
212 })
213 ].filter(isTruthy);
214
215 const vencordSection: SettingsLayoutNode = {
216 key: "vencord_section",
217 type: LayoutTypes.SECTION,
218 useTitle: () => `${BRAND_NAME} Settings`,
219 buildLayout: () => vencordEntries
220 };
221
222 const { settingsLocation } = settings.store;
223
224 const places: Record<SettingsLocation, string> = {
225 top: "user_section",
226 aboveNitro: "billing_section",
227 belowNitro: "billing_section",
228 aboveActivity: "activity_section",
229 belowActivity: "activity_section",
230 bottom: "utility_section"
231 };
232
233 const key = places[settingsLocation] ?? places.top;
234 let idx = layout.findIndex(s => typeof s?.key === "string" && s.key === key);
235
236 if (idx === -1) {
237 idx = 2;
238 } else if (settingsLocation.startsWith("below")) {
239 idx += 1;
240 }
241
242 layout.splice(idx, 0, vencordSection);
243
244 return layout;
245 },
246
247 /** @deprecated Use customEntries */
248 customSections: [] as ((SectionTypes: SectionTypes) => any)[],
249 customEntries: [] as EntryOptions[],
250
251 get electronVersion() {
252 return VencordNative.native.getVersions().electron || window.legcord?.electron || null;
253 },
254
255 get chromiumVersion() {
256 try {
257 return VencordNative.native.getVersions().chrome
258 // @ts-expect-error Typescript will add userAgentData IMMEDIATELY
259 || navigator.userAgentData?.brands?.find(b => b.brand === "Chromium" || b.brand === "Google Chrome")?.version
260 || null;
261 } catch { class="ts-cmt">// inb4 some stupid browser throws unsupported error for navigator.userAgentData, it&#039;s only in chromium
262 return null;
263 }
264 },
265
266 get additionalInfo() {
267 if (IS_DEV) return " (Dev)";
268 if (IS_WEB) return " (Web)";
269 if (IS_VESKTOP) return ` (Vesktop v${VesktopNative.app.getVersion()})`;
270 if (IS_STANDALONE) return " (Standalone)";
271 return "";
272 },
273
274 getInfoRows() {
275 const { electronVersion, chromiumVersion, additionalInfo } = this;
276
277 const rows = [`${BRAND_NAME} ${gitHash}${additionalInfo}`];
278
279 if (electronVersion) rows.push(`Electron ${electronVersion}`);
280 if (chromiumVersion) rows.push(`Chromium ${chromiumVersion}`);
281
282 return rows;
283 },
284
285 getInfoString() {
286 if (!settings.store.includeVencordInfoWhenCopying) return "";
287 return "\n" + this.getInfoRows().join("\n");
288 },
289
290 makeInfoElements(Component: ComponentType<PropsWithChildren>, props: PropsWithChildren) {
291 return this.getInfoRows().map((text, i) =>
292 <Component key={i} {...props}>{text}</Component>
293 );
294 }
295});
296