From 6face7f8ef857edcb2ff12cf4b9701323a869db3 Mon Sep 17 00:00:00 2001
From: CodeF53 <37855219+CodeF53@users.noreply.github.com>
Date: Tue, 21 Nov 2023 12:30:29 -0700
Subject: [PATCH] feat(plugin): ClientTheme (#807)
---
src/plugins/clientTheme/clientTheme.css | 24 +++
src/plugins/clientTheme/index.tsx | 228 ++++++++++++++++++++++++
src/utils/constants.ts | 4 +
3 files changed, 256 insertions(+)
create mode 100644 src/plugins/clientTheme/clientTheme.css
create mode 100644 src/plugins/clientTheme/index.tsx
diff --git a/src/plugins/clientTheme/clientTheme.css b/src/plugins/clientTheme/clientTheme.css
new file mode 100644
index 000000000..023f88bd2
--- /dev/null
+++ b/src/plugins/clientTheme/clientTheme.css
@@ -0,0 +1,24 @@
+.client-theme-settings {
+ display: flex;
+ flex-direction: column;
+}
+
+.client-theme-container {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+}
+
+.client-theme-settings-labels {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+}
+
+.client-theme-container > [class^="colorSwatch"] > [class^="swatch"] {
+ border: thin solid var(--background-modifier-accent) !important;
+}
+
+.client-theme-warning {
+ color: var(--text-danger);
+}
diff --git a/src/plugins/clientTheme/index.tsx b/src/plugins/clientTheme/index.tsx
new file mode 100644
index 000000000..3e07b15fd
--- /dev/null
+++ b/src/plugins/clientTheme/index.tsx
@@ -0,0 +1,228 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2023 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import "./clientTheme.css";
+
+import { definePluginSettings } from "@api/Settings";
+import { Devs } from "@utils/constants";
+import { getTheme, Theme } from "@utils/discord";
+import { Margins } from "@utils/margins";
+import { classes } from "@utils/misc";
+import definePlugin, { OptionType } from "@utils/types";
+import { findByCodeLazy } from "@webpack";
+import { Button, Forms } from "@webpack/common";
+
+const ColorPicker = findByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR");
+
+const colorPresets = [
+ "#1E1514", "#172019", "#13171B", "#1C1C28", "#402D2D",
+ "#3A483D", "#344242", "#313D4B", "#2D2F47", "#322B42",
+ "#3C2E42", "#422938"
+];
+
+function onPickColor(color: number) {
+ let hexColor = color.toString(16);
+
+ while (hexColor.length < 6) {
+ hexColor = "0" + hexColor;
+ }
+
+ settings.store.color = hexColor;
+ updateColorVars(hexColor);
+}
+
+function ThemeSettings() {
+ const lightnessWarning = hexToLightness(settings.store.color) > 45;
+ const lightModeWarning = getTheme() === Theme.Light;
+
+ return (
+
+
+
+ Theme Color
+ Add a color to your Discord client theme
+
+
+
+ {lightnessWarning || lightModeWarning
+ ?
+
+ Your theme won't look good:
+ {lightnessWarning && Selected color is very light}
+ {lightModeWarning && Light mode isn't supported}
+
+ : null}
+
+ );
+}
+
+const settings = definePluginSettings({
+ color: {
+ description: "Color your Discord client theme will be based around. Light mode isn't supported",
+ type: OptionType.COMPONENT,
+ default: "313338",
+ component: () =>
+ },
+ resetColor: {
+ description: "Reset Theme Color",
+ type: OptionType.COMPONENT,
+ default: "313338",
+ component: () => (
+
+ )
+ }
+});
+
+export default definePlugin({
+ name: "ClientTheme",
+ authors: [Devs.F53],
+ description: "Recreation of the old client theme experiment. Add a color to your Discord client theme",
+ settings,
+
+ patches: [
+ {
+ find: "Could not find app-mount",
+ replacement: {
+ match: /(?<=Could not find app-mount"\))/,
+ replace: ",$self.addThemeInitializer()"
+ }
+ }
+ ],
+
+ addThemeInitializer() {
+ document.addEventListener("DOMContentLoaded", this.themeInitializer = () => {
+ updateColorVars(settings.store.color);
+ generateColorOffsets();
+ });
+ },
+
+ stop() {
+ document.removeEventListener("DOMContentLoaded", this.themeInitializer);
+ document.getElementById("clientThemeVars")?.remove();
+ document.getElementById("clientThemeOffsets")?.remove();
+ }
+});
+
+async function generateColorOffsets() {
+ const variableRegex = /(--primary-[5-9]\d{2}-hsl):.*?(\S*)%;/g;
+
+ const styleLinkNodes = document.querySelectorAll('link[rel="stylesheet"]');
+ const variableLightness = {} as Record;
+
+ // Search all stylesheets for color variables
+ for (const styleLinkNode of styleLinkNodes) {
+ const cssLink = styleLinkNode.getAttribute("href");
+ if (!cssLink) continue;
+
+ const res = await fetch(cssLink);
+ const cssString = await res.text();
+
+ // Get lightness values of --primary variables >=500
+ let variableMatch = variableRegex.exec(cssString);
+ while (variableMatch !== null) {
+ const [, variable, lightness] = variableMatch;
+ variableLightness[variable] = parseFloat(lightness);
+ variableMatch = variableRegex.exec(cssString);
+ }
+ }
+
+ // Generate offsets
+ const lightnessOffsets = Object.entries(variableLightness)
+ .map(([key, lightness]) => {
+ const lightnessOffset = lightness - variableLightness["--primary-600-hsl"];
+ const plusOrMinus = lightnessOffset >= 0 ? "+" : "-";
+ return `${key}: var(--theme-h) var(--theme-s) calc(var(--theme-l) ${plusOrMinus} ${Math.abs(lightnessOffset).toFixed(2)}%);`;
+ })
+ .join("\n");
+
+ const style = document.createElement("style");
+ style.setAttribute("id", "clientThemeOffsets");
+ style.textContent = `:root:root {
+ ${lightnessOffsets}
+ }`;
+ document.head.appendChild(style);
+}
+
+function updateColorVars(color: string) {
+ const { hue, saturation, lightness } = hexToHSL(color);
+
+ let style = document.getElementById("clientThemeVars");
+ if (!style) {
+ style = document.createElement("style");
+ style.setAttribute("id", "clientThemeVars");
+ document.head.appendChild(style);
+ }
+
+ style.textContent = `:root {
+ --theme-h: ${hue};
+ --theme-s: ${saturation}%;
+ --theme-l: ${lightness}%;
+ }`;
+}
+
+// https://css-tricks.com/converting-color-spaces-in-javascript/
+function hexToHSL(hexCode: string) {
+ // Hex => RGB normalized to 0-1
+ const r = parseInt(hexCode.substring(0, 2), 16) / 255;
+ const g = parseInt(hexCode.substring(2, 4), 16) / 255;
+ const b = parseInt(hexCode.substring(4, 6), 16) / 255;
+
+ // RGB => HSL
+ const cMax = Math.max(r, g, b);
+ const cMin = Math.min(r, g, b);
+ const delta = cMax - cMin;
+
+ let hue: number, saturation: number, lightness: number;
+
+ lightness = (cMax + cMin) / 2;
+
+ if (delta === 0) {
+ // If r=g=b then the only thing that matters is lightness
+ hue = 0;
+ saturation = 0;
+ } else {
+ // Magic
+ saturation = delta / (1 - Math.abs(2 * lightness - 1));
+
+ if (cMax === r)
+ hue = ((g - b) / delta) % 6;
+ else if (cMax === g)
+ hue = (b - r) / delta + 2;
+ else
+ hue = (r - g) / delta + 4;
+ hue *= 60;
+ if (hue < 0)
+ hue += 360;
+ }
+
+ // Move saturation and lightness from 0-1 to 0-100
+ saturation *= 100;
+ lightness *= 100;
+
+ return { hue, saturation, lightness };
+}
+
+// Minimized math just for lightness, lowers lag when changing colors
+function hexToLightness(hexCode) {
+ // Hex => RGB normalized to 0-1
+ const r = parseInt(hexCode.substring(0, 2), 16) / 255;
+ const g = parseInt(hexCode.substring(2, 4), 16) / 255;
+ const b = parseInt(hexCode.substring(4, 6), 16) / 255;
+
+ const cMax = Math.max(r, g, b);
+ const cMin = Math.min(r, g, b);
+
+ const lightness = 100 * ((cMax + cMin) / 2);
+
+ return lightness;
+}
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 3db2e64f8..7f555d322 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -267,6 +267,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Dziurwa",
id: 1001086404203389018n
},
+ F53: {
+ name: "F53",
+ id: 280411966126948353n
+ },
AutumnVN: {
name: "AutumnVN",
id: 393694671383166998n