Unify styles into single root node & support preload sandboxing (#3797)

This commit is contained in:
V 2025-12-20 01:02:43 +01:00 committed by Vendicated
parent e5df9b394e
commit ce8e48bb6d
No known key found for this signature in database
GPG key ID: D66986BAF75ECF18
50 changed files with 239 additions and 186 deletions

View file

@ -28,7 +28,7 @@ import { getThemeInfo } from "@main/themes";
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: {

View file

@ -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");

View file

@ -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/Vencord.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/Vencord.css"),
}
});
},

View file

@ -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^"
}
}
]

View file

@ -176,14 +176,7 @@ async function buildExtension(target, files) {
}
const appendCssRuntime = readFile("dist/Vencord.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/Vencord.user.js", cssRuntime);
});

View file

@ -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";
@ -178,16 +178,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 });

View file

@ -5,14 +5,14 @@
*/
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 type { UserThemeHeader } from "@main/themes";
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<T = any>(event: IpcEvents, ...args: any[]) {
export function invoke<T = any>(event: IpcEvents, ...args: any[]) {
return ipcRenderer.invoke(event, ...args) as Promise<T>;
}
@ -32,8 +32,12 @@ for (const [plugin, methods] of Object.entries(pluginIpcMap)) {
export default {
themes: {
uploadTheme: (fileName: string, fileData: string) => invoke<void>(IpcEvents.UPLOAD_THEME, fileName, fileData),
deleteTheme: (fileName: string) => invoke<void>(IpcEvents.DELETE_THEME, fileName),
uploadTheme: async (fileName: string, fileData: string): Promise<void> => {
throw new Error("uploadTheme is WEB only");
},
deleteTheme: async (fileName: string): Promise<void> => {
throw new Error("deleteTheme is WEB only");
},
getThemesList: () => invoke<Array<UserThemeHeader>>(IpcEvents.GET_THEMES_LIST),
getThemeData: (fileName: string) => invoke<string | undefined>(IpcEvents.GET_THEME_DATA, fileName),
getSystemValues: () => invoke<Record<string, string>>(IpcEvents.GET_THEME_SYSTEM_VALUES),
@ -69,11 +73,18 @@ export default {
openFile: () => invoke<void>(IpcEvents.OPEN_QUICKCSS),
openEditor: () => invoke<void>(IpcEvents.OPEN_MONACO_EDITOR),
getEditorTheme: () => sendSync<string>(IpcEvents.GET_MONACO_THEME),
},
native: {
getVersions: () => process.versions as Partial<NodeJS.ProcessVersions>,
openExternal: (url: string) => invoke<void>(IpcEvents.OPEN_EXTERNAL, url)
openExternal: (url: string) => invoke<void>(IpcEvents.OPEN_EXTERNAL, url),
getRendererCss: () => invoke<string>(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: {

View file

@ -18,9 +18,9 @@
import * as DataStore from "@api/DataStore";
import { Settings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Flex } from "@components/Flex";
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, Forms, ListScrollerThin, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";

View file

@ -16,6 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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<string, unknown> | 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<string>();
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(" ");
};

View file

@ -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 { themeLinks, enabledThemes } = Settings;
@ -89,7 +81,6 @@ async function initThemes() {
document.addEventListener("DOMContentLoaded", () => {
if (IS_USERSCRIPT) return;
initSystemValues();
initThemes();
toggle(Settings.useQuickCss);

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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();
}

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { classNameFactory } from "@api/Styles";
import { classNameFactory } from "@utils/css";
const marginCls = classNameFactory("vc-margin-");

View file

@ -18,9 +18,9 @@
import "./AddonCard.css";
import { classNameFactory } from "@api/Styles";
import { AddonBadge } from "@components/settings/PluginBadge";
import { Switch } from "@components/Switch";
import { classNameFactory } from "@utils/css";
import { Text, useRef } from "@webpack/common";
import type { MouseEventHandler, ReactNode } from "react";

View file

@ -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-");

View file

@ -18,9 +18,9 @@
import "./SpecialCard.css";
import { classNameFactory } from "@api/Styles";
import { Card } from "@components/Card";
import { Divider } from "@components/Divider";
import { classNameFactory } from "@utils/css";
import { Clickable, Forms } from "@webpack/common";
import type { PropsWithChildren } from "react";

View file

@ -7,10 +7,10 @@
import "./ContributorModal.css";
import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Link } from "@components/Link";
import { DevsById } from "@utils/constants";
import { classNameFactory } from "@utils/css";
import { fetchUserProfile } from "@utils/discord";
import { classes, pluralise } from "@utils/misc";
import { ModalContent, ModalRoot, openModal } from "@utils/modal";

View file

@ -20,11 +20,11 @@ import "./PluginModal.css";
import { generateId } from "@api/Commands";
import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
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";

View file

@ -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";

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { classNameFactory } from "@api/Styles";
import { classNameFactory } from "@utils/css";
import { classes } from "@utils/misc";
import { wordsFromCamel, wordsToTitle } from "@utils/text";
import { DefinedSettings, PluginOptionBase } from "@utils/types";

View file

@ -21,7 +21,6 @@ import "./styles.css";
import * as DataStore from "@api/DataStore";
import { isPluginEnabled } from "@api/PluginManager";
import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Card } from "@components/Card";
import { Divider } from "@components/Divider";
import ErrorBoundary from "@components/ErrorBoundary";
@ -29,6 +28,7 @@ import { HeadingTertiary } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
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";

View file

@ -6,7 +6,6 @@
import { isPluginEnabled } from "@api/PluginManager";
import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Card } from "@components/Card";
import { Flex } from "@components/Flex";
import { FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from "@components/Icons";
@ -15,6 +14,7 @@ import { QuickAction, QuickActionCard } from "@components/settings/QuickAction";
import { openPluginModal } from "@components/settings/tabs/plugins/PluginModal";
import { UserThemeHeader } from "@main/themes";
import ClientThemePlugin from "@plugins/clientTheme";
import { classNameFactory } from "@utils/css";
import { findLazy } from "@webpack";
import { Forms, useEffect, useRef, useState } from "@webpack/common";
import type { ComponentType, Ref, SyntheticEvent } from "react";

View file

@ -22,17 +22,19 @@ 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 } from "fs/promises";
import { join, normalize } from "path";
import { registerCspIpcHandlers } from "./csp/manager";
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
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();
@ -45,7 +47,7 @@ export function ensureSafePath(basePath: string, path: string) {
}
function readCss() {
return readFile(QUICKCSS_PATH, "utf-8").catch(() => "");
return readFile(QUICK_CSS_PATH, "utf-8").catch(() => "");
}
async function listThemes(): Promise<UserThemeHeader[]> {
@ -72,7 +74,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 {
@ -89,38 +91,57 @@ 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_LIST, () => listThemes());
ipcMain.handle(IpcEvents.GET_THEME_DATA, (_, fileName) => getThemeData(fileName));
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 = "Vencord QuickCSS Editor";
@ -146,3 +167,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");
});
}

View file

@ -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";
@ -71,7 +70,7 @@ if (!IS_VANILLA) {
constructor(options: BrowserWindowConstructorOptions) {
if (options?.webPreferences?.preload && options.title) {
const original = options.webPreferences.preload;
options.webPreferences.preload = join(__dirname, IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js");
options.webPreferences.preload = join(__dirname, "preload.js");
options.webPreferences.sandbox = false;
// work around discord unloading when in background
options.webPreferences.backgroundThrottling = false;
@ -109,8 +108,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);
}
}

View file

@ -26,7 +26,7 @@ export const DATA_DIR = process.env.VENCORD_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 ALLOWED_PROTOCOLS = [

View file

@ -4,10 +4,10 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { classNameFactory } from "@api/Styles";
import { ErrorCard } from "@components/ErrorCard";
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, Forms, ThemeStore, useStateFromStores } from "@webpack/common";

View file

@ -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";

View file

@ -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-");

View file

@ -16,11 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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 {

View file

@ -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";

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { classNameFactory } from "@api/Styles";
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";

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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";

View file

@ -4,10 +4,10 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { classNameFactory } from "@api/Styles";
import { Divider } from "@components/Divider";
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, Forms, Text, TextInput, Toasts, useMemo, useState } from "@webpack/common";

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { classNameFactory } from "@api/Styles";
import { classNameFactory } from "@utils/css";
import { Toasts, UserStore } from "@webpack/common";
import { Auth } from "./auth";

View file

@ -20,8 +20,8 @@ import "./styles.css";
import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
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";

View file

@ -6,7 +6,7 @@
import "./styles.css";
import { classNameFactory } from "@api/Styles";
import { classNameFactory } from "@utils/css";
import { getGuildAcronym, openImageModal, openUserProfile } from "@utils/discord";
import { classes } from "@utils/misc";
import { ModalRoot, ModalSize, openModal } from "@utils/modal";

View file

@ -16,10 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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-");

View file

@ -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 { classes } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
import type { Channel, Role } from "@vencord/discord-types";

View file

@ -19,9 +19,9 @@
import "./styles.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 { User } from "@vencord/discord-types";
import { DateUtils, RelationshipStore, Text, TooltipContainer } from "@webpack/common";

View file

@ -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";

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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";

View file

@ -5,9 +5,9 @@
*/
import { isPluginEnabled } from "@api/PluginManager";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import ShowHiddenChannelsPlugin from "@plugins/showHiddenChannels";
import { classNameFactory } from "@utils/css";
import { classes } from "@utils/misc";
import { Channel } from "@vencord/discord-types";
import { filters, findByPropsLazy, mapMangledModuleLazy } from "@webpack";

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { classNameFactory } from "@api/Styles";
import { classNameFactory } from "@utils/css";
import { findStoreLazy } from "@webpack";
export const MediaEngineStore = findStoreLazy("MediaEngineStore");

View file

@ -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, IS_VESKTOP ? "vencordDesktopRenderer.css" : "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<string>(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);
}

View file

@ -17,6 +17,8 @@
*/
export const enum IpcEvents {
INIT_FILE_WATCHERS = "VencordInitFileWatchers",
OPEN_QUICKCSS = "VencordOpenQuickCss",
GET_QUICK_CSS = "VencordGetQuickCss",
SET_QUICK_CSS = "VencordSetQuickCss",
@ -28,8 +30,6 @@ export const enum IpcEvents {
GET_THEMES_LIST = "VencordGetThemesList",
GET_THEME_DATA = "VencordGetThemeData",
GET_THEME_SYSTEM_VALUES = "VencordGetThemeSystemValues",
UPLOAD_THEME = "VencordUploadTheme",
DELETE_THEME = "VencordDeleteTheme",
THEME_UPDATE = "VencordThemeUpdate",
OPEN_EXTERNAL = "VencordOpenExternal",
@ -42,13 +42,15 @@ export const enum IpcEvents {
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",
GET_RENDERER_CSS = "VencordGetRendererCss",
RENDERER_CSS_UPDATE = "VencordRendererCssUpdate",
PRELOAD_GET_RENDERER_JS = "VencordPreloadGetRendererJs",
}

View file

@ -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<string, unknown> | 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<string>();
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(" ");
};

View file

@ -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<void>(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();
}
};