diff --git a/browser/content.js b/browser/content.js index 57964af64..2b51ebabf 100644 --- a/browser/content.js +++ b/browser/content.js @@ -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: { diff --git a/package.json b/package.json index 07ec6a400..ea5f601b8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "equicord", "private": "true", - "version": "1.13.2", + "version": "1.13.3", "description": "The other cutest Discord client mod", "homepage": "https://github.com/Equicord/Equicord#readme", "bugs": { diff --git a/packages/discord-types/src/components.d.ts b/packages/discord-types/src/components.d.ts index d8cc74b55..29e90eab6 100644 --- a/packages/discord-types/src/components.d.ts +++ b/packages/discord-types/src/components.d.ts @@ -1,50 +1,40 @@ import { Moment } from "moment"; 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 & { variant?: TextVariant; tag?: "div" | "span" | "p" | "strong" | HeadingTag; - selectable?: boolean; - lineClamp?: number; }>; export type Text = ComponentType; -export type Heading = ComponentType; -export type FormTitle = ComponentType & PropsWithChildren<{ - /** default is h5 */ - tag?: HeadingTag; - faded?: boolean; - disabled?: boolean; - required?: boolean; - error?: ReactNode; -}>>; +export interface ButtonProps extends PropsWithChildren, "size">> { + /** Button.Looks.FILLED */ + look?: string; + /** Button.Colors.BRAND */ + color?: string; + /** Button.Sizes.MEDIUM */ + size?: string; -export type FormSection = ComponentType>; +} -export type FormDivider = ComponentType<{ - className?: string; - style?: CSSProperties; -}>; +export type Button = ComponentType & { + 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; +// #endregion export type Tooltip = ComponentType<{ text: ReactNode | ComponentType; @@ -122,7 +112,6 @@ export type ComboboxPopout = ComponentType>; -export interface ButtonProps extends PropsWithChildren, "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; - focusProps?: any; - submitting?: boolean; - - submittingStartedLabel?: string; - submittingFinishedLabel?: string; -} - -export type Button = ComponentType & { - 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"; diff --git a/scripts/build/build.mjs b/scripts/build/build.mjs index 925cfd952..8a1186716 100644 --- a/scripts/build/build.mjs +++ b/scripts/build/build.mjs @@ -24,7 +24,7 @@ import { readdir, writeFile } from "fs/promises"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; -import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_COMPANION_TEST, 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_COMPANION_TEST, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, commonRendererPlugins, watch, buildOrWatchAll, stringifyValues, IS_ANTI_CRASH_TEST } from "./common.mjs"; const defines = stringifyValues({ IS_STANDALONE, @@ -32,6 +32,7 @@ const defines = stringifyValues({ IS_REPORTER, IS_COMPANION_TEST, IS_UPDATER_DISABLED, + IS_ANTI_CRASH_TEST, IS_WEB: false, IS_EXTENSION: false, IS_USERSCRIPT: false, diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs index 262832ae2..86eec848d 100644 --- a/scripts/build/buildWeb.mjs +++ b/scripts/build/buildWeb.mjs @@ -23,7 +23,7 @@ import { readFileSync } from "fs"; import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises"; import path, { join } from "path"; -import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, IS_COMPANION_TEST, VERSION, commonRendererPlugins, buildOrWatchAll, stringifyValues } from "./common.mjs"; +import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, IS_COMPANION_TEST, VERSION, commonRendererPlugins, buildOrWatchAll, stringifyValues, IS_ANTI_CRASH_TEST } from "./common.mjs"; import AdmZip from "adm-zip"; /** @@ -48,6 +48,7 @@ const commonOptions = { IS_DEV, IS_REPORTER, IS_COMPANION_TEST, + IS_ANTI_CRASH_TEST, IS_DISCORD_DESKTOP: false, IS_VESKTOP: false, IS_EQUIBOP: false, @@ -185,7 +186,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" diff --git a/scripts/build/common.mjs b/scripts/build/common.mjs index 2293eaa75..dc1949e13 100644 --- a/scripts/build/common.mjs +++ b/scripts/build/common.mjs @@ -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_COMPANION_TEST = IS_REPORTER && process.argv.includes("--companion-test"); if (!IS_COMPANION_TEST && process.argv.includes("--companion-test")) diff --git a/src/Vencord.ts b/src/Vencord.ts index 6aae9872b..bca2e046f 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -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"; @@ -181,12 +183,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 }); diff --git a/src/api/ChatButtons.tsx b/src/api/ChatButtons.tsx index 1cacd06b2..2fea24d3d 100644 --- a/src/api/ChatButtons.tsx +++ b/src/api/ChatButtons.tsx @@ -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; - onContextMenu?: MouseEventHandler; - onAuxClick?: MouseEventHandler; - buttonProps?: Omit, "size" | "onClick" | "onContextMenu" | "onAuxClick">; + onClick: MouseEventHandler; + onContextMenu?: MouseEventHandler; + onAuxClick?: MouseEventHandler; + buttonProps?: Omit, "size" | "onClick" | "onContextMenu" | "onAuxClick">; } export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => { return ( {({ onMouseEnter, onMouseLeave }) => (
- +
)}
diff --git a/src/components/BaseText.css b/src/components/BaseText.css new file mode 100644 index 000000000..d322a5679 --- /dev/null +++ b/src/components/BaseText.css @@ -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; +} \ No newline at end of file diff --git a/src/components/BaseText.tsx b/src/components/BaseText.tsx new file mode 100644 index 000000000..1abddf7a9 --- /dev/null +++ b/src/components/BaseText.tsx @@ -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 = ComponentPropsWithoutRef & { + size?: TextSize; + weight?: TextWeight; + tag?: Tag; +}; + +export function BaseText(props: BaseTextProps): ReactNode { + const { + size = "md", + weight = "normal", + tag: Tag = "div", + children, + className, + ...restProps + } = props; + + return ( + + {children} + + ); +} + +// #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 ; +}; + +// #endregion diff --git a/src/components/Button.css b/src/components/Button.css new file mode 100644 index 000000000..48c0dd5df --- /dev/null +++ b/src/components/Button.css @@ -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); +} \ No newline at end of file diff --git a/src/components/Button.tsx b/src/components/Button.tsx new file mode 100644 index 000000000..b3df68aba --- /dev/null +++ b/src/components/Button.tsx @@ -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 ( + + ); +} + +export type TextButtonVariant = "primary" | "secondary" | "danger" | "link"; + +export type TextButtonProps = ComponentPropsWithRef<"button"> & { + variant?: TextButtonVariant; +}; + +export function TextButton({ variant = "primary", className, ...restProps }: TextButtonProps) { + return ( + ) : ( <> - Plugin Management - Press the cog wheel or info icon to get more info on a plugin - Plugins with a cog wheel have settings you can modify! + Plugin Management + Press the cog wheel or info icon to get more info on a plugin + Plugins with a cog wheel have settings you can modify! )} {enabledPlugins.length > 0 && !required && ( @@ -119,10 +118,10 @@ function ExcludedPluginsList({ search }: { search: string; }) { .filter(([name]) => name.toLowerCase().includes(search)); return ( - + {matchingExcludedPlugins.length ? <> - Are you looking for: + Are you looking for:
    {matchingExcludedPlugins.map(([name, reason]) => (
  • @@ -133,7 +132,7 @@ function ExcludedPluginsList({ search }: { search: string; }) { : "No plugins meet the search criteria." } - + ); } @@ -358,15 +357,15 @@ export default function PluginSettings() { /> - + Filters - +
    -
    +
    onChange(e, index, "find")} - /> - onChange(e, index, "replace")} - /> - onChange(e, index, "onlyIfIncludes")} - /> - + + onChange(e, index, "find")} + /> + onChange(e, index, "replace")} + /> + onChange(e, index, "onlyIfIncludes")} + /> {(index !== rulesArray.length - 1) &&
    @@ -186,7 +185,7 @@ function TextReplace({ title, rulesArray }: TextReplaceProps) { } - {(index !== rulesArray.length - 1) && } + {(index !== rulesArray.length - 1) && } {isRegexRules && renderFindError(rule.find)} ) diff --git a/src/plugins/themeAttributes/index.ts b/src/plugins/themeAttributes/index.ts index ca54ea27c..beb108af8 100644 --- a/src/plugins/themeAttributes/index.ts +++ b/src/plugins/themeAttributes/index.ts @@ -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 {}; + } } }); diff --git a/src/plugins/translate/TranslateModal.tsx b/src/plugins/translate/TranslateModal.tsx index 98306538e..49b75bb5c 100644 --- a/src/plugins/translate/TranslateModal.tsx +++ b/src/plugins/translate/TranslateModal.tsx @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +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; }) { /> ))} - + diff --git a/src/plugins/viewRaw/index.tsx b/src/plugins/viewRaw/index.tsx index 1dcef31b2..0a4103535 100644 --- a/src/plugins/viewRaw/index.tsx +++ b/src/plugins/viewRaw/index.tsx @@ -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) { <> Content - + )} diff --git a/src/plugins/whoReacted/index.tsx b/src/plugins/whoReacted/index.tsx index 85f3107c4..05b64dfeb 100644 --- a/src/plugins/whoReacted/index.tsx +++ b/src/plugins/whoReacted/index.tsx @@ -140,7 +140,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); diff --git a/src/utils/css.ts b/src/utils/css.ts new file mode 100644 index 000000000..3c46b7a4d --- /dev/null +++ b/src/utils/css.ts @@ -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; +} diff --git a/src/utils/discord.tsx b/src/utils/discord.tsx index 837136256..a2b95ac15 100644 --- a/src/utils/discord.tsx +++ b/src/utils/discord.tsx @@ -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) { diff --git a/src/utils/margins.ts b/src/utils/margins.ts index 5d7eed766..057c3c0e2 100644 --- a/src/utils/margins.ts +++ b/src/utils/margins.ts @@ -16,20 +16,5 @@ * along with this program. If not, see . */ -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"; diff --git a/src/utils/quickCss.ts b/src/utils/quickCss.ts index 221bcbf14..82802c65b 100644 --- a/src/utils/quickCss.ts +++ b/src/utils/quickCss.ts @@ -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 { enabledThemeLinks, enabledThemes } = Settings; diff --git a/src/webpack/common/FormText.tsx b/src/webpack/common/FormText.tsx deleted file mode 100644 index 8c371ed48..000000000 --- a/src/webpack/common/FormText.tsx +++ /dev/null @@ -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 ( - - {props.children} - - ); -} as any; - -// @ts-expect-error -FormText.Types = {}; diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index aa5c3626e..cc282fd91 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -16,29 +16,35 @@ * along with this program. If not, see . */ +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, mapMangledModuleLazy, waitFor } from "@webpack"; -import { FormText } from "./FormText"; import { waitForComponent } from "./internal"; -const FormTitle = waitForComponent("FormTitle", filters.componentByCode('["defaultMargin".concat', '="h5"')); -const FormDivider = waitForComponent("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("Card", filters.componentByCode(".editable),", ".outline:")); -export const Button = waitForComponent("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("Card", filters.componentByCode(".editable),", ".outline:")); export const Checkbox = waitForComponent("Checkbox", filters.componentByCode(".checkboxWrapperDisabled:")); const Tooltips = mapMangledModuleLazy(".tooltipTop,bottom:", { @@ -54,8 +60,6 @@ export const TooltipContainer = LazyComponent(() => Tooltips.TooltipContainer); export const TextInput = waitForComponent("TextInput", filters.componentByCode("#{intl::MAXIMUM_LENGTH_ERROR}", '"input"')); export const TextArea = waitForComponent("TextArea", filters.componentByCode("this.getPaddingRight()},id:")); -export const Text = waitForComponent("Text", filters.componentByCode('case"always-white"')); -export const Heading = waitForComponent("Heading", filters.componentByCode(">6?{", "variant:")); export const Select = waitForComponent("Select", filters.componentByCode('"Select"', ".newOptionLabel")); export const SearchableSelect = waitForComponent("SearchableSelect", filters.componentByCode('"SearchableSelect"')); export const Slider = waitForComponent("Slider", filters.componentByCode('"markDash".concat(')); diff --git a/src/webpack/common/internal.tsx b/src/webpack/common/internal.tsx index 2fccbe870..4eb8eb66f 100644 --- a/src/webpack/common/internal.tsx +++ b/src/webpack/common/internal.tsx @@ -16,14 +16,22 @@ * along with this program. If not, see . */ +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 = React.ComponentType & Record>(name: string, filter: FilterFn | string | string[]) { if (IS_REPORTER) lazyWebpackSearchHistory.push(["waitForComponent", Array.isArray(filter) ? filter : [filter]]); let myValue: T = function () { - throw new Error(`Equicord could not find the ${name} Component`); + const error = new Error(`Equicord could not find the ${name} Component`); + logger.error(error); + + if (IS_DEV) throw error; + + return null; } as any; const lazyComponent = LazyComponent(() => myValue) as LazyComponentWrapper; diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 3612aea49..d8f6d95c3 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -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); @@ -597,6 +601,9 @@ function getAllPropertyNames(object: Record, includeNonEnumera export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule(code: string | RegExp | CodeFilter, mappers: Record, includeBlacklistedExports = false): Record { const exports = {} as Record; + // 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; @@ -637,6 +644,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"); @@ -720,6 +729,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")