feat: Proper CSS api & css bundle (#269)

Co-authored-by: Vap0r1ze <superdash993@gmail.com>
This commit is contained in:
Ven 2022-12-25 20:47:35 +01:00 committed by GitHub
parent 2172cae779
commit 2e5d27b6b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 438 additions and 126 deletions

@ -2,7 +2,18 @@ if (typeof browser === "undefined") {
var browser = chrome; var browser = chrome;
} }
var script = document.createElement("script"); const script = document.createElement("script");
script.src = browser.runtime.getURL("dist/Vencord.js"); 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 }
);

@ -18,7 +18,7 @@
"js": ["content.js"] "js": ["content.js"]
} }
], ],
"web_accessible_resources": ["dist/Vencord.js"], "web_accessible_resources": ["dist/Vencord.js", "dist/Vencord.css"],
"background": { "background": {
"scripts": ["background.js"] "scripts": ["background.js"]
} }

@ -23,7 +23,7 @@
"web_accessible_resources": [ "web_accessible_resources": [
{ {
"resources": ["dist/Vencord.js"], "resources": ["dist/Vencord.js", "dist/Vencord.css"],
"matches": ["*://*.discord.com/*"] "matches": ["*://*.discord.com/*"]
} }
], ],

@ -65,6 +65,17 @@
"patchedDependencies": { "patchedDependencies": {
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch", "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" "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": { "webExt": {

66
scripts/build/buildWeb.mjs Executable file → Normal file

@ -20,13 +20,13 @@
import esbuild from "esbuild"; import esbuild from "esbuild";
import { zip } from "fflate"; import { zip } from "fflate";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"; import { readFileSync } from "fs";
import { readFile } from "fs/promises"; import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
import { join, resolve } from "path"; import { join } from "path";
// wtf is this assert syntax // wtf is this assert syntax
import PackageJSON from "../../package.json" assert { type: "json" }; 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} * @type {esbuild.BuildOptions}
@ -39,9 +39,7 @@ const commonOptions = {
external: ["plugins", "git-hash"], external: ["plugins", "git-hash"],
plugins: [ plugins: [
globPlugins, globPlugins,
gitHashPlugin, ...commonOpts.plugins,
gitRemotePlugin,
fileIncludePlugin
], ],
target: ["esnext"], target: ["esnext"],
define: { define: {
@ -77,9 +75,13 @@ await Promise.all(
] ]
); );
/**
* @type {(target: string, files: string[], shouldZip: boolean) => Promise<void>}
*/
async function buildPluginZip(target, files, shouldZip) { async function buildPluginZip(target, files, shouldZip) {
const entries = { 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 => [ ...Object.fromEntries(await Promise.all(files.map(async f => [
(f.startsWith("manifest") ? "manifest.json" : f), (f.startsWith("manifest") ? "manifest.json" : f),
await readFile(join("browser", f)) await readFile(join("browser", f))
@ -87,29 +89,47 @@ async function buildPluginZip(target, files, shouldZip) {
}; };
if (shouldZip) { if (shouldZip) {
return new Promise((resolve, reject) => {
zip(entries, {}, (err, data) => { zip(entries, {}, (err, data) => {
if (err) { if (err) {
console.error(err); reject(err);
process.exitCode = 1;
} else { } else {
writeFileSync("dist/" + target, data); const out = join("dist", target);
console.info("Extension written to dist/" + target); writeFile(out, data).then(() => {
console.info("Extension written to " + out);
resolve();
}).catch(reject);
} }
}); });
});
} else { } else {
if (existsSync(target)) await rm(target, { recursive: true, force: true });
rmSync(target, { recursive: true }); await Promise.all(Object.entries(entries).map(async ([file, content]) => {
for (const entry in entries) { const dest = join("dist", target, file);
const destination = "dist/" + target + "/" + entry; const parentDirectory = join(dest, "..");
const parentDirectory = resolve(destination, ".."); await mkdir(parentDirectory, { recursive: true });
mkdirSync(parentDirectory, { recursive: true }); await writeFile(dest, content);
writeFileSync(destination, entries[entry]); }));
}
console.info("Unpacked Extension written to dist/" + target); console.info("Unpacked Extension written to dist/" + target);
} }
} }
await buildPluginZip("extension-v3.zip", ["modifyResponseHeaders.json", "content.js", "manifestv3.json"], true); const cssText = "`" + readFileSync("dist/Vencord.user.css", "utf-8").replaceAll("`", "\\`") + "`";
await buildPluginZip("extension-v2.zip", ["background.js", "content.js", "manifestv2.json"], true); const cssRuntime = `
await buildPluginZip("extension-v2-unpacked", ["background.js", "content.js", "manifestv2.json"], false); ;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),
]);

@ -17,9 +17,9 @@
*/ */
import { exec, execSync } from "child_process"; import { exec, execSync } from "child_process";
import { existsSync } from "fs"; import { existsSync, readFileSync } from "fs";
import { readdir, readFile } from "fs/promises"; import { readdir, readFile } from "fs/promises";
import { join } from "path"; import { join, relative } from "path";
import { promisify } from "util"; import { promisify } from "util";
export const watch = process.argv.includes("--watch"); export const watch = process.argv.includes("--watch");
@ -35,7 +35,7 @@ export const banner = {
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294 // https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
/** /**
* @type {esbuild.Plugin} * @type {import("esbuild").Plugin}
*/ */
export const makeAllPackagesExternalPlugin = { export const makeAllPackagesExternalPlugin = {
name: "make-all-packages-external", name: "make-all-packages-external",
@ -46,7 +46,7 @@ export const makeAllPackagesExternalPlugin = {
}; };
/** /**
* @type {esbuild.Plugin} * @type {import("esbuild").Plugin}
*/ */
export const globPlugins = { export const globPlugins = {
name: "glob-plugins", name: "glob-plugins",
@ -87,7 +87,7 @@ export const globPlugins = {
}; };
/** /**
* @type {esbuild.Plugin} * @type {import("esbuild").Plugin}
*/ */
export const gitHashPlugin = { export const gitHashPlugin = {
name: "git-hash-plugin", name: "git-hash-plugin",
@ -103,7 +103,7 @@ export const gitHashPlugin = {
}; };
/** /**
* @type {esbuild.Plugin} * @type {import("esbuild").Plugin}
*/ */
export const gitRemotePlugin = { export const gitRemotePlugin = {
name: "git-remote-plugin", name: "git-remote-plugin",
@ -125,7 +125,7 @@ export const gitRemotePlugin = {
}; };
/** /**
* @type {esbuild.Plugin} * @type {import("esbuild").Plugin}
*/ */
export const fileIncludePlugin = { export const fileIncludePlugin = {
name: "file-include-plugin", 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} * @type {import("esbuild").BuildOptions}
*/ */
@ -158,7 +183,7 @@ export const commonOpts = {
sourcemap: watch ? "inline" : "", sourcemap: watch ? "inline" : "",
legalComments: "linked", legalComments: "linked",
banner, banner,
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin], plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
external: ["~plugins", "~git-hash", "~git-remote"], external: ["~plugins", "~git-hash", "~git-remote"],
inject: ["./scripts/build/inject/react.mjs"], inject: ["./scripts/build/inject/react.mjs"],
jsxFactory: "VencordCreateElement", jsxFactory: "VencordCreateElement",

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

@ -18,7 +18,6 @@
export * as Api from "./api"; export * as Api from "./api";
export * as Plugins from "./plugins"; export * as Plugins from "./plugins";
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
export * as Util from "./utils"; export * as Util from "./utils";
export * as QuickCss from "./utils/quickCss"; export * as QuickCss from "./utils/quickCss";
export * as Updater from "./utils/updater"; export * as Updater from "./utils/updater";

162
src/api/Styles.ts Normal file

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

@ -26,6 +26,7 @@ import * as $MessageEventsAPI from "./MessageEvents";
import * as $MessagePopover from "./MessagePopover"; import * as $MessagePopover from "./MessagePopover";
import * as $Notices from "./Notices"; import * as $Notices from "./Notices";
import * as $ServerList from "./ServerList"; import * as $ServerList from "./ServerList";
import * as $Styles from "./Styles";
/** /**
* An API allowing you to listen to Message Clicks or run your own logic * 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 * 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 * An API allowing you to create custom notices
* (snackbars on the top, like the Update prompt) * (snackbars on the top, like the Update prompt)
*/ */
const Notices = $Notices; export const Notices = $Notices;
/** /**
* An API allowing you to register custom commands * An API allowing you to register custom commands
*/ */
const Commands = $Commands; export const Commands = $Commands;
/** /**
* A wrapper around IndexedDB. This can store arbitrarily * A wrapper around IndexedDB. This can store arbitrarily
* large data and supports a lot of datatypes (Blob, Map, ...). * 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! * 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} * @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 * 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 * 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 * 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 * 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 * 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 * An API allowing you to add components to member list users, in both DM's and servers
*/ */
const MemberListDecorators = $MemberListDecorators; export const MemberListDecorators = $MemberListDecorators;
/**
export { Badges, Commands, DataStore, MemberListDecorators, MessageAccessories, MessageDecorations, MessageEvents, MessagePopover, Notices, ServerList }; * An API allowing you to dynamically load styles
* a
*/
export const Styles = $Styles;

@ -16,22 +16,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import "./settingsStyles.css";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { findByCodeLazy } from "@webpack"; import { findByCodeLazy } from "@webpack";
import { Forms, Router, Text } from "@webpack/common"; import { Forms, Router, Text } from "@webpack/common";
import cssText from "~fileContent/settingsStyles.css";
import BackupRestoreTab from "./BackupRestoreTab"; import BackupRestoreTab from "./BackupRestoreTab";
import PluginsTab from "./PluginsTab"; import PluginsTab from "./PluginsTab";
import ThemesTab from "./ThemesTab"; import ThemesTab from "./ThemesTab";
import Updater from "./Updater"; import Updater from "./Updater";
import VencordSettings from "./VencordTab"; import VencordSettings from "./VencordTab";
const style = document.createElement("style");
style.textContent = cssText;
document.head.appendChild(style);
const st = (style: string) => `vcSettings${style}`; const st = (style: string) => `vcSettings${style}`;
const TabBar = findByCodeLazy('[role="tab"][aria-disabled="false"]'); const TabBar = findByCodeLazy('[role="tab"][aria-disabled="false"]');

6
src/globals.d.ts vendored

@ -38,6 +38,12 @@ declare global {
export var VencordNative: typeof import("./VencordNative").default; export var VencordNative: typeof import("./VencordNative").default;
export var Vencord: typeof import("./Vencord"); 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: { export var appSettings: {
set(setting: string, v: any): void; set(setting: string, v: any): void;
}; };

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import "./legacy";
import "./updater"; import "./updater";
import { debounce } from "@utils/debounce"; import { debounce } from "@utils/debounce";

31
src/ipcMain/legacy.ts Normal file

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

@ -24,7 +24,7 @@ export async function calculateHashes() {
const hashes = {} as Record<string, string>; const hashes = {} as Record<string, string>;
await Promise.all( 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 fis = createReadStream(join(__dirname, file));
const hash = createHash("sha1", { encoding: "hex" }); const hash = createHash("sha1", { encoding: "hex" });
fis.once("end", () => { fis.once("end", () => {

@ -69,7 +69,7 @@ async function fetchUpdates() {
return false; return false;
data.assets.forEach(({ name, browser_download_url }) => { 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]); PendingUpdates.push([name, browser_download_url]);
} }
}); });

6
src/modules.d.ts vendored

@ -37,3 +37,9 @@ declare module "~fileContent/*" {
const content: string; const content: string;
export default content; export default content;
} }
declare module "*.css" { }
declare module "*.css?managed" {
const name: string;
export default name;
}

@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import "./messageLogger.css";
import { Settings } from "@api/settings"; import { Settings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
@ -42,51 +44,14 @@ export default definePlugin({
timestampModule: null as any, timestampModule: null as any,
moment: null as Function | null, 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() { start() {
this.moment = findByPropsLazy("relativeTimeRounding", "relativeTimeThreshold"); this.moment = findByPropsLazy("relativeTimeRounding", "relativeTimeThreshold");
this.timestampModule = findByPropsLazy("messageLogger_TimestampComponent"); 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(); addDeleteStyleClass();
}, },
stop() { stop() {
this.style?.remove();
document.querySelectorAll(".messageLogger-deleted").forEach(e => e.remove()); document.querySelectorAll(".messageLogger-deleted").forEach(e => e.remove());
document.querySelectorAll(".messageLogger-edited").forEach(e => e.remove()); document.querySelectorAll(".messageLogger-edited").forEach(e => e.remove());
document.body.classList.remove("messagelogger-red-overlay"); document.body.classList.remove("messagelogger-red-overlay");

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

@ -33,7 +33,7 @@ export function Header({ langName, useDevIcon, shikiLang }: HeaderProps) {
<div className={cl("lang")}> <div className={cl("lang")}>
{useDevIcon !== DeviconSetting.Disabled && shikiLang?.devicon && ( {useDevIcon !== DeviconSetting.Disabled && shikiLang?.devicon && (
<i <i
className={`devicon-${shikiLang.devicon}${useDevIcon === DeviconSetting.Color ? " colored" : ""}`} className={`${cl("devicon")} devicon-${shikiLang.devicon}${useDevIcon === DeviconSetting.Color ? " colored" : ""}`}
/> />
)} )}
{langName} {langName}

@ -90,14 +90,10 @@ export const Highlighter = ({
let langName; let langName;
if (lang) langName = useHljs ? hljs?.getLanguage?.(lang)?.name : shikiLang?.name; 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 ( return (
<div <div
ref={rootRef} ref={rootRef}
className={preClasses.join(" ")} className={cl("root", { plain: !langName, preview: isPreview })}
style={{ style={{
backgroundColor: useHljs backgroundColor: useHljs
? themeBase.backgroundColor ? themeBase.backgroundColor

@ -0,0 +1 @@
@import url('https://cdn.jsdelivr.net/gh/devicons/devicon@v2.10.1/devicon.min.css');

@ -16,23 +16,25 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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 { Devs } from "@utils/constants";
import { parseUrl } from "@utils/misc"; import { parseUrl } from "@utils/misc";
import { wordsFromPascal, wordsToTitle } from "@utils/text"; import { wordsFromPascal, wordsToTitle } from "@utils/text";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import previewExampleText from "~fileContent/previewExample.tsx"; import previewExampleText from "~fileContent/previewExample.tsx";
import cssText from "~fileContent/shiki.css";
import { Settings } from "../../Vencord"; import { Settings } from "../../Vencord";
import { shiki } from "./api/shiki"; import { shiki } from "./api/shiki";
import { themes } from "./api/themes"; import { themes } from "./api/themes";
import { createHighlighter } from "./components/Highlighter"; import { createHighlighter } from "./components/Highlighter";
import { DeviconSetting, HljsSetting, ShikiSettings, StyleSheets } from "./types"; import deviconStyle from "./devicon.css?managed";
import { clearStyles, removeStyle, setStyle } from "./utils/createStyle"; import { DeviconSetting, HljsSetting, ShikiSettings } from "./types";
import { clearStyles } from "./utils/createStyle";
const themeNames = Object.keys(themes); 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; const getSettings = () => Settings.plugins.ShikiCodeblocks as ShikiSettings;
@ -50,9 +52,8 @@ export default definePlugin({
}, },
], ],
start: async () => { start: async () => {
setStyle(cssText, StyleSheets.Main);
if (getSettings().useDevIcon !== DeviconSetting.Disabled) if (getSettings().useDevIcon !== DeviconSetting.Disabled)
setStyle(devIconCss, StyleSheets.DevIcons); enableStyle(deviconStyle);
await shiki.init(getSettings().customTheme || getSettings().theme); await shiki.init(getSettings().customTheme || getSettings().theme);
}, },
@ -135,8 +136,8 @@ export default definePlugin({
}, },
], ],
onChange: (newValue: DeviconSetting) => { onChange: (newValue: DeviconSetting) => {
if (newValue === DeviconSetting.Disabled) removeStyle(StyleSheets.DevIcons); if (newValue === DeviconSetting.Disabled) disableStyle(deviconStyle);
else setStyle(devIconCss, StyleSheets.DevIcons); else enableStyle(deviconStyle);
}, },
}, },
bgOpacity: { bgOpacity: {

@ -1,6 +1,5 @@
.shiki-container { .shiki-container {
border: 4px; border: 4px;
/* fallback background */
background-color: var(--background-secondary); background-color: var(--background-secondary);
} }
@ -22,8 +21,7 @@
border: none; border: none;
} }
.shiki-root [class^='devicon-'], .shiki-devicon {
.shiki-root [class*=' devicon-'] {
margin-right: 8px; margin-right: 8px;
user-select: none; user-select: none;
} }

@ -16,13 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { classNameFactory } from "@api/Styles";
import { hljs } from "@webpack/common"; import { hljs } from "@webpack/common";
import { resolveLang } from "../api/languages"; import { resolveLang } from "../api/languages";
import { HighlighterProps } from "../components/Highlighter"; import { HighlighterProps } from "../components/Highlighter";
import { HljsSetting, ShikiSettings } from "../types"; import { HljsSetting, ShikiSettings } from "../types";
export const cl = (className: string) => `shiki-${className}`; export const cl = classNameFactory("shiki-");
export const shouldUseHljs = ({ export const shouldUseHljs = ({
lang, lang,

@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import "./spotifyStyles.css";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { Link } from "@components/Link"; import { Link } from "@components/Link";

@ -21,8 +21,6 @@ import { proxyLazy } from "@utils/proxyLazy";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { Flux, FluxDispatcher } from "@webpack/common"; import { Flux, FluxDispatcher } from "@webpack/common";
import cssText from "~fileContent/spotifyStyles.css";
export interface Track { export interface Track {
id: string; id: string;
name: string; name: string;
@ -69,11 +67,6 @@ type Repeat = "off" | "track" | "context";
// Don't wanna run before Flux and Dispatcher are ready! // Don't wanna run before Flux and Dispatcher are ready!
export const SpotifyStore = proxyLazy(() => { 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 // For some reason ts hates extends Flux.Store
const { Store } = Flux; const { Store } = Flux;

@ -44,6 +44,34 @@ contextBridge.exposeInMainWorld("VencordNative", VencordNative);
if (location.protocol !== "data:") { if (location.protocol !== "data:") {
// Discord // Discord
webFrame.executeJavaScript(readFileSync(join(__dirname, "renderer.js"), "utf-8")); 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!); require(process.env.DISCORD_PRELOAD!);
} else { } else {
// Monaco Popout // Monaco Popout

@ -44,5 +44,6 @@ export default strEnum({
UPDATE: "VencordUpdate", UPDATE: "VencordUpdate",
BUILD: "VencordBuild", BUILD: "VencordBuild",
GET_DESKTOP_CAPTURE_SOURCES: "VencordGetDesktopCaptureSources", GET_DESKTOP_CAPTURE_SOURCES: "VencordGetDesktopCaptureSources",
OPEN_MONACO_EDITOR: "VencordOpenMonacoEditor" OPEN_MONACO_EDITOR: "VencordOpenMonacoEditor",
DOWNLOAD_VENCORD_CSS: "VencordDownloadVencordCss"
} as const); } as const);

@ -61,7 +61,7 @@ export function getRepo() {
return Unwrap(VencordNative.ipc.invoke<IpcRes<string>>(IpcEvents.GET_REPO)); 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 * @returns true if hard restart is required

@ -36,6 +36,7 @@ export let React: typeof import("react");
export let useState: typeof React.useState; export let useState: typeof React.useState;
export let useEffect: typeof React.useEffect; export let useEffect: typeof React.useEffect;
export let useMemo: typeof React.useMemo; export let useMemo: typeof React.useMemo;
export let useRef: typeof React.useRef;
export const ReactDOM: typeof import("react-dom") = findByPropsLazy("createPortal", "render"); export const ReactDOM: typeof import("react-dom") = findByPropsLazy("createPortal", "render");
@ -158,7 +159,7 @@ export const NavigationRouter = mapMangledModuleLazy("Transitioning to external
waitFor("useState", m => { waitFor("useState", m => {
React = m; React = m;
({ useEffect, useState, useMemo } = React); ({ useEffect, useState, useMemo, useRef } = React);
}); });
waitFor(["dispatch", "subscribe"], m => { waitFor(["dispatch", "subscribe"], m => {