Compare commits

...

3 commits

13 changed files with 615 additions and 9 deletions

28
.github/workflows/flatpak-node.yml vendored Normal file
View file

@ -0,0 +1,28 @@
name: Upload generated-sources.json to release for Flatpak building
on:
release:
types:
- published
workflow_dispatch:
permissions:
contents: write
jobs:
upload:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install flatpak-node-generator
run: pipx install git+https://github.com/flatpak/flatpak-builder-tools.git#subdirectory=node
- name: Create generated-sources.json
run: /root/.local/bin/flatpak-node-generator pnpm pnpm-lock.yaml --node-sdk-extension org.freedesktop.Sdk.Extension.node26 --electron-node-headers
- name: Upload generated-sources.json to release
run: |
gh release upload ${{ github.event.release.tag_name }} generated-sources.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -0,0 +1,82 @@
# Legcord Clipboard Fallback Plugin
Fixes Discord in-page copy actions in Legcord, including:
- Copy User ID
- Copy Message ID
- Copy Message Link
- Other Discord menu actions that call `navigator.clipboard.writeText(...)`
## Why this exists
In affected Legcord/Electron environments, Discord's web UI calls:
```js
navigator.clipboard.writeText(text)
```
but Chromium rejects it, commonly with errors like:
```text
NotAllowedError: Failed to execute 'writeText' on 'Clipboard': Document is not focused.
```
or Legcord logs:
```text
Unable to determine render window for element [object HTMLDocument]
```
This plugin patches `navigator.clipboard.writeText` in the Discord page and falls back to a selection-based `document.execCommand("copy")` copy path.
## Known limitation
Legcord's native **Copy Image** context-menu action does not go through `navigator.clipboard.writeText` or `navigator.clipboard.write` in the page. It is handled by Electron's main-process context menu (`webContents.copyImageAt(...)`), so a renderer/custom-bundle plugin cannot reliably fix image copying. That needs a Legcord main-process fix or a filesystem plugin with main/preload access on newer Legcord versions.
## Install on Legcord versions with filesystem plugins
1. Open the Legcord plugins folder:
```text
~/Library/Application Support/legcord/plugins
```
2. Create this folder:
```text
clipboard-fallback
```
3. Copy these files into it:
```text
manifest.json
renderer.js
```
4. Restart Legcord.
5. Enable **Clipboard Fallback** in Legcord's plugin settings.
## Older Legcord workaround: custom bundle
If your Legcord version does not have filesystem plugins yet, copy `custom-bundle.js` into:
```text
~/Library/Application Support/legcord/custom.js
```
Do **not** use `renderer.js` as `custom.js`; `renderer.js` is the filesystem-plugin entry and expects Legcord's plugin loader to provide `module.exports`.
and add `"custom"` to the `mods` array in:
```text
~/Library/Application Support/legcord/storage/settings.json
```
Example:
```json
"mods": ["equicord", "custom"]
```
Then restart Legcord.

View file

