2024-03-16 02:19:26 +01:00
|
|
|
/*
|
|
|
|
|
* Vencord, a Discord client mod
|
|
|
|
|
* Copyright (c) 2024 Vendicated and contributors
|
|
|
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { definePluginSettings } from "@api/Settings";
|
2026-01-22 20:32:49 -05:00
|
|
|
import { disableStyle, enableStyle } from "@api/Styles";
|
2025-11-21 21:39:38 -05:00
|
|
|
import { buildPluginMenuEntries, buildThemeMenuEntries } from "@equicordplugins/equicordToolbox/menu";
|
2026-02-20 15:55:23 -05:00
|
|
|
import SettingsPlugin from "@plugins/_core/settings";
|
2024-03-16 02:19:26 +01:00
|
|
|
import { Devs } from "@utils/constants";
|
2026-01-22 20:32:49 -05:00
|
|
|
import { classNameFactory } from "@utils/css";
|
2024-05-08 23:42:04 +02:00
|
|
|
import { Logger } from "@utils/Logger";
|
2024-03-16 02:19:26 +01:00
|
|
|
import definePlugin, { OptionType } from "@utils/types";
|
2026-01-23 12:59:32 -05:00
|
|
|
import { findCssClassesLazy } from "@webpack";
|
2024-11-05 16:49:27 -03:00
|
|
|
import { ComponentDispatch, FocusLock, Menu, useEffect, useRef } from "@webpack/common";
|
2024-03-16 02:19:26 +01:00
|
|
|
import type { HTMLAttributes, ReactElement } from "react";
|
|
|
|
|
|
2025-12-06 23:43:30 +01:00
|
|
|
import fullHeightStyle from "./fullHeightContext.css?managed";
|
|
|
|
|
|
2026-02-20 15:55:23 -05:00
|
|
|
type SettingsEntry = {
|
|
|
|
|
ariaLabel?: string;
|
|
|
|
|
element?: any;
|
|
|
|
|
label?: string;
|
|
|
|
|
newIndicator?: any;
|
|
|
|
|
newIndicatorDismissibleContentTypes?: number[];
|
|
|
|
|
predicate?: () => boolean;
|
|
|
|
|
searchableTitles?: string[];
|
|
|
|
|
section: string,
|
|
|
|
|
tabPredicate?: () => boolean;
|
|
|
|
|
};
|
2024-03-16 02:19:26 +01:00
|
|
|
|
|
|
|
|
const cl = classNameFactory("");
|
2026-01-23 12:59:32 -05:00
|
|
|
const Classes = findCssClassesLazy("animating", "baseLayer", "bg", "layer", "layers");
|
2024-03-16 02:19:26 +01:00
|
|
|
|
|
|
|
|
const settings = definePluginSettings({
|
|
|
|
|
disableFade: {
|
|
|
|
|
description: "Disable the crossfade animation",
|
|
|
|
|
type: OptionType.BOOLEAN,
|
|
|
|
|
default: true,
|
|
|
|
|
restartNeeded: true
|
|
|
|
|
},
|
|
|
|
|
organizeMenu: {
|
|
|
|
|
description: "Organizes the settings cog context menu into categories",
|
|
|
|
|
type: OptionType.BOOLEAN,
|
2025-10-06 18:23:50 -04:00
|
|
|
default: true,
|
|
|
|
|
restartNeeded: true
|
2024-03-16 02:19:26 +01:00
|
|
|
},
|
|
|
|
|
eagerLoad: {
|
|
|
|
|
description: "Removes the loading delay when opening the menu for the first time",
|
|
|
|
|
type: OptionType.BOOLEAN,
|
|
|
|
|
default: true,
|
|
|
|
|
restartNeeded: true
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-06 23:43:30 +01:00
|
|
|
interface TransformedSettingsEntry {
|
|
|
|
|
section: string;
|
|
|
|
|
items: SettingsEntry[];
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-16 02:19:26 +01:00
|
|
|
interface LayerProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
|
|
mode: "SHOWN" | "HIDDEN";
|
|
|
|
|
baseLayer?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Layer({ mode, baseLayer = false, ...props }: LayerProps) {
|
|
|
|
|
const hidden = mode === "HIDDEN";
|
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => () => {
|
|
|
|
|
ComponentDispatch.dispatch("LAYER_POP_START");
|
|
|
|
|
ComponentDispatch.dispatch("LAYER_POP_COMPLETE");
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const node = (
|
|
|
|
|
<div
|
|
|
|
|
ref={containerRef}
|
|
|
|
|
aria-hidden={hidden}
|
|
|
|
|
className={cl({
|
|
|
|
|
[Classes.layer]: true,
|
|
|
|
|
[Classes.baseLayer]: baseLayer,
|
|
|
|
|
"stop-animations": hidden
|
|
|
|
|
})}
|
|
|
|
|
style={{ opacity: hidden ? 0 : undefined }}
|
|
|
|
|
{...props}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return baseLayer
|
|
|
|
|
? node
|
|
|
|
|
: <FocusLock containerRef={containerRef}>{node}</FocusLock>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default definePlugin({
|
|
|
|
|
name: "BetterSettings",
|
|
|
|
|
description: "Enhances your settings-menu-opening experience",
|
|
|
|
|
authors: [Devs.Kyuuhachi],
|
|
|
|
|
settings,
|
|
|
|
|
|
2025-12-06 23:43:30 +01:00
|
|
|
start() {
|
|
|
|
|
if (settings.store.organizeMenu)
|
|
|
|
|
enableStyle(fullHeightStyle);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
stop() {
|
|
|
|
|
disableStyle(fullHeightStyle);
|
|
|
|
|
},
|
|
|
|
|
|
2024-03-16 02:19:26 +01:00
|
|
|
patches: [
|
|
|
|
|
{
|
|
|
|
|
find: "this.renderArtisanalHack()",
|
|
|
|
|
replacement: [
|
2026-02-05 16:50:16 -05:00
|
|
|
{
|
|
|
|
|
match: /class (\i)(?= extends \i\.PureComponent.+?static contextType=.+?jsx\)\(\1,\{mode:)/,
|
|
|
|
|
replace: "var $1=$self.Layer;class VencordPatchedOldFadeLayer",
|
2024-03-16 02:19:26 +01:00
|
|
|
predicate: () => settings.store.disableFade
|
|
|
|
|
},
|
2025-10-06 18:23:50 -04:00
|
|
|
{ // Lazy-load contents
|
2024-06-19 07:16:29 +02:00
|
|
|
match: /createPromise:\(\)=>([^:}]*?),webpackId:"?\d+"?,name:(?!="CollectiblesShop")"[^"]+"/g,
|
2024-03-16 02:19:26 +01:00
|
|
|
replace: "$&,_:$1",
|
|
|
|
|
predicate: () => settings.store.eagerLoad
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
2025-10-06 18:23:50 -04:00
|
|
|
{ // For some reason standardSidebarView also has a small fade-in
|
2024-06-19 03:04:15 +02:00
|
|
|
find: 'minimal:"contentColumnMinimal"',
|
2024-03-16 02:19:26 +01:00
|
|
|
replacement: [
|
|
|
|
|
{
|
2025-01-29 01:04:36 -03:00
|
|
|
match: /(?=\(0,\i\.\i\)\((\i),\{from:\{position:"absolute")/,
|
|
|
|
|
replace: "(_cb=>_cb(void 0,$1))||"
|
2024-03-16 02:19:26 +01:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
match: /\i\.animated\.div/,
|
|
|
|
|
replace: '"div"'
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
predicate: () => settings.store.disableFade
|
|
|
|
|
},
|
2026-02-26 02:07:09 +00:00
|
|
|
{ // Disable fade animations for settings menu
|
2026-02-26 04:29:47 +01:00
|
|
|
find: '"data-mana-component":"layer-modal"',
|
2026-02-26 02:07:09 +00:00
|
|
|
replacement: [
|
|
|
|
|
{
|
|
|
|
|
match: /(\i)\.animated\.div(?=,\{"data-mana-component":"layer-modal")/,
|
|
|
|
|
replace: '"div"'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
match: /(?<="data-mana-component":"layer-modal"[^}]*?)style:\i,/,
|
|
|
|
|
replace: "style:{},"
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
predicate: () => settings.store.disableFade
|
|
|
|
|
},
|
|
|
|
|
{ // Disable initial and exit delay for settings menu
|
|
|
|
|
find: "headerId:void 0,headerIdIsManaged:!1",
|
|
|
|
|
replacement: {
|
|
|
|
|
match: /let (\i)=300/,
|
|
|
|
|
replace: "let $1=0"
|
|
|
|
|
},
|
|
|
|
|
predicate: () => settings.store.disableFade
|
|
|
|
|
},
|
2024-03-28 10:17:05 -03:00
|
|
|
{ // Load menu TOC eagerly
|
2026-02-20 09:20:43 +01:00
|
|
|
find: "handleOpenSettingsContextMenu=",
|
2024-03-16 02:19:26 +01:00
|
|
|
replacement: {
|
2026-02-04 18:25:48 -05:00
|
|
|
match: /(?=handleOpenSettingsContextMenu=.{0,100}?null!=\i&&.{0,100}?(await [^};]*?\)\)))/,
|
|
|
|
|
replace: "_vencordBetterSettingsEagerLoad=(async ()=>$1)();"
|
2024-03-16 02:19:26 +01:00
|
|
|
},
|
|
|
|
|
predicate: () => settings.store.eagerLoad
|
|
|
|
|
},
|
2026-02-26 02:07:09 +00:00
|
|
|
{ // Settings cog context menu
|
2024-11-05 16:49:27 -03:00
|
|
|
find: "#{intl::USER_SETTINGS_ACTIONS_MENU_LABEL}",
|
2024-09-17 17:40:11 +02:00
|
|
|
replacement: [
|
|
|
|
|
{
|
2026-02-06 03:46:25 +01:00
|
|
|
match: /=\[\];(\i)(?=\.forEach.{0,200}?"logout"===\i.{0,100}?(\i)\.get\(\i\))/,
|
2026-01-23 12:59:32 -05:00
|
|
|
replace: "=$self.wrapMap([]);$self.transformSettingsEntries($1,$2)",
|
2024-09-17 17:40:11 +02:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
match: /case \i\.\i\.DEVELOPER_OPTIONS:return \i;/,
|
2026-02-20 15:55:23 -05:00
|
|
|
replace: "$&case 'EquicordPlugins':return $self.buildPluginMenuEntries(true);case 'EquicordThemes':return $self.buildThemeMenuEntries();"
|
2024-09-17 17:40:11 +02:00
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
2024-03-16 02:19:26 +01:00
|
|
|
],
|
|
|
|
|
|
2025-11-21 13:04:09 +01:00
|
|
|
buildPluginMenuEntries,
|
|
|
|
|
buildThemeMenuEntries,
|
2024-09-17 17:40:11 +02:00
|
|
|
|
2024-05-08 23:42:04 +02:00
|
|
|
// This is the very outer layer of the entire ui, so we can't wrap this in an ErrorBoundary
|
|
|
|
|
// without possibly also catching unrelated errors of children.
|
|
|
|
|
//
|
2025-04-04 00:12:17 -04:00
|
|
|
// Thus, we sanity check webpack modules
|
2024-03-16 02:19:26 +01:00
|
|
|
Layer(props: LayerProps) {
|
2025-04-04 00:12:17 -04:00
|
|
|
try {
|
2026-01-23 17:52:36 -03:00
|
|
|
[FocusLock.$$vencordGetWrappedComponent(), ComponentDispatch, Classes.layer].forEach(e => e.test);
|
2025-04-04 00:12:17 -04:00
|
|
|
} catch {
|
2024-05-09 03:04:03 +02:00
|
|
|
new Logger("BetterSettings").error("Failed to find some components");
|
2024-05-09 03:12:11 +02:00
|
|
|
return props.children;
|
2024-05-08 23:42:04 +02:00
|
|
|
}
|
|
|
|
|
|
2024-05-09 03:04:03 +02:00
|
|
|
return <Layer {...props} />;
|
2024-03-16 02:19:26 +01:00
|
|
|
},
|
|
|
|
|
|
2025-12-06 23:43:30 +01:00
|
|
|
transformSettingsEntries(list: SettingsEntry[], keyMap: Map<string, string>) {
|
2026-02-25 09:30:12 -05:00
|
|
|
const items: any[] = [];
|
2026-02-20 15:55:23 -05:00
|
|
|
const equicordItems: SettingsEntry[] = [
|
|
|
|
|
{ section: "EquicordSettings", label: "Settings" },
|
|
|
|
|
{ section: "EquicordPlugins", label: "Plugins" },
|
|
|
|
|
{ section: "EquicordThemes", label: "Themes" },
|
|
|
|
|
{ section: "EquicordUpdater", label: "Updater" },
|
|
|
|
|
{ section: "EquicordChangelog", label: "Changelog" },
|
|
|
|
|
{ section: "EquicordCloud", label: "Cloud" },
|
|
|
|
|
{ section: "EquicordBackupAndRestore", label: "Backup & Restore" },
|
|
|
|
|
{ section: "EquicordPatchHelper", label: "Patch Helper" },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const [section, key] of SettingsPlugin.settingsSectionMap) {
|
|
|
|
|
const entry = SettingsPlugin.customEntries.find(e => key.endsWith(e.key));
|
|
|
|
|
if (entry) equicordItems.push({ section, label: entry.title });
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 09:30:12 -05:00
|
|
|
if (settings.store.organizeMenu) {
|
|
|
|
|
for (const item of list) {
|
|
|
|
|
if (!item.label || item.predicate != null && !item.predicate()) continue;
|
|
|
|
|
|
|
|
|
|
if (item.section === "HEADER") {
|
|
|
|
|
keyMap.set(item.label, item.label);
|
|
|
|
|
items.push({ section: item.label, items: [] });
|
|
|
|
|
} else if (item.section !== "DIVIDER" && keyMap.has(item.section)) {
|
|
|
|
|
items.at(-1)?.items.push(item);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
keyMap.set("Equicord", "Equicord");
|
|
|
|
|
items.push({
|
|
|
|
|
section: "Equicord",
|
|
|
|
|
items: equicordItems
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
items.push(...list, ...equicordItems);
|
|
|
|
|
}
|
2026-02-20 15:55:23 -05:00
|
|
|
|
2025-10-06 18:23:50 -04:00
|
|
|
return items;
|
|
|
|
|
},
|
|
|
|
|
|
2025-12-06 23:43:30 +01:00
|
|
|
wrapMap(toWrap: TransformedSettingsEntry[]) {
|
2026-02-25 09:30:12 -05:00
|
|
|
if (!settings.store.organizeMenu) return toWrap;
|
2025-10-06 18:23:50 -04:00
|
|
|
// @ts-expect-error
|
|
|
|
|
toWrap.map = function (render: (item: SettingsEntry) => ReactElement<any>) {
|
|
|
|
|
return this
|
2025-12-06 23:43:30 +01:00
|
|
|
.filter(a => a.items.length > 0)
|
|
|
|
|
.map(({ section, items }) => {
|
2025-10-06 18:23:50 -04:00
|
|
|
const children = items.map(render);
|
2025-12-06 23:43:30 +01:00
|
|
|
if (section) {
|
2025-10-06 18:23:50 -04:00
|
|
|
return (
|
|
|
|
|
<Menu.MenuItem
|
2025-12-06 23:43:30 +01:00
|
|
|
key={section}
|
|
|
|
|
id={section.replace(/\W/, "_")}
|
|
|
|
|
label={section}
|
2025-10-06 18:23:50 -04:00
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</Menu.MenuItem>
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
return children;
|
|
|
|
|
}
|
|
|
|
|
});
|
2024-03-16 02:19:26 +01:00
|
|
|
};
|
2025-10-06 18:23:50 -04:00
|
|
|
|
|
|
|
|
return toWrap;
|
2024-03-16 02:19:26 +01:00
|
|
|
}
|
|
|
|
|
});
|