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
5 changed files with 199 additions and 20 deletions
|
@ -16,11 +16,10 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { findGroupChildrenByChildId } from "@api/ContextMenu";
|
|
||||||
import { Settings } from "@api/Settings";
|
import { Settings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { React, SettingsRouter } from "@webpack/common";
|
import { React } from "@webpack/common";
|
||||||
|
|
||||||
import gitHash from "~git-hash";
|
import gitHash from "~git-hash";
|
||||||
|
|
||||||
|
@ -30,23 +29,6 @@ export default definePlugin({
|
||||||
authors: [Devs.Ven, Devs.Megu],
|
authors: [Devs.Ven, Devs.Megu],
|
||||||
required: true,
|
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: [{
|
patches: [{
|
||||||
find: ".versionHash",
|
find: ".versionHash",
|
||||||
replacement: [
|
replacement: [
|
||||||
|
@ -75,6 +57,12 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
replace: "...$self.makeSettingsCategories($1),$&"
|
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)[],
|
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 ScrollerThin: t.ScrollerThin;
|
||||||
export let Clickable: t.Clickable;
|
export let Clickable: t.Clickable;
|
||||||
export let Avatar: t.Avatar;
|
export let Avatar: t.Avatar;
|
||||||
|
export let FocusLock: t.FocusLock;
|
||||||
// token lagger real
|
// token lagger real
|
||||||
/** css colour resolver stuff, no clue what exactly this does, just copied usage from Discord */
|
/** css colour resolver stuff, no clue what exactly this does, just copied usage from Discord */
|
||||||
export let useToken: t.useToken;
|
export let useToken: t.useToken;
|
||||||
|
@ -58,6 +59,6 @@ export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"
|
||||||
export const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal");
|
export const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal");
|
||||||
|
|
||||||
waitFor(["FormItem", "Button"], m => {
|
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;
|
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-hidden"?: boolean;
|
||||||
"aria-label"?: string;
|
"aria-label"?: string;
|
||||||
}>>;
|
}>>;
|
||||||
|
|
||||||
|
type FocusLock = ComponentType<PropsWithChildren<{
|
||||||
|
containerRef: RefObject<HTMLElement>
|
||||||
|
}>>;
|
||||||
|
|
Loading…
Reference in a new issue