Plugin
Settings
Adds Settings UI and debug info
1
import { BRAND_NAME } from "@utils/branding";2
import { definePluginSettings } from "@api/Settings";3
import { BackupRestoreIcon, CloudIcon, MainSettingsIcon, PaintbrushIcon, PatchHelperIcon, PlaceholderIcon, PluginsIcon, UpdaterIcon, VesktopSettingsIcon } from "@components/Icons";4
import { BackupAndRestoreTab, CloudTab, PatchHelperTab, PluginsTab, ThemesTab, UpdaterTab, VencordTab } from "@components/settings/tabs";5
import { Devs } from "@utils/constants";6
import { isTruthy } from "@utils/guards";7
import definePlugin, { IconProps, OptionType } from "@utils/types";8
import { 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
buildLayout?(): SettingsLayoutNode[];46
icon?(): ReactNode;47
render?(): ReactNode;48
StronglyDiscouragedCustomComponent?(): ReactNode;49
}50
51
interface EntryOptions {52
key: string,53
title: string,54
panelTitle?: string,55
Component: ComponentType<{}>,56
Icon: ComponentType<IconProps>;57
}58
interface SettingsLayoutBuilder {59
key?: string;60
buildLayout(): SettingsLayoutNode[];61
}62
63
const 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: true80
}81
});82
83
export 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: MainSettingsIcon161
}),162
buildEntry({163
key: "vencord_plugins",164
title: "Plugins",165
Component: PluginsTab,166
Icon: PluginsIcon167
}),168
buildEntry({169
key: "vencord_themes",170
title: "Themes",171
Component: ThemesTab,172
Icon: PaintbrushIcon173
}),174
!IS_UPDATER_DISABLED && UpdaterTab && buildEntry({175
key: "vencord_updater",176
title: "Updater",177
panelTitle: `${BRAND_NAME} Updater`,178
Component: UpdaterTab,179
Icon: UpdaterIcon180
}),181
buildEntry({182
key: "vencord_cloud",183
title: "Cloud",184
panelTitle: `${BRAND_NAME} Cloud`,185
Component: CloudTab,186
Icon: CloudIcon187
}),188
buildEntry({189
key: "vencord_backup_restore",190
title: "Backup & Restore",191
Component: BackupAndRestoreTab,192
Icon: BackupRestoreIcon193
}),194
!IS_STANDALONE && PatchHelperTab && buildEntry({195
key: "vencord_patch_helper",196
title: "Patch Helper",197
Component: PatchHelperTab,198
Icon: PatchHelperIcon199
}),200
...this.customEntries.map(buildEntry),201
// TODO: Remove deprecated customSections in a future update202
...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 : PlaceholderIcon211
});212
})213
].filter(isTruthy);214
215
const vencordSection: SettingsLayoutNode = {216
key: "vencord_section",217
type: LayoutTypes.SECTION,218
useTitle: () => `${BRAND_NAME} Settings`,219
buildLayout: () => vencordEntries220
};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().chrome258
// @ts-expect-error Typescript will add userAgentData IMMEDIATELY259
|| navigator.userAgentData?.brands?.find(b => b.brand === "Chromium" || b.brand === "Google Chrome")?.version260
|| null;261
} catch { class="ts-cmt">// inb4 some stupid browser throws unsupported error for navigator.userAgentData, it039;s only in chromium262
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