2024-03-16 01:19:26 +00: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";
|
|
|
|
import { classNameFactory } from "@api/Styles";
|
|
|
|
import { Devs } from "@utils/constants";
|
2024-05-08 21:42:04 +00:00
|
|
|
import { Logger } from "@utils/Logger";
|
2024-03-16 01:19:26 +00:00
|
|
|
import definePlugin, { OptionType } from "@utils/types";
|
|
|
|
import { findByPropsLazy } from "@webpack";
|
|
|
|
import { ComponentDispatch, FocusLock, i18n, Menu, useEffect, useRef } from "@webpack/common";
|
|
|
|
import type { HTMLAttributes, ReactElement } from "react";
|
|
|
|
|
|
|
|
type SettingsEntry = { section: string, label: string; };
|
|
|
|
|
|
|
|
const cl = classNameFactory("");
|
|
|
|
const Classes = findByPropsLazy("animating", "baseLayer", "bg", "layer", "layers");
|
|
|
|
|
|
|
|
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,
|
|
|
|
default: true
|
|
|
|
},
|
|
|
|
eagerLoad: {
|
|
|
|
description: "Removes the loading delay when opening the menu for the first time",
|
|
|
|
type: OptionType.BOOLEAN,
|
|
|
|
default: true,
|
|
|
|
restartNeeded: true
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
|
|
patches: [
|
|
|
|
{
|
|
|
|
find: "this.renderArtisanalHack()",
|
|
|
|
replacement: [
|
|
|
|
{ // Fade in on layer
|
2024-03-28 13:17:05 +00:00
|
|
|
match: /(?<=\((\i),"contextType",\i\.AccessibilityPreferencesContext\);)/,
|
2024-03-16 01:19:26 +00:00
|
|
|
replace: "$1=$self.Layer;",
|
|
|
|
predicate: () => settings.store.disableFade
|
|
|
|
},
|
|
|
|
{ // Lazy-load contents
|
|
|
|
match: /createPromise:\(\)=>([^:}]*?),webpackId:"\d+",name:(?!="CollectiblesShop")"[^"]+"/g,
|
|
|
|
replace: "$&,_:$1",
|
|
|
|
predicate: () => settings.store.eagerLoad
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{ // For some reason standardSidebarView also has a small fade-in
|
|
|
|
find: "DefaultCustomContentScroller:function()",
|
|
|
|
replacement: [
|
|
|
|
{
|
|
|
|
match: /\(0,\i\.useTransition\)\((\i)/,
|
|
|
|
replace: "(_cb=>_cb(void 0,$1))||$&"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
match: /\i\.animated\.div/,
|
|
|
|
replace: '"div"'
|
|
|
|
}
|
|
|
|
],
|
|
|
|
predicate: () => settings.store.disableFade
|
|
|
|
},
|
2024-03-28 13:17:05 +00:00
|
|
|
{ // Load menu TOC eagerly
|
2024-03-16 01:19:26 +00:00
|
|
|
find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format",
|
|
|
|
replacement: {
|
2024-03-28 13:17:05 +00:00
|
|
|
match: /(?<=(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?openContextMenuLazy.{0,100}?(await Promise\.all[^};]*?\)\)).*?,)(?=\1\(this)/,
|
|
|
|
replace: "(async ()=>$2)(),"
|
2024-03-16 01:19:26 +00:00
|
|
|
},
|
|
|
|
predicate: () => settings.store.eagerLoad
|
|
|
|
},
|
|
|
|
{ // Settings cog context menu
|
|
|
|
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
|
|
|
replacement: {
|
|
|
|
match: /\(0,\i.default\)\(\)(?=\.filter\(\i=>\{let\{section:\i\}=)/,
|
|
|
|
replace: "$self.wrapMenu($&)"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
],
|
|
|
|
|
2024-05-08 21:42:04 +00: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.
|
|
|
|
//
|
|
|
|
// Thus, we sanity check webpack modules & do this really hacky try catch to hopefully prevent hard crashes if something goes wrong.
|
|
|
|
// try catch will only catch errors in the Layer function (hence why it's called as a plain function rather than a component), but
|
|
|
|
// not in children
|
2024-03-16 01:19:26 +00:00
|
|
|
Layer(props: LayerProps) {
|
2024-05-08 21:42:04 +00:00
|
|
|
try {
|
|
|
|
if (!FocusLock || !ComponentDispatch)
|
|
|
|
throw new Error("Failed to fetch some webpack modules");
|
|
|
|
|
|
|
|
return Layer(props);
|
|
|
|
} catch (e) {
|
|
|
|
new Logger("BetterSettings").error("Failed to render Layer", e);
|
|
|
|
}
|
|
|
|
|
|
|
|
return props.children;
|
2024-03-16 01:19:26 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
wrapMenu(list: SettingsEntry[]) {
|
|
|
|
if (!settings.store.organizeMenu) return list;
|
|
|
|
|
|
|
|
const items = [{ label: null as string | null, items: [] as SettingsEntry[] }];
|
|
|
|
|
|
|
|
for (const item of list) {
|
|
|
|
if (item.section === "HEADER") {
|
|
|
|
items.push({ label: item.label, items: [] });
|
|
|
|
} else if (item.section === "DIVIDER") {
|
|
|
|
items.push({ label: i18n.Messages.OTHER_OPTIONS, items: [] });
|
|
|
|
} else {
|
|
|
|
items.at(-1)!.items.push(item);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
filter(predicate: (item: SettingsEntry) => boolean) {
|
|
|
|
for (const category of items) {
|
|
|
|
category.items = category.items.filter(predicate);
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
map(render: (item: SettingsEntry) => ReactElement) {
|
|
|
|
return items
|
|
|
|
.filter(a => a.items.length > 0)
|
|
|
|
.map(({ label, items }) => {
|
|
|
|
const children = items.map(render);
|
|
|
|
if (label) {
|
|
|
|
return (
|
|
|
|
<Menu.MenuItem
|
|
|
|
id={label.replace(/\W/, "_")}
|
|
|
|
label={label}
|
|
|
|
children={children}
|
|
|
|
action={children[0].props.action}
|
|
|
|
/>);
|
|
|
|
} else {
|
|
|
|
return children;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
});
|