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