Compare commits

...

4 commits

Author SHA1 Message Date
Cassie
c0e7924058
Merge eaf50679f9 into f12335a371 2024-09-16 14:52:24 -04: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
5 changed files with 217 additions and 35 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

@ -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

@ -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];
}