aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorĐỗ Văn Hoài Tuân <htuan03@gmail.com>2023-04-29 18:19:08 -0700
committerGitHub <noreply@github.com>2023-04-30 01:19:08 +0000
commitb0caa6f4db7b6219f0a07dfb5089ba7767070328 (patch)
tree9e05484624c363c2deaebf18daff31db5805c4a8
parent168d4b4cd90c6fc33dd99dd5a2f106a7812336e5 (diff)
downloadVencord-b0caa6f4db7b6219f0a07dfb5089ba7767070328.tar.gz
Vencord-b0caa6f4db7b6219f0a07dfb5089ba7767070328.tar.bz2
Vencord-b0caa6f4db7b6219f0a07dfb5089ba7767070328.zip
feat(plugin): TextReplace (#994)
Co-authored-by: Vendicated <vendicated@riseup.net>
-rw-r--r--src/components/VencordSettings/settingsStyles.css2
-rw-r--r--src/plugins/textReplace.tsx240
2 files changed, 241 insertions, 1 deletions
diff --git a/src/components/VencordSettings/settingsStyles.css b/src/components/VencordSettings/settingsStyles.css
index 971e9a8..c25022a 100644
--- a/src/components/VencordSettings/settingsStyles.css
+++ b/src/components/VencordSettings/settingsStyles.css
@@ -59,7 +59,7 @@
}
.vc-text-selectable,
-.vc-text-selectable :not(a, button) {
+.vc-text-selectable :not(a, button, a *, button *) {
/* make text selectable, silly discord makes the entirety of settings not selectable */
user-select: text;
diff --git a/src/plugins/textReplace.tsx b/src/plugins/textReplace.tsx
new file mode 100644
index 0000000..73ac1f2
--- /dev/null
+++ b/src/plugins/textReplace.tsx
@@ -0,0 +1,240 @@
+/*
+ * 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 { DataStore } from "@api/index";
+import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
+import { definePluginSettings } from "@api/settings";
+import { Flex } from "@components/Flex";
+import { Devs } from "@utils/constants";
+import Logger from "@utils/Logger";
+import { useForceUpdater } from "@utils/misc";
+import definePlugin, { OptionType } from "@utils/types";
+import { Button, Forms, React, TextInput, useState } from "@webpack/common";
+
+const STRING_RULES_KEY = "TextReplace_rulesString";
+const REGEX_RULES_KEY = "TextReplace_rulesRegex";
+
+type Rule = Record<"find" | "replace" | "onlyIfIncludes", string>;
+
+interface TextReplaceProps {
+ title: string;
+ rulesArray: Rule[];
+ rulesKey: string;
+}
+
+const makeEmptyRule: () => Rule = () => ({
+ find: "",
+ replace: "",
+ onlyIfIncludes: ""
+});
+const makeEmptyRuleArray = () => [makeEmptyRule()];
+
+let stringRules = makeEmptyRuleArray();
+let regexRules = makeEmptyRuleArray();
+
+const settings = definePluginSettings({
+ replace: {
+ type: OptionType.COMPONENT,
+ description: "",
+ component: () =>
+ <>
+ <TextReplace
+ title="Using String"
+ rulesArray={stringRules}
+ rulesKey={STRING_RULES_KEY}
+ />
+ <TextReplace
+ title="Using Regex"
+ rulesArray={regexRules}
+ rulesKey={REGEX_RULES_KEY}
+ />
+ </>
+ },
+});
+
+function stringToRegex(str: string) {
+ const match = str.match(/^(\/)?(.+?)(?:\/([gimsuy]*))?$/); // Regex to match regex
+ return match
+ ? new RegExp(
+ match[2], // Pattern
+ match[3]
+ ?.split("") // Remove duplicate flags
+ .filter((char, pos, flagArr) => flagArr.indexOf(char) === pos)
+ .join("")
+ ?? "g"
+ )
+ : new RegExp(str); // Not a regex, return string
+}
+
+function renderFindError(find: string) {
+ try {
+ stringToRegex(find);
+ return null;
+ } catch (e) {
+ return (
+ <span style={{ color: "var(--text-danger)" }}>
+ {String(e)}
+ </span>
+ );
+ }
+}
+
+function Input({ initialValue, onChange, placeholder }: {
+ placeholder: string;
+ initialValue: string;
+ onChange(value: string): void;
+}) {
+ const [value, setValue] = useState(initialValue);
+ return (
+ <TextInput
+ placeholder={placeholder}
+ value={value}
+ onChange={setValue}
+ spellCheck={false}
+ onBlur={() => value !== initialValue && onChange(value)}
+ />
+ );
+}
+
+function TextReplace({ title, rulesArray, rulesKey }: TextReplaceProps) {
+ const isRegexRules = title === "Using Regex";
+
+ const update = useForceUpdater();
+
+ async function onClickRemove(index: number) {
+ rulesArray.splice(index, 1);
+
+ await DataStore.set(rulesKey, rulesArray);
+ update();
+ }
+
+ async function onChange(e: string, index: number, key: string) {
+ if (index === rulesArray.length - 1)
+ rulesArray.push(makeEmptyRule());
+
+ rulesArray[index][key] = e;
+
+ if (rulesArray[index].find === "" && rulesArray[index].replace === "" && rulesArray[index].onlyIfIncludes === "" && index !== rulesArray.length - 1)
+ rulesArray.splice(index, 1);
+
+ await DataStore.set(rulesKey, rulesArray);
+ update();
+ }
+
+ return (
+ <>
+ <Forms.FormTitle tag="h4">{title}</Forms.FormTitle>
+ <Flex flexDirection="column" style={{ gap: "0.5em" }}>
+ {
+ rulesArray.map((rule, index) =>
+ <React.Fragment key={`${rule.find}-${index}`}>
+ <Flex flexDirection="row" style={{ gap: 0 }}>
+ <Flex flexDirection="row" style={{ flexGrow: 1, gap: "0.5em" }}>
+ <Input
+ placeholder="Find"
+ initialValue={rule.find}
+ onChange={e => onChange(e, index, "find")}
+ />
+ <Input
+ placeholder="Replace"
+ initialValue={rule.replace}
+ onChange={e => onChange(e, index, "replace")}
+ />
+ <Input
+ placeholder="Only if includes"
+ initialValue={rule.onlyIfIncludes}
+ onChange={e => onChange(e, index, "onlyIfIncludes")}
+ />
+ </Flex>
+ <Button
+ size={Button.Sizes.MIN}
+ onClick={() => onClickRemove(index)}
+ style={{
+ background: "none",
+ ...(index === rulesArray.length - 1
+ ? {
+ visibility: "hidden",
+ pointerEvents: "none"
+ }
+ : {}
+ )
+ }}
+ >
+ <svg width="24" height="24" viewBox="0 0 24 24">
+ <title>Delete Rule</title>
+ <path fill="var(--status-danger)" d="M15 3.999V2H9V3.999H3V5.999H21V3.999H15Z" />
+ <path fill="var(--status-danger)" d="M5 6.99902V18.999C5 20.101 5.897 20.999 7 20.999H17C18.103 20.999 19 20.101 19 18.999V6.99902H5ZM11 17H9V11H11V17ZM15 17H13V11H15V17Z" />
+ </svg>
+ </Button>
+ </Flex>
+ {isRegexRules && renderFindError(rule.find)}
+ </React.Fragment>
+ )
+ }
+ </Flex>
+ </>
+ );
+}
+
+export default definePlugin({
+ name: "TextReplace",
+ description: "Replace text in your messages",
+ authors: [Devs.Samu, Devs.AutumnVN],
+ dependencies: ["MessageEventsAPI"],
+
+ settings,
+
+ async start() {
+ stringRules = await DataStore.get(STRING_RULES_KEY) ?? makeEmptyRuleArray();
+ regexRules = await DataStore.get(REGEX_RULES_KEY) ?? makeEmptyRuleArray();
+
+ this.preSend = addPreSendListener((_, msg) => {
+ // pad so that rules can use " word " to only match whole "word"
+ msg.content = " " + msg.content + " ";
+
+ if (stringRules) {
+ for (const rule of stringRules) {
+ if (!rule.find || !rule.replace) continue;
+ if (rule.onlyIfIncludes && !msg.content.includes(rule.onlyIfIncludes)) continue;
+
+ msg.content = msg.content.replaceAll(rule.find, rule.replace);
+ }
+ }
+
+ if (regexRules) {
+ for (const rule of regexRules) {
+ if (!rule.find || !rule.replace) continue;
+ if (rule.onlyIfIncludes && !msg.content.includes(rule.onlyIfIncludes)) continue;
+
+ try {
+ const regex = stringToRegex(rule.find);
+ msg.content = msg.content.replace(regex, rule.replace);
+ } catch (e) {
+ new Logger("TextReplace").error(`Invalid regex: ${rule.find}`);
+ }
+ }
+ }
+
+ msg.content = msg.content.trim();
+ });
+ },
+
+ stop() {
+ removePreSendListener(this.preSend);
+ }
+});