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