Compare commits

...

6 commits

Author SHA1 Message Date
Cassie
07aecc3ae4
Merge eaf50679f9 into 8afd79dd50 2024-09-18 01:38:35 +02:00
Vendicated
8afd79dd50
add Icons to webpack commons
Some checks are pending
Sync to Codeberg / codeberg (push) Waiting to run
test / test (push) Waiting to run
2024-09-18 01:36:52 +02:00
Vendicated
65c5897dc3
remove need to depend on CommandsAPI 2024-09-18 01:26:25 +02:00
F53
eaf50679f9 switch FixHardCodedColors to new StyleListener api 2024-07-31 21:55:00 -06:00
F53
6ae30fa2fa add plugin FixHardcodedColors 2024-07-31 21:38:52 -06:00
F53
67dfaa95ba Introduce StyleListener API 2024-07-31 21:00:47 -06:00
19 changed files with 255 additions and 48 deletions

View file

@ -160,3 +160,6 @@ export const classNameFactory = (prefix: string = "") => (...args: ClassNameFact
}
return Array.from(classNames, name => prefix + name).join(" ");
};
// items are run every time a new style is loaded by webpack, with `styles` being the content of the new file
export const styleListeners = new Set<(styles: string, initial: boolean) => void>();

View file

@ -292,10 +292,10 @@ export default function PluginSettings() {
if (!pluginFilter(p)) continue;
const isRequired = p.required || depMap[p.name]?.some(d => settings.plugins[d].enabled);
const isRequired = p.required || p.isDependency || depMap[p.name]?.some(d => settings.plugins[d].enabled);
if (isRequired) {
const tooltipText = p.required
const tooltipText = p.required || !depMap[p.name]
? "This plugin is required for Vencord to function."
: makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled));

View file

@ -0,0 +1,59 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Styles } from "@api/index";
import { Devs } from "@utils/constants";
import definePlugin, { StartAt } from "@utils/types";
import { beforeInitListeners } from "@webpack";
import { WebpackInstance } from "discord-types/other";
export default definePlugin({
name: "StyleListenerAPI",
description: "API to listen into the contents of css added by webpack",
authors: [Devs.F53, Devs.Nuckyz],
startAt: StartAt.Init,
start: async () => {
window.requestAnimationFrame(async () => {
const initialStyleLink: HTMLLinkElement | null = document.head.querySelector("link[rel=stylesheet]");
if (!initialStyleLink) return console.error("StyleListenerAPI failed to get initial stylesheet");
const styles = await fetch(initialStyleLink.href).then(r => r.text());
for (const listener of Styles.styleListeners)
listener(styles, true);
});
const wreq = await new Promise<WebpackInstance>(r => beforeInitListeners.add(r));
const chunksLoading = new Set<string>();
const handleChunkCss = wreq.f.css;
wreq.f.css = function (this: unknown) {
const result = Reflect.apply(handleChunkCss, this, arguments);
if (chunksLoading.has(arguments[0]))
return result;
chunksLoading.add(arguments[0]);
if (!(Array.isArray(arguments[1]) && arguments[1].length > 0))
return result;
Promise.all(arguments[1]).then(async () => {
await Promise.all(arguments[1]);
chunksLoading.delete(arguments[0]);
const cssFilepath = wreq.p + wreq.k(arguments[0]);
const styles = await fetch(cssFilepath)
.then(r => r.text()).catch(() => { });
if (!styles) return;
for (const listener of Styles.styleListeners)
listener(styles, false);
});
return result;
};
},
});

View file

