new plugin: BetterSettings ~ improves Discord's settings (#2222)
- makes opening settings much faster - removes the scuffed transition animation - organises the settings cog context menu into categories Co-authored-by: Vendicated <vendicated@riseup.net>
This commit is contained in:
parent
f3ee43fe66
commit
6140b95814
|
@ -16,11 +16,10 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { findGroupChildrenByChildId } from "@api/ContextMenu";
|
||||
import { Settings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { React, SettingsRouter } from "@webpack/common";
|
||||
import { React } from "@webpack/common";
|
||||
|
||||
import gitHash from "~git-hash";
|
||||
|
||||
|
@ -30,23 +29,6 @@ export default definePlugin({
|
|||
authors: [Devs.Ven, Devs.Megu],
|
||||
required: true,
|
||||
|
||||
contextMenus: {
|
||||
// The settings shortcuts in the user settings cog context menu
|
||||
// read the elements from a hardcoded map which for obvious reason
|
||||
// doesn't contain our sections. This patches the actions of our
|
||||
// sections to manually use SettingsRouter (which only works on desktop
|
||||
// but the context menu is usually not available on mobile anyway)
|
||||
"user-settings-cog"(children) {
|
||||
const section = findGroupChildrenByChildId("VencordSettings", children);
|
||||
section?.forEach(c => {
|
||||
const id = c?.props?.id;
|
||||
if (id?.startsWith("Vencord") || id?.startsWith("Vesktop")) {
|
||||
c!.props.action = () => SettingsRouter.open(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
patches: [{
|
||||
find: ".versionHash",
|
||||
replacement: [
|
||||
|
@ -75,6 +57,12 @@ export default definePlugin({
|
|||
},
|
||||
replace: "...$self.makeSettingsCategories($1),$&"
|
||||
}
|
||||
}, {
|
||||
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
||||
replacement: {
|
||||
match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.UserSettingsSections\).*?(\i)\.default\.open\()/,
|
||||
replace: "$2.default.open($1);return;"
|
||||
}
|
||||
}],
|
||||
|
||||
customSections: [] as ((SectionTypes: Record<string, unknown>) => any)[],
|
||||
|
|
9
src/plugins/betterSettings/README.md
Normal file
9
src/plugins/betterSettings/README.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# BetterSettings
|
||||
|
||||
Improves Discord's Settings via multiple (toggleable) changes:
|
||||
- makes opening settings much faster
|
||||
- removes the scuffed transition animation
|
||||
- organises the settings cog context menu into categories
|
||||
|
||||
![](https://github.com/Vendicated/Vencord/assets/45497981/e8d67a95-3909-4be5-8281-8cf9d2f1c30e)
|
||||
|
177
src/plugins/betterSettings/index.tsx
Normal file
177
src/plugins/betterSettings/index.tsx
Normal file
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* 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 ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
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
|
||||
match: /(?<=(\i)\.contextType=\i\.AccessibilityPreferencesContext;)/,
|
||||
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
|
||||
},
|
||||
{ // Load menu stuff on hover, not on click
|
||||
find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format",
|
||||
replacement: {
|
||||
match: /(?<=handleOpenSettingsContextMenu.{0,250}?\i\.el\(("[^"]+")\)\.then\([^;]*?("\d+").*?Messages\.USER_SETTINGS,)(?=onClick:)/,
|
||||
replace: "onMouseEnter(){Vencord.Webpack.wreq.el($1).then(()=>Vencord.Webpack.wreq($2));},"
|
||||
},
|
||||
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($&)"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
Layer(props: LayerProps) {
|
||||
return (
|
||||
<ErrorBoundary fallback={() => props.children as any}>
|
||||
<Layer {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
},
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
|
@ -47,6 +47,7 @@ export let Paginator: t.Paginator;
|
|||
export let ScrollerThin: t.ScrollerThin;
|
||||
export let Clickable: t.Clickable;
|
||||
export let Avatar: t.Avatar;
|
||||
export let FocusLock: t.FocusLock;
|
||||
// token lagger real
|
||||
/** css colour resolver stuff, no clue what exactly this does, just copied usage from Discord */
|
||||
export let useToken: t.useToken;
|
||||
|
@ -58,6 +59,6 @@ export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"
|
|||
export const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal");
|
||||
|
||||
waitFor(["FormItem", "Button"], m => {
|
||||
({ useToken, Card, Button, FormSwitch: Switch, Tooltip, TextInput, TextArea, Text, Select, SearchableSelect, Slider, ButtonLooks, TabBar, Popout, Dialog, Paginator, ScrollerThin, Clickable, Avatar } = m);
|
||||
({ useToken, Card, Button, FormSwitch: Switch, Tooltip, TextInput, TextArea, Text, Select, SearchableSelect, Slider, ButtonLooks, TabBar, Popout, Dialog, Paginator, ScrollerThin, Clickable, Avatar, FocusLock } = m);
|
||||
Forms = m;
|
||||
});
|
||||
|
|
4
src/webpack/common/types/components.d.ts
vendored
4
src/webpack/common/types/components.d.ts
vendored
|
@ -453,3 +453,7 @@ export type Avatar = ComponentType<PropsWithChildren<{
|
|||
"aria-hidden"?: boolean;
|
||||
"aria-label"?: string;
|
||||
}>>;
|
||||
|
||||
type FocusLock = ComponentType<PropsWithChildren<{
|
||||
containerRef: RefObject<HTMLElement>
|
||||
}>>;
|
||||
|
|
Loading…
Reference in a new issue