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:
parent
732f7dd4b4
commit
cc01bb70cb
83 changed files with 1233 additions and 524 deletions
|
|
@ -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: {
|
||||
|
|
|
|||
83
packages/discord-types/src/components.d.ts
vendored
83
packages/discord-types/src/components.d.ts
vendored
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
8
src/components/BaseText.css
Normal file
8
src/components/BaseText.css
Normal 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
100
src/components/BaseText.tsx
Normal 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
199
src/components/Button.css
Normal 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
107
src/components/Button.tsx
Normal 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
|
||||
6
src/components/Divider.css
Normal file
6
src/components/Divider.css
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.vc-divider {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
21
src/components/Divider.tsx
Normal file
21
src/components/Divider.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
.vc-form-divider {
|
||||
border-top: thin solid var(--border-subtle);
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
|
|
@ -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)} />;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
65
src/components/Heading.css
Normal file
65
src/components/Heading.css
Normal 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
|
||||
}
|
||||
60
src/components/Heading.tsx
Normal file
60
src/components/Heading.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
17
src/components/Paragraph.tsx
Normal file
17
src/components/Paragraph.tsx
Normal 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
17
src/components/Span.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
15
src/components/css.ts
Normal 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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
43
src/components/margins.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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)}>
|
||||
|
|
|
|||
|
|
@ -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")} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
1
src/globals.d.ts
vendored
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 >
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
10
src/plugins/betterSessions/styles.css
Normal file
10
src/plugins/betterSessions/styles.css
Normal 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,6 @@ const ChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
|
|||
<ChatBarButton
|
||||
tooltip="Encrypt Message"
|
||||
onClick={() => buildEncModal()}
|
||||
|
||||
buttonProps={{
|
||||
"aria-haspopup": "dialog",
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 => (
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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 </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 </span>
|
||||
<span
|
||||
id={cl("album-title")}
|
||||
|
|
@ -337,7 +341,7 @@ function Info({ track }: { track: Track; }) {
|
|||
>
|
||||
{track.album.name}
|
||||
</span>
|
||||
</Forms.FormText>
|
||||
</Paragraph>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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 {};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
12
src/utils/css.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {};
|
||||
|
|
@ -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,"));
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue