Compare commits

...

8 commits

Author SHA1 Message Date
Cassie
0715c0564c
Merge eaf50679f9 into 6cce8a8bc4 2024-09-17 22:49:29 +02:00
Nuckyz
6cce8a8bc4
Experiments: Allow clips to be recorded without streaming
Some checks failed
Sync to Codeberg / codeberg (push) Has been cancelled
test / test (push) Has been cancelled
2024-09-17 14:30:23 -03:00
Nuckyz
1848b16536
ReviewDB: Fix in panel profile 2024-09-17 14:30:23 -03:00
Kyuuhachi
c572116b97
BetterSettings: Add submenu for plugins (#2858)
Co-authored-by: Vendicated <vendicated@riseup.net>
2024-09-17 15:40:11 +00:00
Lumap
e26986f66a
AppleMusicRichPresence: fix formatting when listening to radio (#2869)
Co-authored-by: Ryan Cao <70191398+ryanccn@users.noreply.github.com>
Co-authored-by: v <vendicated@riseup.net>
2024-09-17 17:29:46 +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
12 changed files with 336 additions and 60 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

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

View 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)}
/>
))}
</>
);
}

View file

@ -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: {
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.
//

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

@ -126,7 +126,7 @@ export default definePlugin({
}
},
{
find: '"Handling ping: "',
find: '"_handleLocalVideoDisabled: ',
predicate: () => settings.store.disableNoisyLoggers,
replacement: {
match: /new \i\.\i\("RTCConnection\("\.concat.+?\)\)(?=,)/,

View file

@ -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: () => {

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

@ -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}),"

View file

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