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:
parent
dc51a80d55
commit
bc9ecbd13f
18 changed files with 603 additions and 644 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
14
src/components/Link.css
Normal 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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>— Custom QuickCSS</li>
|
||||
<li>— Theme Links</li>
|
||||
<li>— Plugin Settings</li>
|
||||
<li>— 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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue