diff --git a/README.md b/README.md index 23b87800..3f542f10 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend - Request for plugins from Discord.
-Extra included plugins (120 additional plugins) +Extra included plugins (122 additional plugins) - AllCallTimers by MaxHerbold and D3SOX - AltKrispSwitch by newwares @@ -87,6 +87,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend - LoginWithQR by nexpid - MediaDownloader by Colorman - Meow by Samwich +- MessageColors by hen - MessageLinkTooltip by Kyuuhachi - MessageLoggerEnhanced by Aria - MessageTranslate by Samwich @@ -96,6 +97,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend - NoBulletPoints by Samwich - NoDefaultEmojis by Samwich - NoDeleteSafety by Samwich +- NoMirroredCamera by Nyx - NoModalAnimation by AutumnVN - NoNitroUpsell by thororen - NoRoleHeaders by Samwich diff --git a/src/equicordplugins/messageColors/constants.ts b/src/equicordplugins/messageColors/constants.ts new file mode 100644 index 00000000..30336512 --- /dev/null +++ b/src/equicordplugins/messageColors/constants.ts @@ -0,0 +1,63 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { OptionType } from "@utils/types"; + +export const enum RenderType { + BLOCK, + FOREGROUND, + BACKGROUND, +} + +export const settings = definePluginSettings({ + renderType: { + type: OptionType.SELECT, + description: "How to render colors", + options: [ + { + label: "Text color", + value: RenderType.FOREGROUND, + default: true, + }, + { + label: "Block nearby", + value: RenderType.BLOCK, + }, + { + label: "Background color", + value: RenderType.BACKGROUND + }, + ] + } +}); + +export const enum ColorType { + RGB, + RGBA, + HEX, + HSL +} + +// It's sooo hard to read regex without this, it makes it at least somewhat bearable +export const replaceRegexp = (reg: string) => { + const n = new RegExp(reg + // \c - 'comma' + // \v - 'value' + // \f - 'float' + .replaceAll("\\f", "[+-]?([0-9]*[.])?[0-9]+") + .replaceAll("\\c", "(?:,|\\s)") + .replaceAll("\\v", "\\s*?\\d+?\\s*?"), "g"); + + return n; +}; + +export const regex = [ + { reg: /rgb\(\v\c\v\c\v\)/g, type: ColorType.RGB }, + { reg: /rgba\(\v\c\v\c\v(\c|\/?)\s*\f\)/g, type: ColorType.RGBA }, + { reg: /hsl\(\v°?\c\s*?\d+%?\s*?\c\s*?\d+%?\s*?\)/g, type: ColorType.HSL }, + { reg: /#(?:[0-9a-fA-F]{3}){1,2}/g, type: ColorType.HEX } +].map(v => { v.reg = replaceRegexp(v.reg.source); return v; }); diff --git a/src/equicordplugins/messageColors/index.tsx b/src/equicordplugins/messageColors/index.tsx new file mode 100644 index 00000000..41abc6a3 --- /dev/null +++ b/src/equicordplugins/messageColors/index.tsx @@ -0,0 +1,203 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./styles.css"; + +import { EquicordDevs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { React } from "@webpack/common"; + +import { + ColorType, + regex, + RenderType, + replaceRegexp, + settings, +} from "./constants"; + +const source = regex.map(r => r.reg.source).join("|"); +const matchAllRegExp = new RegExp(`^(${source})`, "i"); + +interface ParsedColorInfo { + type: "color"; + color: string; + colorType: ColorType; + text: string; +} + +const requiredFirstCharacters = ["r", "h", "#"].flatMap(v => [ + v, + v.toUpperCase(), +]); + +export default definePlugin({ + authors: [EquicordDevs.hen], + name: "MessageColors", + description: "Displays color codes like #FF0042 inside of messages", + settings, + patches: [ + // Create a new markdown rule, so it parses just like any other features + // Like bolding, spoilers, mentions, etc + { + find: "roleMention:{order:", + group: true, + replacement: { + match: /roleMention:\{order:(\i\.\i\.order)/, + replace: "color:$self.getColor($1),$&", + }, + }, + // Changes text md rule regex, so it stops right before hsl( | rgb( + // Without it discord will try to pass a string without those to color rule + { + find: ".defaultRules.text,match:", + group: true, + replacement: { + // $)/) + match: /\$\)\/\)}/, + // hsl(|rgb(|$& + replace: requiredFirstCharacters.join("|") + "|$&", + }, + }, + // Discord just requires it to be here + // Or it explodes (bad) + { + find: "Unknown markdown rule:", + group: true, + replacement: { + match: /roleMention:{type:/, + replace: 'color:{type:"inlineObject"},$&', + }, + }, + ], + getColor(order: number) { + return { + order, + // Don't even try to match if the message chunk doesn't start with... + requiredFirstCharacters, + // Match -> Parse -> React + // Result of previous action is dropped as a first argument of the next one + match(content: string) { + return matchAllRegExp.exec(content); + }, + parse( + matchedContent: RegExpExecArray, + _, + parseProps: Record, + ): ParsedColorInfo | { type: "text"; content: string; } { + // This check makes sure that it doesn't try to parse color + // When typing/editing message + // + // Discord doesn't know how to deal with color and crashes + if (!parseProps.messageId) + return { + type: "text", + content: matchedContent[0], + }; + + const content = matchedContent[0]; + try { + const type = getColorType(content); + + return { + type: "color", + colorType: type, + color: parseColor(content, type), + text: content, + }; + } catch (e) { + console.error(e); + return { + type: "text", + content: matchedContent[0], + }; + } + }, + // react(args: ReturnType) + react({ text, colorType, color }: ParsedColorInfo) { + if (settings.store.renderType === RenderType.FOREGROUND) { + return {text}; + } + const styles = { + "--color": color, + } as React.CSSProperties; + + if (settings.store.renderType === RenderType.BACKGROUND) { + const isDark = isColorDark(color, colorType); + const className = `vc-color-bg ${!isDark ? "vc-color-bg-invert" : ""}`; + return ( + + {text} + + ); + } + + return ( + <> + {text} + + + ); + }, + }; + }, +}); + +// https://en.wikipedia.org/wiki/Relative_luminance +const calcRGBLightness = (r: number, g: number, b: number) => { + return 0.2126 * r + 0.7152 * g + 0.0722 * b; +}; +const isColorDark = (color: string, type: ColorType): boolean => { + switch (type) { + case ColorType.RGBA: + case ColorType.RGB: { + const match = color.match(/\d+/g)!; + const lightness = calcRGBLightness(+match[0], +match[1], +match[2]); + return lightness < 140; + } + case ColorType.HEX: { + var rgb = parseInt(color.substring(1), 16); + const r = (rgb >> 16) & 0xff; + const g = (rgb >> 8) & 0xff; + const b = (rgb >> 0) & 0xff; + const lightness = calcRGBLightness(r, g, b); + return lightness < 140; + } + case ColorType.HSL: { + const match = color.match(/\d+/g)!; + const lightness = +match[2]; + return lightness < 50; + } + } +}; + +const getColorType = (color: string): ColorType => { + color = color.toLowerCase().trim(); + if (color.startsWith("#")) return ColorType.HEX; + if (color.startsWith("hsl")) return ColorType.HSL; + if (color.startsWith("rgba")) return ColorType.RGBA; + if (color.startsWith("rgb")) return ColorType.RGB; + + throw new Error(`Can't resolve color type of ${color}`); +}; + +function parseColor(str: string, type: ColorType): string { + str = str + .toLowerCase() + .trim() + .replaceAll(/(\s|,)+/g, " "); + switch (type) { + case ColorType.RGB: + return str; + case ColorType.RGBA: + if (!str.includes("/")) + return str.replaceAll(replaceRegexp(/\f(?=\s*?\))/.source), "/$&"); + return str; + case ColorType.HEX: + return str[0] === "#" ? str : `#${str}`; + case ColorType.HSL: + return str.replace("°", ""); + } +} diff --git a/src/equicordplugins/messageColors/styles.css b/src/equicordplugins/messageColors/styles.css new file mode 100644 index 00000000..87d34e84 --- /dev/null +++ b/src/equicordplugins/messageColors/styles.css @@ -0,0 +1,20 @@ +.vc-color-block { + aspect-ratio: 1/1; + background: var(--color); + height: 1rem; + vertical-align: middle; + border-radius: 4px; + display: inline-block; + user-select: none; + margin-left: 2px; +} + +.vc-color-bg { + background: var(--color); +} + +/* Light color in dark theme */ +.theme-dark .vc-color-bg.vc-color-bg-invert, +.theme-light .vc-color-bg:not(.vc-color-bg-invert) { + color: var(--background-tertiary); +} diff --git a/src/equicordplugins/noMirroredCamera/index.tsx b/src/equicordplugins/noMirroredCamera/index.tsx index 3d635de2..821c3efe 100644 --- a/src/equicordplugins/noMirroredCamera/index.tsx +++ b/src/equicordplugins/noMirroredCamera/index.tsx @@ -4,13 +4,13 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { EquicordDevs } from "@utils/constants"; +import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; export default definePlugin({ name: "NoMirroredCamera", description: "Prevents the camera from being mirrored on your screen", - authors: [EquicordDevs.nyx], + authors: [Devs.nyx], patches: [ // When focused on voice channel or group chat voice call diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 09f47580..145ad648 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -747,6 +747,10 @@ export const EquicordDevs = Object.freeze({ name: "x3rt", id: 131602100332396544n }, + hen: { + id: 279266228151779329n, + name: "Hen", + }, } satisfies Record); // iife so #__PURE__ works correctly