aboutsummaryrefslogtreecommitdiff
path: root/src/components/VencordSettings
diff options
context:
space:
mode:
authorV <vendicated@riseup.net>2023-05-12 01:40:43 +0200
committerGitHub <noreply@github.com>2023-05-12 01:40:43 +0200
commit5c5b009c4180b73603a9c3a6c6663f889a2e2062 (patch)
tree9f586ff1e4491bbd8d7ac046d60bbae081d1d6f8 /src/components/VencordSettings
parent0c54b1fa1d8f9d858baf912bb4b1efd9d3c0ec93 (diff)
downloadVencord-5c5b009c4180b73603a9c3a6c6663f889a2e2062.tar.gz
Vencord-5c5b009c4180b73603a9c3a6c6663f889a2e2062.tar.bz2
Vencord-5c5b009c4180b73603a9c3a6c6663f889a2e2062.zip
Settings: Fix resetting scroll/search when getting a ping (#1106)
Diffstat (limited to 'src/components/VencordSettings')
-rw-r--r--src/components/VencordSettings/BackupAndRestoreTab.tsx (renamed from src/components/VencordSettings/BackupRestoreTab.tsx)11
-rw-r--r--src/components/VencordSettings/CloudTab.tsx9
-rw-r--r--src/components/VencordSettings/PatchHelperTab.tsx310
-rw-r--r--src/components/VencordSettings/PluginsTab.tsx5
-rw-r--r--src/components/VencordSettings/ThemesTab.tsx15
-rw-r--r--src/components/VencordSettings/UpdaterTab.tsx (renamed from src/components/VencordSettings/Updater.tsx)14
-rw-r--r--src/components/VencordSettings/VencordTab.tsx9
-rw-r--r--src/components/VencordSettings/index.tsx96
-rw-r--r--src/components/VencordSettings/settingsStyles.css2
-rw-r--r--src/components/VencordSettings/shared.tsx51
10 files changed, 394 insertions, 128 deletions
diff --git a/src/components/VencordSettings/BackupRestoreTab.tsx b/src/components/VencordSettings/BackupAndRestoreTab.tsx
index 1737470..a9a1c9f 100644
--- a/src/components/VencordSettings/BackupRestoreTab.tsx
+++ b/src/components/VencordSettings/BackupAndRestoreTab.tsx
@@ -16,16 +16,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
-import { Button, Card, Forms, Text } from "@webpack/common";
+import { Button, Card, Text } from "@webpack/common";
+
+import { SettingsTab, wrapTab } from "./shared";
function BackupRestoreTab() {
return (
- <Forms.FormSection title="Settings Sync" className={Margins.top16}>
+ <SettingsTab title="Backup & Restore">
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
<Flex flexDirection="column">
<strong>Warning</strong>
@@ -59,8 +60,8 @@ function BackupRestoreTab() {
Export Settings
</Button>
</Flex>
- </Forms.FormSection>
+ </SettingsTab>
);
}
-export default ErrorBoundary.wrap(BackupRestoreTab);
+export default wrapTab(BackupRestoreTab, "Backup & Restore");
diff --git a/src/components/VencordSettings/CloudTab.tsx b/src/components/VencordSettings/CloudTab.tsx
index 5e48a72..77e5298 100644
--- a/src/components/VencordSettings/CloudTab.tsx
+++ b/src/components/VencordSettings/CloudTab.tsx
@@ -19,13 +19,14 @@
import { showNotification } from "@api/Notifications";
import { Settings, useSettings } from "@api/Settings";
import { CheckedTextInput } from "@components/CheckedTextInput";
-import ErrorBoundary from "@components/ErrorBoundary";
import { Link } from "@components/Link";
import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
import { Margins } from "@utils/margins";
import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
+import { SettingsTab, wrapTab } from "./shared";
+
function validateUrl(url: string) {
try {
new URL(url);
@@ -114,7 +115,7 @@ function CloudTab() {
const settings = useSettings(["cloud.authenticated", "cloud.url"]);
return (
- <>
+ <SettingsTab title="Vencord Cloud">
<Forms.FormSection title="Cloud Settings" className={Margins.top16}>
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
Vencord comes with a cloud integration that adds goodies like settings sync across devices.
@@ -157,8 +158,8 @@ function CloudTab() {
<Forms.FormDivider className={Margins.top16} />
</Forms.FormSection >
<SettingsSyncSection />
- </>
+ </SettingsTab>
);
}
-export default ErrorBoundary.wrap(CloudTab);
+export default wrapTab(CloudTab, "Cloud");
diff --git a/src/components/VencordSettings/PatchHelperTab.tsx b/src/components/VencordSettings/PatchHelperTab.tsx
new file mode 100644
index 0000000..d5bd94c
--- /dev/null
+++ b/src/components/VencordSettings/PatchHelperTab.tsx
@@ -0,0 +1,310 @@
+/*
+ * 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/>.
+*/
+
+import { CheckedTextInput } from "@components/CheckedTextInput";
+import { debounce } from "@utils/debounce";
+import { Margins } from "@utils/margins";
+import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
+import { makeCodeblock } from "@utils/text";
+import { ReplaceFn } from "@utils/types";
+import { search } from "@webpack";
+import { Button, Clipboard, Forms, Parser, React, Switch, TextInput } from "@webpack/common";
+
+import { SettingsTab, wrapTab } from "./shared";
+
+// Do not include diff in non dev builds (side effects import)
+if (IS_DEV) {
+ var differ = require("diff") as typeof import("diff");
+}
+
+const findCandidates = debounce(function ({ find, setModule, setError }) {
+ const candidates = search(find);
+ const keys = Object.keys(candidates);
+ const len = keys.length;
+ if (len === 0)
+ setError("No match. Perhaps that module is lazy loaded?");
+ else if (len !== 1)
+ setError("Multiple matches. Please refine your filter");
+ else
+ setModule([keys[0], candidates[keys[0]]]);
+});
+
+interface ReplacementComponentProps {
+ module: [id: number, factory: Function];
+ match: string | RegExp;
+ replacement: string | ReplaceFn;
+ setReplacementError(error: any): void;
+}
+
+function ReplacementComponent({ module, match, replacement, setReplacementError }: ReplacementComponentProps) {
+ const [id, fact] = module;
+ const [compileResult, setCompileResult] = React.useState<[boolean, string]>();
+
+ const [patchedCode, matchResult, diff] = React.useMemo(() => {
+ const src: string = fact.toString().replaceAll("\n", "");
+ const canonicalMatch = canonicalizeMatch(match);
+ try {
+ const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
+ var patched = src.replace(canonicalMatch, canonicalReplace as string);
+ setReplacementError(void 0);
+ } catch (e) {
+ setReplacementError((e as Error).message);
+ return ["", [], []];
+ }
+ const m = src.match(canonicalMatch);
+ return [patched, m, makeDiff(src, patched, m)];
+ }, [id, match, replacement]);
+
+ function makeDiff(original: string, patched: string, match: RegExpMatchArray | null) {
+ if (!match || original === patched) return null;
+
+ const changeSize = patched.length - original.length;
+
+ // Use 200 surrounding characters of context
+ const start = Math.max(0, match.index! - 200);
+ const end = Math.min(original.length, match.index! + match[0].length + 200);
+ // (changeSize may be negative)
+ const endPatched = end + changeSize;
+
+ const context = original.slice(start, end);
+ const patchedContext = patched.slice(start, endPatched);
+
+ return differ.diffWordsWithSpace(context, patchedContext);
+ }
+
+ function renderMatch() {
+ if (!matchResult)
+ return <Forms.FormText>Regex doesn't match!</Forms.FormText>;
+
+ const fullMatch = matchResult[0] ? makeCodeblock(matchResult[0], "js") : "";
+ const groups = matchResult.length > 1
+ ? makeCodeblock(matchResult.slice(1).map((g, i) => `Group ${i + 1}: ${g}`).join("\n"), "yml")
+ : "";
+
+ return (
+ <>
+ <div style={{ userSelect: "text" }}>{Parser.parse(fullMatch)}</div>
+ <div style={{ userSelect: "text" }}>{Parser.parse(groups)}</div>
+ </>
+ );
+ }
+
+ function renderDiff() {
+ return diff?.map(p => {
+ const color = p.added ? "lime" : p.removed ? "red" : "grey";
+ return <div style={{ color, userSelect: "text" }}>{p.value}</div>;
+ });
+ }
+
+ return (
+ <>
+ <Forms.FormTitle>Module {id}</Forms.FormTitle>
+
+ {!!matchResult?.[0]?.length && (
+ <>
+ <Forms.FormTitle>Match</Forms.FormTitle>
+ {renderMatch()}
+ </>)
+ }
+
+ {!!diff?.length && (
+ <>
+ <Forms.FormTitle>Diff</Forms.FormTitle>
+ {renderDiff()}
+ </>
+ )}
+
+ {!!diff?.length && (
+ <Button className={Margins.top20} onClick={() => {
+ try {
+ Function(patchedCode.replace(/^function\(/, "function patchedModule("));
+ setCompileResult([true, "Compiled successfully"]);
+ } catch (err) {
+ setCompileResult([false, (err as Error).message]);
+ }
+ }}>Compile</Button>
+ )}
+
+ {compileResult &&
+ <Forms.FormText style={{ color: compileResult[0] ? "var(--text-positive)" : "var(--text-danger)" }}>
+ {compileResult[1]}
+ </Forms.FormText>
+ }
+ </>
+ );
+}
+
+function ReplacementInput({ replacement, setReplacement, replacementError }) {
+ const [isFunc, setIsFunc] = React.useState(false);
+ const [error, setError] = React.useState<string>();
+
+ 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 (
+ <>
+ <Forms.FormTitle>replacement</Forms.FormTitle>
+ <TextInput
+ value={replacement?.toString()}
+ onChange={onChange}
+ error={error ?? replacementError}
+ />
+ {!isFunc && (
+ <div className="vc-text-selectable">
+ <Forms.FormTitle>Cheat Sheet</Forms.FormTitle>
+ {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]) => (
+ <Forms.FormText key={placeholder}>
+ {Parser.parse("`" + placeholder + "`")}: {desc}
+ </Forms.FormText>
+ ))}
+ </div>
+ )}
+
+ <Switch
+ className={Margins.top8}
+ value={isFunc}
+ onChange={setIsFunc}
+ note="'replacement' will be evaled if this is toggled"
+ hideBorder={true}
+ >
+ Treat as Function
+ </Switch>
+ </>
+ );
+}
+
+function PatchHelper() {
+ const [find, setFind] = React.useState<string>("");
+ const [match, setMatch] = React.useState<string>("");
+ const [replacement, setReplacement] = React.useState<string | ReplaceFn>("");
+
+ const [replacementError, setReplacementError] = React.useState<string>();
+
+ const [module, setModule] = React.useState<[number, Function]>();
+ const [findError, setFindError] = React.useState<string>();
+
+ const code = React.useMemo(() => {
+ return `
+{
+ find: ${JSON.stringify(find)},
+ replacement: {
+ match: /${match.replace(/(?<!\\)\//g, "\\/")}/,
+ replace: ${typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement)}
+ }
+}
+ `.trim();
+ }, [find, match, replacement]);
+
+ function onFindChange(v: string) {
+ setFindError(void 0);
+ setFind(v);
+ if (v.length) {
+ findCandidates({ find: v, setModule, setError: setFindError });
+ }
+ }
+
+ function onMatchChange(v: string) {
+ try {
+ new RegExp(v);
+ setFindError(void 0);
+ setMatch(v);
+ } catch (e: any) {
+ setFindError((e as Error).message);
+ }
+ }
+
+ return (
+ <SettingsTab title="Patch Helper">
+ <Forms.FormTitle>find</Forms.FormTitle>
+ <TextInput
+ type="text"
+ value={find}
+ onChange={onFindChange}
+ error={findError}
+ />
+
+ <Forms.FormTitle>match</Forms.FormTitle>
+ <CheckedTextInput
+ value={match}
+ onChange={onMatchChange}
+ validate={v => {
+ try {
+ return (new RegExp(v), true);
+ } catch (e) {
+ return (e as Error).message;
+ }
+ }}
+ />
+
+ <ReplacementInput
+ replacement={replacement}
+ setReplacement={setReplacement}
+ replacementError={replacementError}
+ />
+
+ <Forms.FormDivider />
+ {module && (
+ <ReplacementComponent
+ module={module}
+ match={new RegExp(match)}
+ replacement={replacement}
+ setReplacementError={setReplacementError}
+ />
+ )}
+
+ {!!(find && match && replacement) && (
+ <>
+ <Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
+ <div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
+ <Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
+ </>
+ )}
+ </SettingsTab>
+ );
+}
+
+export default IS_DEV ? wrapTab(PatchHelper, "PatchHelper") : null;
diff --git a/src/components/VencordSettings/PluginsTab.tsx b/src/components/VencordSettings/PluginsTab.tsx
index 04b5dc2..6a32095 100644
--- a/src/components/VencordSettings/PluginsTab.tsx
+++ b/src/components/VencordSettings/PluginsTab.tsx
@@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import ErrorBoundary from "@components/ErrorBoundary";
import PluginSettings from "@components/PluginSettings";
-export default ErrorBoundary.wrap(PluginSettings);
+import { wrapTab } from "./shared";
+
+export default wrapTab(PluginSettings, "Plugins");
diff --git a/src/components/VencordSettings/ThemesTab.tsx b/src/components/VencordSettings/ThemesTab.tsx
index 75fea34..79ddc50 100644
--- a/src/components/VencordSettings/ThemesTab.tsx
+++ b/src/components/VencordSettings/ThemesTab.tsx
@@ -17,13 +17,14 @@
*/
import { useSettings } from "@api/Settings";
-import ErrorBoundary from "@components/ErrorBoundary";
import { Link } from "@components/Link";
import { Margins } from "@utils/margins";
import { useAwaiter } from "@utils/react";
import { findLazy } from "@webpack";
import { Card, Forms, React, TextArea } from "@webpack/common";
+import { SettingsTab, wrapTab } from "./shared";
+
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
function Validator({ link }: { link: string; }) {
@@ -74,8 +75,8 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
);
}
-export default ErrorBoundary.wrap(function () {
- const settings = useSettings();
+function ThemesTab() {
+ const settings = useSettings(["themeLinks"]);
const [themeText, setThemeText] = React.useState(settings.themeLinks.join("\n"));
function onBlur() {
@@ -89,7 +90,7 @@ export default ErrorBoundary.wrap(function () {
}
return (
- <>
+ <SettingsTab title="Themes">
<Card className="vc-settings-card vc-text-selectable">
<Forms.FormTitle tag="h5">Paste links to .theme.css files here</Forms.FormTitle>
<Forms.FormText>One link per line</Forms.FormText>
@@ -124,6 +125,8 @@ export default ErrorBoundary.wrap(function () {
onBlur={onBlur}
/>
<Validators themeLinks={settings.themeLinks} />
- </>
+ </SettingsTab>
);
-});
+}
+
+export default wrapTab(ThemesTab, "Themes");
diff --git a/src/components/VencordSettings/Updater.tsx b/src/components/VencordSettings/UpdaterTab.tsx
index 9345d27..4d0b86c 100644
--- a/src/components/VencordSettings/Updater.tsx
+++ b/src/components/VencordSettings/UpdaterTab.tsx
@@ -17,21 +17,20 @@
*/
import { useSettings } from "@api/Settings";
-import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
-import { handleComponentFailed } from "@components/handleComponentFailed";
import { Link } from "@components/Link";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { relaunch } from "@utils/native";
-import { onlyOnce } from "@utils/onlyOnce";
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 { SettingsTab, wrapTab } from "./shared";
+
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
return async () => {
dispatcher(true);
@@ -199,7 +198,7 @@ function Updater() {
};
return (
- <Forms.FormSection className={Margins.top16}>
+ <SettingsTab title="Vencord Updater">
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
<Switch
value={settings.notifyAboutUpdates}
@@ -246,11 +245,8 @@ function Updater() {
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
{isNewer ? <Newer {...commonProps} /> : <Updatable {...commonProps} />}
- </Forms.FormSection >
+ </SettingsTab>
);
}
-export default IS_WEB ? null : ErrorBoundary.wrap(Updater, {
- message: "Failed to render the Updater. If this persists, try using the installer to reinstall!",
- onError: onlyOnce(handleComponentFailed),
-});
+export default IS_WEB ? null : wrapTab(Updater, "Updater");
diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx
index 8c71821..1502bfa 100644
--- a/src/components/VencordSettings/VencordTab.tsx
+++ b/src/components/VencordSettings/VencordTab.tsx
@@ -21,7 +21,6 @@ import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton";
-import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import { Margins } from "@utils/margins";
import { identity } from "@utils/misc";
@@ -29,6 +28,8 @@ import { relaunch, showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react";
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
+import { SettingsTab, wrapTab } from "./shared";
+
const cl = classNameFactory("vc-settings-");
const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png";
@@ -97,7 +98,7 @@ function VencordSettings() {
];
return (
- <React.Fragment>
+ <SettingsTab title="Vencord Settings">
<DonateCard image={donateImage} />
<Forms.FormSection title="Quick Actions">
<Card className={cl("quick-actions-card")}>
@@ -153,7 +154,7 @@ function VencordSettings() {
{typeof Notification !== "undefined" && <NotificationSection settings={settings.notifications} />}
- </React.Fragment>
+ </SettingsTab>
);
}
@@ -263,4 +264,4 @@ function DonateCard({ image }: DonateCardProps) {
);
}
-export default ErrorBoundary.wrap(VencordSettings);
+export default wrapTab(VencordSettings, "Vencord Settings");
diff --git a/src/components/VencordSettings/index.tsx b/src/components/VencordSettings/index.tsx
deleted file mode 100644
index 6d65aa1..0000000
--- a/src/components/VencordSettings/index.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * 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/>.
-*/
-
-import "./settingsStyles.css";
-
-import { classNameFactory } from "@api/Styles";
-import ErrorBoundary from "@components/ErrorBoundary";
-import { handleComponentFailed } from "@components/handleComponentFailed";
-import { isMobile } from "@utils/misc";
-import { onlyOnce } from "@utils/onlyOnce";
-import { Forms, SettingsRouter, TabBar, Text } from "@webpack/common";
-
-import BackupRestoreTab from "./BackupRestoreTab";
-import CloudTab from "./CloudTab";
-import PluginsTab from "./PluginsTab";
-import ThemesTab from "./ThemesTab";
-import Updater from "./Updater";
-import VencordSettings from "./VencordTab";
-
-const cl = classNameFactory("vc-settings-");
-
-interface SettingsProps {
- tab: string;
-}
-
-interface SettingsTab {
- name: string;
- component?: React.ComponentType;
-}
-
-const SettingsTabs: Record<string, SettingsTab> = {
- VencordSettings: { name: "Vencord", component: () => <VencordSettings /> },
- VencordPlugins: { name: "Plugins", component: () => <PluginsTab /> },
- VencordThemes: { name: "Themes", component: () => <ThemesTab /> },
- VencordUpdater: { name: "Updater" }, // Only show updater if IS_WEB is false
- VencordCloud: { name: "Cloud", component: () => <CloudTab /> },
- VencordSettingsSync: { name: "Backup & Restore", component: () => <BackupRestoreTab /> }
-};
-
-if (!IS_WEB) SettingsTabs.VencordUpdater.component = () => Updater && <Updater />;
-
-function Settings(props: SettingsProps) {
- const { tab = "VencordSettings" } = props;
-
- const CurrentTab = SettingsTabs[tab]?.component ?? null;
- if (isMobile) {
- return CurrentTab && <CurrentTab />;
- }
-
- return <Forms.FormSection>
- <Text variant="heading-lg/semibold" style={{ color: "var(--header-primary)" }} tag="h2">Vencord Settings</Text>
-
- <TabBar
- type="top"
- look="brand"
- className={cl("tab-bar")}
- selectedItem={tab}
- onItemSelect={SettingsRouter.open}
- >
- {Object.entries(SettingsTabs).map(([key, { name, component }]) => {
- if (!component) return null;
- return <TabBar.Item
- id={key}
- className={cl("tab-bar-item")}
- key={key}>
- {name}
- </TabBar.Item>;
- })}
- </TabBar>
- <Forms.FormDivider />
- {CurrentTab && <CurrentTab />}
- </Forms.FormSection >;
-}
-
-const onError = onlyOnce(handleComponentFailed);
-
-export default function (props: SettingsProps) {
- return <ErrorBoundary onError={onError}>
- <Settings tab={props.tab} />
- </ErrorBoundary>;
-}
diff --git a/src/components/VencordSettings/settingsStyles.css b/src/components/VencordSettings/settingsStyles.css
index 3652756..f7d75e6 100644
--- a/src/components/VencordSettings/settingsStyles.css
+++ b/src/components/VencordSettings/settingsStyles.css
@@ -29,14 +29,12 @@
.vc-settings-card {
padding: 1em;
margin-bottom: 1em;
- margin-top: 1em;
}
.vc-backup-restore-card {
background-color: var(--info-warning-background);
border-color: var(--info-warning-foreground);
color: var(--info-warning-text);
- margin-top: 0;
}
.vc-settings-theme-links {
diff --git a/src/components/VencordSettings/shared.tsx b/src/components/VencordSettings/shared.tsx
new file mode 100644
index 0000000..0d3910d
--- /dev/null
+++ b/src/components/VencordSettings/shared.tsx
@@ -0,0 +1,51 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2023 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import "./settingsStyles.css";
+
+import ErrorBoundary from "@components/ErrorBoundary";
+import { handleComponentFailed } from "@components/handleComponentFailed";
+import { Margins } from "@utils/margins";
+import { onlyOnce } from "@utils/onlyOnce";
+import { Forms, Text } from "@webpack/common";
+import type { ComponentType, PropsWithChildren } from "react";
+
+export function SettingsTab({ title, children }: PropsWithChildren<{ title: string; }>) {
+ return (
+ <Forms.FormSection>
+ <Text
+ variant="heading-lg/semibold"
+ tag="h2"
+ className={Margins.bottom16}
+ >
+ {title}
+ </Text>
+
+ {children}
+ </Forms.FormSection>
+ );
+}
+
+const onError = onlyOnce(handleComponentFailed);
+
+export function wrapTab(component: ComponentType, tab: string) {
+ return ErrorBoundary.wrap(component, {
+ message: `Failed to render the ${tab} tab. If this issue persists, try using the installer to reinstall!`,
+ onError,
+ });
+}