From b0caa6f4db7b6219f0a07dfb5089ba7767070328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BB=97=20V=C4=83n=20Ho=C3=A0i=20Tu=C3=A2n?= Date: Sat, 29 Apr 2023 18:19:08 -0700 Subject: [PATCH] feat(plugin): TextReplace (#994) Co-authored-by: Vendicated --- .../VencordSettings/settingsStyles.css | 2 +- src/plugins/textReplace.tsx | 240 ++++++++++++++++++ 2 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 src/plugins/textReplace.tsx diff --git a/src/components/VencordSettings/settingsStyles.css b/src/components/VencordSettings/settingsStyles.css index 971e9a87..c25022a5 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 00000000..73ac1f2f --- /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 . +*/ + +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: () => + <> + + + + }, +}); + +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 ( + + {String(e)} + + ); + } +} + +function Input({ initialValue, onChange, placeholder }: { + placeholder: string; + initialValue: string; + onChange(value: string): void; +}) { + const [value, setValue] = useState(initialValue); + return ( + 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 ( + <> + {title} + + { + rulesArray.map((rule, index) => + + + + onChange(e, index, "find")} + /> + onChange(e, index, "replace")} + /> + onChange(e, index, "onlyIfIncludes")} + /> + + + + {isRegexRules && renderFindError(rule.find)} + + ) + } + + + ); +} + +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); + } +});