UI related stuff (#545)

* components changes

* feat(UpdaterTab): redesign

* feat(BackupTab): redesign

* feat(PatchTab): minor changes

* feat(ChangelogTab): minor changes

* feat(EquicordTab): minor changes

* t

* L

* bruh

* t

* framework

* t

* fixes

* hell je

* Revert "hell je"

This reverts commit 3f3134249c690cb50887ed6ee371790e6d4fd262.

* fixes

* fix fix

---------

Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
prism 2025-12-07 21:28:03 +01:00 • committed by GitHub
parent dc51a80d55
commit bc9ecbd13f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 603 additions and 644 deletions

View file

@ -135,7 +135,7 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
const data = await res.arrayBuffer();
const settings = new TextDecoder().decode(inflateSync(new Uint8Array(data)));
await importSettings(settings);
await importSettings(settings, "all", true);
// sync with server timestamp instead of local one
PlainSettings.cloud.settingsSyncVersion = written;

View file

@ -11,6 +11,8 @@ import { moment, Toasts } from "@webpack/common";
import { DataStore } from "..";
type BackupType = "all" | "plugins" | "css" | "datastore";
const toast = (type: string, message: string) =>
Toasts.show({
type,
@ -41,7 +43,7 @@ function isSafeObject(obj: any) {
return true;
}
export async function importSettings(data: string) {
export async function importSettings(data: string, type: BackupType = "all", cloud = false) {
try {
var parsed = JSON.parse(data);
} catch (err) {
@ -52,29 +54,60 @@ export async function importSettings(data: string) {
if (!isSafeObject(parsed))
throw new Error("Unsafe Settings");
if ("settings" in parsed && "quickCss" in parsed) {
Object.assign(PlainSettings, parsed.settings);
await VencordNative.settings.set(parsed.settings);
await VencordNative.quickCss.set(parsed.quickCss);
if (parsed.dataStore) await DataStore.setMany(parsed.dataStore);
} else
throw new Error("Invalid Settings. Is this even a Vencord Settings file?");
switch (type) {
case "all": {
if (!cloud && (!("settings" in parsed) || !("quickCss" in parsed) || !("dataStore" in parsed)))
throw new Error("Invalid Settings. Missing one or more required keys.");
if (parsed.settings) {
Object.assign(PlainSettings, parsed.settings);
await VencordNative.settings.set(parsed.settings);
}
if (parsed.quickCss) await VencordNative.quickCss.set(parsed.quickCss);
if (parsed.dataStore) await DataStore.setMany(parsed.dataStore);
break;
}
case "plugins": {
if (!parsed.settings?.settings) throw new Error("Plugin settings missing");
Object.assign(PlainSettings, parsed.settings.settings);
await VencordNative.settings.set(parsed.settings.settings);
break;
}
case "css": {
if (!parsed.quickCss) throw new Error("CSS missing");
await VencordNative.quickCss.set(parsed.quickCss);
break;
}
case "datastore": {
if (!parsed.dataStore) throw new Error("DataStore data missing");
await DataStore.setMany(parsed.dataStore);
break;
}
}
}
export async function exportSettings({ syncDataStore = true, minify }: { syncDataStore?: boolean; minify?: boolean; }) {
export async function exportSettings({ syncDataStore = true, type = "all", minify }: { syncDataStore?: boolean; type?: BackupType; minify?: boolean; }) {
const settings = VencordNative.settings.get();
const quickCss = await VencordNative.quickCss.get();
const dataStore = syncDataStore ? await DataStore.entries() : undefined;
return JSON.stringify(
{
settings,
quickCss,
...(syncDataStore && { dataStore })
},
null,
minify ? undefined : 4
);
switch (type) {
case "all": {
return JSON.stringify({ settings, quickCss, ...(syncDataStore && { dataStore }) }, null, minify ? undefined : 4);
}
case "plugins": {
return JSON.stringify({ settings: { settings } }, null, minify ? undefined : 4);
}
case "css": {
return JSON.stringify({ quickCss }, null, minify ? undefined : 4);
}
case "datastore": {
return JSON.stringify({ dataStore }, null, minify ? undefined : 4);
}
}
}
export async function exportPlugins({ minify }: { minify?: boolean; } = {}) {
@ -92,28 +125,8 @@ export async function exportDataStores({ minify }: { minify?: boolean; } = {}) {
return JSON.stringify({ dataStore }, null, minify ? undefined : 4);
}
type BackupType = "settings" | "plugins" | "css" | "datastore";
export async function downloadSettingsBackup(type: BackupType, { minify }: { minify?: boolean; } = {}) {
let backup: string;
switch (type) {
case "settings":
backup = await exportSettings({ minify });
break;
case "plugins":
backup = await exportPlugins({ minify });
break;
case "css":
backup = await exportCSS({ minify });
break;
case "datastore":
backup = await exportDataStores({ minify });
break;
default:
throw new Error("Invalid backup type");
}
export async function downloadSettingsBackup(type: BackupType = "all", { minify }: { minify?: boolean; } = {}) {
const backup = await exportSettings({ minify, type });
const filename = `equicord-${type}-backup-${moment().format("YYYY-MM-DD")}.json`;
const data = new TextEncoder().encode(backup);
@ -124,7 +137,7 @@ export async function downloadSettingsBackup(type: BackupType, { minify }: { min
}
}
export async function uploadSettingsBackup(showToast = true): Promise<void> {
export async function uploadSettingsBackup(type: BackupType = "all", showToast = true): Promise<void> {
if (IS_DISCORD_DESKTOP) {
const [file] = await DiscordNative.fileManager.openFiles({
filters: [
@ -135,7 +148,7 @@ export async function uploadSettingsBackup(showToast = true): Promise<void> {
if (file) {
try {
await importSettings(new TextDecoder().decode(file.data));
await importSettings(new TextDecoder().decode(file.data), type);
if (showToast) toastSuccess();
} catch (err) {
logger.error(err);
@ -149,7 +162,7 @@ export async function uploadSettingsBackup(showToast = true): Promise<void> {
const reader = new FileReader();
reader.onload = async () => {
try {
await importSettings(reader.result as string);
await importSettings(reader.result as string, type);
if (showToast) toastSuccess();
} catch (err) {
logger.error(err);

View file

@ -13,7 +13,7 @@ import type { ComponentPropsWithoutRef, ReactNode } from "react";
const textCls = classNameFactory("vc-text-");
const Sizes = {
export const TextSizes = {
xxs: "0.625rem",
xs: "0.75rem",
sm: "0.875rem",
@ -23,7 +23,7 @@ const Sizes = {
xxl: "2rem"
} as const;
const Weights = {
export const TextWeights = {
thin: "100",
extralight: "200",
light: "300",
@ -34,27 +34,42 @@ const Weights = {
extrabold: "800",
} as const;
export const TextColors = {
"header-primary": "var(--header-primary)",
"text-default": "var(--text-default)",
"text-muted": "var(--text-muted)",
"text-link": "var(--text-link)",
"text-danger": "var(--text-feedback-critical)",
"text-brand": "var(--text-brand)",
"text-strong": "var(--text-strong)",
"text-subtle": "var(--text-subtle)",
"control-text-critical": "var(--control-text-critical-secondary-default)",
"control-text-primary": "var(--control-text-primary-default)",
} as const;
export function generateTextCss() {
let css = "";
for (const [size, value] of Object.entries(Sizes)) {
for (const [size, value] of Object.entries(TextSizes)) {
css += `.${textCls(size)}{font-size:${value};}`;
}
for (const [weight, value] of Object.entries(Weights)) {
for (const [weight, value] of Object.entries(TextWeights)) {
css += `.${textCls(weight)}{font-weight:${value};}`;
}
return css;
}
export type TextSize = keyof typeof Sizes;
export type TextWeight = keyof typeof Weights;
export type TextSize = keyof typeof TextSizes;
export type TextWeight = keyof typeof TextWeights;
export type TextColor = keyof typeof TextColors;
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;
color?: TextColor;
tag?: Tag;
};
@ -62,6 +77,7 @@ export function BaseText<T extends TextTag = "div">(props: BaseTextProps<T>): Re
const {
size = "md",
weight = "normal",
color,
tag: Tag = "div",
children,
className,
@ -72,7 +88,10 @@ export function BaseText<T extends TextTag = "div">(props: BaseTextProps<T>): Re
return (
<Tag
className={classes(textCls("base", size, weight), className)}
style={style}
style={{
...style,
...(color && { color: TextColors[color] })
}}
{...restProps}
>
{children}
@ -81,7 +100,7 @@ export function BaseText<T extends TextTag = "div">(props: BaseTextProps<T>): Re
}
// #region Old compability
// #region Old compatibility
export const TextCompat: DiscordText = function TextCompat({ color, variant, ...restProps }) {
const newBaseTextProps = restProps as BaseTextProps;

View file

@ -1,11 +1,15 @@
.vc-error-card {
padding: 2em;
background-color: #e7828430;
border: 1px solid #e78284;
border-radius: 5px;
color: var(--text-default, white);
padding: 1em;
background-color: var(--info-danger-background, rgb(237 66 69 / 10%));
border: 1px solid var(--info-danger-foreground, #ed4245);
border-radius: var(--radius-sm, 8px);
color: var(--text-default);
}
& a:hover {
text-decoration: underline;
}
}
.vc-error-card a {
color: var(--text-link);
}
.vc-error-card a:hover {
text-decoration: underline;
}

View file

@ -1,15 +1,20 @@
.vc-form-switch-wrapper {
.vc-form-switch-container {
padding-bottom: 20px;
margin-bottom: 20px;
cursor: pointer;
border-bottom: 1px solid var(--border-subtle);
}
.vc-form-switch-no-border {
border-bottom: none;
margin-bottom: 0;
}
.vc-form-switch {
display: flex;
width: 100%;
> :last-child {
margin-left: auto;
}
align-items: flex-start;
justify-content: space-between;
cursor: pointer;
gap: 16px;
}
.vc-form-switch-disabled {
@ -19,12 +24,12 @@
}
.vc-form-switch-text {
display: flex;
flex-direction: column;
justify-content: center;
gap: 8px;
flex: 1;
min-width: 0;
max-width: 80%;
}
.vc-form-switch-border {
margin-top: 20px;
}
.vc-form-switch-control {
flex-shrink: 0;
padding-top: 2px;
}

View file

@ -9,7 +9,7 @@ import "./FormSwitch.css";
import { classes } from "@utils/misc";
import type { PropsWithChildren, ReactNode } from "react";
import { Divider } from "./Divider";
import { Flex } from "./Flex";
import { Span } from "./Span";
import { Switch } from "./Switch";
@ -26,24 +26,22 @@ export interface FormSwitchProps {
export function FormSwitch({ onChange, title, value, description, disabled, className, hideBorder }: FormSwitchProps) {
return (
<label className="vc-form-switch-wrapper">
<div className={classes("vc-form-switch", className, disabled && "vc-form-switch-disabled")}>
<div className={"vc-form-switch-text"}>
<Span size="md" weight="medium">{title}</Span>
{description && <Span size="sm" weight="normal">{description}</Span>}
<div className={classes("vc-form-switch-container", hideBorder && "vc-form-switch-no-border")}>
<label className={classes("vc-form-switch", className, disabled && "vc-form-switch-disabled")}>
<div className="vc-form-switch-text">
<Flex flexDirection="column" gap="4px">
<Span size="md" weight="medium" color="header-primary">{title}</Span>
{description && <Span size="sm" color="text-muted">{description}</Span>}
</Flex>
</div>
<Switch checked={value} onChange={onChange} disabled={disabled} />
</div>
{!hideBorder && <Divider className="vc-form-switch-border" />}
</label>
<div className="vc-form-switch-control">
<Switch checked={value} onChange={onChange} disabled={disabled} />
</div>
</label>
</div>
);
}
// #region Old compatibility
export function FormSwitchCompat({ note, children, ...restProps }: PropsWithChildren<any>) {
return <FormSwitch title={children ?? ""} description={note} {...restProps} />;
}
// #endregion

14
src/components/Link.css Normal file
View file

@ -0,0 +1,14 @@
.vc-link {
color: var(--text-link);
text-decoration: none;
cursor: pointer;
}
.vc-link:hover {
text-decoration: underline;
}
.vc-link-disabled {
pointer-events: none;
opacity: 0.5;
}

View file

@ -1,39 +1,29 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 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/>.
*/
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { React } from "@webpack/common";
import "./Link.css";
interface Props extends React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement> {
import { classes } from "@utils/misc";
import type { AnchorHTMLAttributes, PropsWithChildren } from "react";
export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
disabled?: boolean;
}
export function Link(props: React.PropsWithChildren<Props>) {
if (props.disabled) {
props.style ??= {};
props.style.pointerEvents = "none";
props["aria-disabled"] = true;
}
props.rel ??= "noreferrer";
export function Link({ disabled, className, children, ...restProps }: PropsWithChildren<LinkProps>) {
return (
<a role="link" target="_blank" {...props}>
{props.children}
<a
role="link"
target="_blank"
rel="noreferrer noopener"
className={classes("vc-link", disabled && "vc-link-disabled", className)}
aria-disabled={disabled}
{...restProps}
>
{children}
</a>
);
}

View file

@ -8,9 +8,9 @@ import { BaseText, type BaseTextProps } from "./BaseText";
export type SpanProps = BaseTextProps<"span">;
export function Span({ children, ...restProps }: SpanProps) {
export function Span({ children, size = "sm", weight = "normal", ...restProps }: SpanProps) {
return (
<BaseText tag="span" size="sm" weight="normal" {...restProps}>
<BaseText tag="span" size={size} weight={weight} {...restProps}>
{children}
</BaseText>
);

View file

@ -16,7 +16,6 @@ import { Link } from "@components/Link";
import { Paragraph } from "@components/Paragraph";
import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
import { HashLink } from "@components/settings/tabs/updater/Components";
import { gitHashShort } from "@shared/vencordUserAgent";
import { Margins } from "@utils/margins";
import { useAwaiter } from "@utils/react";
import { getRepo, UpdateLogger } from "@utils/updater";
@ -478,127 +477,114 @@ function ChangelogContent() {
return (
<>
<Card defaultPadding={true} className={Margins.bottom16}>
<Paragraph>
View the most recent changes to Equicord.
<br />
This fetches commits directly from the repository to show you what's new.
<br />
<br />
Repository: {" "}
{repoPending ? (
repo
) : repoErr ? (
"Failed to retrieve - check console"
) : (
<Link href={repo}>
{repo.split("/").slice(-2).join("/")}
</Link>
)}
{" "}
(Current:{" "}
<span className="vc-changelog-current-hash">
{gitHashShort}
</span>
)
</Paragraph>
<br />
<div className="vc-changelog-controls">
<Button
size="small"
disabled={isLoading || repoPending || !!repoErr}
onClick={fetchChangelog}
variant={recentlyChecked ? "positive" : "primary"}
>
{isLoading
? "Loading..."
: recentlyChecked
? "Repository Up to Date"
: "Fetch from Repository"}
</Button>
<Heading className={Margins.top16}>Fetch Changes</Heading>
<Paragraph className={Margins.bottom16}>
Check the repository for new commits, plugin updates, and code changes. This will compare your current version with the latest available and show you what's new.
</Paragraph>
{changelogHistory.length > 0 && (
<>
<Button
size="small"
variant={showHistory ? "primary" : "secondary"}
onClick={() => setShowHistory(!showHistory)}
style={{ marginLeft: "8px" }}
>
{showHistory ? "Hide Logs" : "Show Logs"}
</Button>
<Button
size="small"
variant="dangerPrimary"
onClick={() => {
Alerts.show({
title: "Clear All Logs",
body: "Are you sure you would like to clear all logs? This can't be undone.",
confirmText: "Clear All",
confirmColor: "danger",
cancelText: "Cancel",
onConfirm: async () => {
await clearChangelogHistory();
await loadChangelogHistory();
setShowHistory(false);
Toasts.show({
message:
"All logs have been cleared",
id: Toasts.genId(),
type: Toasts.Type.SUCCESS,
options: {
position:
Toasts.Position.BOTTOM,
},
});
},
});
}}
style={{ marginLeft: "8px" }}
>
Clear All Logs
</Button>
</>
)}
</div>
</Card>
<div className="vc-changelog-controls">
<Button
size="small"
disabled={isLoading || repoPending || !!repoErr}
onClick={fetchChangelog}
variant={recentlyChecked ? "positive" : "primary"}
>
{isLoading
? "Loading..."
: recentlyChecked
? "Repository Up to Date"
: "Fetch from Repository"}
</Button>
{changelogHistory.length > 0 && (
<>
<Button
size="small"
variant={showHistory ? "primary" : "secondary"}
onClick={() => setShowHistory(!showHistory)}
style={{ marginLeft: "8px" }}
>
{showHistory ? "Hide Logs" : "Show Logs"}
</Button>
<Button
size="small"
variant="dangerPrimary"
onClick={() => {
Alerts.show({
title: "Clear All Logs",
body: "Are you sure you would like to clear all logs? This can't be undone.",
confirmText: "Clear All",
confirmColor: "danger",
cancelText: "Cancel",
onConfirm: async () => {
await clearChangelogHistory();
await loadChangelogHistory();
setShowHistory(false);
Toasts.show({
message: "All logs have been cleared",
id: Toasts.genId(),
type: Toasts.Type.SUCCESS,
options: {
position: Toasts.Position.BOTTOM,
},
});
},
});
}}
style={{ marginLeft: "8px" }}
>
Clear All Logs
</Button>
</>
)}
</div>
{error && (
<ErrorCard style={{ padding: "1em", marginBottom: "1em" }}>
<p>{error}</p>
<Paragraph
style={{
marginTop: "0.5em",
color: "var(--text-muted)",
}}
>
<ErrorCard style={{ padding: "1em", marginTop: "1em" }}>
<Paragraph>{error}</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: "0.5em" }}>
Make sure you have an internet connection and try again.
</Paragraph>
</ErrorCard>
)}
<Divider className={Margins.bottom16} />
<Divider className={Margins.top20} />
{/* Current Changes Section */}
{hasCurrentChanges ? (
<div className="vc-changelog-current">
<Heading className={Margins.bottom8}>
Recent Changes
</Heading>
<Heading className={Margins.top20}>Repository</Heading>
<Paragraph className={Margins.bottom8}>
This is the GitHub repository where Equicord fetches updates from.
</Paragraph>
<Paragraph color="text-muted">
{repoPending ? (
repo
) : repoErr ? (
"Failed to retrieve - check console"
) : (
<Link href={repo}>
{repo.split("/").slice(-2).join("/")}
</Link>
)}
{" "}(<HashLink repo={repo} hash={gitHash} disabled={repoPending} />)
</Paragraph>
{hasCurrentChanges && (
<>
<Divider className={Margins.top20} />
<Heading className={Margins.top20}>Recent Changes</Heading>
<Paragraph className={Margins.bottom16}>
These are the new commits and plugin updates since your last version. You can see what features were added, bugs were fixed, and which plugins received updates.
</Paragraph>
{/* New Plugins Section */}
{newPlugins.length > 0 && (
<div className={Margins.bottom16}>
<NewPluginsSection
newPlugins={newPlugins}
onPluginToggle={(pluginName, enabled) => {
// Handle plugin toggle if needed
}}
onPluginToggle={() => { }}
/>
</div>
)}
{/* Updated Plugins Section */}
{updatedPlugins.length > 0 && (
<div className={Margins.bottom16}>
<Heading className={Margins.bottom8}>
@ -608,12 +594,10 @@ function ChangelogContent() {
</div>
)}
{/* Code Changes */}
{changelog.length > 0 && (
<div>
<Heading className={Margins.bottom8}>
Code Changes ({changelog.length}{" "}
{changelog.length === 1 ? "commit" : "commits"})
Code Changes ({changelog.length} {changelog.length === 1 ? "commit" : "commits"})
</Heading>
<div className="vc-changelog-commits-list">
{changelog.map(entry => (
@ -627,34 +611,28 @@ function ChangelogContent() {
</div>
</div>
)}
</div>
) : (
!isLoading &&
!error && (
<Card className="vc-changelog-empty">
<Paragraph>
No commits available ahead of your current version.
Click "Fetch from Repository" to check for new
changes.
</Paragraph>
</Card>
)
</>
)}
{!hasCurrentChanges && !isLoading && !error && (
<>
<Divider className={Margins.top20} />
<Heading className={Margins.top20}>Recent Changes</Heading>
<Paragraph color="text-muted">
No commits available ahead of your current version. Click "Fetch from Repository" to check for new changes.
</Paragraph>
</>
)}
{/* History Section */}
{showHistory && changelogHistory.length > 0 && (
<div className="vc-changelog-history">
<Divider
className={Margins.top16}
style={{ marginBottom: "1em" }}
/>
<Heading className={Margins.bottom8}>
Update Logs ({changelogHistory.length}{" "}
{changelogHistory.length === 1 ? "log" : "logs"})
<>
<Divider className={Margins.top20} />
<Heading className={Margins.top20}>
Update Logs ({changelogHistory.length} {changelogHistory.length === 1 ? "log" : "logs"})
</Heading>
<Paragraph className={Margins.bottom16}>
Previous update sessions with their commit history and
plugin changes.
A history of your previous update sessions with their commit history and plugin changes. Click on a log to expand it and see the details.
</Paragraph>
<div className="vc-changelog-history-list">
@ -678,11 +656,7 @@ function ChangelogContent() {
await loadChangelogHistory();
setExpandedLogs(
new Set(
Array.from(
expandedLogs,
).filter(
id => id !== logId,
),
Array.from(expandedLogs).filter(id => id !== logId),
),
);
Toasts.show({
@ -690,8 +664,7 @@ function ChangelogContent() {
id: Toasts.genId(),
type: Toasts.Type.SUCCESS,
options: {
position:
Toasts.Position.BOTTOM,
position: Toasts.Position.BOTTOM,
},
});
},
@ -700,7 +673,7 @@ function ChangelogContent() {
/>
))}
</div>
</div>
</>
)}
</>
);

View file

@ -5,7 +5,6 @@
*/
import { Paragraph } from "@components/Paragraph";
import { Margins } from "@utils/margins";
import { Patch, ReplaceFn } from "@utils/types";
import { TextArea, useEffect, useRef, useState } from "@webpack/common";
@ -69,9 +68,6 @@ export function FullPatchInput({ setFind, setParsedFind, setMatch, setReplacemen
return (
<>
<Paragraph className={Margins.bottom8}>
Paste your full JSON patch here to fill out the fields
</Paragraph>
<TextArea
inputRef={textAreaRef}
value={patch}

View file

@ -16,18 +16,21 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Button } from "@components/Button";
import { CodeBlock } from "@components/CodeBlock";
import { Divider } from "@components/Divider";
import { Flex } from "@components/Flex";
import { HeadingTertiary } from "@components/Heading";
import { Heading } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
import { Span } from "@components/Span";
import { debounce } from "@shared/debounce";
import { copyWithToast } from "@utils/discord";
import { Margins } from "@utils/margins";
import { stripIndent } from "@utils/text";
import { ReplaceFn } from "@utils/types";
import { search } from "@webpack";
import { Button, React, TextInput, useMemo, useState } from "@webpack/common";
import { React, TextInput, useMemo, useState } from "@webpack/common";
import { FullPatchInput } from "./FullPatchInput";
import { PatchPreview } from "./PatchPreview";
@ -105,7 +108,13 @@ function PatchHelper() {
return (
<SettingsTab>
<HeadingTertiary>Full patch</HeadingTertiary>
<Heading className={Margins.top16}>Patch Helper</Heading>
<Paragraph className={Margins.bottom16}>
A developer tool to help you create patches for Equicord plugins.
</Paragraph>
<Heading className="">Full Patch</Heading>
<Paragraph className={Margins.bottom8}>Paste your full JSON patch here to fill out the fields</Paragraph>
<FullPatchInput
setFind={onFindChange}
setParsedFind={setParsedFind}
@ -113,48 +122,58 @@ function PatchHelper() {
setReplacement={setReplacement}
/>
<HeadingTertiary className={Margins.top8}>Find</HeadingTertiary>
<TextInput
type="text"
value={find}
onChange={onFindChange}
error={findError}
/>
<HeadingTertiary className={Margins.top8}>Match</HeadingTertiary>
<TextInput
type="text"
value={match}
onChange={onMatchChange}
error={matchError}
/>
<div className={Margins.top8} />
<ReplacementInput
replacement={replacement}
setReplacement={setReplacement}
replacementError={replacementError}
/>
<Divider />
{module && (
<PatchPreview
module={module}
match={match}
replacement={replacement}
setReplacementError={setReplacementError}
<div className={Margins.top20}>
<Heading className="">Find</Heading>
<TextInput
type="text"
value={find}
onChange={onFindChange}
error={findError}
/>
</div>
<div className={Margins.top20}>
<Heading className="">Match</Heading>
<TextInput
type="text"
value={match}
onChange={onMatchChange}
error={matchError}
/>
</div>
<div className={Margins.top20}>
<ReplacementInput
replacement={replacement}
setReplacement={setReplacement}
replacementError={replacementError}
/>
</div>
{module && (
<>
<Divider className={Margins.top16 + " " + Margins.bottom16} />
<Span size="md" weight="medium" color="header-primary">Preview</Span>
<PatchPreview
module={module}
match={match}
replacement={replacement}
setReplacementError={setReplacementError}
/>
</>
)}
{!!(find && match && replacement) && (
<>
<HeadingTertiary className={Margins.top20}>Code</HeadingTertiary>
<CodeBlock lang="js" content={code} />
<Flex className={Margins.top16}>
<Button onClick={() => copyWithToast(code)}>
<Divider className={Margins.top16 + " " + Margins.bottom16} />
<Span size="md" weight="medium" color="header-primary">Generated Code</Span>
<div style={{ width: "100%", marginTop: 8 }}>
<CodeBlock lang="js" content={code} />
</div>
<Flex className={Margins.top8} gap="8px">
<Button size="small" onClick={() => copyWithToast(code)}>
Copy to Clipboard
</Button>
<Button onClick={() => copyWithToast("```ts\n" + code + "\n```")}>
<Button size="small" onClick={() => copyWithToast("```ts\n" + code + "\n```")}>
Copy as Codeblock
</Button>
</Flex>
@ -164,4 +183,4 @@ function PatchHelper() {
);
}
export default IS_DEV ? wrapTab(PatchHelper, "PatchHelper") : null;
export default IS_DEV ? wrapTab(PatchHelper, "Patch Helper") : null;

View file

@ -19,6 +19,7 @@
import { downloadSettingsBackup, uploadSettingsBackup } from "@api/SettingsSync/offline";
import { Alert } from "@components/Alert";
import { Button } from "@components/Button";
import { Divider } from "@components/Divider";
import { Flex } from "@components/Flex";
import { Heading } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
@ -28,61 +29,93 @@ import { Margins } from "@utils/margins";
function BackupAndRestoreTab() {
return (
<SettingsTab>
<Flex flexDirection="column" gap="0.5em">
<Alert.Warning>
Importing a settings file will overwrite your current settings.
</Alert.Warning>
<Heading className={Margins.top16}>Backup & Restore</Heading>
<Paragraph className={Margins.bottom20}>
Import and export your Equicord settings as a JSON file. This allows you to easily transfer your settings to another device, or recover them after reinstalling Equicord or Discord.
</Paragraph>
<Paragraph className={Margins.bottom8}>
You can import and export your Vencord settings as a JSON file.
This allows you to easily transfer your settings to another device,
or recover your settings after reinstalling Vencord or Discord.
</Paragraph>
<Alert.Warning className={Margins.bottom20}>
Importing a settings file will overwrite your current settings. Make sure to export a backup first if you want to keep your current configuration.
</Alert.Warning>
<Heading tag="h4">Settings Export contains:</Heading>
<Paragraph className={Margins.bottom8}>
<ul>
<li>&mdash; Custom QuickCSS</li>
<li>&mdash; Theme Links</li>
<li>&mdash; Plugin Settings</li>
<li>&mdash; DataStore Data</li>
</ul>
</Paragraph>
<Heading>What's included in a backup</Heading>
<Paragraph className={Margins.bottom20}>
• Custom QuickCSS<br />
• Theme Links<br />
• Plugin Settings<br />
• DataStore Data
</Paragraph>
<Flex>
<Button
onClick={() => uploadSettingsBackup()}
size="small"
>
Import Settings
</Button>
<Button
onClick={() => downloadSettingsBackup("settings")}
size="small"
>
Export Settings
</Button>
<Button
onClick={() => downloadSettingsBackup("plugins")}
size="small"
>
Export Plugins
</Button>
<Button
onClick={() => downloadSettingsBackup("css")}
size="small"
>
Export CSS
</Button>
<Button
onClick={() => downloadSettingsBackup("datastore")}
size="small"
>
Export DataStores
</Button>
</Flex>
<Divider className={Margins.bottom20} />
<Heading>Import Settings</Heading>
<Paragraph className={Margins.bottom16}>
Select a previously exported settings file to restore your configuration. This will replace all your current settings with the ones from the backup.
</Paragraph>
<Flex gap="8px" className={Margins.bottom20} style={{ flexWrap: "wrap" }}>
<Button
onClick={() => uploadSettingsBackup("all")}
size="small"
variant="secondary"
>
Import All Settings
</Button>
<Button
onClick={() => uploadSettingsBackup("plugins")}
size="small"
>
Import Plugins
</Button>
<Button
onClick={() => uploadSettingsBackup("css")}
size="small"
>
Import QuickCSS
</Button>
<Button
onClick={() => uploadSettingsBackup("datastore")}
size="small"
>
Import DataStore
</Button>
</Flex>
</SettingsTab >
<Divider className={Margins.bottom20} />
<Heading>Export Settings</Heading>
<Paragraph className={Margins.bottom16}>
Download your current settings as a backup file. You can export everything at once, or choose to export only specific parts of your configuration.
</Paragraph>
<Flex gap="8px" style={{ flexWrap: "wrap" }}>
<Button
onClick={() => downloadSettingsBackup("all")}
size="small"
variant="secondary"
>
Export All Settings
</Button>
<Button
onClick={() => downloadSettingsBackup("plugins")}
size="small"
>
Export Plugins
</Button>
<Button
onClick={() => downloadSettingsBackup("css")}
size="small"
>
Export QuickCSS
</Button>
<Button
onClick={() => downloadSettingsBackup("datastore")}
size="small"
>
Export DataStore
</Button>
</Flex>
</SettingsTab>
);
}

View file

@ -10,8 +10,8 @@ import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
import { Link } from "@components/Link";
import { Paragraph } from "@components/Paragraph";
import { Span } from "@components/Span";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { relaunch } from "@utils/native";
import { changes, checkForUpdates, update, updateError } from "@utils/updater";
import { Alerts, React, Toasts, useState } from "@webpack/common";
@ -33,25 +33,26 @@ export function HashLink({ repo, hash, disabled = false }: { repo: string, hash:
export function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) {
return (
<Card style={{ padding: "0 0.5em" }} defaultPadding={false}>
{updates.map(({ hash, author, message }) => (
<Card className={Margins.top16} style={{ padding: 0 }} defaultPadding={false}>
{updates.map(({ hash, author, message }, i) => (
<div
key={hash}
style={{
marginTop: "0.5em",
marginBottom: "0.5em"
padding: "12px 16px",
borderBottom: i < updates.length - 1 ? "1px solid var(--border-subtle)" : undefined
}}
>
<code>
<HashLink {...{ repo, hash }} disabled={repoPending} />
</code>
<span style={{
marginLeft: "0.5em",
color: "var(--text-default)"
}}>
{message} - {author}
</span>
<Flex style={{ alignItems: "center", gap: 8 }}>
<code style={{ color: "var(--text-link)" }}>
<HashLink {...{ repo, hash }} disabled={repoPending} />
</code>
<Span size="sm" color="text-default">
{message}
</Span>
<Span size="sm" color="text-muted">
— {author}
</Span>
</Flex>
</div>
))}
</Card>
@ -61,8 +62,8 @@ export function Changes({ updates, repo, repoPending }: CommonProps & { updates:
export function Newer(props: CommonProps) {
return (
<>
<Paragraph className={Margins.bottom8}>
Your local copy has more recent commits. Please stash or reset them.
<Paragraph>
Your local copy has more recent commits than the remote repository. This usually happens when you've made local changes. Please stash or reset them before updating.
</Paragraph>
<Changes {...props} updates={changes} />
</>
@ -80,47 +81,25 @@ export function Updatable(props: CommonProps) {
<>
{!updates && updateError ? (
<>
<Paragraph>Failed to check updates. Check the console for more info</Paragraph>
<ErrorCard style={{ padding: "1em" }}>
<Span size="md" weight="medium" color="header-primary">Error checking for updates</Span>
<ErrorCard className={Margins.top8} style={{ padding: "1em" }}>
<p>{updateError.stderr || updateError.stdout || "An unknown error occurred"}</p>
</ErrorCard>
</>
) : isOutdated ? (
<>
<Paragraph>
There {updates.length === 1 ? "is 1 update" : `are ${updates.length} updates`} available. Click the button below to download and install.
</Paragraph>
<Changes updates={updates} {...props} />
</>
) : (
<Paragraph className={Margins.bottom8}>
{isOutdated ? (updates.length === 1 ? "There is 1 Update" : `There are ${updates.length} Updates`) : "Up to Date!"}
<Paragraph>
You're running the latest version of Equicord.
</Paragraph>
)}
{isOutdated && <Changes updates={updates} {...props} />}
<Flex className={classes(Margins.bottom8, Margins.top8)}>
{isOutdated && (
<Button
size="small"
disabled={isUpdating || isChecking}
onClick={runWithDispatch(setIsUpdating, async () => {
if (await update()) {
setUpdates([]);
await new Promise<void>(r => {
Alerts.show({
title: "Update Success!",
body: "Successfully updated. Restart now to apply the changes?",
confirmText: "Restart",
cancelText: "Not now!",
onConfirm() {
relaunch();
r();
},
onCancel: r
});
});
}
})}
>
Update Now
</Button>
)}
<Flex className={Margins.top16} gap="8px">
<Button
size="small"
disabled={isUpdating || isChecking}
@ -145,6 +124,34 @@ export function Updatable(props: CommonProps) {
>
Check for Updates
</Button>
{isOutdated && (
<Button
size="small"
variant="primary"
disabled={isUpdating || isChecking}
onClick={runWithDispatch(setIsUpdating, async () => {
if (await update()) {
setUpdates([]);
await new Promise<void>(r => {
Alerts.show({
title: "Update Success!",
body: "Successfully updated. Restart now to apply the changes?",
confirmText: "Restart",
cancelText: "Not now!",
onConfirm() {
relaunch();
r();
},
onCancel: r
});
});
}
})}
>
Update Now
</Button>
)}
</Flex>
</>
);

View file

@ -17,156 +17,26 @@
*/
import { useSettings } from "@api/Settings";
import { Button } from "@components/Button";
import { Divider } from "@components/Divider";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
import { FormSwitch } from "@components/FormSwitch";
import { Heading } from "@components/Heading";
import { Link } from "@components/Link";
import { Paragraph } from "@components/Paragraph";
import { SettingsTab, wrapTab } from "@components/settings";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { relaunch } from "@utils/native";
import { useAwaiter } from "@utils/react";
import { changes, checkForUpdates, getRepo, isNewer, update, updateError, UpdateLogger } from "@utils/updater";
import { Alerts, Parser, React, Toasts } from "@webpack/common";
import { getRepo, isNewer, UpdateLogger } from "@utils/updater";
import { React } from "@webpack/common";
import gitHash from "~git-hash";
import { Changes, HashLink } from "./Components";
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
return async () => {
dispatcher(true);
try {
await action();
} catch (e: any) {
UpdateLogger.error("Failed to update", e);
let err: string;
if (!e) {
err = "An unknown error occurred (error is undefined).\nPlease try again.";
} else if (e.code && e.cmd) {
const { code, path, cmd, stderr } = e;
if (code === "ENOENT")
err = `Command \`${path}\` not found.\nPlease install it and try again`;
else {
err = `An error occurred while running \`${cmd}\`:\n`;
err += stderr || `Code \`${code}\`. See the console for more info`;
}
} else {
err = "An unknown error occurred. See the console for more info.";
}
Alerts.show({
title: "Oops!",
body: (
<ErrorCard>
{err.split("\n").map((line, idx) => <div key={idx}>{Parser.parse(line)}</div>)}
</ErrorCard>
)
});
}
finally {
dispatcher(false);
}
};
}
import { HashLink, Newer, Updatable } from "./Components";
interface CommonProps {
repo: string;
repoPending: boolean;
}
function Updatable(props: CommonProps) {
const [updates, setUpdates] = React.useState(changes);
const [isChecking, setIsChecking] = React.useState(false);
const [isUpdating, setIsUpdating] = React.useState(false);
const isOutdated = (updates?.length ?? 0) > 0;
return (
<>
<Flex className={classes(Margins.bottom8, Margins.top8)}>
{isOutdated && <Button
size="small"
disabled={isUpdating || isChecking}
onClick={withDispatcher(setIsUpdating, async () => {
if (await update()) {
setUpdates([]);
return await new Promise<void>(r => {
Alerts.show({
title: "Update Success!",
body: "Successfully updated. Restart now to apply the changes?",
confirmText: "Restart",
cancelText: "Not now!",
onConfirm() {
relaunch();
r();
},
onCancel: r
});
});
}
})}
>
Update Now
</Button>}
<Button
size="small"
disabled={isUpdating || isChecking}
onClick={withDispatcher(setIsChecking, async () => {
const outdated = await checkForUpdates();
if (outdated) {
setUpdates(changes);
} else {
setUpdates([]);
Toasts.show({
message: "No updates found!",
id: Toasts.genId(),
type: Toasts.Type.MESSAGE,
options: {
position: Toasts.Position.BOTTOM
}
});
}
})}
>
Check for Updates
</Button>
</Flex>
{!updates && updateError ? (
<>
<Paragraph>Failed to check updates. Check the console for more info</Paragraph>
<ErrorCard style={{ padding: "1em" }}>
<p>{updateError.stderr || updateError.stdout || "An unknown error occurred"}</p>
</ErrorCard>
</>
) : (
<Paragraph className={Margins.bottom8}>
{isOutdated ? (updates.length === 1 ? "There is 1 Update" : `There are ${updates.length} Updates`) : "Up to Date!"}
</Paragraph>
)}
{isOutdated && <Changes updates={updates} {...props} />}
</>
);
}
function Newer(props: CommonProps) {
return (
<>
<Paragraph className={Margins.bottom8}>
Your local copy has more recent commits. Please stash or reset them.
</Paragraph>
<Changes {...props} updates={changes} />
</>
);
}
function Updater() {
const settings = useSettings(["autoUpdate", "autoUpdateNotification"]);
@ -184,24 +54,34 @@ function Updater() {
return (
<SettingsTab>
<Heading>Updater Settings</Heading>
<Heading className={Margins.top16}>Update Preferences</Heading>
<Paragraph className={Margins.bottom20}>
Control how Equicord keeps itself up to date. You can choose to update automatically in the background or be notified when new updates are available.
</Paragraph>
<FormSwitch
title="Automatically update"
description="Automatically update Equicord without confirmation prompt"
description="When enabled, Equicord will automatically download and install updates in the background without asking for confirmation. You'll need to restart Discord to apply the changes."
value={settings.autoUpdate}
onChange={(v: boolean) => settings.autoUpdate = v}
hideBorder
/>
<FormSwitch
value={settings.autoUpdateNotification}
onChange={(v: boolean) => settings.autoUpdateNotification = v}
title="Get notified when an automatic update completes"
description="Shows a notification when Equicord automatically updates"
description="Receive a notification when Equicord finishes downloading an update in the background, so you know when to restart Discord."
disabled={!settings.autoUpdate}
hideBorder
/>
<Heading>Repo</Heading>
<Divider className={Margins.top20} />
<Paragraph>
<Heading className={Margins.top20}>Repository</Heading>
<Paragraph className={Margins.bottom8}>
This is the GitHub repository where Equicord fetches updates from.
</Paragraph>
<Paragraph color="text-muted">
{repoPending
? repo
: err
@ -215,12 +95,11 @@ function Updater() {
{" "}(<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)
</Paragraph>
<Divider className={Margins.top8 + " " + Margins.bottom8} />
<Heading>Updates</Heading>
<Divider className={Margins.top20} />
<Heading className={Margins.top20}>Updates</Heading>
{isNewer ? <Newer {...commonProps} /> : <Updatable {...commonProps} />}
</SettingsTab >
</SettingsTab>
);
}

View file

@ -69,12 +69,14 @@ function EquicordSettings() {
{
key: "useQuickCss",
title: "Enable Custom CSS",
description: "Load custom CSS from the QuickCSS editor. This allows you to customize Discord's appearance with your own styles.",
restartRequired: true,
warning: { enabled: false },
},
!IS_WEB && {
key: "enableReactDevtools",
title: "Enable React Developer Tools",
description: "Enable the React Developer Tools extension for debugging Discord's React components. Useful for plugin development.",
restartRequired: true,
warning: { enabled: false },
},
@ -83,37 +85,39 @@ function EquicordSettings() {
? {
key: "frameless",
title: "Disable the Window Frame",
description: "Remove the native window frame for a cleaner look. You can still move the window by dragging the title bar area.",
restartRequired: true,
warning: { enabled: false },
}
: {
key: "winNativeTitleBar",
title:
"Use Windows' native title bar instead of Discord's custom one",
title: "Use Windows' native title bar instead of Discord's custom one",
description: "Replace Discord's custom title bar with the standard Windows title bar. This may improve compatibility with some window management tools.",
restartRequired: true,
warning: { enabled: false },
}),
!IS_WEB && {
key: "transparent",
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",
description: "Make the Discord window transparent. A theme that supports transparency is required or this will do nothing. Stops the window from being resizable as a side effect.",
restartRequired: true,
warning: {
enabled: IS_WINDOWS,
message: "Enabling this will prevent you from snapping this window.",
message: "This will prevent you from snapping the window to screen edges.",
},
},
IS_DISCORD_DESKTOP && {
key: "disableMinSize",
title: "Disable Minimum Window Size",
description: "Allow the Discord window to be resized smaller than its default minimum size. Useful for tiling window managers or small screens.",
restartRequired: true,
warning: { enabled: false },
},
!IS_WEB &&
IS_WINDOWS && {
key: "winCtrlQ",
title:
"Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4)",
title: "Register Ctrl+Q as shortcut to close Discord",
description: "Add Ctrl+Q as a keyboard shortcut to close Discord. This provides an alternative to Alt+F4 for quickly closing the application.",
restartRequired: true,
warning: { enabled: false },
},
@ -171,93 +175,96 @@ function EquicordSettings() {
</SpecialCard>
)}
<section>
<Heading>Quick Actions</Heading>
<Heading className={Margins.top16}>Quick Actions</Heading>
<Paragraph className={Margins.bottom16}>
Common actions you might want to perform. These shortcuts give you quick access to frequently used features without navigating through menus.
</Paragraph>
<QuickActionCard>
<QuickActionCard>
<QuickAction
Icon={LogIcon}
text="Notification Log"
action={openNotificationLogModal}
/>
<QuickAction
Icon={PaintbrushIcon}
text="Edit QuickCSS"
action={() => VencordNative.quickCss.openEditor()}
/>
{!IS_WEB && (
<QuickAction
Icon={LogIcon}
text="Notification Log"
action={openNotificationLogModal}
Icon={RestartIcon}
text="Relaunch Discord"
action={relaunch}
/>
)}
{!IS_WEB && (
<QuickAction
Icon={PaintbrushIcon}
text="Edit QuickCSS"
action={() => VencordNative.quickCss.openEditor()}
Icon={FolderIcon}
text="Open Settings Folder"
action={() => VencordNative.settings.openFolder()}
/>
{!IS_WEB && (
<QuickAction
Icon={RestartIcon}
text="Relaunch Discord"
action={relaunch}
/>
)}
{!IS_WEB && (
<QuickAction
Icon={FolderIcon}
text="Open Settings Folder"
action={() => VencordNative.settings.openFolder()}
/>
)}
<QuickAction
Icon={GithubIcon}
text="View Source Code"
action={() =>
VencordNative.native.openExternal(
"https://github.com/" + gitRemote,
)}
<QuickAction
Icon={GithubIcon}
text="View Source Code"
action={() =>
VencordNative.native.openExternal(
"https://github.com/" + gitRemote,
)
}
/>
</QuickActionCard>
<Divider className={Margins.top20} />
<Heading className={Margins.top20}>Client Settings</Heading>
<Paragraph className={Margins.bottom16}>
Configure how Equicord behaves and integrates with Discord. These settings affect the Discord client's appearance and behavior.
</Paragraph>
<Alert.Info className={Margins.bottom20} style={{ width: "100%" }}>
You can customize where this settings section appears in Discord's settings menu by configuring the{" "}
<a
role="button"
onClick={() => openPluginModal(Vencord.Plugins.plugins.Settings)}
style={{ cursor: "pointer", color: "var(--text-link)" }}
>
Settings Plugin
</a>.
</Alert.Info>
{Switches.filter((s): s is Exclude<typeof s, false> => !!s).map(
(s, i, arr) => (
<FormSwitch
key={s.key}
value={settings[s.key]}
onChange={v => (settings[s.key] = v)}
title={s.title}
description={
s.warning.enabled ? (
<>
{s.description}
<Alert.Warning className={Margins.top8} style={{ width: "100%" }}>
{s.warning.message}
</Alert.Warning>
</>
) : (
s.description
)
}
hideBorder={i === arr.length - 1}
/>
</QuickActionCard>
</section>
<Divider />
<section className={Margins.top16}>
<Heading>Settings</Heading>
<Paragraph className={Margins.bottom20} style={{ color: "var(--text-muted)" }}>
Hint: You can change the position of this settings section in the{" "}
<Button
variant="none"
size="small"
className={cl("settings-link")}
onClick={() => openPluginModal(Vencord.Plugins.plugins.Settings)}
>
settings of the Settings plugin
</Button>
!
</Paragraph>
{Switches.map(
s =>
s && (
<FormSwitch
key={s.key}
value={settings[s.key]}
onChange={v => (settings[s.key] = v)}
title={s.title}
description={
s.warning.enabled ? (
<>
{s.description}
<Alert.Warning className={Margins.top8} style={{ width: "100%" }}>
{s.warning.message}
</Alert.Warning>
</>
) : (
s.description
)
}
/>
),
)}
</section>
),
)}
{needsVibrancySettings && (
<>
<Heading>
Window vibrancy style (requires restart)
</Heading>
<Divider className={Margins.top20} />
<Heading className={Margins.top20}>Window Vibrancy</Heading>
<Paragraph className={Margins.bottom16}>
Customize the macOS window vibrancy effect. This controls the blur and transparency style of the Discord window. Changes require a restart to take effect.
</Paragraph>
<Select
className={Margins.bottom20}
placeholder="Window vibrancy style"
@ -323,19 +330,21 @@ function EquicordSettings() {
</>
)}
<section
className={Margins.top16}
title="Equicord Notifications"
>
<Flex>
<Button onClick={openNotificationSettingsModal}>
Notification Settings
</Button>
<Button onClick={openNotificationLogModal} style={{ marginLeft: 16 }}>
View Notification Log
</Button>
</Flex>
</section>
<Divider className={Margins.top20} />
<Heading className={Margins.top20}>Notifications</Heading>
<Paragraph className={Margins.bottom16}>
Configure how Equicord handles notifications. You can customize when and how you receive alerts, or view a history of past notifications.
</Paragraph>
<Flex gap="16px">
<Button onClick={openNotificationSettingsModal}>
Notification Settings
</Button>
<Button variant="secondary" onClick={openNotificationLogModal}>
View Notification Log
</Button>
</Flex>
</SettingsTab>
);
}

View file

@ -137,7 +137,7 @@ function VerifyModal({
<BaseText
size="md"
weight="semibold"
color="text-normal"
color="text-default"
style={{ width: "30rem", textAlign: "center" }}
>
{getIntlMessage("QR_CODE_LOGIN_SUCCESS_FLAVOR")}
@ -163,7 +163,7 @@ function VerifyModal({
<BaseText
size="md"
weight="semibold"
color="text-normal"
color="text-default"
style={{ width: "30rem" }}
>
{getIntlMessage("QR_CODE_NOT_FOUND_DESCRIPTION")}

View file

@ -91,7 +91,7 @@ export default definePlugin({
return (
<div className="vc-std-wrapper">
<TooltipContainer text={text}>{children}</TooltipContainer>
<BaseText size="md" color="status-danger">
<BaseText size="md" color="text-danger">
{renderTimeout(message, true)} timeout remaining
</BaseText>
</div>