feat(UrlHighlighter): add plugin (#535)

* feat(UrlHighlighter): add plugin

* Update index.tsx

---------

Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
prism 2025-12-04 18:27:42 +01:00 • committed by GitHub
parent 90b1360aa7
commit 8d9062f90a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 196 additions and 0 deletions

View file

@ -0,0 +1,149 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Button } from "@components/Button";
import ErrorBoundary from "@components/ErrorBoundary";
import { Heading } from "@components/Heading";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findComponentByCodeLazy } from "@webpack";
import { ColorPicker, TextInput } from "@webpack/common";
interface PatternEntry {
pattern: string;
color: string;
}
const DEFAULT_COLOR = "#eb7396";
const cl = classNameFactory("vc-url-hl-");
const TrashIcon = findComponentByCodeLazy("2.81h8.36a3");
const PlusIcon = findComponentByCodeLazy("0v-5h5a1");
const hexToInt = (hex: string): number => parseInt(hex.replace("#", ""), 16);
const intToHex = (n: number): string => "#" + n.toString(16).padStart(6, "0");
const PatternsComponent = ErrorBoundary.wrap(() => {
const { patterns } = settings.store;
return (
<section>
<Heading>URL Patterns</Heading>
{patterns.map((entry, i) => (
<div key={i} className={cl("pattern-wrapper")}>
<TextInput
value={entry.pattern}
onChange={v => {
patterns[i].pattern = v;
settings.store.patterns = patterns;
}}
placeholder="*.example.com"
/>
<div className={cl("color-picker")}>
<ColorPicker
color={hexToInt(entry.color)}
onChange={(c: number) => {
patterns[i].color = intToHex(c);
settings.store.patterns = patterns;
}}
showEyeDropper={false}
/>
</div>
<Button
className={cl("remove-button")}
variant="secondary"
size="small"
onClick={() => {
patterns.splice(i, 1);
settings.store.patterns = patterns;
}}
>
<TrashIcon className={cl("icon")} />
</Button>
</div>
))}
<Button
onClick={() => settings.store.patterns.push({ pattern: "", color: DEFAULT_COLOR })}
className={cl("add-button")}
variant="secondary"
size="small"
>
<PlusIcon className={cl("icon")} /> Add New
</Button>
</section>
);
}, { noop: true });
const settings = definePluginSettings({
patterns: {
type: OptionType.COMPONENT,
description: "URL patterns to highlight using glob patterns.",
default: [] as PatternEntry[],
component: PatternsComponent
},
boldUrls: {
type: OptionType.BOOLEAN,
description: "Make highlighted URLs bold.",
default: false
},
highlightEmbeds: {
type: OptionType.BOOLEAN,
description: "Also highlight URLs in embed content.",
default: false
}
});
function globToRegex(glob: string): RegExp {
const escaped = glob
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
.replace(/\*/g, ".*")
.replace(/\?/g, ".");
return new RegExp(escaped, "i");
}
function getMatchingPattern(url: string): PatternEntry | null {
for (const entry of settings.store.patterns) {
const pattern = entry.pattern.trim();
if (!pattern) continue;
if (globToRegex(pattern).test(url)) return entry;
}
return null;
}
export default definePlugin({
name: "UrlHighlighter",
description: "Highlights URLs in messages that match your patterns.",
authors: [EquicordDevs.Prism],
settings,
patches: [
{
find: '"messageId","channelId"',
replacement: {
match: /,children:null!=\i\?\i:\i/,
replace: "$&,...$self.getProps(arguments[0])"
}
}
],
getProps(props: { href?: string; title?: string; }) {
if (!props.href) return {};
if (!settings.store.highlightEmbeds && props.href !== props.title) return {};
const match = getMatchingPattern(props.href);
if (!match) return {};
return {
className: cl(settings.store.boldUrls ? "marked" : "highlight"),
style: { "--vc-url-hl-color": match.color } as React.CSSProperties
};
}
});

View file

@ -0,0 +1,47 @@
.vc-url-hl-highlight {
color: var(--vc-url-hl-color) !important;
}
.vc-url-hl-marked {
color: var(--vc-url-hl-color) !important;
font-weight: 600;
}
.vc-url-hl-pattern-wrapper {
display: grid;
grid-template-columns: 1fr auto auto;
gap: 8px;
align-items: center;
padding-bottom: 12px;
}
.vc-url-hl-color-picker {
display: flex;
align-items: center;
}
.vc-url-hl-color-picker [class^="swatch"] {
width: 32px;
height: 32px;
border-radius: 4px;
}
.vc-url-hl-remove-button {
width: 36px !important;
height: 36px !important;
min-width: 36px !important;
min-height: 36px !important;
padding: 0 !important;
box-sizing: border-box;
}
.vc-url-hl-add-button {
display: flex;
align-items: center;
gap: 2px;
}
.vc-url-hl-icon {
width: 20px;
height: 20px;
}