;
+ });
+ }
+
+ return (
+ <>
+ Module {id}
+
+ {!!matchResult?.[0]?.length && (
+ <>
+ Match
+ {renderMatch()}
+ >)
+ }
+
+ {!!diff?.length && (
+ <>
+ Diff
+ {renderDiff()}
+ >
+ )}
+
+ {!!diff?.length && (
+
+ )}
+
+ {compileResult &&
+
+ {compileResult[1]}
+
+ }
+ >
+ );
+}
+
+function ReplacementInput({ replacement, setReplacement, replacementError }) {
+ const [isFunc, setIsFunc] = React.useState(false);
+ const [error, setError] = React.useState();
+
+ function onChange(v: string) {
+ setError(void 0);
+
+ if (isFunc) {
+ try {
+ const func = (0, eval)(v);
+ if (typeof func === "function")
+ setReplacement(() => func);
+ else
+ setError("Replacement must be a function");
+ } catch (e) {
+ setReplacement(v);
+ setError((e as Error).message);
+ }
+ } else {
+ setReplacement(v);
+ }
+ }
+
+ React.useEffect(
+ () => void (isFunc ? onChange(replacement) : setError(void 0)),
+ [isFunc]
+ );
+
+ return (
+ <>
+ {/* FormTitle adds a class if className is not set, so we set it to an empty string to prevent that */}
+ replacement
+
+ {!isFunc && (
+
+ Cheat Sheet
+ {Object.entries({
+ "\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)",
+ "$$": "Insert a $",
+ "$&": "Insert the entire match",
+ "$`\u200b": "Insert the substring before the match",
+ "$'": "Insert the substring after the match",
+ "$n": "Insert the nth capturing group ($1, $2...)",
+ "$self": "Insert the plugin instance",
+ }).map(([placeholder, desc]) => (
+
+ {Parser.parse("`" + placeholder + "`")}: {desc}
+
+ ))}
+
+ )}
+
+
+ Treat as Function
+
+ >
+ );
+}
+
+interface FullPatchInputProps {
+ setFind(v: string): void;
+ setParsedFind(v: string | RegExp): void;
+ setMatch(v: string): void;
+ setReplacement(v: string | ReplaceFn): void;
+}
+
+function FullPatchInput({ setFind, setParsedFind, setMatch, setReplacement }: FullPatchInputProps) {
+ const [fullPatch, setFullPatch] = React.useState("");
+ const [fullPatchError, setFullPatchError] = React.useState("");
+
+ function update() {
+ if (fullPatch === "") {
+ setFullPatchError("");
+
+ setFind("");
+ setParsedFind("");
+ setMatch("");
+ setReplacement("");
+ return;
+ }
+
+ try {
+ const parsed = (0, eval)(`([${fullPatch}][0])`) as Patch;
+
+ if (!parsed.find) throw new Error("No 'find' field");
+ if (!parsed.replacement) throw new Error("No 'replacement' field");
+
+ if (parsed.replacement instanceof Array) {
+ if (parsed.replacement.length === 0) throw new Error("Invalid replacement");
+
+ parsed.replacement = {
+ match: parsed.replacement[0].match,
+ replace: parsed.replacement[0].replace
+ };
+ }
+
+ if (!parsed.replacement.match) throw new Error("No 'replacement.match' field");
+ if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field");
+
+ setFind(parsed.find instanceof RegExp ? parsed.find.toString() : parsed.find);
+ setParsedFind(parsed.find);
+ setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match);
+ setReplacement(parsed.replacement.replace);
+ setFullPatchError("");
+ } catch (e) {
+ setFullPatchError((e as Error).message);
+ }
+ }
+
+ return <>
+ Paste your full JSON patch here to fill out the fields
+
+ {fullPatchError !== "" && {fullPatchError}}
+ >;
+}
+
+function PatchHelper() {
+ const [find, setFind] = React.useState("");
+ const [parsedFind, setParsedFind] = React.useState("");
+ const [match, setMatch] = React.useState("");
+ const [replacement, setReplacement] = React.useState("");
+
+ const [replacementError, setReplacementError] = React.useState();
+
+ const [module, setModule] = React.useState<[number, Function]>();
+ const [findError, setFindError] = React.useState();
+ const [matchError, setMatchError] = React.useState();
+
+ const code = React.useMemo(() => {
+ return `
+{
+ find: ${parsedFind instanceof RegExp ? parsedFind.toString() : JSON.stringify(parsedFind)},
+ replacement: {
+ match: /${match.replace(/(?
+ full patch
+
+
+ find
+
+
+ match
+
+
+
+
+
+
+ {module && (
+
+ )}
+
+ {!!(find && match && replacement) && (
+ <>
+ Code
+
+
+
+ >
+ )}
+
+ );
+}
+
+export default IS_DEV ? wrapTab(PatchHelper, "PatchHelper") : null;
diff --git a/src/components/VencordSettings/PluginsTab.tsx b/src/components/VencordSettings/PluginsTab.tsx
new file mode 100644
index 00000000..6a320959
--- /dev/null
+++ b/src/components/VencordSettings/PluginsTab.tsx
@@ -0,0 +1,23 @@
+/*
+ * 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 .
+*/
+
+import PluginSettings from "@components/PluginSettings";
+
+import { wrapTab } from "./shared";
+
+export default wrapTab(PluginSettings, "Plugins");
diff --git a/src/components/settings/SpecialCard.tsx b/src/components/VencordSettings/SpecialCard.tsx
similarity index 99%
rename from src/components/settings/SpecialCard.tsx
rename to src/components/VencordSettings/SpecialCard.tsx
index 1c0a12c2..6fd952f4 100644
--- a/src/components/settings/SpecialCard.tsx
+++ b/src/components/VencordSettings/SpecialCard.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import "./SpecialCard.css";
+import "./specialCard.css";
import { classNameFactory } from "@api/Styles";
import { Card, Clickable, Forms, React } from "@webpack/common";
diff --git a/src/components/VencordSettings/ThemesTab.tsx b/src/components/VencordSettings/ThemesTab.tsx
new file mode 100644
index 00000000..f718ab11
--- /dev/null
+++ b/src/components/VencordSettings/ThemesTab.tsx
@@ -0,0 +1,356 @@
+/*
+ * 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 .
+*/
+
+import { Settings, useSettings } from "@api/Settings";
+import { classNameFactory } from "@api/Styles";
+import { Flex } from "@components/Flex";
+import { DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from "@components/Icons";
+import { Link } from "@components/Link";
+import { openPluginModal } from "@components/PluginSettings/PluginModal";
+import type { UserThemeHeader } from "@main/themes";
+import { openInviteModal } from "@utils/discord";
+import { Margins } from "@utils/margins";
+import { showItemInFolder } from "@utils/native";
+import { useAwaiter } from "@utils/react";
+import { findLazy } from "@webpack";
+import { Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
+import type { ComponentType, Ref, SyntheticEvent } from "react";
+
+import Plugins from "~plugins";
+
+import { AddonCard } from "./AddonCard";
+import { QuickAction, QuickActionCard } from "./quickActions";
+import { SettingsTab, wrapTab } from "./shared";
+
+type FileInput = ComponentType<{
+ ref: Ref;
+ onChange: (e: SyntheticEvent) => void;
+ multiple?: boolean;
+ filters?: { name?: string; extensions: string[]; }[];
+}>;
+
+const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef);
+
+const cl = classNameFactory("vc-settings-theme-");
+
+function Validator({ link }: { link: string; }) {
+ const [res, err, pending] = useAwaiter(() => fetch(link).then(res => {
+ if (res.status > 300) throw `${res.status} ${res.statusText}`;
+ const contentType = res.headers.get("Content-Type");
+ if (!contentType?.startsWith("text/css") && !contentType?.startsWith("text/plain"))
+ throw "Not a CSS file. Remember to use the raw link!";
+
+ return "Okay!";
+ }));
+
+ const text = pending
+ ? "Checking..."
+ : err
+ ? `Error: ${err instanceof Error ? err.message : String(err)}`
+ : "Valid!";
+
+ return {text};
+}
+
+function Validators({ themeLinks }: { themeLinks: string[]; }) {
+ if (!themeLinks.length) return null;
+
+ return (
+ <>
+ Validator
+ This section will tell you whether your themes can successfully be loaded
+
+
+ >
+ );
+ }
+
+ // When the user leaves the online theme textbox, update the settings
+ function onBlur() {
+ settings.themeLinks = [...new Set(
+ themeText
+ .trim()
+ .split(/\n+/)
+ .map(s => s.trim())
+ .filter(Boolean)
+ )];
+ }
+
+ function renderOnlineThemes() {
+ return (
+ <>
+
+ Paste links to css files here
+ One link per line
+ You can prefix lines with @light or @dark to toggle them based on your Discord theme
+ Make sure to use direct links to files (raw or github.io)!
+
+
+
+
+
+
+ >
+ );
+ }
+
+ return (
+
+
+
+ Local Themes
+
+
+ Online Themes
+
+
+
+ {currentTab === ThemeTab.LOCAL && renderLocalThemes()}
+ {currentTab === ThemeTab.ONLINE && renderOnlineThemes()}
+
+ );
+}
+
+export default wrapTab(ThemesTab, "Themes");
diff --git a/src/components/VencordSettings/UpdaterTab.tsx b/src/components/VencordSettings/UpdaterTab.tsx
new file mode 100644
index 00000000..e29d7dfd
--- /dev/null
+++ b/src/components/VencordSettings/UpdaterTab.tsx
@@ -0,0 +1,268 @@
+/*
+ * 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 .
+*/
+
+import { useSettings } from "@api/Settings";
+import { ErrorCard } from "@components/ErrorCard";
+import { Flex } from "@components/Flex";
+import { Link } from "@components/Link";
+import { Margins } from "@utils/margins";
+import { classes } from "@utils/misc";
+import { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
+import { relaunch } from "@utils/native";
+import { useAwaiter } from "@utils/react";
+import { changes, checkForUpdates, getRepo, isNewer, update, updateError, UpdateLogger } from "@utils/updater";
+import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@webpack/common";
+
+import gitHash from "~git-hash";
+
+import { handleSettingsTabError, SettingsTab, wrapTab } from "./shared";
+
+function withDispatcher(dispatcher: React.Dispatch>, 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: (
+
+ {err.split("\n").map((line, idx) =>