diff options
author | V <vendicated@riseup.net> | 2023-05-12 01:40:43 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-12 01:40:43 +0200 |
commit | 5c5b009c4180b73603a9c3a6c6663f889a2e2062 (patch) | |
tree | 9f586ff1e4491bbd8d7ac046d60bbae081d1d6f8 /src/components/VencordSettings | |
parent | 0c54b1fa1d8f9d858baf912bb4b1efd9d3c0ec93 (diff) | |
download | Vencord-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.tsx | 9 | ||||
-rw-r--r-- | src/components/VencordSettings/PatchHelperTab.tsx | 310 | ||||
-rw-r--r-- | src/components/VencordSettings/PluginsTab.tsx | 5 | ||||
-rw-r--r-- | src/components/VencordSettings/ThemesTab.tsx | 15 | ||||
-rw-r--r-- | src/components/VencordSettings/UpdaterTab.tsx (renamed from src/components/VencordSettings/Updater.tsx) | 14 | ||||
-rw-r--r-- | src/components/VencordSettings/VencordTab.tsx | 9 | ||||
-rw-r--r-- | src/components/VencordSettings/index.tsx | 96 | ||||
-rw-r--r-- | src/components/VencordSettings/settingsStyles.css | 2 | ||||
-rw-r--r-- | src/components/VencordSettings/shared.tsx | 51 |
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, + }); +} |