diff --git a/browser/VencordNativeStub.ts b/browser/VencordNativeStub.ts index 70978f31b..861aa00ca 100644 --- a/browser/VencordNativeStub.ts +++ b/browser/VencordNativeStub.ts @@ -27,7 +27,7 @@ import type { Settings } from "@api/Settings"; import { debounce } from "@shared/debounce"; import { localStorage } from "@utils/localStorage"; import { getStylusWebStoreUrl } from "@utils/web"; -import { EXTENSION_BASE_URL } from "@utils/web-metadata"; +import { EXTENSION_BASE_URL, metaReady, RENDERER_CSS_URL } from "@utils/web-metadata"; // listeners for ipc.on const cssListeners = new Set<(css: string) => void>(); @@ -55,7 +55,18 @@ window.VencordNative = { native: { getVersions: () => ({}), - openExternal: async (url) => void open(url, "_blank") + openExternal: async (url) => void open(url, "_blank"), + getRendererCss: async () => { + if (IS_USERSCRIPT) + // need to wait for next tick for _vcUserScriptRendererCss to be set + return Promise.resolve().then(() => window._vcUserScriptRendererCss); + + await metaReady; + + return fetch(RENDERER_CSS_URL) + .then(res => res.text()); + }, + onRendererCssUpdate: NOOP, }, updater: { @@ -92,18 +103,20 @@ window.VencordNative = { return; } - const { getTheme, Theme } = require("@utils/discord"); - win.baseUrl = EXTENSION_BASE_URL; win.setCss = setCssDebounced; win.getCurrentCss = () => VencordNative.quickCss.get(); - win.getTheme = () => - getTheme() === Theme.Light - ? "vs-light" - : "vs-dark"; + win.getTheme = this.getEditorTheme; win.document.write(monacoHtmlLocal); }, + getEditorTheme: () => { + const { getTheme, Theme } = require("@utils/discord"); + + return getTheme() === Theme.Light + ? "vs-light" + : "vs-dark"; + } }, settings: { diff --git a/browser/background.js b/browser/background.js index 1f2d5ec17..2489f8238 100644 --- a/browser/background.js +++ b/browser/background.js @@ -12,7 +12,7 @@ chrome.webRequest.onHeadersReceived.addListener( ({ responseHeaders, type, url }) => { if (!responseHeaders) return; - if (type === "main_frame") { + if (type === "main_frame" && url.includes("discord.com")) { // In main frame requests, the CSP needs to be removed to enable fetching of custom css // as desired by the user removeFirst(responseHeaders, h => h.name.toLowerCase() === "content-security-policy"); diff --git a/browser/content.js b/browser/content.js index cd1f59793..8329cfa4d 100644 --- a/browser/content.js +++ b/browser/content.js @@ -2,20 +2,15 @@ if (typeof browser === "undefined") { var browser = chrome; } -const style = document.createElement("link"); -style.type = "text/css"; -style.rel = "stylesheet"; -style.href = browser.runtime.getURL("dist/Equicord.css"); - document.addEventListener( "DOMContentLoaded", () => { - document.body.insertAdjacentElement("afterend", style); window.postMessage({ type: "vencord:meta", meta: { EXTENSION_VERSION: browser.runtime.getManifest().version, EXTENSION_BASE_URL: browser.runtime.getURL(""), + RENDERER_CSS_URL: browser.runtime.getURL("dist/Equicord.css"), } }); }, diff --git a/browser/modifyResponseHeaders.json b/browser/modifyResponseHeaders.json index 5b0b3e376..ffe34adda 100644 --- a/browser/modifyResponseHeaders.json +++ b/browser/modifyResponseHeaders.json @@ -15,7 +15,8 @@ ] }, "condition": { - "resourceTypes": ["main_frame", "sub_frame"] + "resourceTypes": ["main_frame", "sub_frame"], + "urlFilter": "||discord.com^" } }, { @@ -32,7 +33,7 @@ }, "condition": { "resourceTypes": ["stylesheet"], - "urlFilter": "https://raw.githubusercontent.com/*" + "urlFilter": "||raw.githubusercontent.com^" } } ] diff --git a/package.json b/package.json index 2babb391f..64f2eb2e2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "equicord", "private": "true", - "version": "1.13.9.1", + "version": "1.13.11", "description": "The other cutest Discord client mod", "homepage": "https://github.com/Equicord/Equicord#readme", "bugs": { diff --git a/packages/discord-types/src/utils.d.ts b/packages/discord-types/src/utils.d.ts index 77ef1212a..fc968160e 100644 --- a/packages/discord-types/src/utils.d.ts +++ b/packages/discord-types/src/utils.d.ts @@ -276,6 +276,7 @@ export interface UsernameUtils { humanizeStatus: any; } +// TODO: fix type export class DisplayProfile { userId: string; banner?: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc9f742fa..24393b75a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2143,7 +2143,6 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. - (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qified@0.5.2: diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs index fc2d6a863..454f0b74b 100644 --- a/scripts/build/buildWeb.mjs +++ b/scripts/build/buildWeb.mjs @@ -185,14 +185,7 @@ async function buildExtension(target, files) { } const appendCssRuntime = readFile("dist/Equicord.user.css", "utf-8").then(content => { - const cssRuntime = ` -;document.addEventListener("DOMContentLoaded", () => document.body.insertAdjacentElement("afterend", - Object.assign(document.createElement("style"), { - textContent: \`${content.replaceAll("`", "\\`")}\`, - id: "vencord-css-core" - }) -), { once: true }); -`; + const cssRuntime = `unsafeWindow._vcUserScriptRendererCss=\`${content.replaceAll("`", "\\`")}\``; return appendFile("dist/Equicord.user.js", cssRuntime); }); diff --git a/scripts/build/common.mjs b/scripts/build/common.mjs index da858e2ec..9df5c521e 100644 --- a/scripts/build/common.mjs +++ b/scripts/build/common.mjs @@ -27,11 +27,11 @@ import { constants as FsConstants, readFileSync } from "fs"; import { access, readdir, readFile } from "fs/promises"; import { minify as minifyHtml } from "html-minifier-terser"; import { dirname, join, relative, resolve } from "path"; +import { optimize as optimizeSvg } from "svgo"; import { fileURLToPath } from "url"; import { promisify } from "util"; import { getPluginTarget } from "../utils.mjs"; -import { builtinModules } from "module"; const PackageJSON = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../../package.json"), "utf-8")); @@ -261,7 +261,7 @@ export const fileUrlPlugin = { build.onLoad({ filter, namespace: "file-uri" }, async ({ pluginData: { path, uri } }) => { const { searchParams } = new URL(uri); const base64 = searchParams.has("base64"); - const minify = IS_STANDALONE === true && searchParams.has("minify"); + const minify = searchParams.has("minify"); const noTrim = searchParams.get("trim") === "false"; const encoding = base64 ? "base64" : "utf-8"; @@ -283,6 +283,12 @@ export const fileUrlPlugin = { removeStyleLinkTypeAttributes: true, useShortDoctype: true }); + } else if (path.endsWith(".svg")) { + content = optimizeSvg(await readFile(path, "utf-8"), { + datauri: base64 ? "base64" : void 0, + multipass: true, + floatPrecision: 2, + }).data; } else if (/[mc]?[jt]sx?$/.test(path)) { const res = await esbuild.build({ entryPoints: [path], @@ -294,7 +300,7 @@ export const fileUrlPlugin = { throw new Error(`Don't know how to minify file type: ${path}`); } - if (base64) + if (base64 && !content.startsWith("data:")) content = Buffer.from(content).toString("base64"); } diff --git a/src/Vencord.ts b/src/Vencord.ts index 8fb08db06..1fc28e225 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -28,7 +28,7 @@ export * as Webpack from "./webpack"; export * as WebpackPatcher from "./webpack/patchWebpack"; export { PlainSettings, Settings }; -import { addVencordUiStyles } from "@components/css"; +import { coreStyleRootNode, initStyles } from "@api/Styles"; import { openSettingsTabModal, UpdaterTab } from "@components/settings"; import { debounce } from "@shared/debounce"; import { IS_WINDOWS } from "@utils/constants"; @@ -39,7 +39,7 @@ import { get as dsGet } from "./api/DataStore"; import { NotificationData, showNotification } from "./api/Notifications"; import { initPluginManager, PMLogger, startAllPlugins } from "./api/PluginManager"; import { PlainSettings, Settings, SettingsStore } from "./api/Settings"; -import { getCloudSettings, putCloudSettings } from "./api/SettingsSync/cloudSync"; +import { getCloudSettings, putCloudSettings, shouldCloudSync } from "./api/SettingsSync/cloudSync"; import { localStorage } from "./utils/localStorage"; import { relaunch } from "./utils/native"; import { checkForUpdates, update, UpdateLogger } from "./utils/updater"; @@ -53,6 +53,10 @@ if (IS_REPORTER) { async function syncSettings() { // Check if cloud auth exists for current user before attempting sync + if (localStorage.Vencord_cloudSyncDirection === undefined) { + // by default, sync bi-directionally + localStorage.Vencord_cloudSyncDirection = "both"; + } const hasCloudAuth = await dsGet("Vencord_cloudSecret"); if (!hasCloudAuth) { if (Settings.cloud.authenticated) { @@ -87,13 +91,12 @@ async function syncSettings() { if ( Settings.cloud.settingsSync && // if it's enabled - Settings.cloud.authenticated // if cloud integrations are enabled + Settings.cloud.authenticated && // if cloud integrations are enabled + localStorage.Vencord_cloudSyncDirection !== "manual" // if we're not in manual mode ) { - if (localStorage.Vencord_settingsDirty) { + if (localStorage.Vencord_settingsDirty && shouldCloudSync("push")) { await putCloudSettings(); - delete localStorage.Vencord_settingsDirty; - } else if (await getCloudSettings(false)) { - // if we synchronized something (false means no sync) + } else if (shouldCloudSync("pull") && await getCloudSettings(false)) { // if we synchronized something (false means no sync) // we show a notification here instead of allowing getCloudSettings() to show one to declutter the amount of // potential notifications that might occur. getCloudSettings() will always send a notification regardless if // there was an error to notify the user, but besides that we only want to show one notification instead of all @@ -108,9 +111,8 @@ async function syncSettings() { } const saveSettingsOnFrequentAction = debounce(async () => { - if (Settings.cloud.settingsSync && Settings.cloud.authenticated) { + if (Settings.cloud.settingsSync && Settings.cloud.authenticated && shouldCloudSync("push")) { await putCloudSettings(); - delete localStorage.Vencord_settingsDirty; } }, 60_000); @@ -192,16 +194,15 @@ async function init() { } initPluginManager(); +initStyles(); startAllPlugins(StartAt.Init); init(); document.addEventListener("DOMContentLoaded", () => { - addVencordUiStyles(); - startAllPlugins(StartAt.DOMContentLoaded); // FIXME if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && IS_WINDOWS) { - createAndAppendStyle("vencord-native-titlebar-style").textContent = "[class*=titleBar]{display: none!important}"; + createAndAppendStyle("vencord-native-titlebar-style", coreStyleRootNode).textContent = "[class*=titleBar]{display: none!important}"; } }, { once: true }); diff --git a/src/VencordNative.ts b/src/VencordNative.ts index 380c42954..5ad69a9be 100644 --- a/src/VencordNative.ts +++ b/src/VencordNative.ts @@ -5,13 +5,13 @@ */ import type { Settings } from "@api/Settings"; -import { CspRequestResult } from "@main/csp/manager"; -import { PluginIpcMappings } from "@main/ipcPlugins"; +import type { CspRequestResult } from "@main/csp/manager"; +import type { PluginIpcMappings } from "@main/ipcPlugins"; import { IpcEvents } from "@shared/IpcEvents"; -import { IpcRes } from "@utils/types"; -import { ipcRenderer } from "electron"; +import type { IpcRes } from "@utils/types"; +import { ipcRenderer } from "electron/renderer"; -function invoke(event: IpcEvents, ...args: any[]) { +export function invoke(event: IpcEvents, ...args: any[]) { return ipcRenderer.invoke(event, ...args) as Promise; } @@ -31,8 +31,12 @@ for (const [plugin, methods] of Object.entries(pluginIpcMap)) { export default { themes: { - uploadTheme: (fileName: string, fileData: string) => invoke(IpcEvents.UPLOAD_THEME, fileName, fileData), - deleteTheme: (fileName: string) => invoke(IpcEvents.DELETE_THEME, fileName), + uploadTheme: async (fileName: string, fileData: string): Promise => { + throw new Error("uploadTheme is WEB only"); + }, + deleteTheme: async (fileName: string): Promise => { + throw new Error("deleteTheme is WEB only"); + }, getThemesDir: () => invoke(IpcEvents.GET_THEMES_DIR), getThemesList: () => invoke>(IpcEvents.GET_THEMES_LIST), getThemeData: (fileName: string) => invoke(IpcEvents.GET_THEME_DATA, fileName), @@ -70,11 +74,18 @@ export default { openFile: () => invoke(IpcEvents.OPEN_QUICKCSS), openEditor: () => invoke(IpcEvents.OPEN_MONACO_EDITOR), + getEditorTheme: () => sendSync(IpcEvents.GET_MONACO_THEME), }, native: { getVersions: () => process.versions as Partial, - openExternal: (url: string) => invoke(IpcEvents.OPEN_EXTERNAL, url) + openExternal: (url: string) => invoke(IpcEvents.OPEN_EXTERNAL, url), + getRendererCss: () => invoke(IpcEvents.GET_RENDERER_CSS), + onRendererCssUpdate: (cb: (newCss: string) => void) => { + if (!IS_DEV) return; + + ipcRenderer.on(IpcEvents.RENDERER_CSS_UPDATE, (_e, newCss: string) => cb(newCss)); + } }, csp: { diff --git a/src/api/Badges.ts b/src/api/Badges.ts index 9ab0f5cb2..a1d7a0c62 100644 --- a/src/api/Badges.ts +++ b/src/api/Badges.ts @@ -50,7 +50,8 @@ export interface ProfileBadge { key?: string; /** - * Allows dynamically returning multiple badges + * Allows dynamically returning multiple badges. + * May call hooks but then you must not use shouldShow */ getBadges?(userInfo: BadgeUserArgs): ProfileBadge[]; } diff --git a/src/api/Notifications/notificationLog.tsx b/src/api/Notifications/notificationLog.tsx index 40958a724..0fcea8a78 100644 --- a/src/api/Notifications/notificationLog.tsx +++ b/src/api/Notifications/notificationLog.tsx @@ -18,11 +18,11 @@ import * as DataStore from "@api/DataStore"; import { Settings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { BaseText } from "@components/BaseText"; import { Flex } from "@components/Flex"; import { Paragraph } from "@components/Paragraph"; import { openNotificationSettingsModal } from "@components/settings/tabs/vencord/NotificationSettings"; +import { classNameFactory } from "@utils/css"; import { closeModal, ModalCloseButton, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { useAwaiter } from "@utils/react"; import { Alerts, Button, ListScrollerThin, React, Timestamp, useEffect, useReducer, useState } from "@webpack/common"; diff --git a/src/api/SettingsSync/cloudSync.ts b/src/api/SettingsSync/cloudSync.ts index 6efe46792..e1e5edf97 100644 --- a/src/api/SettingsSync/cloudSync.ts +++ b/src/api/SettingsSync/cloudSync.ts @@ -6,6 +6,7 @@ import { showNotification } from "@api/Notifications"; import { PlainSettings, Settings } from "@api/Settings"; +import { localStorage } from "@utils/localStorage"; import { Logger } from "@utils/Logger"; import { relaunch } from "@utils/native"; import { openUserSettingsPanel } from "@webpack/common"; @@ -16,6 +17,12 @@ import { exportSettings, importSettings } from "./offline"; const logger = new Logger("SettingsSync:Cloud", "#39b7e0"); +export function shouldCloudSync(direction: "push" | "pull") { + const localDirection = localStorage.Vencord_cloudSyncDirection; + + return localDirection === direction || localDirection === "both"; +} + export async function putCloudSettings(manual?: boolean) { const settings = await exportSettings({ syncDataStore: false, minify: true }); @@ -52,6 +59,8 @@ export async function putCloudSettings(manual?: boolean) { noPersist: true, }); } + + delete localStorage.Vencord_settingsDirty; } catch (e: any) { logger.error("Failed to sync up", e); showNotification({ @@ -151,6 +160,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) { noPersist: true }); + delete localStorage.Vencord_settingsDirty; + return true; } catch (e: any) { logger.error("Failed to sync down", e); diff --git a/src/api/Styles.ts b/src/api/Styles.ts index fb40f72d8..967cef640 100644 --- a/src/api/Styles.ts +++ b/src/api/Styles.ts @@ -16,6 +16,14 @@ * along with this program. If not, see . */ +import { generateTextCss } from "@components/BaseText"; +import { generateMarginCss } from "@components/margins"; +import { classNameFactory as _classNameFactory, classNameToSelector, createAndAppendStyle } from "@utils/css"; + +// Backwards compat for Vesktop +/** @deprecated Import this from `@utils/css` instead */ +export const classNameFactory = _classNameFactory; + export interface Style { name: string; source: string; @@ -25,6 +33,57 @@ export interface Style { export const styleMap = window.VencordStyles ??= new Map(); +export const vencordRootNode = document.createElement("vencord-root"); +/** + * Houses all Vencord core styles. This includes all imported css files + */ +export const coreStyleRootNode = document.createElement("vencord-styles"); +/** + * Houses all plugin specific managed styles + */ +export const managedStyleRootNode = document.createElement("vencord-managed-styles"); +/** + * Houses the user's themes and quick css + */ +export const userStyleRootNode = document.createElement("vencord-user-styles"); + +vencordRootNode.style.display = "none"; +vencordRootNode.append(coreStyleRootNode, managedStyleRootNode, userStyleRootNode); + +export function initStyles() { + const osValuesNode = createAndAppendStyle("vencord-os-theme-values", coreStyleRootNode); + createAndAppendStyle("vencord-text", coreStyleRootNode).textContent = generateTextCss(); + const rendererCssNode = createAndAppendStyle("vencord-css-core", coreStyleRootNode); + const vesktopCssNode = IS_VESKTOP ? createAndAppendStyle("vesktop-css-core", coreStyleRootNode) : null; + createAndAppendStyle("vencord-margins", coreStyleRootNode).textContent = generateMarginCss(); + + VencordNative.native.getRendererCss().then(css => rendererCssNode.textContent = css); + if (IS_DEV) { + VencordNative.native.onRendererCssUpdate(newCss => { + rendererCssNode.textContent = newCss; + }); + } + + if (IS_VESKTOP && VesktopNative.app.getRendererCss) { + VesktopNative.app.getRendererCss().then(css => vesktopCssNode!.textContent = css); + VesktopNative.app.onRendererCssUpdate(newCss => { + vesktopCssNode!.textContent = newCss; + }); + } + + VencordNative.themes.getSystemValues().then(values => { + const variables = Object.entries(values) + .filter(([, v]) => !!v) + .map(([k, v]) => `--${k}: ${v};`) + .join(""); + osValuesNode.textContent = `:root{${variables}}`; + }); +} + +document.addEventListener("DOMContentLoaded", () => { + document.documentElement.append(vencordRootNode); +}, { once: true }); + export function requireStyle(name: string) { const style = styleMap.get(name); if (!style) throw new Error(`Style "${name}" does not exist`); @@ -53,7 +112,7 @@ export function enableStyle(name: string) { } compileStyle(style); - document.head.appendChild(style.dom); + managedStyleRootNode.appendChild(style.dom); return true; } @@ -135,31 +194,4 @@ export const compileStyle = (style: Style) => { }); }; -/** - * @param name The classname - * @param prefix A prefix to add each class, defaults to `""` - * @return A css selector for the classname - * @example - * classNameToSelector("foo bar") // => ".foo.bar" - */ -export const classNameToSelector = (name: string, prefix = "") => name.split(" ").map(n => `.${prefix}${n}`).join(""); -type ClassNameFactoryArg = string | string[] | Record | false | null | undefined | 0 | ""; -/** - * @param prefix The prefix to add to each class, defaults to `""` - * @returns A classname generator function - * @example - * const cl = classNameFactory("plugin-"); - * - * cl("base", ["item", "editable"], { selected: null, disabled: true }) - * // => "plugin-base plugin-item plugin-editable plugin-disabled" - */ -export const classNameFactory = (prefix: string = "") => (...args: ClassNameFactoryArg[]) => { - const classNames = new Set(); - for (const arg of args) { - if (arg && typeof arg === "string") classNames.add(arg); - else if (Array.isArray(arg)) arg.forEach(name => classNames.add(name)); - else if (arg && typeof arg === "object") Object.entries(arg).forEach(([name, value]) => value && classNames.add(name)); - } - return Array.from(classNames, name => prefix + name).join(" "); -}; diff --git a/src/api/Themes.ts b/src/api/Themes.ts index 525018531..81a55cfb1 100644 --- a/src/api/Themes.ts +++ b/src/api/Themes.ts @@ -20,23 +20,15 @@ import { Settings, SettingsStore } from "@api/Settings"; import { createAndAppendStyle } from "@utils/css"; import { ThemeStore } from "@vencord/discord-types"; +import { userStyleRootNode } from "./Styles"; + let style: HTMLStyleElement; let themesStyle: HTMLStyleElement; -async function initSystemValues() { - const values = await VencordNative.themes.getSystemValues(); - const variables = Object.entries(values) - .filter(([, v]) => v !== "#") - .map(([k, v]) => `--${k}: ${v};`) - .join(""); - - createAndAppendStyle("vencord-os-theme-values").textContent = `:root{${variables}}`; -} - async function toggle(isEnabled: boolean) { if (!style) { if (isEnabled) { - style = createAndAppendStyle("vencord-custom-css"); + style = createAndAppendStyle("vencord-custom-css", userStyleRootNode); VencordNative.quickCss.addChangeListener(css => { style.textContent = css; // At the time of writing this, changing textContent resets the disabled state @@ -49,7 +41,7 @@ async function toggle(isEnabled: boolean) { } async function initThemes() { - themesStyle ??= createAndAppendStyle("vencord-themes"); + themesStyle ??= createAndAppendStyle("vencord-themes", userStyleRootNode); const { enabledThemeLinks, enabledThemes } = Settings; @@ -89,7 +81,6 @@ async function initThemes() { document.addEventListener("DOMContentLoaded", () => { if (IS_USERSCRIPT) return; - initSystemValues(); initThemes(); toggle(Settings.useQuickCss); diff --git a/src/components/BaseText.tsx b/src/components/BaseText.tsx index dfd1565af..0f79adb1b 100644 --- a/src/components/BaseText.tsx +++ b/src/components/BaseText.tsx @@ -6,7 +6,7 @@ import "./BaseText.css"; -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { classes } from "@utils/misc"; import type { Text as DiscordText } from "@vencord/discord-types"; import type { ComponentPropsWithoutRef, ReactNode } from "react"; diff --git a/src/components/Button.css b/src/components/Button.css index ab57403d4..3c515ef69 100644 --- a/src/components/Button.css +++ b/src/components/Button.css @@ -124,11 +124,11 @@ } .vc-btn-positive { - background-color: var(--redesign-button-positive-background); + background-color: var(--redesign-button-positive-background, var(--green-430)); color: var(--white); &:hover { - background-color: var(--redesign-button-positive-pressed-background); + background-color: var(--redesign-button-positive-pressed-background, var(--green-460)); } } diff --git a/src/components/Button.tsx b/src/components/Button.tsx index b4390c232..2cfbd83d2 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -6,7 +6,7 @@ import "./Button.css"; -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { classes } from "@utils/misc"; import type { Button as DiscordButton } from "@vencord/discord-types"; import type { ComponentPropsWithRef } from "react"; diff --git a/src/components/Card.tsx b/src/components/Card.tsx index be32d978d..dfa6caaa0 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -6,7 +6,7 @@ import "./Card.css"; -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { classes } from "@utils/misc"; import { ComponentPropsWithRef } from "react"; diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index b3925d1f8..34e2d2e2d 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -686,3 +686,31 @@ export function MagnifyingGlassIcon(props: IconProps) { ); } + +export function CloudDownloadIcon(props: IconProps) { + return ( + + + + ); +} + +export function CloudUploadIcon(props: IconProps) { + return ( + + + + ); +} diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx index df630ca75..2c1d8353e 100644 --- a/src/components/Switch.tsx +++ b/src/components/Switch.tsx @@ -18,7 +18,7 @@ import "./Switch.css"; -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { classes } from "@utils/misc"; import { useState } from "@webpack/common"; import type { FocusEvent } from "react"; diff --git a/src/components/css.ts b/src/components/css.ts deleted file mode 100644 index 51151b1fe..000000000 --- a/src/components/css.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { createAndAppendStyle } from "@utils/css"; - -import { generateTextCss } from "./BaseText"; -import { generateMarginCss } from "./margins"; - -export function addVencordUiStyles() { - createAndAppendStyle("vencord-text", document.head).textContent = generateTextCss(); - createAndAppendStyle("vencord-margins").textContent = generateMarginCss(); -} diff --git a/src/components/margins.ts b/src/components/margins.ts index 24138d2a8..1ac7aceb4 100644 --- a/src/components/margins.ts +++ b/src/components/margins.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; const marginCls = classNameFactory("vc-margin-"); diff --git a/src/components/settings/AddonCard.tsx b/src/components/settings/AddonCard.tsx index f5798afd6..c99452252 100644 --- a/src/components/settings/AddonCard.tsx +++ b/src/components/settings/AddonCard.tsx @@ -18,10 +18,10 @@ import "./AddonCard.css"; -import { classNameFactory } from "@api/Styles"; import { BaseText } from "@components/BaseText"; import { AddonBadge } from "@components/settings/PluginBadge"; import { Switch } from "@components/Switch"; +import { classNameFactory } from "@utils/css"; import { Tooltip, useRef } from "@webpack/common"; import type { MouseEventHandler, ReactNode } from "react"; diff --git a/src/components/settings/QuickAction.tsx b/src/components/settings/QuickAction.tsx index a0cd0bcd3..e9cc1f86b 100644 --- a/src/components/settings/QuickAction.tsx +++ b/src/components/settings/QuickAction.tsx @@ -6,8 +6,8 @@ import "./QuickAction.css"; -import { classNameFactory } from "@api/Styles"; import { Card } from "@components/Card"; +import { classNameFactory } from "@utils/css"; import type { ComponentType, PropsWithChildren, ReactNode } from "react"; const cl = classNameFactory("vc-settings-quickActions-"); diff --git a/src/components/settings/SpecialCard.tsx b/src/components/settings/SpecialCard.tsx index 3388f9604..99b1c04be 100644 --- a/src/components/settings/SpecialCard.tsx +++ b/src/components/settings/SpecialCard.tsx @@ -18,11 +18,11 @@ import "./SpecialCard.css"; -import { classNameFactory } from "@api/Styles"; import { Card } from "@components/Card"; import { Divider } from "@components/Divider"; import { Heading } from "@components/Heading"; import { Paragraph } from "@components/Paragraph"; +import { classNameFactory } from "@utils/css"; import { Clickable } from "@webpack/common"; import type { PropsWithChildren } from "react"; diff --git a/src/components/settings/tabs/plugins/ContributorModal.tsx b/src/components/settings/tabs/plugins/ContributorModal.tsx index 7ae459395..72c8d84c8 100644 --- a/src/components/settings/tabs/plugins/ContributorModal.tsx +++ b/src/components/settings/tabs/plugins/ContributorModal.tsx @@ -7,12 +7,12 @@ import "./ContributorModal.css"; import { useSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { HeadingPrimary } from "@components/Heading"; import { Link } from "@components/Link"; import { Paragraph } from "@components/Paragraph"; import { EquicordDevsById, VencordDevsById } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; import { fetchUserProfile } from "@utils/discord"; import { pluralise } from "@utils/misc"; import { ModalContent, ModalFooter, ModalRoot, openModal } from "@utils/modal"; diff --git a/src/components/settings/tabs/plugins/PluginModal.tsx b/src/components/settings/tabs/plugins/PluginModal.tsx index 8f9920b6a..47c2c0ec6 100644 --- a/src/components/settings/tabs/plugins/PluginModal.tsx +++ b/src/components/settings/tabs/plugins/PluginModal.tsx @@ -20,7 +20,6 @@ import "./PluginModal.css"; import { generateId } from "@api/Commands"; import { useSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { BaseText } from "@components/BaseText"; import { Button } from "@components/Button"; import ErrorBoundary from "@components/ErrorBoundary"; @@ -28,6 +27,7 @@ import { Flex } from "@components/Flex"; import { Paragraph } from "@components/Paragraph"; import { debounce } from "@shared/debounce"; import { gitRemote } from "@shared/vencordUserAgent"; +import { classNameFactory } from "@utils/css"; import { proxyLazy } from "@utils/lazy"; import { Margins } from "@utils/margins"; import { classes, isObjectEmpty } from "@utils/misc"; diff --git a/src/components/settings/tabs/plugins/UIElements.tsx b/src/components/settings/tabs/plugins/UIElements.tsx index df85ef738..1e4600ac2 100644 --- a/src/components/settings/tabs/plugins/UIElements.tsx +++ b/src/components/settings/tabs/plugins/UIElements.tsx @@ -9,12 +9,12 @@ import "./UIElements.css"; import { ChatBarButtonMap } from "@api/ChatButtons"; import { MessagePopoverButtonMap } from "@api/MessagePopover"; import { SettingsPluginUiElements, useSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { BaseText } from "@components/BaseText"; import { Card } from "@components/Card"; import { PlaceholderIcon } from "@components/Icons"; import { Paragraph } from "@components/Paragraph"; import { Switch } from "@components/Switch"; +import { classNameFactory } from "@utils/css"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import { ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; diff --git a/src/components/settings/tabs/plugins/components/Common.tsx b/src/components/settings/tabs/plugins/components/Common.tsx index a8eaa1c3b..56bb24f10 100644 --- a/src/components/settings/tabs/plugins/components/Common.tsx +++ b/src/components/settings/tabs/plugins/components/Common.tsx @@ -4,8 +4,8 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; import { BaseText } from "@components/BaseText"; +import { classNameFactory } from "@utils/css"; import { classes } from "@utils/misc"; import { wordsFromCamel, wordsToTitle } from "@utils/text"; import { DefinedSettings, PluginOptionBase } from "@utils/types"; diff --git a/src/components/settings/tabs/plugins/index.tsx b/src/components/settings/tabs/plugins/index.tsx index 6f8bdbc80..939276c02 100644 --- a/src/components/settings/tabs/plugins/index.tsx +++ b/src/components/settings/tabs/plugins/index.tsx @@ -21,7 +21,6 @@ import "./styles.css"; import * as DataStore from "@api/DataStore"; import { isPluginEnabled, stopPlugin } from "@api/PluginManager"; import { useSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { Button } from "@components/Button"; import { Card } from "@components/Card"; import { Divider } from "@components/Divider"; @@ -31,6 +30,7 @@ import { Paragraph } from "@components/Paragraph"; import { SettingsTab } from "@components/settings"; import { debounce } from "@shared/debounce"; import { ChangeList } from "@utils/ChangeList"; +import { classNameFactory } from "@utils/css"; import { isTruthy } from "@utils/guards"; import { Logger } from "@utils/Logger"; import { Margins } from "@utils/margins"; diff --git a/src/components/settings/tabs/styles.css b/src/components/settings/tabs/styles.css index 83003366c..b5ab95342 100644 --- a/src/components/settings/tabs/styles.css +++ b/src/components/settings/tabs/styles.css @@ -72,6 +72,7 @@ display: grid; grid-template-columns: repeat(3, 1fr); gap: 1em; + margin-top: 16px; } .vc-cloud-erase-data-danger-btn { @@ -79,6 +80,16 @@ background-color: var(--redesign-button-danger-background); } +.vc-cloud-icon-with-button { + display: flex; + gap: 0.5em; + padding-inline: 0.5em 1em; +} + +.vc-cloud-button-icon { + height: 1.25em; +} + .vc-settings-modal { padding: 1.5em !important; } diff --git a/src/equicordplugins/musicControls/spotify/PlayerComponent.tsx b/src/equicordplugins/musicControls/spotify/PlayerComponent.tsx index 6ea78af69..0114c61d1 100644 --- a/src/equicordplugins/musicControls/spotify/PlayerComponent.tsx +++ b/src/equicordplugins/musicControls/spotify/PlayerComponent.tsx @@ -19,12 +19,12 @@ import "./spotifyStyles.css"; import { Settings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { Flex } from "@components/Flex"; import { CopyIcon, ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons"; import { Paragraph } from "@components/Paragraph"; import { Span } from "@components/Span"; import { debounce } from "@shared/debounce"; +import { classNameFactory } from "@utils/css"; import { copyWithToast, openImageModal } from "@utils/discord"; import { classes } from "@utils/misc"; import { ContextMenuApi, FluxDispatcher, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common"; diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts index 4fc317e4e..0718883e9 100644 --- a/src/main/ipcMain.ts +++ b/src/main/ipcMain.ts @@ -22,16 +22,18 @@ import "./settings"; import { debounce } from "@shared/debounce"; import { IpcEvents } from "@shared/IpcEvents"; -import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron"; +import { BrowserWindow, ipcMain, nativeTheme, shell, systemPreferences } from "electron"; import monacoHtml from "file://monacoWin.html?minify&base64"; -import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs"; +import { FSWatcher, mkdirSync, readFileSync, watch, writeFileSync } from "fs"; import { open, readdir, readFile, unlink } from "fs/promises"; import { join, normalize } from "path"; import { registerCspIpcHandlers } from "./csp/manager"; -import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, THEMES_DIR } from "./utils/constants"; +import { ALLOWED_PROTOCOLS, QUICK_CSS_PATH, SETTINGS_DIR, THEMES_DIR } from "./utils/constants"; import { makeLinksOpenExternally } from "./utils/externalLinks"; +const RENDERER_CSS_PATH = join(__dirname, IS_VESKTOP ? "vencordDesktopRenderer.css" : "renderer.css"); + mkdirSync(THEMES_DIR, { recursive: true }); registerCspIpcHandlers(); @@ -43,12 +45,8 @@ export function ensureSafePath(basePath: string, path: string) { return normalizedPath.startsWith(normalizedBasePath) ? normalizedPath : null; } -async function readCss() { - try { - return await readFile(QUICKCSS_PATH, "utf-8"); - } catch { - return ""; - } +function readCss() { + return readFile(QUICK_CSS_PATH, "utf-8").catch(() => ""); } async function listThemes(): Promise<{ fileName: string; content: string; }[]> { @@ -67,7 +65,7 @@ function getThemeData(fileName: string) { return readFile(safePath, "utf-8"); } -ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH)); +ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICK_CSS_PATH)); ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => { try { @@ -83,7 +81,7 @@ ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => { ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss()); ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) => - writeFileSync(QUICKCSS_PATH, css) + writeFileSync(QUICK_CSS_PATH, css) ); ipcMain.handle(IpcEvents.GET_THEMES_DIR, () => THEMES_DIR); @@ -94,33 +92,52 @@ ipcMain.handle(IpcEvents.DELETE_THEME, (_, fileName) => { if (!safePath) return Promise.reject(`Unsafe path ${fileName}`); return unlink(safePath); }); -ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => ({ - // win & mac only - "os-accent-color": `#${systemPreferences.getAccentColor?.() || ""}` -})); +ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => { + let accentColor = systemPreferences.getAccentColor?.() ?? ""; + + if (accentColor.length && accentColor[0] !== "#") { + accentColor = `#${accentColor}`; + } + + return { + "os-accent-color": accentColor + }; +}); ipcMain.handle(IpcEvents.OPEN_THEMES_FOLDER, () => shell.openPath(THEMES_DIR)); ipcMain.handle(IpcEvents.OPEN_SETTINGS_FOLDER, () => shell.openPath(SETTINGS_DIR)); -export function initIpc(mainWindow: BrowserWindow) { +ipcMain.handle(IpcEvents.INIT_FILE_WATCHERS, ({ sender }) => { let quickCssWatcher: FSWatcher | undefined; + let rendererCssWatcher: FSWatcher | undefined; - open(QUICKCSS_PATH, "a+").then(fd => { + open(QUICK_CSS_PATH, "a+").then(fd => { fd.close(); - quickCssWatcher = watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => { - mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss()); + quickCssWatcher = watch(QUICK_CSS_PATH, { persistent: false }, debounce(async () => { + sender.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss()); }, 50)); }).catch(() => { }); const themesWatcher = watch(THEMES_DIR, { persistent: false }, debounce(() => { - mainWindow.webContents.postMessage(IpcEvents.THEME_UPDATE, void 0); + sender.postMessage(IpcEvents.THEME_UPDATE, void 0); })); - mainWindow.once("closed", () => { + if (IS_DEV) { + rendererCssWatcher = watch(RENDERER_CSS_PATH, { persistent: false }, async () => { + sender.postMessage(IpcEvents.RENDERER_CSS_UPDATE, await readFile(RENDERER_CSS_PATH, "utf-8")); + }); + } + + sender.once("destroyed", () => { quickCssWatcher?.close(); themesWatcher.close(); + rendererCssWatcher?.close(); }); -} +}); + +ipcMain.on(IpcEvents.GET_MONACO_THEME, e => { + e.returnValue = nativeTheme.shouldUseDarkColors ? "vs-dark" : "vs-light"; +}); ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => { const title = "Equicord QuickCSS Editor"; @@ -146,3 +163,11 @@ ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => { await win.loadURL(`data:text/html;base64,${monacoHtml}`); }); + +ipcMain.handle(IpcEvents.GET_RENDERER_CSS, () => readFile(RENDERER_CSS_PATH, "utf-8")); + +if (IS_DISCORD_DESKTOP) { + ipcMain.on(IpcEvents.PRELOAD_GET_RENDERER_JS, e => { + e.returnValue = readFileSync(join(__dirname, "renderer.js"), "utf-8"); + }); +} diff --git a/src/main/patcher.ts b/src/main/patcher.ts index 807d28562..6b042400e 100644 --- a/src/main/patcher.ts +++ b/src/main/patcher.ts @@ -20,7 +20,6 @@ import { onceDefined } from "@shared/onceDefined"; import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron"; import { dirname, join } from "path"; -import { initIpc } from "./ipcMain"; import { RendererSettings } from "./settings"; import { IS_VANILLA } from "./utils/constants"; @@ -107,8 +106,6 @@ if (!IS_VANILLA) { // Disable the Electron call entirely so that Discord can't dynamically change the size this.setMinimumSize = (width: number, height: number) => { }; } - - initIpc(this); } else super(options); } } diff --git a/src/main/utils/constants.ts b/src/main/utils/constants.ts index e9fa39ab4..81d9e595d 100644 --- a/src/main/utils/constants.ts +++ b/src/main/utils/constants.ts @@ -30,7 +30,7 @@ export const DATA_DIR = process.env.EQUICORD_USER_DATA_DIR ?? ( export const SETTINGS_DIR = join(DATA_DIR, "settings"); export const THEMES_DIR = join(DATA_DIR, "themes"); -export const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css"); +export const QUICK_CSS_PATH = join(SETTINGS_DIR, "quickCss.css"); export const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json"); export const NATIVE_SETTINGS_FILE = join(SETTINGS_DIR, "native-settings.json"); export const DEV_MIGRATED = join(SETTINGS_DIR, "migration"); diff --git a/src/plugins/_api/badges/index.tsx b/src/plugins/_api/badges/index.tsx index 8169aaaae..ff9117865 100644 --- a/src/plugins/_api/badges/index.tsx +++ b/src/plugins/_api/badges/index.tsx @@ -26,11 +26,8 @@ import { copyWithToast } from "@utils/discord"; import { Logger } from "@utils/Logger"; import { shouldShowContributorBadge, shouldShowEquicordContributorBadge } from "@utils/misc"; import definePlugin from "@utils/types"; -import { User } from "@vencord/discord-types"; -import { ContextMenuApi, Menu, Toasts, UserStore } from "@webpack/common"; - +import { ContextMenuApi, Forms, Menu, Toasts, UserStore } from "@webpack/common"; import Plugins, { PluginMeta } from "~plugins"; - import { EquicordDonorModal, VencordDonorModal } from "./modals"; const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/emojis/1092089799109775453.png?size=64"; @@ -131,16 +128,13 @@ export default definePlugin({ authors: [Devs.Megu, Devs.Ven, Devs.TheSun], required: true, patches: [ - { - find: ".MODAL]:26", - replacement: { - match: /(?=;return 0===(\i)\.length\?)(?<=(\i)\.useMemo.+?)/, - replace: ";$1=$2.useMemo(()=>[...$self.getBadges(arguments[0].displayProfile),...$1],[$1])" - } - }, { find: "#{intl::PROFILE_USER_BADGES}", replacement: [ + { + match: /(?<=\{[^}]*?)badges:\i(?=[^}]*?}=(\i))/, + replace: "_$&=$self.useBadges($1.displayProfile).concat($1.badges)" + }, { match: /alt:" ","aria-hidden":!0,src:.{0,50}(\i).iconSrc/, replace: "...$1.props,$&" @@ -155,13 +149,6 @@ export default definePlugin({ replace: "...$self.getBadgeMouseEventHandlers($1),$&" } ] - }, - { - find: "profileCardUsernameRow,children:", - replacement: { - match: /badges:(\i)(?<=displayProfile:(\i).+?)/, - replace: "badges:[...$self.getBadges($2),...$1]" - } } ], @@ -197,15 +184,14 @@ export default definePlugin({ clearInterval(intervalId); }, - getBadges(props: { userId: string; user?: User; guildId: string; }) { - if (!props) return []; + // doesn't use hooks itself, but some plugins might do so in their getBadges function + useBadges(profile: { userId: string; guildId: string; }) { + if (!profile) return []; try { - props.userId ??= props.user?.id!; - - return _getBadges(props); + return _getBadges(profile); } catch (e) { - new Logger("BadgeAPI#hasBadges").error(e); + new Logger("BadgeAPI#useBadges").error(e); return []; } }, diff --git a/src/plugins/clientTheme/components/Settings.tsx b/src/plugins/clientTheme/components/Settings.tsx index 69ad76efa..e484a8127 100644 --- a/src/plugins/clientTheme/components/Settings.tsx +++ b/src/plugins/clientTheme/components/Settings.tsx @@ -4,12 +4,12 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; import { ErrorCard } from "@components/ErrorCard"; import { HeadingPrimary, HeadingSecondary } from "@components/Heading"; import { Paragraph } from "@components/Paragraph"; import { relativeLuminance } from "@plugins/clientTheme/utils/colorUtils"; import { createOrUpdateThemeColorVars } from "@plugins/clientTheme/utils/styleUtils"; +import { classNameFactory } from "@utils/css"; import { Margins } from "@utils/margins"; import { findByCodeLazy, findStoreLazy } from "@webpack"; import { Button, ColorPicker, ThemeStore, useStateFromStores } from "@webpack/common"; diff --git a/src/plugins/customRPC/RpcSettings.tsx b/src/plugins/customRPC/RpcSettings.tsx index 019cc431b..bdca847d2 100644 --- a/src/plugins/customRPC/RpcSettings.tsx +++ b/src/plugins/customRPC/RpcSettings.tsx @@ -7,11 +7,11 @@ import "./settings.css"; import { isPluginEnabled } from "@api/PluginManager"; -import { classNameFactory } from "@api/Styles"; import { Divider } from "@components/Divider"; import { Heading } from "@components/Heading"; import { resolveError } from "@components/settings/tabs/plugins/components/Common"; import { debounce } from "@shared/debounce"; +import { classNameFactory } from "@utils/css"; import { ActivityType } from "@vencord/discord-types/enums"; import { Select, Text, TextInput, useState } from "@webpack/common"; diff --git a/src/plugins/decor/ui/index.ts b/src/plugins/decor/ui/index.ts index 62040508f..44e58eccf 100644 --- a/src/plugins/decor/ui/index.ts +++ b/src/plugins/decor/ui/index.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { extractAndLoadChunksLazy, findByPropsLazy } from "@webpack"; export const cl = classNameFactory("vc-decor-"); diff --git a/src/plugins/imageZoom/components/Magnifier.tsx b/src/plugins/imageZoom/components/Magnifier.tsx index 7ca83a6d3..f69ab4b37 100644 --- a/src/plugins/imageZoom/components/Magnifier.tsx +++ b/src/plugins/imageZoom/components/Magnifier.tsx @@ -16,11 +16,11 @@ * along with this program. If not, see . */ -import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { settings } from "@plugins/imageZoom"; import { ELEMENT_ID } from "@plugins/imageZoom/constants"; import { waitFor } from "@plugins/imageZoom/utils/waitFor"; +import { classNameFactory } from "@utils/css"; import { FluxDispatcher, useLayoutEffect, useMemo, useRef, useState } from "@webpack/common"; interface Vec2 { diff --git a/src/plugins/memberCount/index.tsx b/src/plugins/memberCount/index.tsx index e5dac9d02..ae9c17936 100644 --- a/src/plugins/memberCount/index.tsx +++ b/src/plugins/memberCount/index.tsx @@ -19,9 +19,9 @@ import "./style.css"; import { definePluginSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; import definePlugin, { OptionType } from "@utils/types"; import { FluxStore } from "@vencord/discord-types"; import { findStoreLazy } from "@webpack"; diff --git a/src/plugins/messageLogger/HistoryModal.tsx b/src/plugins/messageLogger/HistoryModal.tsx index 797a2db7e..58589c5ea 100644 --- a/src/plugins/messageLogger/HistoryModal.tsx +++ b/src/plugins/messageLogger/HistoryModal.tsx @@ -4,9 +4,9 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; import { BaseText } from "@components/BaseText"; import ErrorBoundary from "@components/ErrorBoundary"; +import { classNameFactory } from "@utils/css"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; diff --git a/src/plugins/permissionsViewer/utils.ts b/src/plugins/permissionsViewer/utils.ts index c3f616727..c9280b1b4 100644 --- a/src/plugins/permissionsViewer/utils.ts +++ b/src/plugins/permissionsViewer/utils.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { Guild, GuildMember, Role } from "@vencord/discord-types"; import { findByPropsLazy } from "@webpack"; import { GuildRoleStore } from "@webpack/common"; diff --git a/src/plugins/pinDms/components/CreateCategoryModal.tsx b/src/plugins/pinDms/components/CreateCategoryModal.tsx index 1bda1e8a4..b35d2cd6c 100644 --- a/src/plugins/pinDms/components/CreateCategoryModal.tsx +++ b/src/plugins/pinDms/components/CreateCategoryModal.tsx @@ -4,12 +4,12 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; import { BaseText } from "@components/BaseText"; import { Divider } from "@components/Divider"; import { Heading } from "@components/Heading"; import { DEFAULT_COLOR, SWATCHES } from "@plugins/pinDms/constants"; import { categoryLen, createCategory, getCategory } from "@plugins/pinDms/data"; +import { classNameFactory } from "@utils/css"; import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from "@utils/modal"; import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack"; import { Button, ColorPicker, TextInput, Toasts, useMemo, useState } from "@webpack/common"; diff --git a/src/plugins/platformIndicators/icons/desktopIcon.svg b/src/plugins/platformIndicators/icons/desktopIcon.svg new file mode 100644 index 000000000..df7ac3312 --- /dev/null +++ b/src/plugins/platformIndicators/icons/desktopIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/plugins/platformIndicators/icons/embeddedIcon.svg b/src/plugins/platformIndicators/icons/embeddedIcon.svg new file mode 100644 index 000000000..ce6f62ddb --- /dev/null +++ b/src/plugins/platformIndicators/icons/embeddedIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/plugins/platformIndicators/icons/mobileIcon.svg b/src/plugins/platformIndicators/icons/mobileIcon.svg new file mode 100644 index 000000000..28b23a522 --- /dev/null +++ b/src/plugins/platformIndicators/icons/mobileIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/plugins/platformIndicators/icons/webIcon.svg b/src/plugins/platformIndicators/icons/webIcon.svg new file mode 100644 index 000000000..82627ca90 --- /dev/null +++ b/src/plugins/platformIndicators/icons/webIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/plugins/reviewDB/utils.tsx b/src/plugins/reviewDB/utils.tsx index 916adc45e..ec7609c50 100644 --- a/src/plugins/reviewDB/utils.tsx +++ b/src/plugins/reviewDB/utils.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { Toasts, UserStore } from "@webpack/common"; import { Auth } from "./auth"; diff --git a/src/plugins/sendTimestamps/index.tsx b/src/plugins/sendTimestamps/index.tsx index bf93480d0..65c1f11d7 100644 --- a/src/plugins/sendTimestamps/index.tsx +++ b/src/plugins/sendTimestamps/index.tsx @@ -20,10 +20,10 @@ import "./styles.css"; import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons"; import { definePluginSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { Heading, HeadingPrimary } from "@components/Heading"; import { Paragraph } from "@components/Paragraph"; import { Devs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; import { getTheme, insertTextIntoChatInputBox, Theme } from "@utils/discord"; import { Margins } from "@utils/margins"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal"; diff --git a/src/plugins/serverInfo/GuildInfoModal.tsx b/src/plugins/serverInfo/GuildInfoModal.tsx index 69f438cad..5b074e723 100644 --- a/src/plugins/serverInfo/GuildInfoModal.tsx +++ b/src/plugins/serverInfo/GuildInfoModal.tsx @@ -6,7 +6,7 @@ import "./styles.css"; -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { Heading } from "@components/Heading"; import { Paragraph } from "@components/Paragraph"; import { getGuildAcronym, openImageModal, openUserProfile } from "@utils/discord"; diff --git a/src/plugins/shikiCodeblocks.desktop/utils/misc.ts b/src/plugins/shikiCodeblocks.desktop/utils/misc.ts index ee782ec05..1b2fd27c5 100644 --- a/src/plugins/shikiCodeblocks.desktop/utils/misc.ts +++ b/src/plugins/shikiCodeblocks.desktop/utils/misc.ts @@ -16,10 +16,10 @@ * along with this program. If not, see . */ -import { classNameFactory } from "@api/Styles"; import { resolveLang } from "@plugins/shikiCodeblocks.desktop/api/languages"; import { HighlighterProps } from "@plugins/shikiCodeblocks.desktop/components/Highlighter"; import { HljsSetting } from "@plugins/shikiCodeblocks.desktop/types"; +import { classNameFactory } from "@utils/css"; import { hljs } from "@webpack/common"; export const cl = classNameFactory("vc-shiki-"); diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index b26dca9c9..37aebdb4c 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -19,9 +19,9 @@ import "./style.css"; import { definePluginSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs, EquicordDevs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; import { classes } from "@utils/misc"; import definePlugin, { OptionType } from "@utils/types"; import type { Channel, Role } from "@vencord/discord-types"; diff --git a/src/plugins/sortFriendRequests/index.tsx b/src/plugins/sortFriendRequests/index.tsx index 7f53c5702..b3bedb1bd 100644 --- a/src/plugins/sortFriendRequests/index.tsx +++ b/src/plugins/sortFriendRequests/index.tsx @@ -19,7 +19,7 @@ import "./styles.css"; import { definePluginSettings, migratePluginSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { BaseText } from "@components/BaseText"; import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; diff --git a/src/plugins/translate/utils.ts b/src/plugins/translate/utils.ts index 9bb175adf..4ee16c93e 100644 --- a/src/plugins/translate/utils.ts +++ b/src/plugins/translate/utils.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { onlyOnce } from "@utils/onlyOnce"; import { PluginNative } from "@utils/types"; import { showToast, Toasts } from "@webpack/common"; diff --git a/src/plugins/userVoiceShow/components.tsx b/src/plugins/userVoiceShow/components.tsx index ebf9df9e1..e121dde6f 100644 --- a/src/plugins/userVoiceShow/components.tsx +++ b/src/plugins/userVoiceShow/components.tsx @@ -5,7 +5,7 @@ */ import { isPluginEnabled } from "@api/PluginManager"; -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { BaseText } from "@components/BaseText"; import ErrorBoundary from "@components/ErrorBoundary"; import ShowHiddenChannelsPlugin from "@plugins/showHiddenChannels"; diff --git a/src/preload.ts b/src/preload.ts index 7c61a181d..5f7adb991 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -17,48 +17,25 @@ */ import { debounce } from "@shared/debounce"; -import { contextBridge, webFrame } from "electron"; -import { readFileSync, watch } from "fs"; -import { join } from "path"; +import { IpcEvents } from "@shared/IpcEvents"; +import { contextBridge, webFrame } from "electron/renderer"; -import VencordNative from "./VencordNative"; +import VencordNative, { invoke, sendSync } from "./VencordNative"; contextBridge.exposeInMainWorld("VencordNative", VencordNative); // Discord if (location.protocol !== "data:") { - // #region cssInsert - const rendererCss = join(__dirname, "renderer.css"); - - const style = document.createElement("style"); - style.id = "vencord-css-core"; - style.textContent = readFileSync(rendererCss, "utf-8"); - - if (document.readyState === "complete") { - document.documentElement.appendChild(style); - } else { - document.addEventListener("DOMContentLoaded", () => document.documentElement.appendChild(style), { - once: true - }); - } - - if (IS_DEV) { - // persistent means keep process running if watcher is the only thing still running - // which we obviously don't want - watch(rendererCss, { persistent: false }, () => { - document.getElementById("vencord-css-core")!.textContent = readFileSync(rendererCss, "utf-8"); - }); - } - // #endregion + invoke(IpcEvents.INIT_FILE_WATCHERS); if (IS_DISCORD_DESKTOP) { - webFrame.executeJavaScript(readFileSync(join(__dirname, "renderer.js"), "utf-8")); + webFrame.executeJavaScript(sendSync(IpcEvents.PRELOAD_GET_RENDERER_JS)); + // Not supported in sandboxed preload scripts but Discord doesn't support it either so who cares require(process.env.DISCORD_PRELOAD!); } } // Monaco popout else { contextBridge.exposeInMainWorld("setCss", debounce(VencordNative.quickCss.set)); contextBridge.exposeInMainWorld("getCurrentCss", VencordNative.quickCss.get); - // shrug - contextBridge.exposeInMainWorld("getTheme", () => "vs-dark"); + contextBridge.exposeInMainWorld("getTheme", VencordNative.quickCss.getEditorTheme); } diff --git a/src/shared/IpcEvents.ts b/src/shared/IpcEvents.ts index 0354e6979..974fe0b5f 100644 --- a/src/shared/IpcEvents.ts +++ b/src/shared/IpcEvents.ts @@ -17,8 +17,9 @@ */ export const enum IpcEvents { + INIT_FILE_WATCHERS = "VencordInitFileWatchers", QUICK_CSS_UPDATE = "VencordQuickCssUpdate", - THEME_UPDATE = "VencordThemeUpdate", + OPEN_QUICKCSS = "VencordOpenQuickCss", GET_QUICK_CSS = "VencordGetQuickCss", SET_QUICK_CSS = "VencordSetQuickCss", UPLOAD_THEME = "VencordUploadTheme", @@ -30,23 +31,24 @@ export const enum IpcEvents { GET_SETTINGS_DIR = "VencordGetSettingsDir", GET_SETTINGS = "VencordGetSettings", SET_SETTINGS = "VencordSetSettings", + THEME_UPDATE = "VencordThemeUpdate", OPEN_EXTERNAL = "VencordOpenExternal", - OPEN_QUICKCSS = "VencordOpenQuickCss", GET_UPDATES = "VencordGetUpdates", GET_REPO = "VencordGetRepo", UPDATE = "VencordUpdate", BUILD = "VencordBuild", OPEN_MONACO_EDITOR = "VencordOpenMonacoEditor", + GET_MONACO_THEME = "VencordGetMonacoTheme", GET_PLUGIN_IPC_METHOD_MAP = "VencordGetPluginIpcMethodMap", - OPEN_IN_APP__RESOLVE_REDIRECT = "VencordOIAResolveRedirect", - VOICE_MESSAGES_READ_RECORDING = "VencordVMReadRecording", - CSP_IS_DOMAIN_ALLOWED = "VencordCspIsDomainAllowed", CSP_REMOVE_OVERRIDE = "VencordCspRemoveOverride", CSP_REQUEST_ADD_OVERRIDE = "VencordCspRequestAddOverride", OPEN_THEMES_FOLDER = "VencordOpenThemesFolder", OPEN_SETTINGS_FOLDER = "VencordOpenSettingsFolder", + GET_RENDERER_CSS = "VencordGetRendererCss", + RENDERER_CSS_UPDATE = "VencordRendererCssUpdate", + PRELOAD_GET_RENDERER_JS = "VencordPreloadGetRendererJs", } diff --git a/src/utils/css.ts b/src/utils/css.ts index 3c46b7a4d..80fd3dedf 100644 --- a/src/utils/css.ts +++ b/src/utils/css.ts @@ -4,9 +4,32 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -export function createAndAppendStyle(id: string, target = document.documentElement) { +export function createAndAppendStyle(id: string, target: HTMLElement) { const style = document.createElement("style"); style.id = id; target.append(style); return style; } + +export const classNameToSelector = (name: string, prefix = "") => name.split(" ").map(n => `.${prefix}${n}`).join(""); + +export type ClassNameFactoryArg = string | string[] | Record | false | null | undefined | 0 | ""; + +/** + * @param prefix The prefix to add to each class, defaults to `""` + * @returns A classname generator function + * @example + * const cl = classNameFactory("plugin-"); + * + * cl("base", ["item", "editable"], { selected: null, disabled: true }) + * // => "plugin-base plugin-item plugin-editable plugin-disabled" + */ +export const classNameFactory = (prefix: string = "") => (...args: ClassNameFactoryArg[]) => { + const classNames = new Set(); + for (const arg of args) { + if (arg && typeof arg === "string") classNames.add(arg); + else if (Array.isArray(arg)) arg.forEach(name => classNames.add(name)); + else if (arg && typeof arg === "object") Object.entries(arg).forEach(([name, value]) => value && classNames.add(name)); + } + return Array.from(classNames, name => prefix + name).join(" "); +}; diff --git a/src/utils/web-metadata.ts b/src/utils/web-metadata.ts index cca07ad4c..8e39766fd 100644 --- a/src/utils/web-metadata.ts +++ b/src/utils/web-metadata.ts @@ -4,14 +4,19 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -export let EXTENSION_BASE_URL: string; export let EXTENSION_VERSION: string; +export let EXTENSION_BASE_URL: string; +export let RENDERER_CSS_URL: string; + +let resolveMetaReady: Function; +export const metaReady = new Promise(res => resolveMetaReady = res); if (IS_EXTENSION) { const listener = (e: MessageEvent) => { if (e.data?.type === "vencord:meta") { - ({ EXTENSION_BASE_URL, EXTENSION_VERSION } = e.data.meta); + ({ EXTENSION_BASE_URL, EXTENSION_VERSION, RENDERER_CSS_URL } = e.data.meta); window.removeEventListener("message", listener); + resolveMetaReady(); } };