Equicord/src/components/settings/tabs/patchHelper/index.tsx
V cc01bb70cb
Replace Forms with our own independent components (#3618)
Vastly improves resilience of Settings Ui against Discord changes. Should now never break even if hell breaks loose

Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
2025-10-11 01:42:14 +02:00

167 lines
5.7 KiB
TypeScript

/*
* 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 { CodeBlock } from "@components/CodeBlock";
import { Divider } from "@components/Divider";
import { Flex } from "@components/Flex";
import { HeadingTertiary } from "@components/Heading";
import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
import { debounce } from "@shared/debounce";
import { Margins } from "@utils/margins";
import { copyWithToast } from "@utils/misc";
import { stripIndent } from "@utils/text";
import { ReplaceFn } from "@utils/types";
import { search } from "@webpack";
import { Button, React, TextInput, useMemo, useState } from "@webpack/common";
import { FullPatchInput } from "./FullPatchInput";
import { PatchPreview } from "./PatchPreview";
import { ReplacementInput } from "./ReplacementInput";
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]]]);
});
function PatchHelper() {
const [find, setFind] = useState("");
const [match, setMatch] = useState("");
const [replacement, setReplacement] = useState<string | ReplaceFn>("");
const [parsedFind, setParsedFind] = useState<string | RegExp>("");
const [findError, setFindError] = useState<string>();
const [matchError, setMatchError] = useState<string>();
const [replacementError, setReplacementError] = useState<string>();
const [module, setModule] = useState<[number, Function]>();
const code = useMemo(() => {
const find = parsedFind instanceof RegExp ? parsedFind.toString() : JSON.stringify(parsedFind);
const replace = typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement);
return stripIndent`
{
find: ${find},
replacement: {
match: /${match.replace(/(?<!\\)\//g, "\\/")}/,
replace: ${replace}
}
}
`;
}, [parsedFind, match, replacement]);
function onFindChange(v: string) {
setFind(v);
try {
let parsedFind = v as string | RegExp;
if (/^\/.+?\/$/.test(v)) parsedFind = new RegExp(v.slice(1, -1));
setFindError(void 0);
setParsedFind(parsedFind);
if (v.length) {
findCandidates({ find: parsedFind, setModule, setError: setFindError });
}
} catch (e: any) {
setFindError((e as Error).message);
}
}
function onMatchChange(v: string) {
setMatch(v);
try {
new RegExp(v);
setMatchError(void 0);
} catch (e: any) {
setMatchError((e as Error).message);
}
}
return (
<SettingsTab title="Patch Helper">
<HeadingTertiary>Full patch</HeadingTertiary>
<FullPatchInput
setFind={onFindChange}
setParsedFind={setParsedFind}
setMatch={onMatchChange}
setReplacement={setReplacement}
/>
<HeadingTertiary className={Margins.top8}>Find</HeadingTertiary>
<TextInput
type="text"
value={find}
onChange={onFindChange}
error={findError}
/>
<HeadingTertiary className={Margins.top8}>Match</HeadingTertiary>
<TextInput
type="text"
value={match}
onChange={onMatchChange}
error={matchError}
/>
<div className={Margins.top8} />
<ReplacementInput
replacement={replacement}
setReplacement={setReplacement}
replacementError={replacementError}
/>
<Divider />
{module && (
<PatchPreview
module={module}
match={match}
replacement={replacement}
setReplacementError={setReplacementError}
/>
)}
{!!(find && match && replacement) && (
<>
<HeadingTertiary className={Margins.top20}>Code</HeadingTertiary>
<CodeBlock lang="js" content={code} />
<Flex className={Margins.top16}>
<Button onClick={() => copyWithToast(code)}>
Copy to Clipboard
</Button>
<Button onClick={() => copyWithToast("```ts\n" + code + "\n```")}>
Copy as Codeblock
</Button>
</Flex>
</>
)}
</SettingsTab>
);
}
export default IS_DEV ? wrapTab(PatchHelper, "PatchHelper") : null;