@ -0,0 +1,216 @@
(() => {
const module = { exports: {} };
const api = {
logger: {
log: (...args) => console.log("[ClipboardFallback]", ...args),
warn: (...args) => console.warn("[ClipboardFallback]", ...args),
error: (...args) => console.error("[ClipboardFallback]", ...args),
},
};
/**
* Legcord Clipboard Fallback
*
* Discord's web UI uses navigator.clipboard.writeText for actions such as
* "Copy User ID" and "Copy Message Link", and navigator.clipboard.write for
* richer clipboard payloads such as images. In some Legcord/Electron/macOS
* combinations Chromium rejects those calls because the document is not focused
* or the clipboard permission is not granted, leaving the clipboard unchanged.
*
* This renderer plugin replaces those APIs with selection-based copy fallbacks
* that run inside the original click gesture.
*/
module.exports.activate = (api) => {
const PATCH_KEY = Symbol.for("legcord.clipboardFallback.installed");
function install() {
try {
if (!navigator.clipboard) {
api.logger.warn("navigator.clipboard is unavailable");
return;
}
if (navigator.clipboard[PATCH_KEY]) return;
const originalWriteText = navigator.clipboard.writeText?.bind(navigator.clipboard);
const originalWrite = navigator.clipboard.write?.bind(navigator.clipboard);
async function blobToDataUrl(blob) {
if (typeof FileReader !== "undefined") {
return await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result));
reader.onerror = () => reject(reader.error ?? new Error("Failed to read clipboard image"));
reader.readAsDataURL(blob);
});
}
// Test/runtime fallback for environments with Blob but no FileReader.
const bytes = new Uint8Array(await blob.arrayBuffer());
let binary = "";
for (const byte of bytes) binary += String.fromCharCode(byte);
const base64 = typeof btoa === "function" ? btoa(binary) : Buffer.from(bytes).toString("base64");
return `data:${blob.type || "application/octet-stream"};base64,${base64}`;
}
function selectElementForCopy(element) {
const previousActiveElement = document.activeElement;
const selection = window.getSelection?.() ?? globalThis.getSelection?.();
const range = document.createRange();
element.focus?.();
range.selectNodeContents(element);
selection?.removeAllRanges();
selection?.addRange(range);
const copied = document.execCommand("copy");
selection?.removeAllRanges();
if (previousActiveElement && typeof previousActiveElement.focus === "function") {
try {
previousActiveElement.focus();
} catch {}
}
if (!copied) throw new Error("document.execCommand('copy') returned false");
}
async function fallbackCopyText(text) {
const value = String(text);
const parent = document.body || document.documentElement;
if (!parent) throw new Error("No document body available for clipboard fallback");
const textarea = document.createElement("textarea");
textarea.value = value;
textarea.setAttribute("readonly", "");
textarea.setAttribute("aria-hidden", "true");
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
textarea.style.top = "0";
textarea.style.width = "1px";
textarea.style.height = "1px";
textarea.style.opacity = "0";
textarea.style.pointerEvents = "none";
parent.appendChild(textarea);
const previousActiveElement = document.activeElement;
textarea.focus();
textarea.select();
textarea.setSelectionRange(0, value.length);
const copied = document.execCommand("copy");
textarea.remove();
if (previousActiveElement && typeof previousActiveElement.focus === "function") {
try {
previousActiveElement.focus();
} catch {}
}
if (!copied) throw new Error("document.execCommand('copy') returned false");
}
async function getClipboardItemType(item, type) {
if (!item?.types?.includes(type) || typeof item.getType !== "function") return null;
return await item.getType(type);
}
async function fallbackCopyItems(items) {
const parent = document.body || document.documentElement;
if (!parent) throw new Error("No document body available for clipboard fallback");
const container = document.createElement("div");
container.contentEditable = "true";
container.setAttribute("aria-hidden", "true");
container.style.position = "fixed";
container.style.left = "-9999px";
container.style.top = "0";
container.style.width = "1px";
container.style.height = "1px";
container.style.overflow = "hidden";
for (const item of items) {
const htmlBlob = await getClipboardItemType(item, "text/html");
if (htmlBlob) {
container.innerHTML += await htmlBlob.text();
continue;
}
const textBlob = await getClipboardItemType(item, "text/plain");
if (textBlob) {
const span = document.createElement("span");
span.textContent = await textBlob.text();
container.appendChild(span);
continue;
}
const imageType = item?.types?.find((type) => type.startsWith("image/"));
if (imageType && typeof item.getType === "function") {
const imageBlob = await item.getType(imageType);
const image = document.createElement("img");
image.src = await blobToDataUrl(imageBlob);
image.alt = "";
container.appendChild(image);
}
}
if (!container.innerHTML && !container.textContent && !container.children?.length) {
throw new Error("No supported clipboard item types found");
}
parent.appendChild(container);
selectElementForCopy(container);
container.remove();
}
if (originalWriteText) {
Object.defineProperty(navigator.clipboard, "writeText", {
configurable: true,
value: async (text) => {
try {
await fallbackCopyText(text);
api.logger.log("copied text via fallback", text);
} catch (fallbackError) {
api.logger.warn("text fallback failed; trying original writeText", fallbackError);
return originalWriteText(text);
}
},
});
}
if (originalWrite) {
Object.defineProperty(navigator.clipboard, "write", {
configurable: true,
value: async (items) => {
try {
await fallbackCopyItems(items);
api.logger.log("copied rich clipboard payload via fallback");
} catch (fallbackError) {
api.logger.warn("rich clipboard fallback failed; trying original write", fallbackError);
return originalWrite(items);
}
},
});
}
Object.defineProperty(navigator.clipboard, PATCH_KEY, {
configurable: false,
enumerable: false,
value: true,
});
api.logger.log("installed");
} catch (error) {
api.logger.error("install failed", error);
}
}
install();
window.addEventListener("DOMContentLoaded", install, { once: true });
};
if (typeof module.exports.activate === "function") {
module.exports.activate(api);
}
})();

