mirror of
https://github.com/Vendicated/Vencord.git
synced 2024-09-19 22:20:34 +00:00
Compare commits
8 commits
c0e7924058
...
0715c0564c
Author | SHA1 | Date | |
---|---|---|---|
|
0715c0564c | ||
|
6cce8a8bc4 | ||
|
1848b16536 | ||
|
c572116b97 | ||
|
e26986f66a | ||
eaf50679f9 | |||
6ae30fa2fa | |||
67dfaa95ba |
12 changed files with 336 additions and 60 deletions
|
@ -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>();
|
||||
|
|
59
src/plugins/_api/styleListener.ts
Normal file
59
src/plugins/_api/styleListener.ts
Normal 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;
|
||||
};
|
||||
},
|
||||
});
|
|
@ -24,7 +24,7 @@ interface ActivityButton {
|
|||
}
|
||||
|
||||
interface Activity {
|
||||
state: string;
|
||||
state?: string;
|
||||
details?: string;
|
||||
timestamps?: {
|
||||
start?: number;
|
||||
|
@ -52,8 +52,8 @@ const enum ActivityFlag {
|
|||
|
||||
export interface TrackData {
|
||||
name: string;
|
||||
album: string;
|
||||
artist: string;
|
||||
album?: string;
|
||||
artist?: string;
|
||||
|
||||
appleMusicLink?: string;
|
||||
songLink?: string;
|
||||
|
@ -61,8 +61,8 @@ export interface TrackData {
|
|||
albumArtwork?: string;
|
||||
artistArtwork?: string;
|
||||
|
||||
playerPosition: number;
|
||||
duration: number;
|
||||
playerPosition?: number;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
const enum AssetImageType {
|
||||
|
@ -155,8 +155,8 @@ const settings = definePluginSettings({
|
|||
function customFormat(formatStr: string, data: TrackData) {
|
||||
return formatStr
|
||||
.replaceAll("{name}", data.name)
|
||||
.replaceAll("{album}", data.album)
|
||||
.replaceAll("{artist}", data.artist);
|
||||
.replaceAll("{album}", data.album ?? "")
|
||||
.replaceAll("{artist}", data.artist ?? "");
|
||||
}
|
||||
|
||||
function getImageAsset(type: AssetImageType, data: TrackData) {
|
||||
|
@ -212,14 +212,16 @@ export default definePlugin({
|
|||
|
||||
const assets: ActivityAssets = {};
|
||||
|
||||
const isRadio = Number.isNaN(trackData.duration) && (trackData.playerPosition === 0);
|
||||
|
||||
if (settings.store.largeImageType !== AssetImageType.Disabled) {
|
||||
assets.large_image = largeImageAsset;
|
||||
assets.large_text = customFormat(settings.store.largeTextString, trackData);
|
||||
if (!isRadio) assets.large_text = customFormat(settings.store.largeTextString, trackData);
|
||||
}
|
||||
|
||||
if (settings.store.smallImageType !== AssetImageType.Disabled) {
|
||||
assets.small_image = smallImageAsset;
|
||||
assets.small_text = customFormat(settings.store.smallTextString, trackData);
|
||||
if (!isRadio) assets.small_text = customFormat(settings.store.smallTextString, trackData);
|
||||
}
|
||||
|
||||
const buttons: ActivityButton[] = [];
|
||||
|
@ -243,17 +245,17 @@ export default definePlugin({
|
|||
|
||||
name: customFormat(settings.store.nameString, trackData),
|
||||
details: customFormat(settings.store.detailsString, trackData),
|
||||
state: customFormat(settings.store.stateString, trackData),
|
||||
state: isRadio ? undefined : customFormat(settings.store.stateString, trackData),
|
||||
|
||||
timestamps: (settings.store.enableTimestamps ? {
|
||||
timestamps: (trackData.playerPosition && trackData.duration && settings.store.enableTimestamps) ? {
|
||||
start: Date.now() - (trackData.playerPosition * 1000),
|
||||
end: Date.now() - (trackData.playerPosition * 1000) + (trackData.duration * 1000),
|
||||
} : undefined),
|
||||
} : undefined,
|
||||
|
||||
assets,
|
||||
|
||||
buttons: buttons.length ? buttons.map(v => v.label) : undefined,
|
||||
metadata: { button_urls: buttons.map(v => v.url) || undefined, },
|
||||
buttons: !isRadio && buttons.length ? buttons.map(v => v.label) : undefined,
|
||||
metadata: !isRadio && buttons.length ? { button_urls: buttons.map(v => v.url) } : undefined,
|
||||
|
||||
type: settings.store.activityType,
|
||||
flags: ActivityFlag.INSTANCE,
|
||||
|
|
68
src/plugins/betterSettings/PluginsSubmenu.tsx
Normal file
68
src/plugins/betterSettings/PluginsSubmenu.tsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { openPluginModal } from "@components/PluginSettings/PluginModal";
|
||||
import { isObjectEmpty } from "@utils/misc";
|
||||
import { Alerts, i18n, Menu, useMemo, useState } from "@webpack/common";
|
||||
|
||||
import Plugins from "~plugins";
|
||||
|
||||
function onRestartNeeded() {
|
||||
Alerts.show({
|
||||
title: "Restart required",
|
||||
body: <p>You have changed settings that require a restart.</p>,
|
||||
confirmText: "Restart now",
|
||||
cancelText: "Later!",
|
||||
onConfirm: () => location.reload()
|
||||
});
|
||||
}
|
||||
|
||||
export default function PluginsSubmenu() {
|
||||
const sortedPlugins = useMemo(() => Object.values(Plugins)
|
||||
.sort((a, b) => a.name.localeCompare(b.name)), []);
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const search = query.toLowerCase();
|
||||
const include = (p: typeof Plugins[keyof typeof Plugins]) => (
|
||||
Vencord.Plugins.isPluginEnabled(p.name)
|
||||
&& p.options && !isObjectEmpty(p.options)
|
||||
&& (
|
||||
p.name.toLowerCase().includes(search)
|
||||
|| p.description.toLowerCase().includes(search)
|
||||
|| p.tags?.some(t => t.toLowerCase().includes(search))
|
||||
)
|
||||
);
|
||||
|
||||
const plugins = sortedPlugins.filter(include);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.MenuControlItem
|
||||
id="vc-plugins-search"
|
||||
control={(props, ref) => (
|
||||
<Menu.MenuSearchControl
|
||||
{...props}
|
||||
query={query}
|
||||
onChange={setQuery}
|
||||
ref={ref}
|
||||
placeholder={i18n.Messages.SEARCH}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!!plugins.length && <Menu.MenuSeparator />}
|
||||
|
||||
{plugins.map(p => (
|
||||
<Menu.MenuItem
|
||||
key={p.name}
|
||||
id={p.name}
|
||||
label={p.name}
|
||||
action={() => openPluginModal(p, onRestartNeeded)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -13,6 +13,8 @@ import { waitFor } from "@webpack";
|
|||
import { ComponentDispatch, FocusLock, i18n, Menu, useEffect, useRef } from "@webpack/common";
|
||||
import type { HTMLAttributes, ReactElement } from "react";
|
||||
|
||||
import PluginsSubmenu from "./PluginsSubmenu";
|
||||
|
||||
type SettingsEntry = { section: string, label: string; };
|
||||
|
||||
const cl = classNameFactory("");
|
||||
|
@ -118,13 +120,21 @@ export default definePlugin({
|
|||
},
|
||||
{ // Settings cog context menu
|
||||
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
||||
replacement: {
|
||||
match: /(EXPERIMENTS:.+?)(\(0,\i.\i\)\(\))(?=\.filter\(\i=>\{let\{section:\i\}=)/,
|
||||
replace: "$1$self.wrapMenu($2)"
|
||||
}
|
||||
}
|
||||
replacement: [
|
||||
{
|
||||
match: /(EXPERIMENTS:.+?)(\(0,\i.\i\)\(\))(?=\.filter\(\i=>\{let\{section:\i\}=)/,
|
||||
replace: "$1$self.wrapMenu($2)"
|
||||
},
|
||||
{
|
||||
match: /case \i\.\i\.DEVELOPER_OPTIONS:return \i;/,
|
||||
replace: "$&case 'VencordPlugins':return $self.PluginsSubmenu();"
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
|
||||
PluginsSubmenu,
|
||||
|
||||
// This is the very outer layer of the entire ui, so we can't wrap this in an ErrorBoundary
|
||||
// without possibly also catching unrelated errors of children.
|
||||
//
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -126,7 +126,7 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
{
|
||||
find: '"Handling ping: "',
|
||||
find: '"_handleLocalVideoDisabled: ',
|
||||
predicate: () => settings.store.disableNoisyLoggers,
|
||||
replacement: {
|
||||
match: /new \i\.\i\("RTCConnection\("\.concat.+?\)\)(?=,)/,
|
||||
|
|
|
@ -23,12 +23,13 @@ import { ErrorCard } from "@components/ErrorCard";
|
|||
import { Devs } from "@utils/constants";
|
||||
import { Margins } from "@utils/margins";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { findByPropsLazy, findLazy } from "@webpack";
|
||||
import { Forms, React } from "@webpack/common";
|
||||
|
||||
import hideBugReport from "./hideBugReport.css?managed";
|
||||
|
||||
const KbdStyles = findByPropsLazy("key", "combo");
|
||||
const BugReporterExperiment = findLazy(m => m?.definition?.id === "2024-09_bug_reporter");
|
||||
|
||||
const settings = definePluginSettings({
|
||||
toolbarDevMenu: {
|
||||
|
@ -78,8 +79,8 @@ export default definePlugin({
|
|||
{
|
||||
find: "toolbar:function",
|
||||
replacement: {
|
||||
match: /\i\.isStaff\(\)/,
|
||||
replace: "true"
|
||||
match: /hasBugReporterAccess:(\i)/,
|
||||
replace: "_hasBugReporterAccess:$1=true"
|
||||
},
|
||||
predicate: () => settings.store.toolbarDevMenu
|
||||
},
|
||||
|
@ -91,10 +92,18 @@ export default definePlugin({
|
|||
match: /\i\.isDM\(\)\|\|\i\.isThread\(\)/,
|
||||
replace: "false",
|
||||
}
|
||||
},
|
||||
// enable option to always record clips even if you are not streaming
|
||||
{
|
||||
find: "isDecoupledGameClippingEnabled(){",
|
||||
replacement: {
|
||||
match: /\i\.isStaff\(\)/,
|
||||
replace: "true"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
start: () => enableStyle(hideBugReport),
|
||||
start: () => !BugReporterExperiment.getCurrentConfig().hasBugReporterAccess && enableStyle(hideBugReport),
|
||||
stop: () => disableStyle(hideBugReport),
|
||||
|
||||
settingsAboutComponent: () => {
|
||||
|
|
22
src/plugins/fixHardcodedColors/README.md
Normal file
22
src/plugins/fixHardcodedColors/README.md
Normal 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%);
|
||||
}
|
||||
```
|
106
src/plugins/fixHardcodedColors/index.ts
Normal file
106
src/plugins/fixHardcodedColors/index.ts
Normal 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];
|
||||
}
|
|
@ -91,7 +91,7 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
{
|
||||
find: ".PANEL,isInteractionSource:",
|
||||
find: ".PANEL,interactionType:",
|
||||
replacement: {
|
||||
match: /{profileType:\i\.\i\.PANEL,children:\[/,
|
||||
replace: "$&$self.BiteSizeReviewsButton({user:arguments[0].user}),"
|
||||
|
|
5
src/webpack/common/types/menu.d.ts
vendored
5
src/webpack/common/types/menu.d.ts
vendored
|
@ -72,6 +72,11 @@ export interface Menu {
|
|||
onChange(value: number): void,
|
||||
renderValue?(value: number): string,
|
||||
}>;
|
||||
MenuSearchControl: RC<{
|
||||
query: string
|
||||
onChange(query: string): void;
|
||||
placeholder?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ContextMenuApi {
|
||||
|
|
Loading…
Reference in a new issue