mirror of
https://github.com/smartfrigde/armcord.git
synced 2026-08-19 14:23:23 +00:00
Compare commits
3 commits
022676cf65
...
dc7be47cbc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc7be47cbc |
||
|
|
69b8d4202e |
||
|
|
74231ca190 |
13 changed files with 615 additions and 9 deletions
28
.github/workflows/flatpak-node.yml
vendored
Normal file
28
.github/workflows/flatpak-node.yml
vendored
Normal 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 }}
|
||||
82
docs/examples/clipboard-fallback-plugin/README.md
Normal file
82
docs/examples/clipboard-fallback-plugin/README.md
Normal 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.
|
||||
216
docs/examples/clipboard-fallback-plugin/custom-bundle.js
Normal file
216
docs/examples/clipboard-fallback-plugin/custom-bundle.js
Normal 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);
|
||||
}
|
||||
})();
|
||||
9
docs/examples/clipboard-fallback-plugin/manifest.json
Normal file
9
docs/examples/clipboard-fallback-plugin/manifest.json
Normal 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"
|
||||
}
|
||||
201
docs/examples/clipboard-fallback-plugin/renderer.js
Normal file
201
docs/examples/clipboard-fallback-plugin/renderer.js
Normal 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 });
|
||||
};
|
||||
|
|
@ -115,6 +115,11 @@ A complete example is available at:
|
|||
- `docs/examples/hello-plugin/preload.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:
|
||||
|
||||
1. Copy `docs/examples/hello-plugin` into your runtime plugins directory:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { applyAppImageSandboxFix } from "./scripts/build/sandboxFix.mjs";
|
|||
export const config: Configuration = {
|
||||
appId: "app.legcord.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}",
|
||||
beforePack: applyAppImageSandboxFix,
|
||||
protocols: [
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@
|
|||
"node": ">=26"
|
||||
},
|
||||
"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": "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",
|
||||
"startThemeManager": "pnpm run build:dev && electron ./ts-out/main.js themes",
|
||||
"package": "pnpm run build && electron-builder",
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
"lint:fix": "biome check --write",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"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": {
|
||||
"type": "git",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export function openCssEditor(file: string) {
|
|||
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) => {
|
||||
fs.writeFileSync(file, css);
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import {
|
|||
app,
|
||||
BrowserWindow,
|
||||
type BrowserWindowConstructorOptions,
|
||||
clipboard,
|
||||
dialog,
|
||||
type MessageBoxOptions,
|
||||
nativeImage,
|
||||
net,
|
||||
screen,
|
||||
shell,
|
||||
} 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({
|
||||
showSaveImageAs: true,
|
||||
showCopyImage: false,
|
||||
showCopyImageAddress: true,
|
||||
showSearchWithGoogle: false,
|
||||
append: (_defaultActions, parameters, win) => [
|
||||
{
|
||||
label: "Copy Image",
|
||||
visible: parameters.mediaType === "image",
|
||||
click: () => {
|
||||
void copyImageFromContext(parameters, win as BrowserWindow | undefined);
|
||||
},
|
||||
},
|
||||
],
|
||||
prepend: (_defaultActions, parameters) => [
|
||||
{
|
||||
label: getLang("contextMenu-searchGoogle"),
|
||||
|
|
@ -423,7 +457,7 @@ function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
|
|||
lastPolledBounds = { x, y, width, height };
|
||||
saveWindowState(passedWindow);
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
// ignore transient errors
|
||||
}
|
||||
}, 1000);
|
||||
|
|
@ -520,7 +554,7 @@ export function createWindow() {
|
|||
mainWindow.setPosition(storedBounds.x, storedBounds.y);
|
||||
mainWindow.setSize(storedBounds.width, storedBounds.height);
|
||||
}
|
||||
|
||||
|
||||
mainWindows.push(mainWindow);
|
||||
doAfterDefiningTheWindow(mainWindow);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,26 @@ void app.whenReady().then(() => {
|
|||
});
|
||||
}
|
||||
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/")) {
|
||||
const file = req.url.replace("legcord://assets/", "");
|
||||
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());
|
||||
} 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/")) {
|
||||
const file = req.url.replace("legcord://local/", "");
|
||||
const userDataPath = path.join(app.getPath("userData"), "userAssets");
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ export async function createSetupWindow(): Promise<void> {
|
|||
maximizable: false,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
sandbox: true,
|
||||
spellcheck: false,
|
||||
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
|
||||
handleRestart();
|
||||
});
|
||||
void setupWindow.loadFile(path.join(import.meta.dirname, "/html/setup.html"));
|
||||
void setupWindow.loadURL("legcord://html/setup.html");
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,5 +31,5 @@ export async function createSplashWindow(): Promise<void> {
|
|||
ipcMain.on("splash-clientmod", (event) => {
|
||||
event.returnValue = getConfig("mods");
|
||||
});
|
||||
await splashWindow.loadFile(path.join(import.meta.dirname, "html", "splash.html"));
|
||||
await splashWindow.loadURL("legcord://html/splash.html");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue