From 5c5b009c4180b73603a9c3a6c6663f889a2e2062 Mon Sep 17 00:00:00 2001 From: V Date: Fri, 12 May 2023 01:40:43 +0200 Subject: Settings: Fix resetting scroll/search when getting a ping (#1106) --- src/components/PatchHelper.tsx | 311 --------------------- src/components/PluginSettings/index.tsx | 19 +- .../VencordSettings/BackupAndRestoreTab.tsx | 67 +++++ .../VencordSettings/BackupRestoreTab.tsx | 66 ----- src/components/VencordSettings/CloudTab.tsx | 9 +- src/components/VencordSettings/PatchHelperTab.tsx | 310 ++++++++++++++++++++ src/components/VencordSettings/PluginsTab.tsx | 5 +- src/components/VencordSettings/ThemesTab.tsx | 15 +- src/components/VencordSettings/Updater.tsx | 256 ----------------- src/components/VencordSettings/UpdaterTab.tsx | 252 +++++++++++++++++ src/components/VencordSettings/VencordTab.tsx | 9 +- src/components/VencordSettings/index.tsx | 96 ------- src/components/VencordSettings/settingsStyles.css | 2 - src/components/VencordSettings/shared.tsx | 51 ++++ src/components/index.ts | 21 -- 15 files changed, 709 insertions(+), 780 deletions(-) delete mode 100644 src/components/PatchHelper.tsx create mode 100644 src/components/VencordSettings/BackupAndRestoreTab.tsx delete mode 100644 src/components/VencordSettings/BackupRestoreTab.tsx create mode 100644 src/components/VencordSettings/PatchHelperTab.tsx delete mode 100644 src/components/VencordSettings/Updater.tsx create mode 100644 src/components/VencordSettings/UpdaterTab.tsx delete mode 100644 src/components/VencordSettings/index.tsx create mode 100644 src/components/VencordSettings/shared.tsx delete mode 100644 src/components/index.ts (limited to 'src/components') diff --git a/src/components/PatchHelper.tsx b/src/components/PatchHelper.tsx deleted file mode 100644 index 6c95a8a..0000000 --- a/src/components/PatchHelper.tsx +++ /dev/null @@ -1,311 +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 . -*/ - -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, Text, TextInput } from "@webpack/common"; - -import { CheckedTextInput } from "./CheckedTextInput"; -import ErrorBoundary from "./ErrorBoundary"; - -// 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 Regex doesn't match!; - - 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 ( - <> -
{Parser.parse(fullMatch)}
-
{Parser.parse(groups)}
- - ); - } - - function renderDiff() { - return diff?.map(p => { - const color = p.added ? "lime" : p.removed ? "red" : "grey"; - return
{p.value}
; - }); - } - - 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 ( - <> - 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 - - - ); -} - -function PatchHelper() { - const [find, setFind] = 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 code = React.useMemo(() => { - return ` -{ - find: ${JSON.stringify(find)}, - replacement: { - match: /${match.replace(/(? - Patch Helper - find - - - match - { - try { - return (new RegExp(v), true); - } catch (e) { - return (e as Error).message; - } - }} - /> - - - - - {module && ( - - )} - - {!!(find && match && replacement) && ( - <> - Code -
{Parser.parse(makeCodeblock(code, "ts"))}
- - - )} - - ); -} - -export default IS_DEV ? ErrorBoundary.wrap(PatchHelper) : null; diff --git a/src/components/PluginSettings/index.tsx b/src/components/PluginSettings/index.tsx index 3fb9bb4..8ccc740 100644 --- a/src/components/PluginSettings/index.tsx +++ b/src/components/PluginSettings/index.tsx @@ -20,20 +20,18 @@ import "./styles.css"; import * as DataStore from "@api/DataStore"; import { showNotice } from "@api/Notices"; -import { useSettings } from "@api/Settings"; +import { Settings, useSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; -import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; -import { handleComponentFailed } from "@components/handleComponentFailed"; import { Badge } from "@components/PluginSettings/components"; import PluginModal from "@components/PluginSettings/PluginModal"; import { Switch } from "@components/Switch"; +import { SettingsTab } from "@components/VencordSettings/shared"; import { ChangeList } from "@utils/ChangeList"; import { Logger } from "@utils/Logger"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import { openModalLazy } from "@utils/modal"; -import { onlyOnce } from "@utils/onlyOnce"; import { LazyComponent, useAwaiter } from "@utils/react"; import { Plugin } from "@utils/types"; import { findByCode, findByPropsLazy } from "@webpack"; @@ -96,7 +94,7 @@ interface PluginCardProps extends React.HTMLProps { } function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) { - const settings = useSettings([`plugins.${plugin.name}.enabled`]).plugins[plugin.name]; + const settings = Settings.plugins[plugin.name]; const isEnabled = () => settings.enabled ?? false; @@ -179,7 +177,7 @@ enum SearchStatus { DISABLED } -export default ErrorBoundary.wrap(function PluginSettings() { +export default function PluginSettings() { const settings = useSettings(); const changes = React.useMemo(() => new ChangeList(), []); @@ -303,7 +301,7 @@ export default ErrorBoundary.wrap(function PluginSettings() { } return ( - + @@ -342,12 +340,9 @@ export default ErrorBoundary.wrap(function PluginSettings() {
{requiredPlugins}
-
+ ); -}, { - message: "Failed to render the Plugin Settings. If this persists, try using the installer to reinstall!", - onError: onlyOnce(handleComponentFailed), -}); +} function makeDependencyList(deps: string[]) { return ( diff --git a/src/components/VencordSettings/BackupAndRestoreTab.tsx b/src/components/VencordSettings/BackupAndRestoreTab.tsx new file mode 100644 index 0000000..a9a1c9f --- /dev/null +++ b/src/components/VencordSettings/BackupAndRestoreTab.tsx @@ -0,0 +1,67 @@ +/* + * 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 { Flex } from "@components/Flex"; +import { Margins } from "@utils/margins"; +import { classes } from "@utils/misc"; +import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync"; +import { Button, Card, Text } from "@webpack/common"; + +import { SettingsTab, wrapTab } from "./shared"; + +function BackupRestoreTab() { + return ( + + + + Warning + Importing a settings file will overwrite your current settings. + + + + 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. + + + Settings Export contains: +
    +
  • — Custom QuickCSS
  • +
  • — Theme Links
  • +
  • — Plugin Settings
  • +
+
+ + + + +
+ ); +} + +export default wrapTab(BackupRestoreTab, "Backup & Restore"); diff --git a/src/components/VencordSettings/BackupRestoreTab.tsx b/src/components/VencordSettings/BackupRestoreTab.tsx deleted file mode 100644 index 1737470..0000000 --- a/src/components/VencordSettings/BackupRestoreTab.tsx +++ /dev/null @@ -1,66 +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 . -*/ - -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"; - -function BackupRestoreTab() { - return ( - - - - Warning - Importing a settings file will overwrite your current settings. - - - - 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. - - - Settings Export contains: -
    -
  • — Custom QuickCSS
  • -
  • — Theme Links
  • -
  • — Plugin Settings
  • -
-
- - - - -
- ); -} - -export default ErrorBoundary.wrap(BackupRestoreTab); 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 ( - <> + Vencord comes with a cloud integration that adds goodies like settings sync across devices. @@ -157,8 +158,8 @@ function CloudTab() { - + ); } -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 . +*/ + +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 Regex doesn't match!; + + 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 ( + <> +
{Parser.parse(fullMatch)}
+
{Parser.parse(groups)}
+ + ); + } + + function renderDiff() { + return diff?.map(p => { + const color = p.added ? "lime" : p.removed ? "red" : "grey"; + return
{p.value}
; + }); + } + + 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 ( + <> + 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 + + + ); +} + +function PatchHelper() { + const [find, setFind] = 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 code = React.useMemo(() => { + return ` +{ + find: ${JSON.stringify(find)}, + replacement: { + match: /${match.replace(/(? + find + + + match + { + try { + return (new RegExp(v), true); + } catch (e) { + return (e as Error).message; + } + }} + /> + + + + + {module && ( + + )} + + {!!(find && match && replacement) && ( + <> + Code +
{Parser.parse(makeCodeblock(code, "ts"))}
+ + + )} + + ); +} + +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 . */ -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 ( - <> + Paste links to .theme.css files here One link per line @@ -124,6 +125,8 @@ export default ErrorBoundary.wrap(function () { onBlur={onBlur} /> - + ); -}); +} + +export default wrapTab(ThemesTab, "Themes"); diff --git a/src/components/VencordSettings/Updater.tsx b/src/components/VencordSettings/Updater.tsx deleted file mode 100644 index 9345d27..0000000 --- a/src/components/VencordSettings/Updater.tsx +++ /dev/null @@ -1,256 +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 . -*/ - -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"; - -function withDispatcher(dispatcher: React.Dispatch>, action: () => any) { - return async () => { - dispatcher(true); - try { - await action(); - } catch (e: any) { - UpdateLogger.error("Failed to update", e); - if (!e) { - var 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") - var err = `Command \`${path}\` not found.\nPlease install it and try again`; - else { - var err = `An error occured while running \`${cmd}\`:\n`; - err += stderr || `Code \`${code}\`. See the console for more info`; - } - - } else { - var err = "An unknown error occurred. See the console for more info."; - } - Alerts.show({ - title: "Oops!", - body: ( - - {err.split("\n").map(line =>
{Parser.parse(line)}
)} -
- ) - }); - } - finally { - dispatcher(false); - } - }; -} - -interface CommonProps { - repo: string; - repoPending: boolean; -} - -function HashLink({ repo, hash, disabled = false }: { repo: string, hash: string, disabled?: boolean; }) { - return - {hash} - ; -} - -function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) { - return ( - - {updates.map(({ hash, author, message }) => ( -
- - {message} - {author} -
- ))} -
- ); -} - -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 ( - <> - {!updates && updateError ? ( - <> - Failed to check updates. Check the console for more info - -

{updateError.stderr || updateError.stdout || "An unknown error occurred"}

-
- - ) : ( - - {isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"} - - )} - - {isOutdated && } - - - {isOutdated && } - - - - ); -} - -function Newer(props: CommonProps) { - return ( - <> - - Your local copy has more recent commits. Please stash or reset them. - - - - ); -} - -function Updater() { - const settings = useSettings(["notifyAboutUpdates", "autoUpdate", "autoUpdateNotification"]); - - const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." }); - - React.useEffect(() => { - if (err) - UpdateLogger.error("Failed to retrieve repo", err); - }, [err]); - - const commonProps: CommonProps = { - repo, - repoPending - }; - - return ( - - Updater Settings - settings.notifyAboutUpdates = v} - note="Shows a notification on startup" - disabled={settings.autoUpdate} - > - Get notified about new updates - - settings.autoUpdate = v} - note="Automatically update Vencord without confirmation prompt" - > - Automatically update - - settings.autoUpdateNotification = v} - note="Shows a notification when Vencord automatically updates" - disabled={!settings.autoUpdate} - > - Get notified when an automatic update completes - - - Repo - - - {repoPending - ? repo - : err - ? "Failed to retrieve - check console" - : ( - - {repo.split("/").slice(-2).join("/")} - - ) - } - {" "}() - - - - - Updates - - {isNewer ? : } - - ); -} - -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), -}); diff --git a/src/components/VencordSettings/UpdaterTab.tsx b/src/components/VencordSettings/UpdaterTab.tsx new file mode 100644 index 0000000..4d0b86c --- /dev/null +++ b/src/components/VencordSettings/UpdaterTab.tsx @@ -0,0 +1,252 @@ +/* + * 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 { 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 { 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); + if (!e) { + var 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") + var err = `Command \`${path}\` not found.\nPlease install it and try again`; + else { + var err = `An error occured while running \`${cmd}\`:\n`; + err += stderr || `Code \`${code}\`. See the console for more info`; + } + + } else { + var err = "An unknown error occurred. See the console for more info."; + } + Alerts.show({ + title: "Oops!", + body: ( + + {err.split("\n").map(line =>
{Parser.parse(line)}
)} +
+ ) + }); + } + finally { + dispatcher(false); + } + }; +} + +interface CommonProps { + repo: string; + repoPending: boolean; +} + +function HashLink({ repo, hash, disabled = false }: { repo: string, hash: string, disabled?: boolean; }) { + return + {hash} + ; +} + +function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) { + return ( + + {updates.map(({ hash, author, message }) => ( +
+ + {message} - {author} +
+ ))} +
+ ); +} + +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 ( + <> + {!updates && updateError ? ( + <> + Failed to check updates. Check the console for more info + +

{updateError.stderr || updateError.stdout || "An unknown error occurred"}

+
+ + ) : ( + + {isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"} + + )} + + {isOutdated && } + + + {isOutdated && } + + + + ); +} + +function Newer(props: CommonProps) { + return ( + <> + + Your local copy has more recent commits. Please stash or reset them. + + + + ); +} + +function Updater() { + const settings = useSettings(["notifyAboutUpdates", "autoUpdate", "autoUpdateNotification"]); + + const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." }); + + React.useEffect(() => { + if (err) + UpdateLogger.error("Failed to retrieve repo", err); + }, [err]); + + const commonProps: CommonProps = { + repo, + repoPending + }; + + return ( + + Updater Settings + settings.notifyAboutUpdates = v} + note="Shows a notification on startup" + disabled={settings.autoUpdate} + > + Get notified about new updates + + settings.autoUpdate = v} + note="Automatically update Vencord without confirmation prompt" + > + Automatically update + + settings.autoUpdateNotification = v} + note="Shows a notification when Vencord automatically updates" + disabled={!settings.autoUpdate} + > + Get notified when an automatic update completes + + + Repo + + + {repoPending + ? repo + : err + ? "Failed to retrieve - check console" + : ( + + {repo.split("/").slice(-2).join("/")} + + ) + } + {" "}() + + + + + Updates + + {isNewer ? : } + + ); +} + +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 ( - + @@ -153,7 +154,7 @@ function VencordSettings() { {typeof Notification !== "undefined" && } - + ); } @@ -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 . -*/ - -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 = { - VencordSettings: { name: "Vencord", component: () => }, - VencordPlugins: { name: "Plugins", component: () => }, - VencordThemes: { name: "Themes", component: () => }, - VencordUpdater: { name: "Updater" }, // Only show updater if IS_WEB is false - VencordCloud: { name: "Cloud", component: () => }, - VencordSettingsSync: { name: "Backup & Restore", component: () => } -}; - -if (!IS_WEB) SettingsTabs.VencordUpdater.component = () => Updater && ; - -function Settings(props: SettingsProps) { - const { tab = "VencordSettings" } = props; - - const CurrentTab = SettingsTabs[tab]?.component ?? null; - if (isMobile) { - return CurrentTab && ; - } - - return - Vencord Settings - - - {Object.entries(SettingsTabs).map(([key, { name, component }]) => { - if (!component) return null; - return - {name} - ; - })} - - - {CurrentTab && } - ; -} - -const onError = onlyOnce(handleComponentFailed); - -export default function (props: SettingsProps) { - return - - ; -} 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 . +*/ + +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 ( + + + {title} + + + {children} + + ); +} + +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, + }); +} diff --git a/src/components/index.ts b/src/components/index.ts deleted file mode 100644 index 3ee53b0..0000000 --- a/src/components/index.ts +++ /dev/null @@ -1,21 +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 . -*/ - -export { default as PatchHelper } from "./PatchHelper"; -export { default as PluginSettings } from "./PluginSettings"; -export { default as VencordSettings } from "./VencordSettings"; -- cgit