View file

@ -0,0 +1,9 @@
{
"id": "clipboard-fallback",
"name": "Clipboard Fallback",
"version": "1.1.0",
"description": "Fixes Discord in-page text copy actions in Legcord by falling back to document.execCommand('copy') when navigator.clipboard.writeText is blocked.",
"author": "Nigel Thornberry",
"compatibleVersions": ["*"],
"renderer": "renderer.js"
}

View file

@ -0,0 +1,201 @@
/**
* Legcord Clipboard Fallback
*
* Discord's web UI uses navigator.clipboard.writeText for actions such as
* "Copy User ID" and "Copy Message Link", and navigator.clipboard.write for
* richer clipboard payloads such as images. In some Legcord/Electron/macOS
* combinations Chromium rejects those calls because the document is not focused
* or the clipboard permission is not granted, leaving the clipboard unchanged.
*
* This renderer plugin replaces those APIs with selection-based copy fallbacks
* that run inside the original click gesture.
*/
module.exports.activate = (api) => {
const PATCH_KEY = Symbol.for("legcord.clipboardFallback.installed");
function install() {
try {
if (!navigator.clipboard) {
api.logger.warn("navigator.clipboard is unavailable");
return;
}
if (navigator.clipboard[PATCH_KEY]) return;
const originalWriteText = navigator.clipboard.writeText?.bind(navigator.clipboard);
const originalWrite = navigator.clipboard.write?.bind(navigator.clipboard);
async function blobToDataUrl(blob) {
if (typeof FileReader !== "undefined") {
return await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result));
reader.onerror = () => reject(reader.error ?? new Error("Failed to read clipboard image"));
reader.readAsDataURL(blob);
});
}
// Test/runtime fallback for environments with Blob but no FileReader.
const bytes = new Uint8Array(await blob.arrayBuffer());
let binary = "";
for (const byte of bytes) binary += String.fromCharCode(byte);
const base64 = typeof btoa === "function" ? btoa(binary) : Buffer.from(bytes).toString("base64");
return `data:${blob.type || "application/octet-stream"};base64,${base64}`;
}
function selectElementForCopy(element) {
const previousActiveElement = document.activeElement;
const selection = window.getSelection?.() ?? globalThis.getSelection?.();
const range = document.createRange();
element.focus?.();
range.selectNodeContents(element);
selection?.removeAllRanges();
selection?.addRange(range);
const copied = document.execCommand("copy");
selection?.removeAllRanges();
if (previousActiveElement && typeof previousActiveElement.focus === "function") {
try {
previousActiveElement.focus();
} catch {}
}
if (!copied) throw new Error("document.execCommand('copy') returned false");
}
async function fallbackCopyText(text) {
const value = String(text);
const parent = document.body || document.documentElement;
if (!parent) throw new Error("No document body available for clipboard fallback");
const textarea = document.createElement("textarea");
textarea.value = value;
textarea.setAttribute("readonly", "");
textarea.setAttribute("aria-hidden", "true");
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
textarea.style.top = "0";
textarea.style.width = "1px";
textarea.style.height = "1px";
textarea.style.opacity = "0";
textarea.style.pointerEvents = "none";
parent.appendChild(textarea);
const previousActiveElement = document.activeElement;
textarea.focus();
textarea.select();
textarea.setSelectionRange(0, value.length);
const copied = document.execCommand("copy");
textarea.remove();
if (previousActiveElement && typeof previousActiveElement.focus === "function") {
try {
previousActiveElement.focus();
} catch {}
}
if (!copied) throw new Error("document.execCommand('copy') returned false");
}
async function getClipboardItemType(item, type) {
if (!item?.types?.includes(type) || typeof item.getType !== "function") return null;
return await item.getType(type);
}
async function fallbackCopyItems(items) {
const parent = document.body || document.documentElement;
if (!parent) throw new Error("No document body available for clipboard fallback");
const container = document.createElement("div");
container.contentEditable = "true";
container.setAttribute("aria-hidden", "true");
container.style.position = "fixed";
container.style.left = "-9999px";
container.style.top = "0";
container.style.width = "1px";
container.style.height = "1px";
container.style.overflow = "hidden";
for (const item of items) {
const htmlBlob = await getClipboardItemType(item, "text/html");
if (htmlBlob) {
container.innerHTML += await htmlBlob.text();
continue;
}
const textBlob = await getClipboardItemType(item, "text/plain");
if (textBlob) {
const span = document.createElement("span");
span.textContent = await textBlob.text();
container.appendChild(span);
continue;
}
const imageType = item?.types?.find((type) => type.startsWith("image/"));
if (imageType && typeof item.getType === "function") {
const imageBlob = await item.getType(imageType);
const image = document.createElement("img");
image.src = await blobToDataUrl(imageBlob);
image.alt = "";
container.appendChild(image);
}
}
if (!container.innerHTML && !container.textContent && !container.children?.length) {
throw new Error("No supported clipboard item types found");
}
parent.appendChild(container);
selectElementForCopy(container);
container.remove();
}
if (originalWriteText) {
Object.defineProperty(navigator.clipboard, "writeText", {
configurable: true,
value: async (text) => {
try {
await fallbackCopyText(text);
api.logger.log("copied text via fallback", text);
} catch (fallbackError) {
api.logger.warn("text fallback failed; trying original writeText", fallbackError);
return originalWriteText(text);
}
},
});
}
if (originalWrite) {
Object.defineProperty(navigator.clipboard, "write", {
configurable: true,
value: async (items) => {
try {
await fallbackCopyItems(items);
api.logger.log("copied rich clipboard payload via fallback");
} catch (fallbackError) {
api.logger.warn("rich clipboard fallback failed; trying original write", fallbackError);
return originalWrite(items);
}
},
});
}
Object.defineProperty(navigator.clipboard, PATCH_KEY, {
configurable: false,
enumerable: false,
value: true,
});
api.logger.log("installed");
} catch (error) {
api.logger.error("install failed", error);
}
}
install();
window.addEventListener("DOMContentLoaded", install, { once: true });
};

