mirror of
https://github.com/System-End/Vencord.git
synced 2026-04-19 19:45:09 +00:00
Improve TextReplace settings UX
This commit is contained in:
parent
0f74e798d4
commit
4f629294c3
5 changed files with 206 additions and 61 deletions
23
src/components/ExpandableCard.css
Normal file
23
src/components/ExpandableCard.css
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
.vc-expandable-card {
|
||||
color: var(--text-default);
|
||||
}
|
||||
|
||||
.vc-expandable-card-header {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
padding: .75em 1em;
|
||||
margin: 4px;
|
||||
border-radius: var(--radius-xs);
|
||||
|
||||
.vc-expandable-card[data-expanded="true"] & {
|
||||
background: var(--card-secondary-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.vc-expandable-card-icon {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.vc-expandable-card-content {
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
45
src/components/ExpandableCard.tsx
Normal file
45
src/components/ExpandableCard.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./ExpandableCard.css";
|
||||
|
||||
import { classes } from "@utils/misc";
|
||||
import { Clickable, useState } from "@webpack/common";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
import { Card } from "./Card";
|
||||
import { DownArrow, RightArrow } from "./Icons";
|
||||
|
||||
export type ExpandableSectionProps = PropsWithChildren<{
|
||||
renderContent: () => React.ReactNode;
|
||||
className?: string;
|
||||
initialExpanded?: boolean;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* A card component that can expand and collapse to show/hide content. The header (props.children) is always visible, and the content (props.renderContent) is only visible when expanded.
|
||||
*/
|
||||
export function ExpandableSection({ children, renderContent: Content, className, initialExpanded = false }: ExpandableSectionProps) {
|
||||
const [expanded, setExpanded] = useState(initialExpanded);
|
||||
|
||||
const Icon = expanded ? DownArrow : RightArrow;
|
||||
|
||||
return (
|
||||
<Card data-expanded={expanded} className={classes("vc-expandable-card", className)}>
|
||||
<Clickable className="vc-expandable-card-header" onClick={() => setExpanded(c => !c)} >
|
||||
{children}
|
||||
<Icon className="vc-expandable-card-icon" />
|
||||
</Clickable>
|
||||
|
||||
{expanded
|
||||
? <div className="vc-expandable-card-content">
|
||||
<Content />
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -571,7 +571,7 @@ export function CloudUploadIcon(props: IconProps) {
|
|||
);
|
||||
}
|
||||
|
||||
export const ClockIcon = (props?: any) => {
|
||||
export function ClockIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
|
|
@ -585,4 +585,32 @@ export const ClockIcon = (props?: any) => {
|
|||
/>
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function DownArrow(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
d="M5.3 9.3a1 1 0 0 1 1.4 0l5.3 5.29 5.3-5.3a1 1 0 1 1 1.4 1.42l-6 6a1 1 0 0 1-1.4 0l-6-6a1 1 0 0 1 0-1.42Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function RightArrow(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
d="M9.3 5.3a1 1 0 0 0 0 1.4l5.29 5.3-5.3 5.3a1 1 0 1 0 1.42 1.4l6-6a1 1 0 0 0 0-1.4l-6-6a1 1 0 0 0-1.42 0Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,22 +16,31 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Button } from "@components/Button";
|
||||
import { ExpandableSection } from "@components/ExpandableCard";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { DeleteIcon } from "@components/Icons";
|
||||
import { HeadingSecondary } from "@components/Heading";
|
||||
import { Paragraph } from "@components/Paragraph";
|
||||
import { Span } from "@components/Span";
|
||||
import { TooltipContainer } from "@components/TooltipContainer";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Button, Forms, React, TextInput, useState } from "@webpack/common";
|
||||
import { React, TextInput, useState } from "@webpack/common";
|
||||
|
||||
const STRING_RULES_KEY = "TextReplace_rulesString";
|
||||
const REGEX_RULES_KEY = "TextReplace_rulesRegex";
|
||||
const cl = classNameFactory("vc-textReplace-");
|
||||
|
||||
type Rule = Record<"find" | "replace" | "onlyIfIncludes", string>;
|
||||
|
||||
interface TextReplaceProps {
|
||||
title: string;
|
||||
description: string;
|
||||
rulesArray: Rule[];
|
||||
isRegex?: boolean;
|
||||
}
|
||||
|
||||
const makeEmptyRule: () => Rule = () => ({
|
||||
|
|
@ -49,15 +58,18 @@ const settings = definePluginSettings({
|
|||
|
||||
return (
|
||||
<>
|
||||
<TextReplaceTesting />
|
||||
<TextReplace
|
||||
title="Using String"
|
||||
title="Simple Replacements"
|
||||
description="Simple find and replace rules. For example, find 'brb' and replace it with 'be right back'"
|
||||
rulesArray={stringRules}
|
||||
/>
|
||||
<TextReplace
|
||||
title="Using Regex"
|
||||
title="Regex Replacements"
|
||||
description="More powerful replacements using Regular Expressions. This section is for advanced users. If you don't understand it, just ignore it"
|
||||
rulesArray={regexRules}
|
||||
isRegex
|
||||
/>
|
||||
<TextReplaceTesting />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -116,21 +128,32 @@ function Input({ initialValue, onChange, placeholder }: {
|
|||
);
|
||||
}
|
||||
|
||||
function TextReplace({ title, rulesArray }: TextReplaceProps) {
|
||||
const isRegexRules = title === "Using Regex";
|
||||
function TextRow({ label, description, value, onChange }: { label: string; description: string; value: string; onChange(value: string): void; }) {
|
||||
return (
|
||||
<>
|
||||
<TooltipContainer text={description}>
|
||||
<Span weight="medium" size="md">{label}</Span>
|
||||
</TooltipContainer>
|
||||
<Input
|
||||
placeholder={description}
|
||||
initialValue={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
async function onClickRemove(index: number) {
|
||||
if (index === rulesArray.length - 1) return;
|
||||
const isEmptyRule = (rule: Rule) => !rule.find;
|
||||
|
||||
function TextReplace({ title, description, rulesArray, isRegex = false }: TextReplaceProps) {
|
||||
function onClickRemove(index: number) {
|
||||
rulesArray.splice(index, 1);
|
||||
}
|
||||
|
||||
async function onChange(e: string, index: number, key: string) {
|
||||
if (index === rulesArray.length - 1) {
|
||||
rulesArray.push(makeEmptyRule());
|
||||
}
|
||||
|
||||
function onChange(e: string, index: number, key: string) {
|
||||
rulesArray[index][key] = e;
|
||||
|
||||
// If a rule is empty after editing and is not the last rule, remove it
|
||||
if (rulesArray[index].find === "" && rulesArray[index].replace === "" && rulesArray[index].onlyIfIncludes === "" && index !== rulesArray.length - 1) {
|
||||
rulesArray.splice(index, 1);
|
||||
}
|
||||
|
|
@ -138,49 +161,61 @@ function TextReplace({ title, rulesArray }: TextReplaceProps) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Forms.FormTitle tag="h4">{title}</Forms.FormTitle>
|
||||
<div>
|
||||
<HeadingSecondary>{title}</HeadingSecondary>
|
||||
<Paragraph>{description}</Paragraph>
|
||||
</div>
|
||||
<Flex flexDirection="column" style={{ gap: "0.5em" }}>
|
||||
{
|
||||
rulesArray.map((rule, index) =>
|
||||
<React.Fragment key={`${rule.find}-${index}`}>
|
||||
<Flex gap="0.5em" flexDirection="row" style={{ flexGrow: 1 }}>
|
||||
<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")}
|
||||
/>
|
||||
{rulesArray.map((rule, index) =>
|
||||
<ExpandableSection
|
||||
key={`${rule.find}-${index}`}
|
||||
renderContent={() => (
|
||||
<>
|
||||
<div className={cl("input-grid")}>
|
||||
<TextRow
|
||||
label="Find"
|
||||
description={isRegex ? "The regex pattern" : "The text to replace"}
|
||||
value={rule.find}
|
||||
onChange={e => onChange(e, index, "find")}
|
||||
/>
|
||||
<TextRow
|
||||
label="Replace"
|
||||
description="The text to replace the found text with"
|
||||
value={rule.replace}
|
||||
onChange={e => onChange(e, index, "replace")}
|
||||
/>
|
||||
<TextRow
|
||||
label="Only if includes"
|
||||
description="This rule will only be applied if the message includes this text. This is optional"
|
||||
value={rule.onlyIfIncludes}
|
||||
onChange={e => onChange(e, index, "onlyIfIncludes")}
|
||||
/>
|
||||
</div>
|
||||
{isRegex && renderFindError(rule.find)}
|
||||
<Button
|
||||
size={Button.Sizes.MIN}
|
||||
className={cl("delete-button")}
|
||||
variant="dangerPrimary"
|
||||
onClick={() => onClickRemove(index)}
|
||||
style={{
|
||||
background: "none",
|
||||
color: "var(--status-danger)",
|
||||
...(index === rulesArray.length - 1
|
||||
? {
|
||||
visibility: "hidden",
|
||||
pointerEvents: "none"
|
||||
}
|
||||
: {}
|
||||
)
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
Delete Rule
|
||||
</Button>
|
||||
</Flex>
|
||||
{isRegexRules && renderFindError(rule.find)}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<Paragraph weight="medium" size="md">
|
||||
{isEmptyRule(rule)
|
||||
? `Empty Rule ${index + 1}`
|
||||
: `Rule ${index + 1} - ${rule.find}`
|
||||
}
|
||||
</Paragraph>
|
||||
</ExpandableSection>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => rulesArray.push(makeEmptyRule())}
|
||||
disabled={rulesArray.length > 0 && isEmptyRule(rulesArray[rulesArray.length - 1])}
|
||||
>
|
||||
Add Rule
|
||||
</Button>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
|
|
@ -188,12 +223,15 @@ function TextReplace({ title, rulesArray }: TextReplaceProps) {
|
|||
|
||||
function TextReplaceTesting() {
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Forms.FormTitle tag="h4">Test Rules</Forms.FormTitle>
|
||||
<TextInput placeholder="Type a message" onChange={setValue} />
|
||||
<TextInput placeholder="Message with rules applied" editable={false} value={applyRules(value)} />
|
||||
</>
|
||||
<div>
|
||||
<HeadingSecondary>Rule Tester</HeadingSecondary>
|
||||
<Flex flexDirection="column" gap={6}>
|
||||
<TextInput placeholder="Type a message to test rules on" onChange={setValue} />
|
||||
<TextInput placeholder="Message with rules applied" editable={false} value={applyRules(value)} style={{ opacity: 0.7 }} />
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
11
src/plugins/textReplace/styles.css
Normal file
11
src/plugins/textReplace/styles.css
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
.vc-textReplace-input-grid {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
gap: 0.25em 1em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vc-textReplace-delete-button {
|
||||
width: 100%;
|
||||
margin-block: 0.75em 0.5em
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue