Replace Forms with our own independent components (#3618)

Vastly improves resilience of Settings Ui against Discord changes. Should now never break even if hell breaks loose

Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
This commit is contained in:
V 2025-10-11 01:42:14 +02:00 • committed by GitHub
parent 732f7dd4b4
commit cc01bb70cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
83 changed files with 1233 additions and 524 deletions

View file

@ -10,7 +10,7 @@ style.href = browser.runtime.getURL("dist/Vencord.css");
document.addEventListener(
"DOMContentLoaded",
() => {
document.documentElement.append(style);
document.body.insertAdjacentElement("afterend", style);
window.postMessage({
type: "vencord:meta",
meta: {

View file

@ -1,49 +1,39 @@
import type { ComponentClass, ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PointerEvent, PropsWithChildren, ReactNode, Ref, RefObject } from "react";
// copy(find(m => Array.isArray(m) && m.includes("heading-sm/normal")).map(JSON.stringify).join("|"))
export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-sm/extrabold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-md/extrabold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-lg/extrabold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/semibold" | "heading-xl/bold" | "heading-xl/extrabold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/semibold" | "heading-xxl/bold" | "heading-xxl/extrabold" | "eyebrow" | "heading-deprecated-12/normal" | "heading-deprecated-12/medium" | "heading-deprecated-12/semibold" | "heading-deprecated-12/bold" | "heading-deprecated-12/extrabold" | "redesign/heading-18/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "redesign/message-preview/normal" | "redesign/message-preview/medium" | "redesign/message-preview/semibold" | "redesign/message-preview/bold" | "redesign/channel-title/normal" | "redesign/channel-title/medium" | "redesign/channel-title/semibold" | "redesign/channel-title/bold" | "display-sm" | "display-md" | "display-lg" | "code";
export type FormTextTypes = Record<"DEFAULT" | "INPUT_PLACEHOLDER" | "DESCRIPTION" | "LABEL_BOLD" | "LABEL_SELECTED" | "LABEL_DESCRIPTOR" | "ERROR" | "SUCCESS", string>;
export type HeadingTag = `h${1 | 2 | 3 | 4 | 5 | 6}`;
// #region Old compability
export type HeadingTag = `h${1 | 2 | 3 | 4 | 5 | 6}`;
export type Margins = Record<"marginTop16" | "marginTop8" | "marginBottom8" | "marginTop20" | "marginBottom20", string>;
// copy(find(m => Array.isArray(m) && m.includes("heading-sm/normal")).map(JSON.stringify).join("|"))
export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-sm/extrabold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-md/extrabold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-lg/extrabold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/semibold" | "heading-xl/bold" | "heading-xl/extrabold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/semibold" | "heading-xxl/bold" | "heading-xxl/extrabold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold";
export type TextProps = PropsWithChildren<HtmlHTMLAttributes<HTMLDivElement> & {
variant?: TextVariant;
tag?: "div" | "span" | "p" | "strong" | HeadingTag;
selectable?: boolean;
lineClamp?: number;
}>;
export type Text = ComponentType<TextProps>;
export type Heading = ComponentType<TextProps>;
export type FormTitle = ComponentType<HTMLProps<HTMLTitleElement> & PropsWithChildren<{
/** default is h5 */
tag?: HeadingTag;
faded?: boolean;
disabled?: boolean;
required?: boolean;
error?: ReactNode;
}>>;
export interface ButtonProps extends PropsWithChildren<Omit<HTMLProps<HTMLButtonElement>, "size">> {
/** Button.Looks.FILLED */
look?: string;
/** Button.Colors.BRAND */
color?: string;
/** Button.Sizes.MEDIUM */
size?: string;
export type FormSection = ComponentType<PropsWithChildren<{
/** default is h5 */
tag?: HeadingTag;
className?: string;
titleClassName?: string;
titleId?: string;
title?: ReactNode;
disabled?: boolean;
htmlFor?: unknown;
}>>;
}
export type FormDivider = ComponentType<{
className?: string;
style?: CSSProperties;
}>;
export type Button = ComponentType<ButtonProps> & {
Colors: Record<"BRAND" | "RED" | "GREEN" | "PRIMARY" | "LINK" | "WHITE" | "TRANSPARENT" | "CUSTOM", string>;
Looks: Record<"FILLED" | "LINK", string>;
Sizes: Record<"NONE" | "SMALL" | "MEDIUM" | "LARGE" | "XLARGE" | "MIN", string>;
};
export type FormText = ComponentType<TextProps>;
// #endregion
export type Tooltip = ComponentType<{
text: ReactNode | ComponentType;
@ -121,7 +111,6 @@ export type ComboboxPopout = ComponentType<PropsWithChildren<{
className?: string;
listClassName?: string;
autoFocus?: boolean;
multiSelect?: boolean;
maxVisibleItems?: number;
@ -129,38 +118,6 @@ export type ComboboxPopout = ComponentType<PropsWithChildren<{
}>>;
export interface ButtonProps extends PropsWithChildren<Omit<HTMLProps<HTMLButtonElement>, "size">> {
/** Button.Looks.FILLED */
look?: string;
/** Button.Colors.BRAND */
color?: string;
/** Button.Sizes.MEDIUM */
size?: string;
/** Button.BorderColors.BLACK */
borderColor?: string;
wrapperClassName?: string;
className?: string;
innerClassName?: string;
buttonRef?: Ref<HTMLButtonElement>;
focusProps?: any;
submitting?: boolean;
submittingStartedLabel?: string;
submittingFinishedLabel?: string;
}
export type Button = ComponentType<ButtonProps> & {
BorderColors: Record<"BLACK" | "BRAND" | "BRAND_NEW" | "GREEN" | "LINK" | "PRIMARY" | "RED" | "TRANSPARENT" | "WHITE" | "YELLOW", string>;
Colors: Record<"BRAND" | "RED" | "GREEN" | "YELLOW" | "PRIMARY" | "LINK" | "WHITE" | "BLACK" | "TRANSPARENT" | "BRAND_NEW" | "CUSTOM", string>;
Hovers: Record<"DEFAULT" | "BRAND" | "RED" | "GREEN" | "YELLOW" | "PRIMARY" | "LINK" | "WHITE" | "BLACK" | "TRANSPARENT", string>;
Looks: Record<"FILLED" | "INVERTED" | "OUTLINED" | "LINK" | "BLANK", string>;
Sizes: Record<"NONE" | "TINY" | "SMALL" | "MEDIUM" | "LARGE" | "XLARGE" | "MIN" | "MAX" | "ICON", string>;
Link: any;
};
export type CheckboxAligns = {
CENTER: "center";
TOP: "top";

View file

@ -22,12 +22,13 @@
import { readdir } from "fs/promises";
import { join } from "path";
import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, commonRendererPlugins, watch, buildOrWatchAll, stringifyValues } from "./common.mjs";
import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_ANTI_CRASH_TEST, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, commonRendererPlugins, watch, buildOrWatchAll, stringifyValues } from "./common.mjs";
const defines = stringifyValues({
IS_STANDALONE,
IS_DEV,
IS_REPORTER,
IS_ANTI_CRASH_TEST,
IS_UPDATER_DISABLED,
IS_WEB: false,
IS_EXTENSION: false,

View file

@ -24,7 +24,7 @@ import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises
import { join } from "path";
import Zip from "zip-local";
import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, VERSION, commonRendererPlugins, buildOrWatchAll, stringifyValues } from "./common.mjs";
import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, IS_ANTI_CRASH_TEST, VERSION, commonRendererPlugins, buildOrWatchAll, stringifyValues } from "./common.mjs";
/**
* @type {import("esbuild").BuildOptions}
@ -47,6 +47,7 @@ const commonOptions = {
IS_STANDALONE: true,
IS_DEV,
IS_REPORTER,
IS_ANTI_CRASH_TEST,
IS_DISCORD_DESKTOP: false,
IS_VESKTOP: false,
IS_UPDATER_DISABLED: true,
@ -176,7 +177,7 @@ async function buildExtension(target, files) {
const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content => {
const cssRuntime = `
;document.addEventListener("DOMContentLoaded", () => document.documentElement.appendChild(
;document.addEventListener("DOMContentLoaded", () => document.body.insertAdjacentElement("afterend",
Object.assign(document.createElement("style"), {
textContent: \`${content.replaceAll("`", "\\`")}\`,
id: "vencord-css-core"

View file

@ -42,6 +42,7 @@ export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now
export const watch = process.argv.includes("--watch");
export const IS_DEV = watch || process.argv.includes("--dev");
export const IS_REPORTER = process.argv.includes("--reporter");
export const IS_ANTI_CRASH_TEST = process.argv.includes("--anti-crash-test");
export const IS_STANDALONE = process.argv.includes("--standalone");
export const IS_UPDATER_DISABLED = process.argv.includes("--disable-updater");

View file

@ -32,8 +32,10 @@ export { PlainSettings, Settings };
import "./utils/quickCss";
import "./webpack/patchWebpack";
import { addVencordUiStyles } from "@components/css";
import { openUpdaterModal } from "@components/settings/tabs/updater";
import { IS_WINDOWS } from "@utils/constants";
import { createAndAppendStyle } from "@utils/css";
import { StartAt } from "@utils/types";
import { get as dsGet } from "./api/DataStore";
@ -163,12 +165,12 @@ startAllPlugins(StartAt.Init);
init();
document.addEventListener("DOMContentLoaded", () => {
addVencordUiStyles();
startAllPlugins(StartAt.DOMContentLoaded);
// FIXME
if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && IS_WINDOWS) {
document.head.append(Object.assign(document.createElement("style"), {
id: "vencord-native-titlebar-style",
textContent: "[class*=titleBar]{display: none!important}"
}));
createAndAppendStyle("vencord-native-titlebar-style").textContent = "[class*=titleBar]{display: none!important}";
}
}, { once: true });

View file

@ -8,9 +8,10 @@ import "./ChatButton.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { Logger } from "@utils/Logger";
import { classes } from "@utils/misc";
import { Channel } from "@vencord/discord-types";
import { waitFor } from "@webpack";
import { Button, ButtonWrapperClasses, Tooltip } from "@webpack/common";
import { ButtonWrapperClasses, Clickable, Tooltip } from "@webpack/common";
import { HTMLProps, JSX, MouseEventHandler, ReactNode } from "react";
let ChannelTextAreaClasses: Record<"button" | "buttonContainer", string>;
@ -97,32 +98,30 @@ export const removeChatBarButton = (id: string) => buttonFactories.delete(id);
export interface ChatBarButtonProps {
children: ReactNode;
tooltip: string;
onClick: MouseEventHandler<HTMLButtonElement>;
onContextMenu?: MouseEventHandler<HTMLButtonElement>;
onAuxClick?: MouseEventHandler<HTMLButtonElement>;
buttonProps?: Omit<HTMLProps<HTMLButtonElement>, "size" | "onClick" | "onContextMenu" | "onAuxClick">;
onClick: MouseEventHandler;
onContextMenu?: MouseEventHandler;
onAuxClick?: MouseEventHandler;
buttonProps?: Omit<HTMLProps<HTMLDivElement>, "size" | "onClick" | "onContextMenu" | "onAuxClick">;
}
export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
return (
<Tooltip text={props.tooltip}>
{({ onMouseEnter, onMouseLeave }) => (
<div className={`expression-picker-chat-input-button ${ChannelTextAreaClasses?.buttonContainer ?? ""} vc-chatbar-button`}>
<Button
<Clickable
aria-label={props.tooltip}
size=""
look={Button.Looks.BLANK}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
innerClassName={`${ButtonWrapperClasses.button} ${ChannelTextAreaClasses?.button}`}
className={classes(ButtonWrapperClasses?.button, ChannelTextAreaClasses?.button)}
onClick={props.onClick}
onContextMenu={props.onContextMenu}
onAuxClick={props.onAuxClick}
{...props.buttonProps}
>
<div className={ButtonWrapperClasses.buttonWrapper}>
<div className={ButtonWrapperClasses?.buttonWrapper}>
{props.children}
</div>
</Button>
</Clickable>
</div>
)}
</Tooltip>

View file

@ -0,0 +1,8 @@
.vc-text-base {
font-family: var(--font-primary);
color: var(--text-default);
line-height: normal;
/* Discord puts an insane default margin on p tags, so reset that here */
margin: 0;
}

100
src/components/BaseText.tsx Normal file
View file

@ -0,0 +1,100 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./BaseText.css";
import { classNameFactory } from "@api/Styles";
import { classes } from "@utils/misc";
import type { Text as DiscordText } from "@vencord/discord-types";
import type { ComponentPropsWithoutRef, ReactNode } from "react";
const textCls = classNameFactory("vc-text-");
const Sizes = {
xxs: "0.625rem",
xs: "0.75rem",
sm: "0.875rem",
md: "1rem",
lg: "1.25rem",
xl: "1.5rem",
xxl: "2rem"
} as const;
const Weights = {
thin: "100",
extralight: "200",
light: "300",
normal: "400",
medium: "500",
semibold: "600",
bold: "700",
extrabold: "800",
} as const;
export function generateTextCss() {
let css = "";
for (const [size, value] of Object.entries(Sizes)) {
css += `.${textCls(size)}{font-size:${value};}`;
}
for (const [weight, value] of Object.entries(Weights)) {
css += `.${textCls(weight)}{font-weight:${value};}`;
}
return css;
}
export type TextSize = keyof typeof Sizes;
export type TextWeight = keyof typeof Weights;
export type TextTag = "div" | "span" | "p" | `h${1 | 2 | 3 | 4 | 5 | 6}`;
export type BaseTextProps<Tag extends TextTag = "div"> = ComponentPropsWithoutRef<Tag> & {
size?: TextSize;
weight?: TextWeight;
tag?: Tag;
};
export function BaseText<T extends TextTag = "div">(props: BaseTextProps<T>): ReactNode {
const {
size = "md",
weight = "normal",
tag: Tag = "div",
children,
className,
...restProps
} = props;
return (
<Tag className={classes(textCls("base", size, weight), className)} {...restProps}>
{children}
</Tag>
);
}
// #region Old compability
export const TextCompat: DiscordText = function TextCompat({ color, variant, ...restProps }) {
const newBaseTextProps = restProps as BaseTextProps;
if (variant) {
const [left, right] = variant.split("/");
if (left && right) {
const size = left.split("-").pop();
newBaseTextProps.size = size as TextSize;
newBaseTextProps.weight = right as TextWeight;
}
}
if (color) {
newBaseTextProps.style ??= {};
newBaseTextProps.style.color = `var(--${color}, var(--text-default))`;
}
return <BaseText {...newBaseTextProps} />;
};
// #endregion

199
src/components/Button.css Normal file
View file

@ -0,0 +1,199 @@
.vc-btn-base {
position: relative;
display: flex;
justify-content: center;
align-items: center;
max-width: 100%;
border: 1px solid transparent;
border-radius: var(--radius-sm, 8px);
font-family: var(--font-primary);
text-align: start;
transition: 50ms ease-in;
transition-property: background-color, color, border-color, opacity;
background: var(--control-background-secondary-default);
color: var(--text-default);
white-space: nowrap;
&:hover {
transition: .15s ease-out;
}
&:disabled {
opacity: .5;
pointer-events: none;
cursor: not-allowed;
}
&:focus-visible {
/* stylelint-disable-next-line custom-property-pattern */
box-shadow: 0 0 0 4px var(--__adaptive-focus-ring-color, var(--focus-primary, #00b0f4));
}
}
.vc-btn-min,
.vc-btn-xs {
padding: 3px 7px;
min-height: 22px;
min-width: unset;
font-size: 12px;
font-weight: 400;
line-height: 1.3333;
}
.vc-btn-xs {
min-width: 60px;
}
.vc-btn-small {
padding: 3px 11px;
min-height: 30px;
min-width: 60px;
font-size: 14px;
font-weight: 500;
line-height: 1.2857;
}
.vc-btn-medium {
padding: 7px 15px;
min-height: 38px;
min-width: 100px;
font-size: 16px;
font-weight: 500;
line-height: 1.25;
}
.vc-btn-primary {
background-color: var(--control-background-primary-default);
border-color: var(--control-border-primary-default);
color: var(--control-text-primary-default);
&:hover {
background-color: var(--control-background-primary-hover);
border-color: var(--control-border-primary-hover);
color: var(--control-text-primary-hover);
}
}
.vc-btn-secondary,
.vc-btn-link {
background-color: var(--control-background-secondary-default);
border-color: var(--control-border-secondary-default);
color: var(--control-text-secondary-default);
&:hover {
background-color: var(--control-background-secondary-hover);
border-color: var(--control-border-secondary-hover);
color: var(--control-text-secondary-hover);
}
}
.vc-btn-dangerPrimary {
background-color: var(--control-background-critical-primary-default);
border-color: var(--control-border-critical-primary-default);
color: var(--control-text-critical-primary-default);
&:hover {
background-color: var(--control-background-critical-primary-hover);
border-color: var(--control-border-critical-primary-hover);
color: var(--control-text-critical-primary-hover);
}
}
.vc-btn-dangerSecondary {
background-color: var(--control-background-critical-secondary-default);
border-color: var(--control-border-critical-secondary-default);
color: var(--control-text-critical-secondary-default);
&:hover {
background-color: var(--control-background-critical-secondary-hover);
border-color: var(--control-border-critical-secondary-hover);
color: var(--control-text-critical-secondary-hover);
}
}
.vc-btn-overlayPrimary {
background-color: var(--control-background-overlay-primary-default);
border-color: var(--control-border-overlay-primary-default);
color: var(--control-text-overlay-primary-default);
&:hover {
background-color: var(--control-background-overlay-primary-hover);
border-color: var(--control-border-overlay-primary-hover);
color: var(--control-text-overlay-primary-hover);
}
}
.vc-btn-positive {
background-color: var(--button-positive-background);
border-color: var(--button-positive-border);
color: var(--white);
&:hover {
background-color: var(--button-positive-background-hover);
}
}
.vc-btn-none {
background-color: transparent;
border-color: transparent;
color: var(--control-icon-icon-only-default);
&:hover {
background-color: var(--control-background-icon-only-hover);
border-color: var(--control-border-icon-only-hover);
color: var(--control-icon-icon-only-hover);
}
}
.vc-btn-link-icon {
width: 0.875em;
height: 0.875em;
margin-left: 8px;
flex-shrink: 0;
}
.vc-text-btn-base {
display: inline-flex;
justify-content: center;
align-items: center;
gap: var(--space-4, 4px);
background: initial;
color: var(--text-default);
font-size: medium;
font-weight: 400;
margin: 0;
padding: 0;
text-align: start;
text-decoration: none;
max-width: 100%;
white-space: nowrap;
&:disabled {
opacity: 0.5;
}
&:hover {
text-decoration: underline;
}
&:focus-visible {
/* stylelint-disable-next-line custom-property-pattern */
box-shadow: 0 0 0 4px var(--__adaptive-focus-ring-color, var(--focus-primary, #00b0f4));
}
}
.vc-text-btn-primary {
color: var(--text-brand);
}
.vc-text-btn-secondary {
color: var(--text-primary);
}
.vc-text-btn-danger {
color: var(--text-feedback-critical, var(--text-danger));
}
.vc-text-btn-link {
color: var(--text-link);
}

107
src/components/Button.tsx Normal file
View file

@ -0,0 +1,107 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./Button.css";
import { classNameFactory } from "@api/Styles";
import { classes } from "@utils/misc";
import type { Button as DiscordButton } from "@vencord/discord-types";
import type { ComponentPropsWithRef } from "react";
import { OpenExternalIcon } from "./Icons";
const btnCls = classNameFactory("vc-btn-");
const textBtnCls = classNameFactory("vc-text-btn-");
export type ButtonVariant =
"primary" | "secondary" | "dangerPrimary" | "dangerSecondary" | "overlayPrimary" | "positive" | "link" | "none";
export type ButtonSize = "min" | "xs" | "small" | "medium";
export type ButtonProps = ComponentPropsWithRef<"button"> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
export function Button({ variant = "primary", size = "medium", children, className, ...restProps }: ButtonProps) {
return (
<button data-mana-component="button" className={classes(btnCls("base", variant, size), className)} {...restProps}>
{children}
{variant === "link" && <OpenExternalIcon className={btnCls("link-icon")} />}
</button>
);
}
export type TextButtonVariant = "primary" | "secondary" | "danger" | "link";
export type TextButtonProps = ComponentPropsWithRef<"button"> & {
variant?: TextButtonVariant;
};
export function TextButton({ variant = "primary", className, ...restProps }: TextButtonProps) {
return (
<button className={classes(textBtnCls("base", variant), className)} {...restProps} />
);
}
// #region Old compability
export const ButtonCompat: DiscordButton = function ButtonCompat({ look, color = "BRAND", size = "medium", ...restProps }) {
return look === "LINK"
? <TextButton variant={TextButtonPropsColorMapping[color]} {...restProps as TextButtonProps} />
: <Button variant={ButtonColorMapping[color]} size={size as ButtonSize} {...restProps as ButtonProps} />;
};
/** @deprecated */
ButtonCompat.Looks = {
FILLED: "",
LINK: "LINK"
} as const;
/** @deprecated */
ButtonCompat.Colors = {
BRAND: "BRAND",
PRIMARY: "PRIMARY",
RED: "RED",
TRANSPARENT: "TRANSPARENT",
CUSTOM: "CUSTOM",
GREEN: "GREEN",
LINK: "LINK",
WHITE: "WHITE",
} as const;
const ButtonColorMapping: Record<keyof typeof ButtonCompat["Colors"], ButtonProps["variant"]> = {
BRAND: "primary",
PRIMARY: "secondary",
RED: "dangerPrimary",
TRANSPARENT: "secondary",
CUSTOM: "none",
GREEN: "positive",
LINK: "link",
WHITE: "overlayPrimary"
};
const TextButtonPropsColorMapping: Record<keyof typeof ButtonCompat["Colors"], TextButtonProps["variant"]> = {
BRAND: "primary",
PRIMARY: "primary",
RED: "danger",
TRANSPARENT: "secondary",
CUSTOM: "secondary",
GREEN: "primary",
LINK: "link",
WHITE: "secondary"
};
/** @deprecated */
ButtonCompat.Sizes = {
SMALL: "small",
MEDIUM: "medium",
LARGE: "medium",
XLARGE: "medium",
NONE: "min",
MIN: "min"
} as const;
// #endregion

View file

@ -0,0 +1,6 @@
.vc-divider {
height: 1px;
width: 100%;
border: none;
border-top: 1px solid var(--border-subtle);
}

View file

@ -0,0 +1,21 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./Divider.css";
import { classes } from "@utils/misc";
import type { ComponentPropsWithoutRef } from "react";
export type DividerProps = ComponentPropsWithoutRef<"hr">;
export function Divider({ className, ...restProps }: DividerProps) {
return (
<hr
className={classes("vc-divider", className)}
{...restProps}
/>
);
}

View file

@ -1,5 +0,0 @@
.vc-form-divider {
border-top: thin solid var(--border-subtle);
width: 100%;
height: 1px;
}

View file

@ -1,13 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./FormDivider.css";
import { classes } from "@utils/misc";
export function FormDivider({ className }: { className?: string; }) {
return <div className={classes("vc-form-divider", className)} />;
}

View file

@ -7,10 +7,10 @@
import "./FormSwitch.css";
import { classes } from "@utils/misc";
import { Text } from "@webpack/common";
import type { PropsWithChildren, ReactNode } from "react";
import { FormDivider } from "./FormDivider";
import { Divider } from "./Divider";
import { Span } from "./Span";
import { Switch } from "./Switch";
export interface FormSwitchProps {
@ -29,18 +29,21 @@ export function FormSwitch({ onChange, title, value, description, disabled, clas
<div className="vc-form-switch-wrapper">
<div className={classes("vc-form-switch", className, disabled && "vc-form-switch-disabled")}>
<div className={"vc-form-switch-text"}>
<Text variant="text-md/medium">{title}</Text>
{description && <Text variant="text-sm/normal">{description}</Text>}
<Span size="md" weight="medium">{title}</Span>
{description && <Span size="sm" weight="normal">{description}</Span>}
</div>
<Switch checked={value} onChange={onChange} disabled={disabled} />
</div>
{!hideBorder && <FormDivider className="vc-form-switch-border" />}
{!hideBorder && <Divider className="vc-form-switch-border" />}
</div>
);
}
/** Compatibility with Discord's old FormSwitch */
export function FormSwitchCompat(props: PropsWithChildren<any>) {
return <FormSwitch {...props} title={props.children ?? ""} description={props.note} />;
// #region Old compatibility
export function FormSwitchCompat({ note, children, ...restProps }: PropsWithChildren<any>) {
return <FormSwitch title={children ?? ""} description={note} {...restProps} />;
}
// #endregion

View file

@ -0,0 +1,65 @@
.vc-h1,
.vc-h2 {
color: var(--header-primary);
font-weight: 600;
}
.vc-h3,
.vc-h4,
.vc-h5 {
color: var(--header-secondary);
}
.vc-h1 {
font-size: 20px;
line-height: 24px
}
.vc-h2 {
font-size: 16px;
line-height: 20px
}
.vc-h3 {
font-weight: 500;
line-height: 24px
}
.vc-h3,
.vc-h4 {
font-size: 16px
}
.vc-h4 {
font-weight: 600;
letter-spacing: .3px
}
.vc-h4,
.vc-h5 {
line-height: 20px
}
.vc-h5 {
color: var(--header-primary);
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
text-transform: unset
}
.vc-h1-defaultMargin,
.vc-h2-defaultMargin {
margin-bottom: 20px
}
/* This is copied from Discord, don't ask me why 0 margin */
.vc-h4-defaultMargin {
margin-bottom: 0;
margin-top: 0;
}
.vc-h3-defaultMargin,
.vc-h5-defaultMargin {
margin-bottom: 8px
}

View file

@ -0,0 +1,60 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./Heading.css";
import { classes } from "@utils/misc";
import type { ComponentPropsWithoutRef } from "react";
export type HeadingTag = `h${1 | 2 | 3 | 4 | 5 | 6}`;
export type HeadingProps<Tag extends HeadingTag> = ComponentPropsWithoutRef<Tag> & {
tag?: Tag;
};
/**
* A simple heading component that automatically sizes according to the tag used.
*
* If you need more control, use the BaseText component instead.
*/
export function Heading<T extends HeadingTag>(props: HeadingProps<T>) {
const {
tag: Tag = "h5",
children,
className,
...restProps
} = props;
return (
<Tag className={classes(`vc-${Tag}`, !className && `vc-${Tag}-defaultMargin`, className)} {...restProps}>
{children}
</Tag>
);
}
export function HeadingPrimary({ children, ...restProps }: HeadingProps<"h2">) {
return (
<Heading tag="h2" {...restProps}>
{children}
</Heading>
);
}
export function HeadingSecondary({ children, ...restProps }: HeadingProps<"h3">) {
return (
<Heading tag="h3" {...restProps}>
{children}
</Heading>
);
}
export function HeadingTertiary({ children, ...restProps }: HeadingProps<"h4">) {
return (
<Heading tag="h4" {...restProps}>
{children}
</Heading>
);
}

View file

@ -91,11 +91,8 @@ export function OpenExternalIcon(props: IconProps) {
className={classes(props.className, "vc-open-external-icon")}
viewBox="0 0 24 24"
>
<polygon
fill="currentColor"
fillRule="nonzero"
points="13 20 11 20 11 8 5.5 13.5 4.08 12.08 12 4.16 19.92 12.08 18.5 13.5 13 8"
/>
<path fill="currentColor" d="M15 2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0V4.41l-4.3 4.3a1 1 0 1 1-1.4-1.42L19.58 3H16a1 1 0 0 1-1-1Z" />
<path fill="currentColor" d="M5 2a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-6a1 1 0 1 0-2 0v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h6a1 1 0 1 0 0-2H5Z" />
</Icon>
);
}

View file

@ -0,0 +1,17 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { BaseText, type BaseTextProps } from "./BaseText";
export type ParagraphProps = BaseTextProps<"p">;
export function Paragraph({ children, ...restProps }: ParagraphProps) {
return (
<BaseText tag="p" size="sm" weight="normal" {...restProps}>
{children}
</BaseText>
);
}

17
src/components/Span.tsx Normal file
View file

@ -0,0 +1,17 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { BaseText, type BaseTextProps } from "./BaseText";
export type SpanProps = BaseTextProps<"span">;
export function Span({ children, ...restProps }: SpanProps) {
return (
<BaseText tag="span" size="sm" weight="normal" {...restProps}>
{children}
</BaseText>
);
}

View file

@ -1,3 +1,52 @@
.vc-switch-container {
background: var(--primary-400);
border: 1px solid transparent;
border-radius: 16px;
box-sizing: border-box;
cursor: pointer;
height: 28px;
position: relative;
width: 44px;
.high-contrast-mode & {
border-color: var(--border-strong)
}
}
.vc-switch-checked {
background: var(--brand-500);
border-color: var(--control-border-primary-default)
}
.vc-switch-disabled {
cursor: not-allowed;
opacity: 0.3;
}
.vc-switch-slider {
display: block;
height: 20px;
left: 0;
margin: 3px;
position: absolute;
width: 28px;
transition: 100ms transform ease-in-out;
overflow: visible
}
.vc-switch-input {
border-radius: 14px;
cursor: pointer;
height: 100%;
left: 0;
margin: 0;
opacity: 0;
position: absolute;
top: 0;
width: 100%;
&[disabled] {
pointer-events: none
}
}

View file

@ -18,28 +18,26 @@
import "./Switch.css";
import { classNameFactory } from "@api/Styles";
import { classes } from "@utils/misc";
import { findByPropsLazy } from "@webpack";
interface SwitchProps {
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
}
const switchCls = classNameFactory("vc-switch-");
const SWITCH_ON = "var(--brand-500)";
const SWITCH_OFF = "var(--primary-400)";
const SwitchClasses = findByPropsLazy("slider", "input", "container");
export interface SwitchProps {
disabled?: boolean;
checked: boolean;
onChange: (checked: boolean) => void;
}
export function Switch({ checked, onChange, disabled }: SwitchProps) {
return (
<div>
<div className={classes(SwitchClasses.container, "default-colors", checked ? SwitchClasses.checked : void 0)} style={{
backgroundColor: checked ? SWITCH_ON : SWITCH_OFF,
opacity: disabled ? 0.3 : 1
}}>
<div className={classes(switchCls("container"), "default-colors", switchCls({ checked, disabled }))}>
<svg
className={SwitchClasses.slider + " vc-switch-slider"}
className={switchCls("slider")}
viewBox="0 0 28 20"
preserveAspectRatio="xMinYMid meet"
aria-hidden="true"
@ -66,7 +64,7 @@ export function Switch({ checked, onChange, disabled }: SwitchProps) {
<input
disabled={disabled}
type="checkbox"
className={SwitchClasses.input}
className={switchCls("input")}
tabIndex={0}
checked={checked}
onChange={e => onChange(e.currentTarget.checked)}

15
src/components/css.ts Normal file
View file

@ -0,0 +1,15 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { createAndAppendStyle } from "@utils/css";
import { generateTextCss } from "./BaseText";
import { generateMarginCss } from "./margins";
export function addVencordUiStyles() {
createAndAppendStyle("vencord-text", document.head).textContent = generateTextCss();
createAndAppendStyle("vencord-margins").textContent = generateMarginCss();
}

View file

@ -1,12 +1,3 @@
.vc-open-external-icon {
transform: rotate(45deg);
}
.vc-owner-crown-icon {
color: var(--status-warning);
}
.vc-heart-icon {
margin-right: 0.5em;
translate: 0 2px;
}
}

View file

@ -4,16 +4,21 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export * from "./BaseText";
export * from "./CheckedTextInput";
export * from "./CodeBlock";
export * from "./Divider";
export { default as ErrorBoundary } from "./ErrorBoundary";
export * from "./ErrorCard";
export * from "./Flex";
export * from "./FormDivider";
export * from "./FormSwitch";
export * from "./Grid";
export * from "./Heading";
export * from "./Heart";
export * from "./Icons";
export * from "./Link";
export * from "./margins";
export * from "./Paragraph";
export * from "./settings";
export * from "./Span";
export * from "./Switch";

43
src/components/margins.ts Normal file
View file

@ -0,0 +1,43 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { classNameFactory } from "@api/Styles";
const marginCls = classNameFactory("vc-margin-");
const Directions = ["top", "bottom", "left", "right"] as const;
const Sizes = [8, 16, 20] as const;
export type MarginDirection = (typeof Directions)[number];
export type MarginSize = (typeof Sizes)[number];
export const Margins: Record<`${MarginDirection}${MarginSize}`, string> = {} as any;
export function generateMarginCss() {
let css = "";
for (const direction of Directions) {
for (const size of Sizes) {
const cl = marginCls(`${direction}-${size}`);
Margins[`${direction}${size}`] = cl;
css += `.${cl}{margin-${direction}:${size}px;}`;
}
}
return css;
}

View file

@ -31,7 +31,7 @@ export default function DonateButton({
look={look}
color={color}
onClick={() => VencordNative.native.openExternal("https://github.com/sponsors/Vendicated")}
innerClassName="vc-donate-button"
className="vc-donate-button"
>
<Heart />
Donate

View file

@ -4,6 +4,7 @@
.vc-donate-button .vc-heart-icon {
transition: transform 0.3s;
margin-right: 0.5em;
}
.vc-donate-button:hover .vc-heart-icon {
@ -84,4 +85,4 @@
.vc-special-image {
width: 65%;
}
}

View file

@ -19,6 +19,7 @@
import "./SpecialCard.css";
import { classNameFactory } from "@api/Styles";
import { Divider } from "@components/Divider";
import { Card, Clickable, Forms, React } from "@webpack/common";
import type { PropsWithChildren } from "react";
@ -64,7 +65,7 @@ export function SpecialCard({ title, subtitle, description, cardImage, backgroun
</div>
{buttonTitle && (
<>
<Forms.FormDivider className={cl("seperator")} />
<Divider className={cl("seperator")} />
<Clickable onClick={onClick} className={cl("hyperlink")}>
<Forms.FormText className={cl("hyperlink-text")}>
{buttonTitle}

View file

@ -16,24 +16,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { BaseText } from "@components/BaseText";
import ErrorBoundary from "@components/ErrorBoundary";
import { handleComponentFailed } from "@components/handleComponentFailed";
import { Margins } from "@utils/margins";
import { onlyOnce } from "@utils/onlyOnce";
import { Text } from "@webpack/common";
import type { ComponentType, PropsWithChildren } from "react";
export function SettingsTab({ title, children }: PropsWithChildren<{ title: string; }>) {
return (
<section>
<Text
variant="heading-lg/semibold"
tag="h2"
className={Margins.bottom16}
>
{title}
</Text>
<BaseText tag="h2" size="xl" weight="semibold" className={Margins.bottom16}>{title}</BaseText>
{children}
</section>
);

View file

@ -74,8 +74,8 @@ export function ReplacementInput({ replacement, setReplacement, replacementError
className={Margins.top16}
value={isFunc}
onChange={setIsFunc}
title={"Treat as Function"}
description="'replacement' will be evaled if this is toggled"
title={"Treat Replacement as function"}
description='"Replacement" will be evaluated as a function if this is enabled'
hideBorder
/>
</>

View file

@ -17,7 +17,9 @@
*/
import { CodeBlock } from "@components/CodeBlock";
import { Divider } from "@components/Divider";
import { Flex } from "@components/Flex";
import { HeadingTertiary } from "@components/Heading";
import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
import { debounce } from "@shared/debounce";
import { Margins } from "@utils/margins";
@ -25,7 +27,7 @@ import { copyWithToast } from "@utils/misc";
import { stripIndent } from "@utils/text";
import { ReplaceFn } from "@utils/types";
import { search } from "@webpack";
import { Button, Forms, React, TextInput, useMemo, useState } from "@webpack/common";
import { Button, React, TextInput, useMemo, useState } from "@webpack/common";
import { FullPatchInput } from "./FullPatchInput";
import { PatchPreview } from "./PatchPreview";
@ -103,7 +105,7 @@ function PatchHelper() {
return (
<SettingsTab title="Patch Helper">
<Forms.FormTitle>full patch</Forms.FormTitle>
<HeadingTertiary>Full patch</HeadingTertiary>
<FullPatchInput
setFind={onFindChange}
setParsedFind={setParsedFind}
@ -111,7 +113,7 @@ function PatchHelper() {
setReplacement={setReplacement}
/>
<Forms.FormTitle className={Margins.top8}>find</Forms.FormTitle>
<HeadingTertiary className={Margins.top8}>Find</HeadingTertiary>
<TextInput
type="text"
value={find}
@ -119,7 +121,7 @@ function PatchHelper() {
error={findError}
/>
<Forms.FormTitle className={Margins.top8}>match</Forms.FormTitle>
<HeadingTertiary className={Margins.top8}>Match</HeadingTertiary>
<TextInput
type="text"
value={match}
@ -134,7 +136,7 @@ function PatchHelper() {
replacementError={replacementError}
/>
<Forms.FormDivider />
<Divider />
{module && (
<PatchPreview
module={module}
@ -146,7 +148,7 @@ function PatchHelper() {
{!!(find && match && replacement) && (
<>
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
<HeadingTertiary className={Margins.top20}>Code</HeadingTertiary>
<CodeBlock lang="js" content={code} />
<Flex className={Margins.top16}>
<Button onClick={() => copyWithToast(code)}>

View file

@ -8,9 +8,8 @@ import { showNotice } from "@api/Notices";
import { CogWheel, InfoIcon } from "@components/Icons";
import { AddonCard } from "@components/settings/AddonCard";
import { proxyLazy } from "@utils/lazy";
import { classes, isObjectEmpty } from "@utils/misc";
import { isObjectEmpty } from "@utils/misc";
import { Plugin } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { React, showToast, Toasts } from "@webpack/common";
import { Settings } from "Vencord";
@ -20,8 +19,6 @@ import { openPluginModal } from "./PluginModal";
// Avoid circular dependency
const { startDependenciesRecursive, startPlugin, stopPlugin, isPluginEnabled } = proxyLazy(() => require("plugins") as typeof import("plugins"));
export const ButtonClasses = findByPropsLazy("button", "disabled", "enabled");
interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
plugin: Plugin;
disabled: boolean;
@ -98,7 +95,7 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
<button
role="switch"
onClick={() => openPluginModal(plugin, onRestartNeeded)}
className={classes(ButtonClasses.button, cl("info-button"))}
className={cl("info-button")}
>
{plugin.options && !isObjectEmpty(plugin.options)
? <CogWheel className={cl("info-icon")} />

View file

@ -77,12 +77,16 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
useEffect(() => {
(async () => {
for (const user of plugin.authors.slice(0, 6)) {
const author = user.id
? await UserUtils.getUser(`${user.id}`)
.catch(() => makeDummyUser({ username: user.name }))
: makeDummyUser({ username: user.name });
try {
const author = user.id
? await UserUtils.getUser(String(user.id))
.catch(() => makeDummyUser({ username: user.name }))
: makeDummyUser({ username: user.name });
setAuthors(a => [...a, author]);
setAuthors(a => [...a, author]);
} catch (e) {
continue;
}
}
})();
}, [plugin.authors]);
@ -172,27 +176,29 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
</Flex>
<Text variant="heading-lg/semibold" className={classes(Margins.top8, Margins.bottom8)}>Authors</Text>
<div style={{ width: "fit-content" }}>
<UserSummaryItem
users={authors}
guildId={undefined}
renderIcon={false}
max={6}
showDefaultAvatarsForNullUsers
renderMoreUsers={renderMoreUsers}
renderUser={(user: User) => (
<Clickable
className={AvatarStyles.clickableAvatar}
onClick={() => openContributorModal(user)}
>
<img
className={AvatarStyles.avatar}
src={user.getAvatarURL(void 0, 80, true)}
alt={user.username}
title={user.username}
/>
</Clickable>
)}
/>
<ErrorBoundary noop>
<UserSummaryItem
users={authors}
guildId={undefined}
renderIcon={false}
max={6}
showDefaultAvatarsForNullUsers
renderMoreUsers={renderMoreUsers}
renderUser={(user: User) => (
<Clickable
className={AvatarStyles.clickableAvatar}
onClick={() => openContributorModal(user)}
>
<img
className={AvatarStyles.avatar}
src={user.getAvatarURL(void 0, 80, true)}
alt={user.username}
title={user.username}
/>
</Clickable>
)}
/>
</ErrorBoundary>
</div>
</section>

View file

@ -21,15 +21,17 @@ import "./styles.css";
import * as DataStore from "@api/DataStore";
import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Divider } from "@components/Divider";
import ErrorBoundary from "@components/ErrorBoundary";
import { HeadingTertiary } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
import { ChangeList } from "@utils/ChangeList";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { useAwaiter, useCleanupEffect } from "@utils/react";
import { findByPropsLazy } from "@webpack";
import { Alerts, Button, Card, Forms, lodash, Parser, React, Select, Text, TextInput, Tooltip, useMemo, useState } from "@webpack/common";
import { Alerts, Button, Card, lodash, Parser, React, Select, TextInput, Tooltip, useMemo, useState } from "@webpack/common";
import { JSX } from "react";
import Plugins, { ExcludedPlugins } from "~plugins";
@ -39,18 +41,16 @@ import { PluginCard } from "./PluginCard";
export const cl = classNameFactory("vc-plugins-");
export const logger = new Logger("PluginSettings", "#a6d189");
const InputStyles = findByPropsLazy("inputWrapper", "inputError", "error");
function ReloadRequiredCard({ required }: { required: boolean; }) {
return (
<Card className={classes(cl("info-card"), required && "vc-warning-card")}>
{required
? (
<>
<Forms.FormTitle tag="h5">Restart required!</Forms.FormTitle>
<Forms.FormText className={cl("dep-text")}>
<HeadingTertiary>Restart required!</HeadingTertiary>
<Paragraph className={cl("dep-text")}>
Restart now to apply new plugins and their settings
</Forms.FormText>
</Paragraph>
<Button onClick={() => location.reload()} className={cl("restart-button")}>
Restart
</Button>
@ -58,9 +58,9 @@ function ReloadRequiredCard({ required }: { required: boolean; }) {
)
: (
<>
<Forms.FormTitle tag="h5">Plugin Management</Forms.FormTitle>
<Forms.FormText>Press the cog wheel or info icon to get more info on a plugin</Forms.FormText>
<Forms.FormText>Plugins with a cog wheel have settings you can modify!</Forms.FormText>
<HeadingTertiary>Plugin Management</HeadingTertiary>
<Paragraph>Press the cog wheel or info icon to get more info on a plugin</Paragraph>
<Paragraph>Plugins with a cog wheel have settings you can modify!</Paragraph>
</>
)}
</Card>
@ -87,10 +87,10 @@ function ExcludedPluginsList({ search }: { search: string; }) {
};
return (
<Text variant="text-md/normal" className={Margins.top16}>
<Paragraph className={Margins.top16}>
{matchingExcludedPlugins.length
? <>
<Forms.FormText>Are you looking for:</Forms.FormText>
<Paragraph>Are you looking for:</Paragraph>
<ul>
{matchingExcludedPlugins.map(([name, reason]) => (
<li key={name}>
@ -101,7 +101,7 @@ function ExcludedPluginsList({ search }: { search: string; }) {
</>
: "No plugins meet the search criteria."
}
</Text>
</Paragraph>
);
}
@ -245,15 +245,15 @@ function PluginSettings() {
<SettingsTab title="Plugins">
<ReloadRequiredCard required={changes.hasChanges} />
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
<HeadingTertiary className={classes(Margins.top20, Margins.bottom8)}>
Filters
</Forms.FormTitle>
</HeadingTertiary>
<div className={classes(Margins.bottom20, cl("filter-controls"))}>
<ErrorBoundary noop>
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} />
</ErrorBoundary>
<div className={InputStyles.inputWrapper}>
<div>
<ErrorBoundary noop>
<Select
options={[
@ -271,14 +271,14 @@ function PluginSettings() {
</div>
</div>
<Forms.FormTitle className={Margins.top20}>Plugins</Forms.FormTitle>
<HeadingTertiary className={Margins.top20}>Plugins</HeadingTertiary>
{plugins.length || requiredPlugins.length
? (
<div className={cl("grid")}>
{plugins.length
? plugins
: <Text variant="text-md/normal">No plugins meet the search criteria.</Text>
: <Paragraph>No plugins meet the search criteria.</Paragraph>
}
</div>
)
@ -286,15 +286,15 @@ function PluginSettings() {
}
<Forms.FormDivider className={Margins.top20} />
<Divider className={Margins.top20} />
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
<HeadingTertiary className={classes(Margins.top20, Margins.bottom8)}>
Required Plugins
</Forms.FormTitle>
</HeadingTertiary>
<div className={cl("grid")}>
{requiredPlugins.length
? requiredPlugins
: <Text variant="text-md/normal">No plugins meet the search criteria.</Text>
: <Paragraph>No plugins meet the search criteria.</Paragraph>
}
</div>
</SettingsTab >
@ -304,8 +304,8 @@ function PluginSettings() {
function makeDependencyList(deps: string[]) {
return (
<>
<Forms.FormText>This plugin is required by:</Forms.FormText>
{deps.map((dep: string) => <Forms.FormText key={dep} className={cl("dep-text")}>{dep}</Forms.FormText>)}
<Paragraph>This plugin is required by:</Paragraph>
{deps.map((dep: string) => <Paragraph key={dep} className={cl("dep-text")}>{dep}</Paragraph>)}
</>
);
}

View file

@ -29,6 +29,12 @@
padding: 0;
background: transparent;
margin-right: 8px;
color: var(--icon-secondary);
transition: background-color .2s ease;
&:hover {
color: var(--interactive-hover)
}
}
.vc-plugins-settings-button:hover {
@ -79,4 +85,4 @@
display: flex;
flex-direction: column;
gap: 1.25em;
}
}

View file

@ -19,9 +19,11 @@
import { showNotification } from "@api/Notifications";
import { Settings, useSettings } from "@api/Settings";
import { CheckedTextInput } from "@components/CheckedTextInput";
import { Divider } from "@components/Divider";
import { FormSwitch } from "@components/FormSwitch";
import { Grid } from "@components/Grid";
import { Link } from "@components/Link";
import { Paragraph } from "@components/Paragraph";
import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
import { authorizeCloud, checkCloudUrlCsp, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
import { Margins } from "@utils/margins";
@ -73,10 +75,10 @@ function SettingsSyncSection() {
<section className={Margins.top16}>
<Forms.FormTitle tag="h5">Settings Sync</Forms.FormTitle>
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
<Paragraph size="md" className={Margins.bottom20}>
Synchronize your settings to the cloud. This allows easy synchronization across multiple devices with
minimal effort.
</Forms.FormText>
</Paragraph>
<FormSwitch
key="cloud-sync"
title="Settings Sync"
@ -127,12 +129,12 @@ function CloudTab() {
<section className={Margins.top16}>
<Forms.FormTitle tag="h5">Cloud Settings</Forms.FormTitle>
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
<Paragraph size="md" className={Margins.bottom20}>
Vencord comes with a cloud integration that adds goodies like settings sync across devices.
It <Link href="https://vencord.dev/cloud/privacy">respects your privacy</Link>, and
the <Link href="https://github.com/Vencord/Backend">source code</Link> is AGPL 3.0 licensed so you
can host it yourself.
</Forms.FormText>
</Paragraph>
<FormSwitch
key="backend"
title="Enable Cloud Integrations"
@ -189,7 +191,7 @@ function CloudTab() {
</Button>
</Grid>
<Forms.FormDivider className={Margins.top16} />
<Divider className={Margins.top16} />
</section >
<SettingsSyncSection />
</SettingsTab>

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Divider } from "@components/Divider";
import { ErrorCard } from "@components/ErrorCard";
import { Link } from "@components/Link";
import { CspBlockedUrls, useCspErrors } from "@utils/cspViolations";
@ -61,7 +62,7 @@ export function CspErrorCard() {
<div className="vc-settings-csp-list">
{errors.map((url, i) => (
<div key={url}>
{i !== 0 && <Forms.FormDivider className={Margins.bottom8} />}
{i !== 0 && <Divider className={Margins.bottom8} />}
<div className="vc-settings-csp-row">
<Link href={url}>{url}</Link>
<Button color={Button.Colors.PRIMARY} onClick={() => allowUrl(url)} disabled={isImgurHtmlDomain(url)}>
@ -74,7 +75,7 @@ export function CspErrorCard() {
{hasImgurHtmlDomain && (
<>
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom16)} />
<Divider className={classes(Margins.top8, Margins.bottom16)} />
<Forms.FormText>
Imgur links should be direct links in the form of <code>https://i.imgur.com/...</code>
</Forms.FormText>

View file

@ -28,7 +28,7 @@ export function OnlineThemesTab() {
return (
<>
<Card className={classes("vc-warning-card", Margins.bottom16)}>
<Forms.FormText>
<Forms.FormText size="md">
This section is for advanced users. If you are having difficulties using it, use the
Local Themes tab instead.
</Forms.FormText>

View file

@ -17,6 +17,7 @@
*/
import { useSettings } from "@api/Settings";
import { Divider } from "@components/Divider";
import { FormSwitch } from "@components/FormSwitch";
import { Link } from "@components/Link";
import { handleSettingsTabError, SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
@ -78,7 +79,7 @@ function Updater() {
(<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)
</Forms.FormText>
<Forms.FormDivider className={Margins.top8 + " " + Margins.bottom8} />
<Divider className={Margins.top8 + " " + Margins.bottom8} />
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>

View file

@ -11,7 +11,7 @@ import BadgeAPI from "plugins/_api/badges";
export const isDonor = (userId: string) => !!(
BadgeAPI.getDonorBadges(userId)?.length > 0
|| GuildMemberStore.getMember(VENCORD_GUILD_ID, userId)?.roles.includes(DONOR_ROLE_ID)
|| GuildMemberStore?.getMember(VENCORD_GUILD_ID, userId)?.roles.includes(DONOR_ROLE_ID)
);
export function DonateButtonComponent() {

View file

@ -16,7 +16,11 @@ import { Button, Forms, Select, Slider, Text } from "@webpack/common";
export function NotificationSection() {
return (
<section className={Margins.top16}>
<Forms.FormTitle tag="h5">Vencord Notifications</Forms.FormTitle>
<Forms.FormTitle tag="h5">Notifications</Forms.FormTitle>
<Forms.FormText className={Margins.bottom8}>
Settings for Notifications sent by Vencord.
This does NOT include Discord notifications (messages, etc)
</Forms.FormText>
<Flex>
<Button onClick={openNotificationSettingsModal}>
Notification Settings

View file

@ -18,6 +18,7 @@
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { useSettings } from "@api/Settings";
import { Divider } from "@components/Divider";
import { FormSwitch } from "@components/FormSwitch";
import { FolderIcon, GithubIcon, LogIcon, PaintbrushIcon, RestartIcon } from "@components/index";
import { QuickAction, QuickActionCard } from "@components/settings/QuickAction";
@ -30,7 +31,7 @@ import { IS_MAC, IS_WINDOWS } from "@utils/constants";
import { Margins } from "@utils/margins";
import { isPluginDev } from "@utils/misc";
import { relaunch } from "@utils/native";
import { Forms, React, useMemo, UserStore } from "@webpack/common";
import { Alerts, Forms, React, useMemo, UserStore } from "@webpack/common";
import { DonateButtonComponent, isDonor } from "./DonateButton";
import { VibrancySettings } from "./MacVibrancySettings";
@ -54,52 +55,73 @@ function Switches() {
{
key: "useQuickCss",
title: "Enable Custom CSS",
note: "Loads your Custom CSS"
},
!IS_WEB && {
key: "enableReactDevtools",
title: "Enable React Developer Tools",
note: "Requires a full restart"
restartRequired: true
},
!IS_WEB && (!IS_DISCORD_DESKTOP || !IS_WINDOWS ? {
key: "frameless",
title: "Disable the window frame",
note: "Requires a full restart"
restartRequired: true
} : {
key: "winNativeTitleBar",
title: "Use Windows' native title bar instead of Discord's custom one",
note: "Requires a full restart"
restartRequired: true
}),
!IS_WEB && {
key: "transparent",
title: "Enable window transparency.",
note: "You need a theme that supports transparency or this will do nothing. WILL STOP THE WINDOW FROM BEING RESIZABLE!! Requires a full restart"
},
!IS_WEB && IS_WINDOWS && {
key: "winCtrlQ",
title: "Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4)",
note: "Requires a full restart"
title: "Enable window transparency",
description: "A theme that supports transparency is required or this will do nothing. Stops the window from being resizable as a side effect",
restartRequired: true
},
IS_DISCORD_DESKTOP && {
key: "disableMinSize",
title: "Disable minimum window size",
note: "Requires a full restart"
restartRequired: true
},
!IS_WEB && IS_WINDOWS && {
key: "winCtrlQ",
title: "Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4)",
restartRequired: true
},
] satisfies Array<false | {
key: KeysOfType<typeof settings, boolean>;
title: string;
note: string;
description?: string;
restartRequired?: boolean;
}>;
return Switches.map(s => s && (
<FormSwitch
key={s.key}
title={s.title}
description={s.note}
value={settings[s.key]}
onChange={v => settings[s.key] = v}
/>
));
return Switches.map(setting => {
if (!setting) {
return null;
}
const { key, title, description, restartRequired } = setting;
return (
<FormSwitch
key={key}
title={title}
description={description}
value={settings[key]}
onChange={v => {
settings[key] = v;
if (restartRequired) {
Alerts.show({
title: "Restart Required",
body: "A restart is required to apply this change",
confirmText: "Restart now",
cancelText: "Later!",
onConfirm: relaunch
});
}
}}
/>
);
});
}
function VencordSettings() {
@ -110,7 +132,7 @@ function VencordSettings() {
const needsVibrancySettings = IS_DISCORD_DESKTOP && IS_MAC;
const user = UserStore.getCurrentUser();
const user = UserStore?.getCurrentUser();
return (
<SettingsTab title="Vencord Settings">
@ -189,7 +211,7 @@ function VencordSettings() {
</QuickActionCard>
</section>
<Forms.FormDivider />
<Divider />
<section className={Margins.top16}>
<Forms.FormTitle tag="h5">Settings</Forms.FormTitle>

1
src/globals.d.ts vendored
View file

@ -37,6 +37,7 @@ declare global {
export var IS_UPDATER_DISABLED: boolean;
export var IS_DEV: boolean;
export var IS_REPORTER: boolean;
export var IS_ANTI_CRASH_TEST: boolean;
export var IS_DISCORD_DESKTOP: boolean;
export var IS_VESKTOP: boolean;
export var VERSION: string;

View file

@ -204,19 +204,19 @@ export default definePlugin({
}}>
<ModalRoot {...props}>
<ModalHeader>
<Flex style={{ width: "100%", justifyContent: "center" }}>
<Forms.FormTitle
tag="h2"
style={{
width: "100%",
textAlign: "center",
margin: 0
}}
>
<Forms.FormTitle
tag="h2"
style={{
width: "100%",
textAlign: "center",
margin: 0
}}
>
<Flex style={{ justifyContent: "center", alignItems: "center", gap: "0.5em" }}>
<Heart />
Vencord Donor
</Forms.FormTitle>
</Flex>
</Flex>
</Forms.FormTitle>
</ModalHeader>
<ModalContent>
<Flex>

View file

@ -271,12 +271,14 @@ export default definePlugin({
buttons.push(
<Button
key="vc-dbg"
color={Button.Colors.PRIMARY}
onClick={async () => sendMessage(props.channel.id, { content: await generateDebugInfoMessage() })}
>
Run /vencord-debug
</Button>,
<Button
key="vc-plg-list"
color={Button.Colors.PRIMARY}
onClick={async () => sendMessage(props.channel.id, { content: generatePluginList() })}
>
Run /vencord-plugins

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Button } from "@components/Button";
import { openModal } from "@utils/modal";
import { Button } from "@webpack/common";
import { SessionInfo } from "../types";
import { RenameModal } from "./RenameModal";
@ -13,14 +13,9 @@ import { RenameModal } from "./RenameModal";
export function RenameButton({ session, state }: { session: SessionInfo["session"], state: [string, React.Dispatch<React.SetStateAction<string>>]; }) {
return (
<Button
look={Button.Looks.LINK}
color={Button.Colors.LINK}
size={Button.Sizes.NONE}
style={{
paddingTop: "0px",
paddingBottom: "0px",
top: "-2px"
}}
variant="secondary"
size="xs"
className="vc-betterSessions-rename-btn"
onClick={() =>
openModal(props => (
<RenameModal

View file

@ -75,19 +75,20 @@ export function RenameModal({ props, session, state }: { props: ModalProps, sess
</ModalContent>
<ModalFooter>
<Button
color={Button.Colors.BRAND}
onClick={onSaveClick}
>
Save
</Button>
<Button
color={Button.Colors.TRANSPARENT}
look={Button.Looks.LINK}
onClick={() => props.onClose()}
>
Cancel
</Button>
<div className="vc-betterSessions-footer-buttons">
<Button
color={Button.Colors.PRIMARY}
onClick={() => props.onClose()}
>
Cancel
</Button>
<Button
color={Button.Colors.BRAND}
onClick={onSaveClick}
>
Save
</Button>
</div>
</ModalFooter>
</ModalRoot >
);

View file

@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./styles.css";
import { showNotification } from "@api/Notifications";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";

View file

@ -0,0 +1,10 @@
.vc-betterSessions-footer-buttons {
display: flex;
gap: 0.5em;
align-items: center;
}
.vc-betterSessions-rename-btn {
margin-left: 4px;
translate: 0 -2px;
}

View file

@ -18,6 +18,7 @@
import { definePluginSettings, Settings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings";
import { Divider } from "@components/Divider";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
import { Link } from "@components/Link";
@ -422,7 +423,7 @@ export default definePlugin({
</Forms.FormText>
</Flex>
<Forms.FormDivider className={Margins.top8} />
<Divider className={Margins.top8} />
<div style={{ width: "284px", ...profileThemeStyle, marginTop: 8, borderRadius: 8, background: "var(--background-mod-faint)" }}>
{activity[0] && <ActivityView

View file

@ -4,9 +4,10 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Button } from "@components/Button";
import { Flex } from "@components/Flex";
import { findComponentByCodeLazy } from "@webpack";
import { Button, useEffect } from "@webpack/common";
import { useEffect } from "@webpack/common";
import { useAuthorizationStore } from "../../lib/stores/AuthorizationStore";
import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore";
@ -35,22 +36,22 @@ export default function DecorSection({ hideTitle = false, hideDivider = false, n
hideDivider={hideDivider}
className={noMargin && cl("section-remove-margin")}
>
<Flex>
<Flex style={{ gap: "4px " }}>
<Button
onClick={() => {
if (!authorization.isAuthorized()) {
authorization.authorize().then(openChangeDecorationModal).catch(() => { });
} else openChangeDecorationModal();
}}
size={Button.Sizes.SMALL}
variant="primary"
size="small"
>
Change Decoration
</Button>
{selectedDecoration && authorization.isAuthorized() && <Button
onClick={() => selectDecoration(null)}
color={Button.Colors.PRIMARY}
size={Button.Sizes.SMALL}
look={Button.Looks.LINK}
variant="secondary"
size={"small"}
>
Remove Decoration
</Button>}

View file

@ -4,11 +4,12 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Button as NewButton } from "@components/Button";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins";
import { classes, copyWithToast } from "@utils/misc";
import { copyWithToast } from "@utils/misc";
import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Queue } from "@utils/Queue";
import { User } from "@vencord/discord-types";
@ -222,8 +223,14 @@ function ChangeDecorationModal(props: ModalProps) {
</div>
</ErrorBoundary>
</ModalContent>
<ModalFooter className={classes(cl("change-decoration-modal-footer", cl("modal-footer")))}>
<div className={cl("change-decoration-modal-footer-btn-container")}>
<ModalFooter className={cl("change-decoration-modal-footer", "modal-footer")}>
<div className={cl("modal-footer-btn-container")}>
<Button
onClick={props.onClose}
color={Button.Colors.PRIMARY}
>
Cancel
</Button>
<Button
onClick={() => {
selectDecoration(tryingDecoration!).then(props.onClose);
@ -232,34 +239,10 @@ function ChangeDecorationModal(props: ModalProps) {
>
Apply
</Button>
<Button
onClick={props.onClose}
color={Button.Colors.PRIMARY}
look={Button.Looks.LINK}
>
Cancel
</Button>
</div>
<div className={cl("change-decoration-modal-footer-btn-container")}>
<Button
onClick={() => Alerts.show({
title: "Log Out",
body: "Are you sure you want to log out of Decor?",
confirmText: "Log Out",
confirmColor: cl("danger-btn"),
cancelText: "Cancel",
onConfirm() {
useAuthorizationStore.getState().remove(UserStore.getCurrentUser().id);
props.onClose();
}
})}
color={Button.Colors.PRIMARY}
look={Button.Looks.LINK}
>
Log Out
</Button>
<div className={cl("modal-footer-btn-container")}>
<Tooltip text="Join Decor's Discord Server for notifications on your decoration's review, and when new presets are released">
{tooltipProps => <Button
{tooltipProps => <NewButton
{...tooltipProps}
onClick={async () => {
if (!GuildStore.getGuild(GUILD_ID)) {
@ -274,12 +257,27 @@ function ChangeDecorationModal(props: ModalProps) {
NavigationRouter.transitionToGuild(GUILD_ID);
}
}}
color={Button.Colors.PRIMARY}
look={Button.Looks.LINK}
variant="link"
>
Discord Server
</Button>}
</NewButton>}
</Tooltip>
<NewButton
onClick={() => Alerts.show({
title: "Log Out",
body: "Are you sure you want to log out of Decor?",
confirmText: "Log Out",
confirmColor: cl("danger-btn"),
cancelText: "Cancel",
onConfirm() {
useAuthorizationStore.getState().remove(UserStore.getCurrentUser().id);
props.onClose();
}
})}
variant="dangerSecondary"
>
Log Out
</NewButton>
</div>
</ModalFooter>
</ModalRoot>;

View file

@ -145,24 +145,24 @@ function CreateDecorationModal(props: ModalProps) {
</ErrorBoundary>
</ModalContent>
<ModalFooter className={cl("modal-footer")}>
<Button
onClick={() => {
setSubmitting(true);
createDecoration({ alt: name, file: file! })
.then(props.onClose).catch(e => { setSubmitting(false); setError(e); });
}}
disabled={!file || !name}
submitting={submitting}
>
Submit for Review
</Button>
<Button
onClick={props.onClose}
color={Button.Colors.PRIMARY}
look={Button.Looks.LINK}
>
Cancel
</Button>
<div className={cl("modal-footer-btn-container")}>
<Button
onClick={props.onClose}
color={Button.Colors.PRIMARY}
>
Cancel
</Button>
<Button
onClick={() => {
setSubmitting(true);
createDecoration({ alt: name, file: file! })
.then(props.onClose).catch(e => { setSubmitting(false); setError(e); });
}}
disabled={!file || !name || submitting}
>
Submit for Review
</Button>
</div>
</ModalFooter>
</ModalRoot>;
}

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Flex } from "@components/Flex";
import { Link } from "@components/Link";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Button, Forms, Text } from "@webpack/common";
@ -41,22 +42,24 @@ function GuidelinesModal(props: ModalProps) {
</Forms.FormText>
</ModalContent>
<ModalFooter className={cl("modal-footer")}>
<Button
onClick={() => {
settings.store.agreedToGuidelines = true;
props.onClose();
openCreateDecorationModal();
}}
>
Continue
</Button>
<Button
onClick={props.onClose}
color={Button.Colors.PRIMARY}
look={Button.Looks.LINK}
>
Go Back
</Button>
<Flex style={{ gap: "4px" }}>
<Button
onClick={() => {
settings.store.agreedToGuidelines = true;
props.onClose();
openCreateDecorationModal();
}}
>
Continue
</Button>
<Button
onClick={props.onClose}
color={Button.Colors.PRIMARY}
look={Button.Looks.LINK}
>
Go Back
</Button>
</Flex>
</ModalFooter>
</ModalRoot>;
}

View file

@ -28,11 +28,6 @@
justify-content: space-between;
}
.vc-decor-change-decoration-modal-footer-btn-container {
display: flex;
flex-direction: row-reverse;
}
.vc-decor-create-decoration-modal-content {
display: flex;
flex-direction: column;
@ -53,6 +48,12 @@
padding: 16px;
}
.vc-decor-modal-footer-btn-container {
display: flex;
flex-direction: row;
gap: 0.5em;
}
.vc-decor-create-decoration-modal-form {
display: flex;
flex-direction: column;
@ -65,8 +66,10 @@
flex-direction: column;
overflow: hidden scroll;
max-height: 512px;
width: 352px; /* ((80 + 8 (grid gap)) * desired columns) (scrolled takes the extra 8 padding off conveniently) */
gap: 12px;
/* ((80 + 8 (grid gap)) * desired columns) (scrolled takes the extra 8 padding off conveniently) */
width: 352px;
}
.vc-decor-sectioned-grid-list-grid {
@ -77,4 +80,4 @@
.vc-decor-section-remove-margin {
margin-bottom: 0;
}
}

View file

@ -20,6 +20,7 @@ import { definePluginSettings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import { Paragraph } from "@components/Paragraph";
import { Devs, IS_MAC } from "@utils/constants";
import { Margins } from "@utils/margins";
import definePlugin, { OptionType } from "@utils/types";
@ -78,7 +79,7 @@ export default definePlugin({
replace: "$self.WarningCard(),"
}
},
// Change top right chat toolbar button from the help one to the dev one
// Change top right toolbar button from the help one to the dev one
{
find: '?"BACK_FORWARD_NAVIGATION":',
replacement: {
@ -87,7 +88,14 @@ export default definePlugin({
},
predicate: () => settings.store.toolbarDevMenu
},
// Disable opening the bug report menu when clicking the top right toolbar dev button
{
find: 'navId:"staff-help-popout"',
replacement: {
match: /(isShown.+?)onClick:\i/,
replace: (_, rest) => `${rest}onClick:()=>{}`
}
},
// Make the Favourites Server experiment allow favouriting DMs and threads
{
find: "useCanFavoriteChannel",
@ -137,14 +145,14 @@ export default definePlugin({
return (
<React.Fragment>
<Forms.FormTitle tag="h3">More Information</Forms.FormTitle>
<Forms.FormText variant="text-md/normal">
<Paragraph size="md">
You can open Discord's DevTools via {" "}
<div className={KbdStyles.combo} style={{ display: "inline-flex" }}>
<kbd className={KbdStyles.key}>{modKey}</kbd> +{" "}
<kbd className={KbdStyles.key}>{altKey}</kbd> +{" "}
<kbd className={KbdStyles.key}>O</kbd>{" "}
</div>
</Forms.FormText>
</Paragraph>
</React.Fragment>
);
},

View file

@ -20,6 +20,7 @@
import "./index.css";
import { definePluginSettings } from "@api/Settings";
import { Divider } from "@components/Divider";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { fetchUserProfile } from "@utils/discord";
@ -127,7 +128,7 @@ function SettingsAboutComponent() {
<li>• click the "Copy 3y3" button</li>
<li>• paste the invisible text anywhere in your bio</li>
</ul><br />
<Forms.FormDivider
<Divider
className={classes(Margins.top8, Margins.bottom8)}
/>
<Forms.FormTitle tag="h3">Color pickers</Forms.FormTitle>
@ -174,7 +175,7 @@ function SettingsAboutComponent() {
Copy 3y3
</Button>
</Flex>
<Forms.FormDivider
<Divider
className={classes(Margins.top8, Margins.bottom8)}
/>
<Forms.FormTitle tag="h3">Preview</Forms.FormTitle>

View file

@ -18,6 +18,7 @@
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
import { findStoreLazy } from "@webpack";
import { Constants, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserStore } from "@webpack/common";
@ -143,13 +144,17 @@ export default definePlugin({
// with will not be fetched; so, if they're not otherwise cached, they will not be shown
// This should not be a big deal as these should be rare
const callback = ({ chunks }) => {
const chunkCount = chunks.filter(chunk => chunk.nonce === sentNonce).length;
if (chunkCount === 0) return;
try {
const chunkCount = chunks.filter(chunk => chunk.nonce === sentNonce).length;
if (chunkCount === 0) return;
count -= chunkCount;
RelationshipStore.emitChange();
if (count <= 0) {
FluxDispatcher.unsubscribe("GUILD_MEMBERS_CHUNK_BATCH", callback);
count -= chunkCount;
RelationshipStore.emitChange();
if (count <= 0) {
FluxDispatcher.unsubscribe("GUILD_MEMBERS_CHUNK_BATCH", callback);
}
} catch (e) {
new Logger("ImplicitRelationships").error("Error in GUILD_MEMBERS_CHUNK_BATCH handler", e);
}
};

View file

@ -72,7 +72,6 @@ const ChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
<ChatBarButton
tooltip="Encrypt Message"
onClick={() => buildEncModal()}
buttonProps={{
"aria-haspopup": "dialog",
}}

View file

@ -285,18 +285,22 @@ export default definePlugin({
},
shouldIgnore(message: any, isEdit = false) {
const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds, logEdits, logDeletes } = Settings.plugins.MessageLogger;
const myId = UserStore.getCurrentUser().id;
try {
const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds, logEdits, logDeletes } = Settings.plugins.MessageLogger;
const myId = UserStore.getCurrentUser().id;
return ignoreBots && message.author?.bot ||
ignoreSelf && message.author?.id === myId ||
ignoreUsers.includes(message.author?.id) ||
ignoreChannels.includes(message.channel_id) ||
ignoreChannels.includes(ChannelStore.getChannel(message.channel_id)?.parent_id) ||
(isEdit ? !logEdits : !logDeletes) ||
ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id) ||
// Ignore Venbot in the support channels
(message.author?.id === VENBOT_USER_ID && ChannelStore.getChannel(message.channel_id)?.parent_id === SUPPORT_CATEGORY_ID);
return ignoreBots && message.author?.bot ||
ignoreSelf && message.author?.id === myId ||
ignoreUsers.includes(message.author?.id) ||
ignoreChannels.includes(message.channel_id) ||
ignoreChannels.includes(ChannelStore.getChannel(message.channel_id)?.parent_id) ||
(isEdit ? !logEdits : !logDeletes) ||
ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id) ||
// Ignore Venbot in the support channels
(message.author?.id === VENBOT_USER_ID && ChannelStore.getChannel(message.channel_id)?.parent_id === SUPPORT_CATEGORY_ID);
} catch (e) {
return false;
}
},
EditMarker({ message, className, children, ...props }: any) {

View file

@ -92,7 +92,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
size={ModalSize.LARGE}
>
<ModalHeader>
<Text className={cl("modal-title")} variant="heading-lg/semibold">{header} permissions:</Text>
<Text className={cl("modal-title")} variant="heading-lg/semibold">{header} Permissions</Text>
<ModalCloseButton onClick={modalProps.onClose} />
</ModalHeader>

View file

@ -17,6 +17,7 @@
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { HeadingTertiary } from "@components/Heading";
import { getIntlMessage } from "@utils/discord";
import { classes } from "@utils/misc";
import type { Guild, GuildMember } from "@vencord/discord-types";
@ -140,7 +141,7 @@ function UserPermissionsComponent({ guild, guildMember, closePopout }: { guild:
return <div>
<div className={cl("user-header-container")}>
<Text variant="eyebrow">Permissions</Text>
<HeadingTertiary>Permissions</HeadingTertiary>
<div className={cl("user-header-btns")}>
<Tooltip text={`Sorting by ${permissionsSortOrder === PermissionsSortOrder.HighestRole ? "Highest Role" : "Lowest Role"}`}>
{tooltipProps => (

View file

@ -191,11 +191,10 @@ export default definePlugin({
<TooltipContainer text="View Permissions">
<Button
{...popoutProps}
buttonRef={buttonRef}
ref={buttonRef}
color={Button.Colors.CUSTOM}
look={Button.Looks.FILLED}
size={Button.Sizes.NONE}
innerClassName={classes(RoleButtonClasses.buttonInner, RoleButtonClasses.icon)}
className={classes(RoleButtonClasses.button, RoleButtonClasses.icon, "vc-permviewer-role-button")}
>
<SafetyIcon height="16" width="16" />

View file

@ -5,6 +5,7 @@
*/
import { classNameFactory } from "@api/Styles";
import { Divider } from "@components/Divider";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from "@utils/modal";
import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
import { Button, ColorPicker, Forms, Text, TextInput, Toasts, useMemo, useState } from "@webpack/common";
@ -88,7 +89,7 @@ export function NewCategoryModal({ categoryId, modalProps, initialChannelId }: P
onChange={e => setName(e)}
/>
</section>
<Forms.FormDivider />
<Divider />
<section>
<Forms.FormTitle>Color</Forms.FormTitle>
<ColorPickerWithSwatches

View file

@ -19,12 +19,13 @@
import "./style.css";
import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList";
import { TextButton } from "@components/Button";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Channel } from "@vencord/discord-types";
import { findStoreLazy } from "@webpack";
import { Button, FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from "@webpack/common";
import { FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from "@webpack/common";
interface ThreadJoined {
channel: Channel;
@ -69,15 +70,13 @@ function onClick() {
}
const ReadAllButton = () => (
<Button
<TextButton
variant="secondary"
onClick={onClick}
size={Button.Sizes.MIN}
look={Button.Looks.BLANK}
color={Button.Colors.CUSTOM}
className="vc-ranb-button"
>
Read All
</Button>
</TextButton>
);
export default definePlugin({

View file

@ -2,9 +2,11 @@
color: var(--interactive-normal);
padding: 0 0.5em;
width: 100%;
font-size: 14px;
white-space: nowrap;
box-sizing: border-box;
}
.vc-ranb-button:hover {
color: var(--interactive-active);
}
}

View file

@ -26,7 +26,7 @@ import { classes } from "@utils/misc";
import definePlugin from "@utils/types";
import { Guild, User } from "@vencord/discord-types";
import { findByPropsLazy } from "@webpack";
import { Alerts, Button, Menu, Parser, TooltipContainer } from "@webpack/common";
import { Alerts, Clickable, Menu, Parser, TooltipContainer } from "@webpack/common";
import { Auth, initAuth, updateAuth } from "./auth";
import { openReviewsModal } from "./components/ReviewModal";
@ -35,7 +35,7 @@ import { getCurrentUserInfo, readNotification } from "./reviewDbApi";
import { settings } from "./settings";
import { showToast } from "./utils";
const RoleButtonClasses = findByPropsLazy("button", "buttonInner", "icon", "banner");
const BannerButtonClasses = findByPropsLazy("bannerButton");
const guildPopoutPatch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild, onClose(): void; }) => {
if (!guild) return;
@ -149,16 +149,12 @@ export default definePlugin({
BiteSizeReviewsButton: ErrorBoundary.wrap(({ user }: { user: User; }) => {
return (
<TooltipContainer text="View Reviews">
<Button
<Clickable
onClick={() => openReviewsModal(user.id, user.username, ReviewType.User)}
look={Button.Looks.FILLED}
size={Button.Sizes.NONE}
color={RoleButtonClasses.bannerColor}
className={classes(RoleButtonClasses.button, RoleButtonClasses.icon, RoleButtonClasses.banner)}
innerClassName={classes(RoleButtonClasses.buttonInner, RoleButtonClasses.icon, RoleButtonClasses.banner)}
className={classes(BannerButtonClasses.bannerButton)}
>
<NotesIcon height={16} width={16} />
</Button>
</Clickable>
</TooltipContainer>
);
}, { noop: true })

View file

@ -22,10 +22,12 @@ import { Settings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Flex } from "@components/Flex";
import { CopyIcon, ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons";
import { Paragraph } from "@components/Paragraph";
import { Span } from "@components/Span";
import { debounce } from "@shared/debounce";
import { openImageModal } from "@utils/discord";
import { classes, copyWithToast } from "@utils/misc";
import { ContextMenuApi, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
import { ContextMenuApi, FluxDispatcher, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
import { SeekBar } from "./SeekBar";
import { SpotifyStore, Track } from "./SpotifyStore";
@ -185,13 +187,14 @@ function SpotifySeekBar() {
return (
<div id={cl("progress-bar")}>
<Forms.FormText
variant="text-xs/medium"
<Span
size="xs"
weight="medium"
className={cl("progress-time") + " " + cl("time-left")}
aria-label="Progress"
>
{msToHuman(position)}
</Forms.FormText>
</Span>
<SeekBar
initialValue={position}
minValue={0}
@ -200,13 +203,14 @@ function SpotifySeekBar() {
asValueChanges={onChange}
onValueRender={msToHuman}
/>
<Forms.FormText
variant="text-xs/medium"
<Span
size="xs"
weight="medium"
className={cl("progress-time") + " " + cl("time-right")}
aria-label="Total Duration"
>
{msToHuman(duration)}
</Forms.FormText>
</Span>
</div>
);
}
@ -298,17 +302,17 @@ function Info({ track }: { track: Track; }) {
<div id={cl("info-wrapper")}>
{i}
<div id={cl("titles")}>
<Forms.FormText
variant="text-sm/semibold"
<Paragraph
weight="semibold"
id={cl("song-title")}
className={cl("ellipoverflow")}
title={track.name}
{...makeLinkProps("Song", track.id, track.name, `/track/${track.id}`)}
>
{track.name}
</Forms.FormText>
</Paragraph>
{track.artists.some(a => a.name) && (
<Forms.FormText variant="text-sm/normal" className={cl(["ellipoverflow", "secondary-song-info"])}>
<Paragraph className={cl(["ellipoverflow", "secondary-song-info"])}>
<span className={cl("song-info-prefix")}>by&nbsp;</span>
{track.artists.map((a, i) => (
<React.Fragment key={a.name}>
@ -323,10 +327,10 @@ function Info({ track }: { track: Track; }) {
{i !== track.artists.length - 1 && <span className={cl("comma")}>{", "}</span>}
</React.Fragment>
))}
</Forms.FormText>
</Paragraph>
)}
{track.album.name && (
<Forms.FormText variant="text-sm/normal" className={cl(["ellipoverflow", "secondary-song-info"])}>
<Paragraph className={cl(["ellipoverflow", "secondary-song-info"])}>
<span className={cl("song-info-prefix")}>on&nbsp;</span>
<span
id={cl("album-title")}
@ -337,7 +341,7 @@ function Info({ track }: { track: Track; }) {
>
{track.album.name}
</span>
</Forms.FormText>
</Paragraph>
)}
</div>
</div>

View file

@ -143,24 +143,22 @@ function TextReplace({ title, rulesArray }: TextReplaceProps) {
{
rulesArray.map((rule, index) =>
<React.Fragment key={`${rule.find}-${index}`}>
<Flex flexDirection="row" style={{ gap: 0 }}>
<Flex flexDirection="row" style={{ flexGrow: 1, gap: "0.5em" }}>
<Input
placeholder="Find"
initialValue={rule.find}
onChange={e => onChange(e, index, "find")}
/>
<Input
placeholder="Replace"
initialValue={rule.replace}
onChange={e => onChange(e, index, "replace")}
/>
<Input
placeholder="Only if includes"
initialValue={rule.onlyIfIncludes}
onChange={e => onChange(e, index, "onlyIfIncludes")}
/>
</Flex>
<Flex flexDirection="row" style={{ flexGrow: 1, gap: "0.5em" }}>
<Input
placeholder="Find"
initialValue={rule.find}
onChange={e => onChange(e, index, "find")}
/>
<Input
placeholder="Replace"
initialValue={rule.replace}
onChange={e => onChange(e, index, "replace")}
/>
<Input
placeholder="Only if includes"
initialValue={rule.onlyIfIncludes}
onChange={e => onChange(e, index, "onlyIfIncludes")}
/>
<Button
size={Button.Sizes.MIN}
onClick={() => onClickRemove(index)}

View file

@ -5,6 +5,7 @@
*/
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin from "@utils/types";
import { Message } from "@vencord/discord-types";
import { UserStore } from "@webpack/common";
@ -66,12 +67,17 @@ export default definePlugin({
},
getMessageProps(props: { message: Message; }) {
const author = props.message?.author;
const authorId = author?.id;
return {
"data-author-id": authorId,
"data-author-username": author?.username,
"data-is-self": authorId && authorId === UserStore.getCurrentUser()?.id,
};
try {
const author = props.message?.author;
const authorId = author?.id;
return {
"data-author-id": authorId,
"data-author-username": author?.username,
"data-is-self": authorId && authorId === UserStore.getCurrentUser()?.id,
};
} catch (e) {
new Logger("ThemeAttributes").error("Error in getMessageProps", e);
return {};
}
}
});

View file

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Divider } from "@components/Divider";
import { FormSwitch } from "@components/FormSwitch";
import { Margins } from "@utils/margins";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
@ -91,7 +92,7 @@ export function TranslateModal({ rootProps }: { rootProps: ModalProps; }) {
/>
))}
<Forms.FormDivider className={Margins.bottom16} />
<Divider className={Margins.bottom16} />
<AutoTranslateToggle />
</ModalContent>

View file

@ -19,6 +19,7 @@
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { CodeBlock } from "@components/CodeBlock";
import { Divider } from "@components/Divider";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants";
@ -75,7 +76,7 @@ function openViewRawModal(json: string, type: string, msgContent?: string) {
<>
<Forms.FormTitle tag="h5">Content</Forms.FormTitle>
<CodeBlock content={msgContent} lang="" />
<Forms.FormDivider className={Margins.bottom20} />
<Divider className={Margins.bottom20} />
</>
)}

View file

@ -130,7 +130,7 @@ export default definePlugin({
useEffect(() => {
const cb = (e: any) => {
if (e.messageId === message.id)
if (e?.messageId === message.id)
forceUpdate();
};
FluxDispatcher.subscribe("MESSAGE_REACTION_ADD_USERS", cb);

12
src/utils/css.ts Normal file
View file

@ -0,0 +1,12 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export function createAndAppendStyle(id: string, target = document.documentElement) {
const style = document.createElement("style");
style.id = id;
target.append(style);
return style;
}

View file

@ -100,7 +100,11 @@ export const enum Theme {
}
export function getTheme(): Theme {
return UserSettingsActionCreators.PreloadedUserSettingsActionCreators.getCurrentValue()?.appearance?.theme;
try {
return UserSettingsActionCreators.PreloadedUserSettingsActionCreators.getCurrentValue()?.appearance?.theme;
} catch {
return Theme.Dark;
}
}
export function insertTextIntoChatInputBox(text: string) {

View file

@ -16,20 +16,5 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
let styleStr = "";
export const Margins: Record<`${"top" | "bottom" | "left" | "right"}${8 | 16 | 20}`, string> = {} as any;
for (const dir of ["top", "bottom", "left", "right"] as const) {
for (const size of [8, 16, 20] as const) {
const cl = `vc-m-${dir}-${size}`;
Margins[`${dir}${size}`] = cl;
styleStr += `.${cl}{margin-${dir}:${size}px;}`;
}
}
document.addEventListener("DOMContentLoaded", () =>
document.head.append(Object.assign(document.createElement("style"), {
textContent: styleStr,
id: "vencord-margins"
})), { once: true });
// TODO: Migrate all usages from utils to components
export { Margins } from "@components/margins";

View file

@ -19,16 +19,11 @@
import { Settings, SettingsStore } from "@api/Settings";
import { ThemeStore } from "@webpack/common";
import { createAndAppendStyle } from "./css";
let style: HTMLStyleElement;
let themesStyle: HTMLStyleElement;
function createStyle(id: string) {
const style = document.createElement("style");
style.id = id;
document.documentElement.append(style);
return style;
}
async function initSystemValues() {
const values = await VencordNative.themes.getSystemValues();
const variables = Object.entries(values)
@ -36,13 +31,13 @@ async function initSystemValues() {
.map(([k, v]) => `--${k}: ${v};`)
.join("");
createStyle("vencord-os-theme-values").textContent = `:root{${variables}}`;
createAndAppendStyle("vencord-os-theme-values").textContent = `:root{${variables}}`;
}
async function toggle(isEnabled: boolean) {
if (!style) {
if (isEnabled) {
style = createStyle("vencord-custom-css");
style = createAndAppendStyle("vencord-custom-css");
VencordNative.quickCss.addChangeListener(css => {
style.textContent = css;
// At the time of writing this, changing textContent resets the disabled state
@ -55,7 +50,7 @@ async function toggle(isEnabled: boolean) {
}
async function initThemes() {
themesStyle ??= createStyle("vencord-themes");
themesStyle ??= createAndAppendStyle("vencord-themes");
const { themeLinks, enabledThemes } = Settings;

View file

@ -1,25 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import * as t from "@vencord/discord-types";
import { Text } from "./components";
// TODO: replace with our own component soon
export const FormText: t.FormText = function FormText(props) {
const variant = props.variant || "text-sm/normal";
return (
<Text
variant={variant}
{...props}
>
{props.children}
</Text>
);
} as any;
// @ts-expect-error
FormText.Types = {};

View file

@ -16,29 +16,35 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { TextCompat } from "@components/BaseText";
import { ButtonCompat } from "@components/Button";
import { Divider } from "@components/Divider";
import { FormSwitchCompat } from "@components/FormSwitch";
import { Heading } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
import { LazyComponent } from "@utils/lazyReact";
import * as t from "@vencord/discord-types";
import { filters, find, LazyComponentWebpack, mapMangledModuleLazy, waitFor } from "@webpack";
import { filters, mapMangledModuleLazy, waitFor } from "@webpack";
import { FormText } from "./FormText";
import { waitForComponent } from "./internal";
const FormTitle = waitForComponent<t.FormTitle>("FormTitle", filters.componentByCode('["defaultMargin".concat', '="h5"'));
const FormDivider = waitForComponent<t.FormDivider>("FormDivider", filters.componentByCode(".divider,", ",style:", '"div"', /\.divider,\i\),style:/));
export const Forms = {
FormTitle,
FormText,
// TODO: Stop using this and use Heading/Paragraph directly
FormTitle: Heading,
FormText: Paragraph,
/** @deprecated don't use this */
FormSection: "section" as never, // Backwards compat since Vesktop uses this
FormDivider
/** @deprecated use `@components/Divider` */
FormDivider: Divider as never, // Backwards compat since Vesktop uses this
};
export const Card = waitForComponent<t.Card>("Card", filters.componentByCode(".editable),", ".outline:"));
export const Button = waitForComponent<t.Button>("Button", filters.componentByCode("#{intl::A11Y_LOADING_STARTED}))),!1"));
// TODO: Stop using this and use Paragraph/Span directly
export const Text = TextCompat;
export const Button = ButtonCompat;
/** @deprecated Use FormSwitch from Vencord */
export const Switch = FormSwitchCompat as never;
export const Card = waitForComponent<t.Card>("Card", filters.componentByCode(".editable),", ".outline:"));
export const Checkbox = waitForComponent<t.Checkbox>("Checkbox", filters.componentByCode(".checkboxWrapperDisabled:"));
const Tooltips = mapMangledModuleLazy(".tooltipTop,bottom:", {
@ -54,17 +60,7 @@ export const TooltipContainer = LazyComponent(() => Tooltips.TooltipContainer);
export const TextInput = waitForComponent<t.TextInput>("TextInput", filters.componentByCode("#{intl::MAXIMUM_LENGTH_ERROR}", '"input"'));
export const TextArea = waitForComponent<t.TextArea>("TextArea", filters.componentByCode("this.getPaddingRight()},id:"));
export const Text = waitForComponent<t.Text>("Text", filters.componentByCode('case"always-white"'));
export const Heading = waitForComponent<t.Heading>("Heading", filters.componentByCode(">6?{", "variant:"));
export const Select: t.Select = LazyComponentWebpack(() => {
const oldFilter = filters.componentByCode('="bottom",', ".select,", '"Escape"===');
const newFilter = filters.componentByCode('"Select"', ".newOptionLabel");
const res = find(oldFilter) ?? find(newFilter);
if (res) return res;
throw new Error("Vencord failed to find the Select component");
});
export const Select = waitForComponent<t.Select>("Select", filters.componentByCode('"Select"', ".newOptionLabel"));
export const SearchableSelect = waitForComponent<t.SearchableSelect>("SearchableSelect", filters.componentByCode('"SearchableSelect"'));
export const Slider = waitForComponent<t.Slider>("Slider", filters.componentByCode('"markDash".concat('));
export const Popout = waitForComponent<t.Popout>("Popout", filters.componentByCode("ref:this.ref,", "renderPopout:this.renderPopout,"));

View file

@ -16,14 +16,22 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Logger } from "@utils/Logger";
import { LazyComponent, LazyComponentWrapper } from "@utils/react";
import { FilterFn, filters, lazyWebpackSearchHistory, waitFor } from "@webpack";
const logger = new Logger("Webpack");
export function waitForComponent<T extends React.ComponentType<any> = React.ComponentType<any> & Record<string, any>>(name: string, filter: FilterFn | string | string[]) {
if (IS_REPORTER) lazyWebpackSearchHistory.push(["waitForComponent", Array.isArray(filter) ? filter : [filter]]);
let myValue: T = function () {
throw new Error(`Vencord could not find the ${name} Component`);
const error = new Error(`Vencord could not find the ${name} Component`);
logger.error(error);
if (IS_DEV) throw error;
return null;
} as any;
const lazyComponent = LazyComponent(() => myValue) as LazyComponentWrapper<T>;

View file

@ -211,6 +211,8 @@ export function handleModuleNotFound(method: string, ...filter: unknown[]) {
* Find the first module that matches the filter
*/
export const find = traceFunction("find", function find(filter: FilterFn, { isIndirect = false, isWaitFor = false }: { isIndirect?: boolean; isWaitFor?: boolean; } = {}) {
if (IS_ANTI_CRASH_TEST) return null;
if (typeof filter !== "function")
throw new Error("Invalid filter. Expected a function got " + typeof filter);
@ -270,6 +272,8 @@ export function findAll(filter: FilterFn) {
* @returns Array of results in the same order as the passed filters
*/
export const findBulk = traceFunction("findBulk", function findBulk(...filterFns: FilterFn[]) {
if (IS_ANTI_CRASH_TEST) return [];
if (!Array.isArray(filterFns))
throw new Error("Invalid filters. Expected function[] got " + typeof filterFns);
@ -595,6 +599,9 @@ function getAllPropertyNames(object: Record<PropertyKey, any>, includeNonEnumera
export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule<S extends string>(code: string | RegExp | CodeFilter, mappers: Record<S, FilterFn>, includeBlacklistedExports = false): Record<S, any> {
const exports = {} as Record<S, any>;
// whitelist Modal API to be able to test modals
if (IS_ANTI_CRASH_TEST && code !== ':"thin")' && code !== ".modalKey?") return exports;
const id = findModuleId(...Array.isArray(code) ? code : [code]);
if (id === null)
return exports;
@ -635,6 +642,8 @@ export const ChunkIdsRegex = /\("([^"]+?)"\)/g;
* @returns A promise that resolves with a boolean whether the chunks were loaded
*/
export async function extractAndLoadChunks(code: CodeFilter, matcher = DefaultExtractAndLoadChunksRegex) {
if (IS_ANTI_CRASH_TEST) return false;
const module = findModuleFactory(...code);
if (!module) {
const err = new Error("extractAndLoadChunks: Couldn't find module factory");
@ -718,6 +727,9 @@ export function extractAndLoadChunksLazy(code: CodeFilter, matcher = DefaultExtr
* then call the callback with the module as the first argument
*/
export function waitFor(filter: string | PropsFilter | FilterFn, callback: CallbackFn, { isIndirect = false }: { isIndirect?: boolean; } = {}) {
// if react find fails then we are fully cooked
if (IS_ANTI_CRASH_TEST && filter !== "useState") return;
if (IS_REPORTER && !isIndirect) lazyWebpackSearchHistory.push(["waitFor", Array.isArray(filter) ? filter : [filter]]);
if (typeof filter === "string")