View file

@ -115,6 +115,11 @@ A complete example is available at:
- `docs/examples/hello-plugin/preload.js` - `docs/examples/hello-plugin/preload.js`
- `docs/examples/hello-plugin/renderer.js` - `docs/examples/hello-plugin/renderer.js`
A practical renderer-only workaround plugin is also available at:
- `docs/examples/clipboard-fallback-plugin/manifest.json`
- `docs/examples/clipboard-fallback-plugin/renderer.js`
To test it: To test it:
1. Copy `docs/examples/hello-plugin` into your runtime plugins directory: 1. Copy `docs/examples/hello-plugin` into your runtime plugins directory:

View file

@ -5,6 +5,8 @@ import { applyAppImageSandboxFix } from "./scripts/build/sandboxFix.mjs";
export const config: Configuration = { export const config: Configuration = {
appId: "app.legcord.Legcord", appId: "app.legcord.Legcord",
productName: "Legcord", productName: "Legcord",
// Biome treats electron-builder macro placeholders as template syntax.
// biome-ignore lint/suspicious/noTemplateCurlyInString: electron-builder expands these placeholders.
artifactName: "Legcord-${version}-${os}-${arch}.${ext}", artifactName: "Legcord-${version}-${os}-${arch}.${ext}",
beforePack: applyAppImageSandboxFix, beforePack: applyAppImageSandboxFix,
protocols: [ protocols: [

View file

@ -7,9 +7,9 @@
"node": ">=26" "node": ">=26"
}, },
"scripts": { "scripts": {
"build:dev": "rollup -c --environment BUILD:dev && node scripts/copyVenmic.ts", "build:dev": "rollup -c --environment BUILD:dev && node --experimental-strip-types scripts/copyVenmic.ts",
"build:plugins": "lune ci --repoSubDir src/shelter --to ts-out/plugins", "build:plugins": "lune ci --repoSubDir src/shelter --to ts-out/plugins",
"build": "pnpm build:plugins && rolldown -c rolldown.config.ts && node scripts/copyVenmic.ts", "build": "pnpm build:plugins && rolldown -c rolldown.config.ts && node --experimental-strip-types scripts/copyVenmic.ts",
"start": "pnpm run build && electron --trace-warnings --ozone-platform-hint=auto ./ts-out/main.js", "start": "pnpm run build && electron --trace-warnings --ozone-platform-hint=auto ./ts-out/main.js",
"startThemeManager": "pnpm run build:dev && electron ./ts-out/main.js themes", "startThemeManager": "pnpm run build:dev && electron ./ts-out/main.js themes",
"package": "pnpm run build && electron-builder", "package": "pnpm run build && electron-builder",
@ -18,7 +18,7 @@
"lint:fix": "biome check --write", "lint:fix": "biome check --write",
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",
"CIbuild": "pnpm run build && electron-builder --linux zip && electron-builder --windows zip && electron-builder --macos zip", "CIbuild": "pnpm run build && electron-builder --linux zip && electron-builder --windows zip && electron-builder --macos zip",
"updateMeta": "node scripts/utils/updateMeta.ts" "updateMeta": "node --experimental-strip-types scripts/utils/updateMeta.ts"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -14,7 +14,7 @@ export function openCssEditor(file: string) {
preload: path.join(import.meta.dirname, "cssEditor", "preload.mjs"), preload: path.join(import.meta.dirname, "cssEditor", "preload.mjs"),
}, },
}); });
cssWindow.loadURL(`file://${import.meta.dirname}/html/editor.html`); cssWindow.loadURL("legcord://html/editor.html");
ipcMain.on("editor-setCSS", (_event, css: string) => { ipcMain.on("editor-setCSS", (_event, css: string) => {
fs.writeFileSync(file, css); fs.writeFileSync(file, css);

View file

@ -5,9 +5,11 @@ import {
app, app,
BrowserWindow, BrowserWindow,
type BrowserWindowConstructorOptions, type BrowserWindowConstructorOptions,
clipboard,
dialog, dialog,
type MessageBoxOptions, type MessageBoxOptions,
nativeImage, nativeImage,
net,
screen, screen,
shell, shell,
} from "electron"; } from "electron";
@ -75,10 +77,42 @@ function saveWindowState(win: BrowserWindow): void {
} }
} }
async function copyImageFromContext(
parameters: { srcURL: string; x: number; y: number },
win?: BrowserWindow,
): Promise<void> {
if (parameters.srcURL) {
try {
const response = await net.fetch(parameters.srcURL);
if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
const image = nativeImage.createFromBuffer(Buffer.from(await response.arrayBuffer()));
if (!image.isEmpty()) {
clipboard.writeImage(image);
return;
}
} catch (error) {
console.warn("[ContextMenu] Failed to copy image from URL, falling back to copyImageAt:", error);
}
}
win?.webContents.copyImageAt(parameters.x, parameters.y);
}
contextMenu({ contextMenu({
showSaveImageAs: true, showSaveImageAs: true,
showCopyImage: false,
showCopyImageAddress: true, showCopyImageAddress: true,
showSearchWithGoogle: false, showSearchWithGoogle: false,
append: (_defaultActions, parameters, win) => [
{
label: "Copy Image",
visible: parameters.mediaType === "image",
click: () => {
void copyImageFromContext(parameters, win as BrowserWindow | undefined);
},
},
],
prepend: (_defaultActions, parameters) => [ prepend: (_defaultActions, parameters) => [
{ {
label: getLang("contextMenu-searchGoogle"), label: getLang("contextMenu-searchGoogle"),
@ -423,7 +457,7 @@ function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
lastPolledBounds = { x, y, width, height }; lastPolledBounds = { x, y, width, height };
saveWindowState(passedWindow); saveWindowState(passedWindow);
} }
} catch (e) { } catch (_e) {
// ignore transient errors // ignore transient errors
} }
}, 1000); }, 1000);
@ -520,7 +554,7 @@ export function createWindow() {
mainWindow.setPosition(storedBounds.x, storedBounds.y); mainWindow.setPosition(storedBounds.x, storedBounds.y);
mainWindow.setSize(storedBounds.width, storedBounds.height); mainWindow.setSize(storedBounds.width, storedBounds.height);
} }
mainWindows.push(mainWindow); mainWindows.push(mainWindow);
doAfterDefiningTheWindow(mainWindow); doAfterDefiningTheWindow(mainWindow);
} }