@ -142,7 +142,7 @@ export default definePlugin({
required: true,
description: "Helps us provide support to you",
authors: [Devs.Ven],
dependencies: ["CommandsAPI", "UserSettingsAPI", "MessageAccessoriesAPI"],
dependencies: ["UserSettingsAPI", "MessageAccessoriesAPI"],
settings,

View file

@ -6,6 +6,7 @@
import "./clientTheme.css";
import { Styles } from "@api/index";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins";
@ -113,19 +114,24 @@ export default definePlugin({
authors: [Devs.F53, Devs.Nuckyz],
description: "Recreation of the old client theme experiment. Add a color to your Discord client theme",
settings,
dependencies: ["StyleListenerAPI"],
startAt: StartAt.DOMContentLoaded,
startAt: StartAt.Init,
async start() {
updateColorVars(settings.store.color);
const styles = await getStyles();
generateColorOffsets(styles);
generateLightModeFixes(styles);
const lightFixes = createStyle("clientThemeLightModeFixes");
const offsets = createStyle("clientThemeOffsets");
Styles.styleListeners.add((styles, initial) => {
if (initial) offsets.textContent = generateColorOffsets(styles);
lightFixes.textContent += generateLightModeFixes(styles);
});
},
stop() {
document.getElementById("clientThemeVars")?.remove();
document.getElementById("clientThemeOffsets")?.remove();
document.getElementById("clientThemeLightModeFixes")?.remove();
}
});
@ -158,13 +164,15 @@ function generateColorOffsets(styles) {
variableMatch = variableRegex.exec(styles);
}
createStyleSheet("clientThemeOffsets", [
return [
`.theme-light {\n ${genThemeSpecificOffsets(variableLightness, lightVariableRegex, "--primary-345-hsl")} \n}`,
`.theme-dark {\n ${genThemeSpecificOffsets(variableLightness, darkVariableRegex, "--primary-600-hsl")} \n}`,
].join("\n\n"));
].join("\n\n");
}
function generateLightModeFixes(styles) {
const out: string[] = [];
const groupLightUsesW500Regex = /\.theme-light[^{]*\{[^}]*var\(--white-500\)[^}]*}/gm;
// get light capturing groups that mention --white-500
const relevantStyles = [...styles.matchAll(groupLightUsesW500Regex)].flat();
@ -175,8 +183,10 @@ function generateLightModeFixes(styles) {
const backgroundGroups = mapReject(relevantStyles, entry => captureOne(entry, groupBackgroundRegex)).join(",\n");
const backgroundColorGroups = mapReject(relevantStyles, entry => captureOne(entry, groupBackgroundColorRegex)).join(",\n");
// create css to reassign them to --primary-100
const reassignBackgrounds = `${backgroundGroups} {\n background: var(--primary-100) \n}`;
const reassignBackgroundColors = `${backgroundColorGroups} {\n background-color: var(--primary-100) \n}`;
if (backgroundGroups.length > 0)
out.push(`${backgroundGroups} {\n background: var(--primary-100) \n}`);
if (backgroundColorGroups.length > 0)
out.push(`${backgroundColorGroups} {\n background-color: var(--primary-100) \n}`);
const groupBgVarRegex = /\.theme-light\{([^}]*--[^:}]*(?:background|bg)[^:}]*:var\(--white-500\)[^}]*)\}/m;
const bgVarRegex = /^(--[^:]*(?:background|bg)[^:]*):var\(--white-500\)/m;
@ -184,14 +194,11 @@ function generateLightModeFixes(styles) {
const lightVars = mapReject(relevantStyles, style => captureOne(style, groupBgVarRegex)) // get the insides of capture groups that have at least one background var with w500
.map(str => str.split(";")).flat(); // captureGroupInsides[] -> cssRule[]
const lightBgVars = mapReject(lightVars, variable => captureOne(variable, bgVarRegex)); // remove vars that aren't for backgrounds or w500
// create css to reassign every var
const reassignVariables = `.theme-light {\n ${lightBgVars.map(variable => `${variable}: var(--primary-100);`).join("\n")} \n}`;
// create css to reassign every usage of w500 to p100
if (lightBgVars.length > 0)
out.push(`.theme-light{\n${lightBgVars.map(variable => `${variable}: var(--primary-100);`).join("\n")}\n}`);
createStyleSheet("clientThemeLightModeFixes", [
reassignBackgrounds,
reassignBackgroundColors,
reassignVariables,
].join("\n\n"));
return out.join("\n\n");
}
function captureOne(str, regex) {
@ -207,8 +214,7 @@ function updateColorVars(color: string) {
const { hue, saturation, lightness } = hexToHSL(color);
let style = document.getElementById("clientThemeVars");
if (!style)
style = createStyleSheet("clientThemeVars");
if (!style) style = createStyle("clientThemeVars");
style.textContent = `:root {
--theme-h: ${hue};
@ -217,28 +223,14 @@ function updateColorVars(color: string) {
}`;
}
function createStyleSheet(id, content = "") {
export function createStyle(id: string) {
const style = document.createElement("style");
style.setAttribute("id", id);
style.textContent = content.split("\n").map(line => line.trim()).join("\n");
document.body.appendChild(style);
style.id = id;
if (document.documentElement) document.documentElement.append(style);
else window.requestAnimationFrame(() => document.documentElement.append(style));
return style;
}
// returns all of discord's native styles in a single string
async function getStyles(): Promise<string> {
let out = "";
const styleLinkNodes = document.querySelectorAll('link[rel="stylesheet"]');
for (const styleLinkNode of styleLinkNodes) {
const cssLink = styleLinkNode.getAttribute("href");
if (!cssLink) continue;
const res = await fetch(cssLink);
out += await res.text();
}
return out;
}
// https://css-tricks.com/converting-color-spaces-in-javascript/
function hexToHSL(hexCode: string) {
// Hex => RGB normalized to 0-1

View file

@ -0,0 +1,22 @@
Discord often hardcodes colors despite having css variables for all it's colors.
For example, `--primary-160` is `#ebedef`.
But in the code, they have hardcoded the color hex instead of using the variable
```css
.defaultLightModeCustomGradient_e77fa3 {
background: linear-gradient(rgba(0,0,0,0) 20%, #ebedef 100%);
}
```
This causes issues for theme devs who want to make stuff by directly modifying color variables as they need to manually fix all these problems.
This is very prevalent when using ClientTheme and looking at "channels and roles"
![Discord_tn6oWjipFv](https://github.com/Vendicated/Vencord/assets/37855219/e74e41af-b277-4b28-83be-f87807bad16d)
This plugin addresses this issue by generating css to make the problematic code use color variables instead, for example:
```css
.defaultLightModeCustomGradient_e77fa3 {
background: linear-gradient(rgba(0,0,0,0) 20%, var(--primary-160) 100%);
}
```

View file

@ -0,0 +1,106 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Styles } from "@api/index";
import { Devs } from "@utils/constants";
import definePlugin, { StartAt } from "@utils/types";
import { createStyle } from "plugins/clientTheme";
export default definePlugin({
name: "Fix hardcoded colors",
description: "replace hardcoded colors with color variables",
authors: [Devs.F53],
startAt: StartAt.Init,
dependencies: ["StyleListenerAPI"],
async start() {
let colorVariables: ColorVariable[];
const stylesToParse: string[] = [];
const fixes = createStyle("hardcodedColorFixes");
Styles.styleListeners.add((styles, initial) => {
// we can't generate fixes before getting colorVariables from the initial stylesheet
if (colorVariables) // gen fixes if we already have colorVariables
return fixes.innerText += generateFixes(colorVariables, styles);
else // queue fix generation if we don't have colorVariables
stylesToParse.push(styles);
if (!initial) return;
colorVariables = getColorVariables(styles);
while (stylesToParse.length > 0) // gen fixes for queue now that we have them (and remove them from memory)
fixes.innerText += generateFixes(colorVariables, stylesToParse.shift()!);
});
},
stop() {
document.getElementById("hardcodedColorFixes")?.remove();
}
});
function generateFixes(colorVariables: ColorVariable[], styles: string) {
const stylesToFix = getStylesWithColors(styles);
let out = "";
for (const style of stylesToFix)
out += generateFix(colorVariables, style);
return out;
}
function generateFix(colorVariables: ColorVariable[], problematicStyle: string) {
const selector = /^[^{]*/.exec(problematicStyle)?.[0];
const rules = Array.from(problematicStyle.matchAll(/(?:{|;)([a-z-]+):([^;}]+)/g), match => ({ property: match[1], value: match[2] }));
if (!selector) return "";
const fixes: string[] = [];
for (const rule of rules) {
let fixedValue = rule.value;
for (const match of Array.from(rule.value.matchAll(/#[a-f\d]{6}|rgb\(\d+,\d+\d+\)/g))) {
const rgb = toRGB(match[0]);
for (const color of colorVariables) {
const distance = rgb.reduce((totalDistance, b, i) => totalDistance + Math.abs(b - color.rgb[i]), 0);
if (distance > 5) continue;
fixedValue = fixedValue.replaceAll(match[0], ` var(${color.variable}) `);
break; // already found variable to replace it, don't keep looking
}
}
fixedValue.replaceAll(" ", "");
if (fixedValue !== rule.value)
fixes.push(`${rule.property}:${fixedValue}`);
}
if (fixes.length === 0) return "";
return `${selector}{${fixes.join(";")}}`;
}
const cssWithColorRegex = /(?:^|})[^{}]+?{[^}]*?(?:#[a-f\d]{6}|rgb\(\d+,\d+,\d+\))[^}]*?}/g;
// gets array of styles that hardcode color
function getStylesWithColors(styles: string) {
return Array.from(styles.matchAll(cssWithColorRegex), match => {
if (match[0][0] === "}") return match[0].slice(1);
return match[0];
});
}
const variableRegex = /(--[a-z-\d]*?)-hsl:(\d+).*?(\d+\.?\d*)%.*?(\d+\.?\d*)%/g;
interface ColorVariable { variable: string, rgb: [number, number, number]; }
function getColorVariables(styles: string): ColorVariable[] {
return Array.from(styles.matchAll(variableRegex), match => {
const variable = match[1];
const [h, s, l] = match.slice(2);
return { variable, rgb: toRGB(`hsl(${h},${s}%,${l}%)`) };
}).filter(color => // ignore solid white/black colors because they are weird
color.rgb.some(b => b !== 255) && color.rgb.some(b => b !== 0)
).toSorted(a => // prefer --primary colors over anything else
a.variable.startsWith("--primary") ? -1 : 0
);
}
function toRGB(color: string) {
// https://stackoverflow.com/a/74662179/8133370
const { style } = new Option();
style.color = color; // for some reason this is immediately translated into "rgb(x, x, x)", no matter the input
// turn into array [r: number, g: number, b: number]
return Array.from(style.color.matchAll(/\b\d+\b/g)).flatMap(Number) as [number, number, number];
}

View file

@ -27,7 +27,6 @@ export default definePlugin({
name: "FriendInvites",
description: "Create and manage friend invite links via slash commands (/create friend invite, /view friend invites, /revoke friend invites).",
authors: [Devs.afn, Devs.Dziurwa],
dependencies: ["CommandsAPI"],
commands: [
{
name: "create friend invite",

View file

@ -105,6 +105,11 @@ for (const p of pluginsValues) if (isPluginEnabled(p.name)) {
settings[d].enabled = true;
dep.isDependency = true;
});
if (p.commands?.length) {
Plugins.CommandsAPI.isDependency = true;
settings.CommandsAPI.enabled = true;
}
}
for (const p of pluginsValues) {

View file

@ -82,7 +82,6 @@ export default definePlugin({
default: true
}
},
dependencies: ["CommandsAPI"],
async start() {
for (const tag of await getTags()) createTagCommand(tag);

View file

@ -33,7 +33,6 @@ export default definePlugin({
name: "MoreCommands",
description: "echo, lenny, mock",
authors: [Devs.Arjix, Devs.echo, Devs.Samu],
dependencies: ["CommandsAPI"],
commands: [
{
name: "echo",

View file

@ -24,7 +24,6 @@ export default definePlugin({
name: "MoreKaomoji",
description: "Adds more Kaomoji to discord. ヽ(´▽`)/",
authors: [Devs.JacobTm],
dependencies: ["CommandsAPI"],
commands: [
{ name: "dissatisfaction", description: " " },
{ name: "smug", description: "ಠ_ಠ" },

View file

@ -88,7 +88,6 @@ export default definePlugin({
name: "petpet",
description: "Adds a /petpet slash command to create headpet gifs from any image",
authors: [Devs.Ven],
dependencies: ["CommandsAPI"],
commands: [
{
inputType: ApplicationCommandInputType.BUILT_IN,

View file

@ -88,7 +88,7 @@ export default definePlugin({
name: "SilentTyping",
authors: [Devs.Ven, Devs.Rini, Devs.ImBanana],
description: "Hide that you are typing",
dependencies: ["CommandsAPI", "ChatInputButtonAPI"],
dependencies: ["ChatInputButtonAPI"],
settings,
contextMenus: {
"textarea-context": ChatBarContextCheckbox

View file

@ -76,7 +76,6 @@ export default definePlugin({
name: "SpotifyShareCommands",
description: "Share your current Spotify track, album or artist via slash command (/track, /album, /artist)",
authors: [Devs.katlyn],
dependencies: ["CommandsAPI"],
commands: [
{
name: "track",

View file

@ -72,13 +72,13 @@ export interface PluginDef {
stop?(): void;
patches?: Omit<Patch, "plugin">[];
/**
* List of commands. If you specify these, you must add CommandsAPI to dependencies
* List of commands that your plugin wants to register
*/
commands?: Command[];
/**
* A list of other plugins that your plugin depends on.
* These will automatically be enabled and loaded before your plugin
* Common examples are CommandsAPI, MessageEventsAPI...
* Generally these will be API plugins
*/
dependencies?: string[],
/**

View file

@ -28,6 +28,8 @@ export let Forms = {} as {
FormText: t.FormText,
};
export let Icons = {} as t.Icons;
export let Card: t.Card;
export let Button: t.Button;
export let Switch: t.Switch;
@ -85,4 +87,5 @@ waitFor(["FormItem", "Button"], m => {
Heading
} = m);
Forms = m;
Icons = m;
});

View file

@ -18,6 +18,8 @@
import type { ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, KeyboardEvent, MouseEvent, PropsWithChildren, PropsWithRef, ReactNode, Ref } from "react";
import { IconNames } from "./iconNames";
export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code";
export type FormTextTypes = Record<"DEFAULT" | "INPUT_PLACEHOLDER" | "DESCRIPTION" | "LABEL_BOLD" | "LABEL_SELECTED" | "LABEL_DESCRIPTOR" | "ERROR" | "SUCCESS", string>;
export type HeadingTag = `h${1 | 2 | 3 | 4 | 5 | 6}`;
@ -69,7 +71,7 @@ export type FormText = ComponentType<PropsWithChildren<{
}> & TextProps> & { Types: FormTextTypes; };
export type Tooltip = ComponentType<{
text: ReactNode;
text: ReactNode | ComponentType;
children: FunctionComponent<{
onClick(): void;
onMouseEnter(): void;
@ -502,3 +504,10 @@ export type Avatar = ComponentType<PropsWithChildren<{
type FocusLock = ComponentType<PropsWithChildren<{
containerRef: RefObject<HTMLElement>;
}>>;
export type Icon = ComponentType<JSX.IntrinsicElements["svg"] & {
size?: string;
colorClass?: string;
} & Record<string, any>>;
export type Icons = Record<IconNames, Icon>;

14
src/webpack/common/types/iconNames.d.ts vendored Normal file

File diff suppressed because one or more lines are too long