Plugin

BetterSettings

Enhances your settings-menu-opening experience

Appearance Customisation Organisation
index.tsx
Download

Source

src/plugins/betterSettings/index.tsx
1import { definePluginSettings } from "@api/Settings";
2import { disableStyle, enableStyle } from "@api/Styles";
3import { buildPluginMenuEntries, buildThemeMenuEntries } from "@plugins/vencordToolbox/menu";
4import { Devs } from "@utils/constants";
5import { classNameFactory } from "@utils/css";
6import { Logger } from "@utils/Logger";
7import definePlugin, { OptionType } from "@utils/types";
8import { findCssClassesLazy } from "@webpack";
9import { ComponentDispatch, FocusLock, Menu, useEffect, useRef } from "@webpack/common";
10import type { HTMLAttributes, ReactNode } from "react";
11
12import fullHeightStyle from "./fullHeightContext.css?managed";
13
14const cl = classNameFactory("");
15const Classes = findCssClassesLazy("animating", "baseLayer", "bg", "layer", "layers");
16
17const settings = definePluginSettings({
18 disableFade: {
19 description: "Disable the crossfade animation",
20 type: OptionType.BOOLEAN,
21 default: true,
22 restartNeeded: true
23 },
24 organizeMenu: {
25 description: "Organizes the settings cog context menu into categories",
26 type: OptionType.BOOLEAN,
27 default: true,
28 restartNeeded: true
29 },
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: true
35 }
36});
37
38interface LayerProps extends HTMLAttributes<HTMLDivElement> {
39 mode: "SHOWN" | "HIDDEN";
40 baseLayer?: boolean;
41}
42
43function 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 <div
54 ref={containerRef}
55 aria-hidden={hidden}
56 className={cl({
57 [Classes.layer]: true,
58 [Classes.baseLayer]: baseLayer,
59 "stop-animations": hidden
60 })}
61 style={{ opacity: hidden ? 0 : undefined }}
62 {...props}
63 />
64 );
65
66 return baseLayer
67 ? node
68 : <FocusLock containerRef={containerRef}>{node}</FocusLock>;
69}
70
71export 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.disableFade
95 },
96 { class="ts-cmt">// Lazy-load contents
97 match: /createPromise:\(\)=>([^:}]*?),webpackId:"?\d+"?,name:(?!="CollectiblesShop")"[^"]+"/g,
98 replace: "$&,_:$1",
99 predicate: () => settings.store.eagerLoad
100 }
101 ]
102 },
103 { class="ts-cmt">// For some reason standardSidebarView also has a small fade-in
104 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.disableFade
116 },
117 { class="ts-cmt">// Disable fade animations for settings menu
118 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.disableFade
130 },
131 { class="ts-cmt">// Disable initial and exit delay for settings menu
132 find: "headerId:void 0,headerIdIsManaged:!1",
133 replacement: {
134 match: /let (\i)=300/,
135 replace: "let $1=0"
136 },
137 predicate: () => settings.store.disableFade
138 },
139 { class="ts-cmt">// Load menu TOC eagerly
140 find: "handleOpenSettingsContextMenu=",
141 replacement: {
142 match: /(?=handleOpenSettingsContextMenu=.{0,100}?null!=\i&&.{0,100}?(await [^};]*?\)\)))/,
143 replace: "_vencordBetterSettingsEagerLoad=(async ()=>$1)();"
144 },
145 predicate: () => settings.store.eagerLoad
146 },
147 { class="ts-cmt">// Settings cog context menu
148 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 compat
153 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 ErrorBoundary
160 // without possibly also catching unrelated errors of children.
161 //
162 // Thus, we sanity check webpack modules
163 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