View file

@ -29,6 +29,26 @@ void app.whenReady().then(() => {
}); });
} }
return net.fetch(Url.pathToFileURL(filePath).toString()); return net.fetch(Url.pathToFileURL(filePath).toString());
} else if (req.url.startsWith("legcord://html/")) {
const file = req.url.replace("legcord://html/", "");
const filePath = path.join(import.meta.dirname, "html", `${file}`);
if (filePath.includes("..")) {
return new Response("bad", {
status: 400,
headers: { "content-type": "text/html" },
});
}
return net.fetch(Url.pathToFileURL(filePath).toString());
} else if (req.url.startsWith("legcord://js/")) {
const file = req.url.replace("legcord://js/", "");
const filePath = path.join(import.meta.dirname, "js", `${file}`);
if (filePath.includes("..")) {
return new Response("bad", {
status: 400,
headers: { "content-type": "text/html" },
});
}
return net.fetch(Url.pathToFileURL(filePath).toString());
} else if (req.url.startsWith("legcord://assets/")) { } else if (req.url.startsWith("legcord://assets/")) {
const file = req.url.replace("legcord://assets/", ""); const file = req.url.replace("legcord://assets/", "");
const filePath = path.join(import.meta.dirname, "assets", "app", `${file}`); const filePath = path.join(import.meta.dirname, "assets", "app", `${file}`);
@ -39,6 +59,16 @@ void app.whenReady().then(() => {
}); });
} }
return net.fetch(Url.pathToFileURL(filePath).toString()); return net.fetch(Url.pathToFileURL(filePath).toString());
} else if (req.url.startsWith("legcord://css/")) {
const file = req.url.replace("legcord://css/", "");
const filePath = path.join(import.meta.dirname, "css", `${file}`);
if (filePath.includes("..")) {
return new Response("bad", {
status: 400,
headers: { "content-type": "text/html" },
});
}
return net.fetch(Url.pathToFileURL(filePath).toString());
} else if (req.url.startsWith("legcord://local/")) { } else if (req.url.startsWith("legcord://local/")) {
const file = req.url.replace("legcord://local/", ""); const file = req.url.replace("legcord://local/", "");
const userDataPath = path.join(app.getPath("userData"), "userAssets"); const userDataPath = path.join(app.getPath("userData"), "userAssets");

View file

@ -22,7 +22,6 @@ export async function createSetupWindow(): Promise<void> {
maximizable: false, maximizable: false,
autoHideMenuBar: true, autoHideMenuBar: true,
webPreferences: { webPreferences: {
sandbox: true,
spellcheck: false, spellcheck: false,
preload: path.join(import.meta.dirname, "setup", "preload.mjs"), preload: path.join(import.meta.dirname, "setup", "preload.mjs"),
}, },
@ -70,6 +69,6 @@ export async function createSetupWindow(): Promise<void> {
// workaround electron trying to relaunch from squashfs // workaround electron trying to relaunch from squashfs
handleRestart(); handleRestart();
}); });
void setupWindow.loadFile(path.join(import.meta.dirname, "/html/setup.html")); void setupWindow.loadURL("legcord://html/setup.html");
}); });
} }

View file

@ -31,5 +31,5 @@ export async function createSplashWindow(): Promise<void> {
ipcMain.on("splash-clientmod", (event) => { ipcMain.on("splash-clientmod", (event) => {
event.returnValue = getConfig("mods"); event.returnValue = getConfig("mods");
}); });
await splashWindow.loadFile(path.join(import.meta.dirname, "html", "splash.html")); await splashWindow.loadURL("legcord://html/splash.html");
} }