Plugin
BetterSettings
Enhances your settings-menu-opening experience
1
/*2
* FiveCord — a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
import { definePluginSettings } from "@api/Settings";8
import { disableStyle, enableStyle } from "@api/Styles";9
import { buildPluginMenuEntries, buildThemeMenuEntries } from "@plugins/vencordToolbox/menu";10
import { Devs } from "@utils/constants";11
import { classNameFactory } from "@utils/css";12
import { Logger } from "@utils/Logger";13
import definePlugin, { OptionType } from "@utils/types";14
import { findCssClassesLazy } from "@webpack";15
import { ComponentDispatch, FocusLock, Menu, useEffect, useRef } from "@webpack/common";16
import type { HTMLAttributes, ReactNode } from "react";17
18
import fullHeightStyle from "./fullHeightContext.css?managed";19
20
const cl = classNameFactory("");21
const Classes = findCssClassesLazy("animating", "baseLayer", "bg", "layer", "layers");22
23
const settings = definePluginSettings({24
disableFade: {25
description: "Disable the crossfade animation",26
type: OptionType.BOOLEAN,27
default: true,28
restartNeeded: true29
},30
organizeMenu: {31
description: "Organizes the settings cog context menu into categories",32
type: OptionType.BOOLEAN,33
default: true,34
restartNeeded: true35
},36
eagerLoad: {37
description: "Removes the loading delay when opening the menu for the first time",38
type: OptionType.BOOLEAN,39
default: true,40
restartNeeded: true41
}42
});43
44
interface LayerProps extends HTMLAttributes<HTMLDivElement> {45
mode: "SHOWN" | "HIDDEN";46
baseLayer?: boolean;47
}48
49
function Layer({ mode, baseLayer = false, ...props }: LayerProps) {50
const hidden = mode === "HIDDEN";51
const containerRef = useRef<HTMLDivElement>(null);52
53
useEffect(() => () => {54
ComponentDispatch.dispatch("LAYER_POP_START");55
ComponentDispatch.dispatch("LAYER_POP_COMPLETE");56
}, []);57
58
const node = (59
<div60
ref={containerRef}61
aria-hidden={hidden}62
className={cl({63
[Classes.layer]: true,64
[Classes.baseLayer]: baseLayer,65
"stop-animations": hidden66
})}67
style={{ opacity: hidden ? 0 : undefined }}68
{...props}69
/>70
);71
72
return baseLayer73
? node74
: <FocusLock containerRef={containerRef}>{node}</FocusLock>;75
}76
77
export default definePlugin({78
name: "BetterSettings",79
description: "Enhances your settings-menu-opening experience",80
authors: [Devs.Kyuuhachi],81
tags: ["Appearance", "Customisation", "Organisation"],82
settings,83
84
start() {85
if (settings.store.organizeMenu)86
enableStyle(fullHeightStyle);87
},88
89
stop() {90
disableStyle(fullHeightStyle);91
},92
93
patches: [94
{95
find: "this.renderArtisanalHack()",96
replacement: [97
{98
match: /class (\i)(?= extends \i\.PureComponent.+?static contextType=.+?jsx\)\(\1,\{mode:)/,99
replace: "var $1=$self.Layer;class VencordPatchedOldFadeLayer",100
predicate: () => settings.store.disableFade101
},102
{ class="ts-cmt">// Lazy-load contents103
match: /createPromise:\(\)=>([^:}]*?),webpackId:"?\d+"?,name:(?!="CollectiblesShop")"[^"]+"/g,104
replace: "$&,_:$1",105
predicate: () => settings.store.eagerLoad106
}107
]108
},109
{ class="ts-cmt">// For some reason standardSidebarView also has a small fade-in110
find: 039;minimal:"contentColumnMinimal"039;,111
replacement: [112
{113
match: /(?=\(0,\i\.\i\)\((\i),\{from:\{position:"absolute")/,114
replace: "(_cb=>_cb(void 0,$1))||"115
},116
{117
match: /\i\.animated\.div/,118
replace: 039;"div"039;119
}120
],121
predicate: () => settings.store.disableFade122
},123
{ class="ts-cmt">// Disable fade animations for settings menu124
find: 039;"data-mana-component":"layer-modal"039;,125
replacement: [126
{127
match: /(\i)\.animated\.div(?=,\{"data-mana-component":"layer-modal")/,128
replace: 039;"div"039;129
},130
{131
match: /(?<="data-mana-component":"layer-modal"[^}]*?)style:\i,/,132
replace: "style:{},"133
}134
],135
predicate: () => settings.store.disableFade136
},137
{ class="ts-cmt">// Disable initial and exit delay for settings menu138
find: "headerId:void 0,headerIdIsManaged:!1",139
replacement: {140
match: /let (\i)=300/,141
replace: "let $1=0"142
},143
predicate: () => settings.store.disableFade144
},145
{ class="ts-cmt">// Load menu TOC eagerly146
find: "handleOpenSettingsContextMenu=",147
replacement: {148
match: /(?=handleOpenSettingsContextMenu=.{0,100}?null!=\i&&.{0,100}?(await [^};]*?\)\)))/,149
replace: "_vencordBetterSettingsEagerLoad=(async ()=>$1)();"150
},151
predicate: () => settings.store.eagerLoad152
},153
{ class="ts-cmt">// Settings cog context menu154
find: "#{intl::USER_SETTINGS_ACTIONS_MENU_LABEL}",155
predicate: () => settings.store.organizeMenu,156
replacement: [157
{158
match: /children:\[(\i),(?<=\1=(?:function|.{0,30}\.openUserSettings).+?)/, class="ts-cmt">// TODO .{0,30}\.openUserSettings is stable compat159
replace: "children:[$self.transformSettingsEntries($1),",160
},161
]162
},163
],164
165
// This is the very outer layer of the entire ui, so we can't wrap this in an ErrorBoundary166
// without possibly also catching unrelated errors of children.167
//168
// Thus, we sanity check webpack modules169
Layer(props: LayerProps) {170
try {171
[FocusLock.$$vencordGetWrappedComponent(), ComponentDispatch, Classes.layer].forEach(e => e.test);172
} catch {173
new Logger("BetterSettings").error("Failed to find some components");174
return props.children;175
}176
177
return <Layer {...props} />;178
},179
180
transformSettingsEntries(list) {181
const items: ReactNode[] = [];182
183
for (const item of list) {184
const { key, props } = item;185
if (!props) continue;186
187
if (key === "vencord_plugins" || key === "vencord_themes") {188
const children = key === "vencord_plugins"189
? buildPluginMenuEntries()190
: buildThemeMenuEntries();191
192
items.push(193
<Menu.MenuItem key={key} label={props.label} id={props.label} {...props}>194
{children}195
</Menu.MenuItem>196
);197
} else if (key.endsWith("_section") && props.label) {198
items.push(199
<Menu.MenuItem key={key} label={props.label} id={props.label}>200
{this.transformSettingsEntries(props.children)}201
</Menu.MenuItem>202
);203
} else {204
items.push(item);205
}206
}207
208
return items;209
}210
});211