diff options
author | Ven <vendicated@riseup.net> | 2022-12-25 20:47:35 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-12-25 20:47:35 +0100 |
commit | 2e5d27b6b63097e96e25819df7a8cdd667c521b3 (patch) | |
tree | 082b0f1c7cb0210d208c7cb8017e9da97b3b4196 | |
parent | 2172cae779fb24f9bcc8c54a0b6538da0b52bafd (diff) | |
download | Vencord-2e5d27b6b63097e96e25819df7a8cdd667c521b3.tar.gz Vencord-2e5d27b6b63097e96e25819df7a8cdd667c521b3.tar.bz2 Vencord-2e5d27b6b63097e96e25819df7a8cdd667c521b3.zip |
feat: Proper CSS api & css bundle (#269)
Co-authored-by: Vap0r1ze <superdash993@gmail.com>
31 files changed, 438 insertions, 126 deletions
diff --git a/browser/content.js b/browser/content.js index 2c4b40e..e47ef83 100644 --- a/browser/content.js +++ b/browser/content.js @@ -2,7 +2,18 @@ if (typeof browser === "undefined") { var browser = chrome; } -var script = document.createElement("script"); +const script = document.createElement("script"); script.src = browser.runtime.getURL("dist/Vencord.js"); -// documentElement because we load before body/head are ready -document.documentElement.appendChild(script); + +const style = document.createElement("link"); +style.type = "text/css"; +style.rel = "stylesheet"; +style.href = browser.runtime.getURL("dist/Vencord.css"); + +document.documentElement.append(script); + +document.addEventListener( + "DOMContentLoaded", + () => document.documentElement.append(style), + { once: true } +); diff --git a/browser/manifestv2.json b/browser/manifestv2.json index 405b2dc..b28b73f 100644 --- a/browser/manifestv2.json +++ b/browser/manifestv2.json @@ -18,7 +18,7 @@ "js": ["content.js"] } ], - "web_accessible_resources": ["dist/Vencord.js"], + "web_accessible_resources": ["dist/Vencord.js", "dist/Vencord.css"], "background": { "scripts": ["background.js"] } diff --git a/browser/manifestv3.json b/browser/manifestv3.json index ea79d12..d15b80a 100644 --- a/browser/manifestv3.json +++ b/browser/manifestv3.json @@ -23,7 +23,7 @@ "web_accessible_resources": [ { - "resources": ["dist/Vencord.js"], + "resources": ["dist/Vencord.js", "dist/Vencord.css"], "matches": ["*://*.discord.com/*"] } ], diff --git a/package.json b/package.json index 35c4aaf..51d384d 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,17 @@ "patchedDependencies": { "eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch", "eslint@8.28.0": "patches/eslint@8.28.0.patch" + }, + "peerDependencyRules": { + "ignoreMissing": [ + "eslint-plugin-import" + ] + }, + "allowedDeprecatedVersions": { + "source-map-resolve": "*", + "resolve-url": "*", + "source-map-url": "*", + "urix": "*" } }, "webExt": { diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs index c85d8aa..3ad43b2 100755..100644 --- a/scripts/build/buildWeb.mjs +++ b/scripts/build/buildWeb.mjs @@ -20,13 +20,13 @@ import esbuild from "esbuild"; import { zip } from "fflate"; -import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"; -import { readFile } from "fs/promises"; -import { join, resolve } from "path"; +import { readFileSync } from "fs"; +import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises"; +import { join } from "path"; // wtf is this assert syntax import PackageJSON from "../../package.json" assert { type: "json" }; -import { commonOpts, fileIncludePlugin, gitHashPlugin, gitRemotePlugin, globPlugins, watch } from "./common.mjs"; +import { commonOpts, globPlugins, watch } from "./common.mjs"; /** * @type {esbuild.BuildOptions} @@ -39,9 +39,7 @@ const commonOptions = { external: ["plugins", "git-hash"], plugins: [ globPlugins, - gitHashPlugin, - gitRemotePlugin, - fileIncludePlugin + ...commonOpts.plugins, ], target: ["esnext"], define: { @@ -77,9 +75,13 @@ await Promise.all( ] ); +/** + * @type {(target: string, files: string[], shouldZip: boolean) => Promise<void>} + */ async function buildPluginZip(target, files, shouldZip) { const entries = { - "dist/Vencord.js": readFileSync("dist/browser.js"), + "dist/Vencord.js": await readFile("dist/browser.js"), + "dist/Vencord.css": await readFile("dist/browser.css"), ...Object.fromEntries(await Promise.all(files.map(async f => [ (f.startsWith("manifest") ? "manifest.json" : f), await readFile(join("browser", f)) @@ -87,29 +89,47 @@ async function buildPluginZip(target, files, shouldZip) { }; if (shouldZip) { - zip(entries, {}, (err, data) => { - if (err) { - console.error(err); - process.exitCode = 1; - } else { - writeFileSync("dist/" + target, data); - console.info("Extension written to dist/" + target); - } + return new Promise((resolve, reject) => { + zip(entries, {}, (err, data) => { + if (err) { + reject(err); + } else { + const out = join("dist", target); + writeFile(out, data).then(() => { + console.info("Extension written to " + out); + resolve(); + }).catch(reject); + } + }); }); } else { - if (existsSync(target)) - rmSync(target, { recursive: true }); - for (const entry in entries) { - const destination = "dist/" + target + "/" + entry; - const parentDirectory = resolve(destination, ".."); - mkdirSync(parentDirectory, { recursive: true }); - writeFileSync(destination, entries[entry]); - } + await rm(target, { recursive: true, force: true }); + await Promise.all(Object.entries(entries).map(async ([file, content]) => { + const dest = join("dist", target, file); + const parentDirectory = join(dest, ".."); + await mkdir(parentDirectory, { recursive: true }); + await writeFile(dest, content); + })); + console.info("Unpacked Extension written to dist/" + target); } } -await buildPluginZip("extension-v3.zip", ["modifyResponseHeaders.json", "content.js", "manifestv3.json"], true); -await buildPluginZip("extension-v2.zip", ["background.js", "content.js", "manifestv2.json"], true); -await buildPluginZip("extension-v2-unpacked", ["background.js", "content.js", "manifestv2.json"], false); +const cssText = "`" + readFileSync("dist/Vencord.user.css", "utf-8").replaceAll("`", "\\`") + "`"; +const cssRuntime = ` +;document.addEventListener("DOMContentLoaded", () => document.documentElement.appendChild( + Object.assign(document.createElement("style"), { + textContent: ${cssText}, + id: "vencord-css-core" + }), + { once: true } +)); +`; + +await Promise.all([ + appendFile("dist/Vencord.user.js", cssRuntime), + buildPluginZip("extension-v3.zip", ["modifyResponseHeaders.json", "content.js", "manifestv3.json"], true), + buildPluginZip("extension-v2.zip", ["background.js", "content.js", "manifestv2.json"], true), + buildPluginZip("extension-v2-unpacked", ["background.js", "content.js", "manifestv2.json"], false), +]); diff --git a/scripts/build/common.mjs b/scripts/build/common.mjs index 11aaa81..2743c70 100644 --- a/scripts/build/common.mjs +++ b/scripts/build/common.mjs @@ -17,9 +17,9 @@ */ import { exec, execSync } from "child_process"; -import { existsSync } from "fs"; +import { existsSync, readFileSync } from "fs"; import { readdir, readFile } from "fs/promises"; -import { join } from "path"; +import { join, relative } from "path"; import { promisify } from "util"; export const watch = process.argv.includes("--watch"); @@ -35,7 +35,7 @@ export const banner = { // https://github.com/evanw/esbuild/issues/619#issuecomment-751995294 /** - * @type {esbuild.Plugin} + * @type {import("esbuild").Plugin} */ export const makeAllPackagesExternalPlugin = { name: "make-all-packages-external", @@ -46,7 +46,7 @@ export const makeAllPackagesExternalPlugin = { }; /** - * @type {esbuild.Plugin} + * @type {import("esbuild").Plugin} */ export const globPlugins = { name: "glob-plugins", @@ -87,7 +87,7 @@ export const globPlugins = { }; /** - * @type {esbuild.Plugin} + * @type {import("esbuild").Plugin} */ export const gitHashPlugin = { name: "git-hash-plugin", @@ -103,7 +103,7 @@ export const gitHashPlugin = { }; /** - * @type {esbuild.Plugin} + * @type {import("esbuild").Plugin} */ export const gitRemotePlugin = { name: "git-remote-plugin", @@ -125,7 +125,7 @@ export const gitRemotePlugin = { }; /** - * @type {esbuild.Plugin} + * @type {import("esbuild").Plugin} */ export const fileIncludePlugin = { name: "file-include-plugin", @@ -147,6 +147,31 @@ export const fileIncludePlugin = { } }; +const styleModule = readFileSync("./scripts/build/module/style.js", "utf-8"); +/** + * @type {import("esbuild").Plugin} + */ +export const stylePlugin = { + name: "style-plugin", + setup: ({ onResolve, onLoad }) => { + onResolve({ filter: /\.css\?managed$/, namespace: "file" }, ({ path, resolveDir }) => ({ + path: relative(process.cwd(), join(resolveDir, path.replace("?managed", ""))), + namespace: "managed-style", + })); + onLoad({ filter: /\.css$/, namespace: "managed-style" }, async ({ path }) => { + const css = await readFile(path, "utf-8"); + const name = relative(process.cwd(), path).replaceAll("\\", "/"); + + return { + loader: "js", + contents: styleModule + .replaceAll("STYLE_SOURCE", JSON.stringify(css)) + .replaceAll("STYLE_NAME", JSON.stringify(name)) + }; + }); + } +}; + /** * @type {import("esbuild").BuildOptions} */ @@ -158,7 +183,7 @@ export const commonOpts = { sourcemap: watch ? "inline" : "", legalComments: "linked", banner, - plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin], + plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin], external: ["~plugins", "~git-hash", "~git-remote"], inject: ["./scripts/build/inject/react.mjs"], jsxFactory: "VencordCreateElement", diff --git a/scripts/build/module/style.js b/scripts/build/module/style.js new file mode 100644 index 0000000..5981a3d --- /dev/null +++ b/scripts/build/module/style.js @@ -0,0 +1,26 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +(window.VencordStyles ??= new Map()).set(STYLE_NAME, { + name: STYLE_NAME, + source: STYLE_SOURCE, + classNames: {}, + dom: null, +}); + +export default STYLE_NAME; diff --git a/src/Vencord.ts b/src/Vencord.ts index 464be2d..48e628f 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -18,7 +18,6 @@ export * as Api from "./api"; export * as Plugins from "./plugins"; -// eslint-disable-next-line @typescript-eslint/no-restricted-imports export * as Util from "./utils"; export * as QuickCss from "./utils/quickCss"; export * as Updater from "./utils/updater"; diff --git a/src/api/Styles.ts b/src/api/Styles.ts new file mode 100644 index 0000000..6b189ca --- /dev/null +++ b/src/api/Styles.ts @@ -0,0 +1,162 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import type { MapValue } from "type-fest/source/entry"; + +export type Style = MapValue<typeof VencordStyles>; + +export const styleMap = window.VencordStyles ??= new Map(); + +export function requireStyle(name: string) { + const style = styleMap.get(name); + if (!style) throw new Error(`Style "${name}" does not exist`); + return style; +} + +/** + * A style's name can be obtained from importing a stylesheet with `?managed` at the end of the import + * @param name The name of the style + * @returns `false` if the style was already enabled, `true` otherwise + * @example + * import pluginStyle from "./plugin.css?managed"; + * + * // Inside some plugin method like "start()" or "[option].onChange()" + * enableStyle(pluginStyle); + */ +export function enableStyle(name: string) { + const style = requireStyle(name); + + if (style.dom?.isConnected) + return false; + + if (!style.dom) { + style.dom = document.createElement("style"); + style.dom.dataset.vencordName = style.name; + } + compileStyle(style); + + document.head.appendChild(style.dom); + return true; +} + +/** + * @param name The name of the style + * @returns `false` if the style was already disabled, `true` otherwise + * @see {@link enableStyle} for info on getting the name of an imported style + */ +export function disableStyle(name: string) { + const style = requireStyle(name); + if (!style.dom?.isConnected) + return false; + + style.dom.remove(); + style.dom = null; + return true; +} + +/** + * @param name The name of the style + * @returns `true` in most cases, may return `false` in some edge cases + * @see {@link enableStyle} for info on getting the name of an imported style + */ +export const toggleStyle = (name: string) => isStyleEnabled(name) ? disableStyle(name) : enableStyle(name); + +/** + * @param name The name of the style + * @returns Whether the style is enabled + * @see {@link enableStyle} for info on getting the name of an imported style + */ +export const isStyleEnabled = (name: string) => requireStyle(name).dom?.isConnected ?? false; + +/** + * Sets the variables of a style + * ```ts + * // -- plugin.ts -- + * import pluginStyle from "./plugin.css?managed"; + * import { setStyleVars } from "@api/Styles"; + * import { findByPropsLazy } from "@webpack"; + * const classNames = findByPropsLazy("thin", "scrollerBase"); // { thin: "thin-31rlnD scrollerBase-_bVAAt", ... } + * + * // Inside some plugin method like "start()" + * setStyleClassNames(pluginStyle, classNames); + * enableStyle(pluginStyle); + * ``` + * ```scss + * // -- plugin.css -- + * .plugin-root [--thin]::-webkit-scrollbar { ... } + * ``` + * ```scss + * // -- final stylesheet -- + * .plugin-root .thin-31rlnD.scrollerBase-_bVAAt::-webkit-scrollbar { ... } + * ``` + * @param name The name of the style + * @param classNames An object where the keys are the variable names and the values are the variable values + * @param recompile Whether to recompile the style after setting the variables, defaults to `true` + * @see {@link enableStyle} for info on getting the name of an imported style + */ +export const setStyleClassNames = (name: string, classNames: Record<string, string>, recompile = true) => { + const style = requireStyle(name); + style.classNames = classNames; + if (recompile && isStyleEnabled(style.name)) + compileStyle(style); +}; + +/** + * Updates the stylesheet after doing the following to the sourcecode: + * - Interpolate style classnames + * @param style **_Must_ be a style with a DOM element** + * @see {@link setStyleClassNames} for more info on style classnames + */ +export const compileStyle = (style: Style) => { + if (!style.dom) throw new Error("Style has no DOM element"); + + style.dom.textContent = style.source + .replace(/\[--(\w+)\]/g, (match, name) => { + const className = style.classNames[name]; + return className ? classNameToSelector(className) : match; + }); +}; + +/** + * @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>; +/** + * @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 (typeof arg === "string") classNames.add(arg); + else if (Array.isArray(arg)) arg.forEach(name => classNames.add(name)); + else if (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/index.ts b/src/api/index.ts index 7e981e2..0fef99c 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -26,6 +26,7 @@ import * as $MessageEventsAPI from "./MessageEvents"; import * as $MessagePopover from "./MessagePopover"; import * as $Notices from "./Notices"; import * as $ServerList from "./ServerList"; +import * as $Styles from "./Styles"; /** * An API allowing you to listen to Message Clicks or run your own logic @@ -33,16 +34,16 @@ import * as $ServerList from "./ServerList"; * * If your plugin uses this, you must add MessageEventsAPI to its dependencies */ -const MessageEvents = $MessageEventsAPI; +export const MessageEvents = $MessageEventsAPI; /** * An API allowing you to create custom notices * (snackbars on the top, like the Update prompt) */ -const Notices = $Notices; +export const Notices = $Notices; /** * An API allowing you to register custom commands */ -const Commands = $Commands; +export const Commands = $Commands; /** * A wrapper around IndexedDB. This can store arbitrarily * large data and supports a lot of datatypes (Blob, Map, ...). @@ -57,30 +58,33 @@ const Commands = $Commands; * This is actually just idb-keyval, so if you're familiar with that, you're golden! * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types} */ -const DataStore = $DataStore; +export const DataStore = $DataStore; /** * An API allowing you to add custom components as message accessories */ -const MessageAccessories = $MessageAccessories; +export const MessageAccessories = $MessageAccessories; /** * An API allowing you to add custom buttons in the message popover */ -const MessagePopover = $MessagePopover; +export const MessagePopover = $MessagePopover; /** * An API allowing you to add badges to user profiles */ -const Badges = $Badges; +export const Badges = $Badges; /** * An API allowing you to add custom elements to the server list */ -const ServerList = $ServerList; +export const ServerList = $ServerList; /** * An API allowing you to add components as message accessories */ -const MessageDecorations = $MessageDecorations; +export const MessageDecorations = $MessageDecorations; /** * An API allowing you to add components to member list users, in both DM's and servers */ -const MemberListDecorators = $MemberListDecorators; - -export { Badges, Commands, DataStore, MemberListDecorators, MessageAccessories, MessageDecorations, MessageEvents, MessagePopover, Notices, ServerList }; +export const MemberListDecorators = $MemberListDecorators; +/** + * An API allowing you to dynamically load styles + * a + */ +export const Styles = $Styles; diff --git a/src/components/VencordSettings/index.tsx b/src/components/VencordSettings/index.tsx index b49e4b4..b3a3322 100644 --- a/src/components/VencordSettings/index.tsx +++ b/src/components/VencordSettings/index.tsx @@ -16,22 +16,18 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import "./settingsStyles.css"; + import ErrorBoundary from "@components/ErrorBoundary"; import { findByCodeLazy } from "@webpack"; import { Forms, Router, Text } from "@webpack/common"; -import cssText from "~fileContent/settingsStyles.css"; - import BackupRestoreTab from "./BackupRestoreTab"; import PluginsTab from "./PluginsTab"; import ThemesTab from "./ThemesTab"; import Updater from "./Updater"; import VencordSettings from "./VencordTab"; -const style = document.createElement("style"); -style.textContent = cssText; -document.head.appendChild(style); - const st = (style: string) => `vcSettings${style}`; const TabBar = findByCodeLazy('[role="tab"][aria-disabled="false"]'); diff --git a/src/globals.d.ts b/src/globals.d.ts index 2e8d444..6c5b437 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -38,6 +38,12 @@ declare global { export var VencordNative: typeof import("./VencordNative").default; export var Vencord: typeof import("./Vencord"); + export var VencordStyles: Map<string, { + name: string; + source: string; + classNames: Record<string, string>; + dom: HTMLStyleElement | null; + }>; export var appSettings: { set(setting: string, v: any): void; }; diff --git a/src/ipcMain/index.ts b/src/ipcMain/index.ts index 86a233c..ae8a96d 100644 --- a/src/ipcMain/index.ts +++ b/src/ipcMain/index.ts @@ -16,6 +16,7 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import "./legacy"; import "./updater"; import { debounce } from "@utils/debounce"; diff --git a/src/ipcMain/legacy.ts b/src/ipcMain/legacy.ts new file mode 100644 index 0000000..567ad3d --- /dev/null +++ b/src/ipcMain/legacy.ts @@ -0,0 +1,31 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import IpcEvents from "@utils/IpcEvents"; +import { ipcMain } from "electron"; +import { writeFile } from "fs/promises"; +import { join } from "path"; + +import { get } from "./simpleGet"; + +ipcMain.handleOnce(IpcEvents.DOWNLOAD_VENCORD_CSS, async () => { + const buf = await get("https://github.com/Vendicated/Vencord/releases/download/devbuild/renderer.css"); + await writeFile(join(__dirname, "renderer.css"), buf); + return buf.toString("utf-8"); +}); + diff --git a/src/ipcMain/updater/common.ts b/src/ipcMain/updater/common.ts index 41f08e8..3729c6d 100644 --- a/src/ipcMain/updater/common.ts +++ b/src/ipcMain/updater/common.ts @@ -24,7 +24,7 @@ export async function calculateHashes() { const hashes = {} as Record<string, string>; await Promise.all( - ["patcher.js", "preload.js", "renderer.js"].map(file => new Promise<void>(r => { + ["patcher.js", "preload.js", "renderer.js", "renderer.css"].map(file => new Promise<void>(r => { const fis = createReadStream(join(__dirname, file)); const hash = createHash("sha1", { encoding: "hex" }); fis.once("end", () => { diff --git a/src/ipcMain/updater/http.ts b/src/ipcMain/updater/http.ts index 902d644..3b38144 100644 --- a/src/ipcMain/updater/http.ts +++ b/src/ipcMain/updater/http.ts @@ -69,7 +69,7 @@ async function fetchUpdates() { return false; data.assets.forEach(({ name, browser_download_url }) => { - if (["patcher.js", "preload.js", "renderer.js"].some(s => name.startsWith(s))) { + if (["patcher.js", "preload.js", "renderer.js", "renderer.css"].some(s => name.startsWith(s))) { PendingUpdates.push([name, browser_download_url]); } }); diff --git a/src/modules.d.ts b/src/modules.d.ts index 6901260..c1a1996 100644 --- a/src/modules.d.ts +++ b/src/modules.d.ts @@ -37,3 +37,9 @@ declare module "~fileContent/*" { const content: string; export default content; } + +declare module "*.css" { } +declare module "*.css?managed" { + const name: string; + export default name; +} diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index abdb2f2..e650dbb 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -16,6 +16,8 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import "./messageLogger.css"; + import { Settings } from "@api/settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; @@ -42,51 +44,14 @@ export default definePlugin({ timestampModule: null as any, moment: null as Function | null, - css: ` - .messagelogger-red-overlay .messageLogger-deleted { - background-color: rgba(240, 71, 71, 0.15); - } - .messagelogger-red-text .messageLogger-deleted div { - color: #f04747; - } - - .messageLogger-deleted [class^="buttons"] { - display: none; - } - - .messageLogger-deleted-attachment { - filter: grayscale(1); - } - - .messageLogger-deleted-attachment:hover { - filter: grayscale(0); - transition: 250ms filter linear; - } - - .theme-dark .messageLogger-edited { - filter: brightness(80%); - } - - .theme-light .messageLogger-edited { - opacity: 0.5; - } - `, - start() { this.moment = findByPropsLazy("relativeTimeRounding", "relativeTimeThreshold"); this.timestampModule = findByPropsLazy("messageLogger_TimestampComponent"); - const style = this.style = document.createElement("style"); - style.textContent = this.css; - style.id = "MessageLogger-css"; - document.head.appendChild(style); - addDeleteStyleClass(); }, stop() { - this.style?.remove(); - document.querySelectorAll(".messageLogger-deleted").forEach(e => e.remove()); document.querySelectorAll(".messageLogger-edited").forEach(e => e.remove()); document.body.classList.remove("messagelogger-red-overlay"); diff --git a/src/plugins/messageLogger/messageLogger.css b/src/plugins/messageLogger/messageLogger.css new file mode 100644 index 0000000..94a3e25 --- /dev/null +++ b/src/plugins/messageLogger/messageLogger.css @@ -0,0 +1,27 @@ +.messagelogger-red-overlay .messageLogger-deleted { + background-color: rgba(240, 71, 71, 0.15); +} +.messagelogger-red-text .messageLogger-deleted div { + color: #f04747; +} + +.messageLogger-deleted [class^="buttons"] { + display: none; +} + +.messageLogger-deleted-attachment { + filter: grayscale(1); +} + +.messageLogger-deleted-attachment:hover { + filter: grayscale(0); + transition: 250ms filter linear; +} + +.theme-dark .messageLogger-edited { + filter: brightness(80%); +} + +.theme-light .messageLogger-edited { + opacity: 0.5; +} diff --git a/src/plugins/shikiCodeblocks/components/Header.tsx b/src/plugins/shikiCodeblocks/components/Header.tsx index c2db386..320dde9 100644 --- a/src/plugins/shikiCodeblocks/components/Header.tsx +++ b/src/plugins/shikiCodeblocks/components/Header.tsx @@ -33,7 +33,7 @@ export function Header({ langName, useDevIcon, shikiLang }: HeaderProps) { <div className={cl("lang")}> {useDevIcon !== DeviconSetting.Disabled && shikiLang?.devicon && ( <i - className={`devicon-${shikiLang.devicon}${useDevIcon === DeviconSetting.Color ? " colored" : ""}`} + className={`${cl("devicon")} devicon-${shikiLang.devicon}${useDevIcon === DeviconSetting.Color ? " colored" : ""}`} /> )} {langName} diff --git a/src/plugins/shikiCodeblocks/components/Highlighter.tsx b/src/plugins/shikiCodeblocks/components/Highlighter.tsx index d26cd81..badb3c8 100644 --- a/src/plugins/shikiCodeblocks/components/Highlighter.tsx +++ b/src/plugins/shikiCodeblocks/components/Highlighter.tsx @@ -90,14 +90,10 @@ export const Highlighter = ({ let langName; if (lang) langName = useHljs ? hljs?.getLanguage?.(lang)?.name : shikiLang?.name; - const preClasses = [cl("root")]; - if (!langName) preClasses.push(cl("plain")); - if (isPreview) preClasses.push(cl("preview")); - return ( <div ref={rootRef} - className={preClasses.join(" ")} + className={cl("root", { plain: !langName, preview: isPreview })} style={{ backgroundColor: useHljs ? themeBase.backgroundColor diff --git a/src/plugins/shikiCodeblocks/devicon.css b/src/plugins/shikiCodeblocks/devicon.css new file mode 100644 index 0000000..f5c4921 --- /dev/null +++ b/src/plugins/shikiCodeblocks/devicon.css @@ -0,0 +1 @@ +@import url('https://cdn.jsdelivr.net/gh/devicons/devicon@v2.10.1/devicon.min.css'); diff --git a/src/plugins/shikiCodeblocks/index.ts b/src/plugins/shikiCodeblocks/index.ts index 58e0048..428a273 100644 --- a/src/plugins/shikiCodeblocks/index.ts +++ b/src/plugins/shikiCodeblocks/index.ts @@ -16,23 +16,25 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import "./shiki.css"; + +import { disableStyle, enableStyle } from "@api/Styles"; import { Devs } from "@utils/constants"; import { parseUrl } from "@utils/misc"; import { wordsFromPascal, wordsToTitle } from "@utils/text"; import definePlugin, { OptionType } from "@utils/types"; import previewExampleText from "~fileContent/previewExample.tsx"; -import cssText from "~fileContent/shiki.css"; import { Settings } from "../../Vencord"; import { shiki } from "./api/shiki"; import { themes } from "./api/themes"; import { createHighlighter } from "./components/Highlighter"; -import { DeviconSetting, HljsSetting, ShikiSettings, StyleSheets } from "./types"; -import { clearStyles, removeStyle, setStyle } from "./utils/createStyle"; +import deviconStyle from "./devicon.css?managed"; +import { DeviconSetting, HljsSetting, ShikiSettings } from "./types"; +import { clearStyles } from "./utils/createStyle"; const themeNames = Object.keys(themes); -const devIconCss = "@import url('https://cdn.jsdelivr.net/gh/devicons/devicon@v2.10.1/devicon.min.css');"; const getSettings = () => Settings.plugins.ShikiCodeblocks as ShikiSettings; @@ -50,9 +52,8 @@ export default definePlugin({ }, ], start: async () => { - setStyle(cssText, StyleSheets.Main); if (getSettings().useDevIcon !== DeviconSetting.Disabled) - setStyle(devIconCss, StyleSheets.DevIcons); + enableStyle(deviconStyle); await shiki.init(getSettings().customTheme || getSettings().theme); }, @@ -135,8 +136,8 @@ export default definePlugin({ }, ], onChange: (newValue: DeviconSetting) => { - if (newValue === DeviconSetting.Disabled) removeStyle(StyleSheets.DevIcons); - else setStyle(devIconCss, StyleSheets.DevIcons); + if (newValue === DeviconSetting.Disabled) disableStyle(deviconStyle); + else enableStyle(deviconStyle); }, }, bgOpacity: { diff --git a/src/plugins/shikiCodeblocks/shiki.css b/src/plugins/shikiCodeblocks/shiki.css index b871d99..d71b673 100644 --- a/src/plugins/shikiCodeblocks/shiki.css +++ b/src/plugins/shikiCodeblocks/shiki.css @@ -1,6 +1,5 @@ .shiki-container { border: 4px; - /* fallback background */ background-color: var(--background-secondary); } @@ -22,8 +21,7 @@ border: none; } -.shiki-root [class^='devicon-'], -.shiki-root [class*=' devicon-'] { +.shiki-devicon { margin-right: 8px; user-select: none; } diff --git a/src/plugins/shikiCodeblocks/utils/misc.ts b/src/plugins/shikiCodeblocks/utils/misc.ts index 1342ff5..fefe938 100644 --- a/src/plugins/shikiCodeblocks/utils/misc.ts +++ b/src/plugins/shikiCodeblocks/utils/misc.ts @@ -16,13 +16,14 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { classNameFactory } from "@api/Styles"; import { hljs } from "@webpack/common"; import { resolveLang } from "../api/languages"; import { HighlighterProps } from "../components/Highlighter"; import { HljsSetting, ShikiSettings } from "../types"; -export const cl = (className: string) => `shiki-${className}`; +export const cl = classNameFactory("shiki-"); export const shouldUseHljs = ({ lang, diff --git a/src/plugins/spotifyControls/PlayerComponent.tsx b/src/plugins/spotifyControls/PlayerComponent.tsx index af53f59..f6ad08b 100644 --- a/src/plugins/spotifyControls/PlayerComponent.tsx +++ b/src/plugins/spotifyControls/PlayerComponent.tsx @@ -16,6 +16,8 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import "./spotifyStyles.css"; + import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; import { Link } from "@components/Link"; diff --git a/src/plugins/spotifyControls/SpotifyStore.ts b/src/plugins/spotifyControls/SpotifyStore.ts index 75448dc..641ba1a 100644 --- a/src/plugins/spotifyControls/SpotifyStore.ts +++ b/src/plugins/spotifyControls/SpotifyStore.ts @@ -21,8 +21,6 @@ import { proxyLazy } from "@utils/proxyLazy"; import { findByPropsLazy } from "@webpack"; import { Flux, FluxDispatcher } from "@webpack/common"; -import cssText from "~fileContent/spotifyStyles.css"; - export interface Track { id: string; name: string; @@ -69,11 +67,6 @@ type Repeat = "off" | "track" | "context"; // Don't wanna run before Flux and Dispatcher are ready! export const SpotifyStore = proxyLazy(() => { - // TODO: Move this elsewhere - const style = document.createElement("style"); - style.innerText = cssText; - document.head.appendChild(style); - // For some reason ts hates extends Flux.Store const { Store } = Flux; diff --git a/src/preload.ts b/src/preload.ts index dcf2554..7460081 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -44,6 +44,34 @@ contextBridge.exposeInMainWorld("VencordNative", VencordNative); if (location.protocol !== "data:") { // Discord webFrame.executeJavaScript(readFileSync(join(__dirname, "renderer.js"), "utf-8")); + const rendererCss = join(__dirname, "renderer.css"); + + function insertCss(css: string) { + const style = document.createElement("style"); + style.id = "vencord-css-core"; + style.textContent = css; + + if (document.readyState === "complete") { + document.documentElement.appendChild(style); + } else { + document.addEventListener("DOMContentLoaded", () => document.documentElement.appendChild(style), { + once: true + }); + } + } + + try { + const css = readFileSync(rendererCss, "utf-8"); + insertCss(css); + } catch (err) { + if ((err as NodeJS.ErrnoException)?.code !== "ENOENT") + throw err; + + // hack: the pre update updater does not download this file, so manually download it + // TODO: remove this in a future version + ipcRenderer.invoke(IpcEvents.DOWNLOAD_VENCORD_CSS) + .then(insertCss); + } require(process.env.DISCORD_PRELOAD!); } else { // Monaco Popout diff --git a/src/utils/IpcEvents.ts b/src/utils/IpcEvents.ts index c6696f8..345146b 100644 --- a/src/utils/IpcEvents.ts +++ b/src/utils/IpcEvents.ts @@ -44,5 +44,6 @@ export default strEnum({ UPDATE: "VencordUpdate", BUILD: "VencordBuild", GET_DESKTOP_CAPTURE_SOURCES: "VencordGetDesktopCaptureSources", - OPEN_MONACO_EDITOR: "VencordOpenMonacoEditor" + OPEN_MONACO_EDITOR: "VencordOpenMonacoEditor", + DOWNLOAD_VENCORD_CSS: "VencordDownloadVencordCss" } as const); diff --git a/src/utils/updater.ts b/src/utils/updater.ts index 2ea4953..04205a5 100644 --- a/src/utils/updater.ts +++ b/src/utils/updater.ts @@ -61,7 +61,7 @@ export function getRepo() { return Unwrap(VencordNative.ipc.invoke<IpcRes<string>>(IpcEvents.GET_REPO)); } -type Hashes = Record<"patcher.js" | "preload.js" | "renderer.js", string>; +type Hashes = Record<"patcher.js" | "preload.js" | "renderer.js" | "renderer.css", string>; /** * @returns true if hard restart is required diff --git a/src/webpack/common.tsx b/src/webpack/common.tsx index f2c42d1..a732d6b 100644 --- a/src/webpack/common.tsx +++ b/src/webpack/common.tsx @@ -36,6 +36,7 @@ export let React: typeof import("react"); export let useState: typeof React.useState; export let useEffect: typeof React.useEffect; export let useMemo: typeof React.useMemo; +export let useRef: typeof React.useRef; export const ReactDOM: typeof import("react-dom") = findByPropsLazy("createPortal", "render"); @@ -158,7 +159,7 @@ export const NavigationRouter = mapMangledModuleLazy("Transitioning to external waitFor("useState", m => { React = m; - ({ useEffect, useState, useMemo } = React); + ({ useEffect, useState, useMemo, useRef } = React); }); waitFor(["dispatch", "subscribe"], m => { |