Plugin

BetterSettings

Enhances your settings-menu-opening experience

Appearance Customisation Organisation
index.tsx
Download

Source

src/plugins/betterSettings/index.tsx
1/*
2 * FiveCord — a Discord client mod
3 * Copyright (c) 2025 FiveCord
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7import { definePluginSettings } from "@api/Settings";
8import { disableStyle, enableStyle } from "@api/Styles";
9import { buildPluginMenuEntries, buildThemeMenuEntries } from "@plugins/vencordToolbox/menu";
10import { Devs } from "@utils/constants";
11import { classNameFactory } from "@utils/css";
12import { Logger } from "@utils/Logger";
13import definePlugin, { OptionType } from "@utils/types";
14import { findCssClassesLazy } from "@webpack";
15import { ComponentDispatch, FocusLock, Menu, useEffect, useRef } from "@webpack/common";
16import type { HTMLAttributes, ReactNode } from "react";
17
18import fullHeightStyle from "./fullHeightContext.css?managed";
19
20const cl = classNameFactory("");
21const Classes = findCssClassesLazy("animating", "baseLayer", "bg", "layer", "layers");
22
23const settings = definePluginSettings({
24 disableFade: {
25 description: "Disable the crossfade animation",
26 type: OptionType.BOOLEAN,
27 default: true,
28 restartNeeded: true
29 },
30 organizeMenu: {
31 description: "Organizes the settings cog context menu into categories",
32 type: OptionType.BOOLEAN,
33 default: true,
34 restartNeeded: true
35 },
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: true
41 }
42});
43
44interface LayerProps extends HTMLAttributes<HTMLDivElement> {
45 mode: "SHOWN" | "HIDDEN";
46 baseLayer?: boolean;
47}
48
49function 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 <div
60 ref={containerRef}
61 aria-hidden={hidden}
62 className={cl({
63 [Classes.layer]: true,
64 [Classes.baseLayer]: baseLayer,
65 "stop-animations": hidden
66 })}
67 style={{ opacity: hidden ? 0 : undefined }}
68 {...props}
69 />
70 );
71
72 return baseLayer
73 ? node
74 : <FocusLock containerRef={containerRef}>{node}</FocusLock>;
75}
76
77export 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.disableFade
101 },
102 { class="ts-cmt">// Lazy-load contents
103 match: /createPromise:\(\)=>([^:}]*?),webpackId:"?\d+"?,name:(?!="CollectiblesShop")"[^"]+"/g,
104 replace: "$&,_:$1",
105 predicate: () => settings.store.eagerLoad
106 }
107 ]
108 },
109 { class="ts-cmt">// For some reason standardSidebarView also has a small fade-in
110 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.disableFade
122 },
123 { class="ts-cmt">// Disable fade animations for settings menu
124 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.disableFade
136 },
137 { class="ts-cmt">// Disable initial and exit delay for settings menu
138 find: "headerId:void 0,headerIdIsManaged:!1",
139 replacement: {
140 match: /let (\i)=300/,
141 replace: "let $1=0"
142 },
143 predicate: () => settings.store.disableFade
144 },
145 { class="ts-cmt">// Load menu TOC eagerly
146 find: "handleOpenSettingsContextMenu=",
147 replacement: {
148 match: /(?=handleOpenSettingsContextMenu=.{0,100}?null!=\i&&.{0,100}?(await [^};]*?\)\)))/,
149 replace: "_vencordBetterSettingsEagerLoad=(async ()=>$1)();"
150 },
151 predicate: () => settings.store.eagerLoad
152 },
153 { class="ts-cmt">// Settings cog context menu
154 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 compat
159 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 ErrorBoundary
166 // without possibly also catching unrelated errors of children.
167 //
168 // Thus, we sanity check webpack modules
169 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