clipboard fixes

This commit is contained in:
Nystik 2026-03-29 20:02:22 +02:00
parent c02e6829ad
commit f14cac6490
7 changed files with 168 additions and 18 deletions

View file

@ -2,6 +2,17 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [0.7.2] - Orm (2026-03-29)
### Added
- utils shim
### Fixed
- right sidebar toggle with css overrides
- clipboard functionality
## [0.7.1] - Orm (2026-03-29) ## [0.7.1] - Orm (2026-03-29)
### Added ### Added

View file

@ -1,6 +1,6 @@
{ {
"name": "ignis", "name": "ignis",
"version": "0.7.1", "version": "0.7.2",
"private": true, "private": true,
"description": "An Electron shim and server bridge for running Obsidian in a browser.", "description": "An Electron shim and server bridge for running Obsidian in a browser.",
"scripts": { "scripts": {

View file

@ -1,7 +1,7 @@
{ {
"id": "ignis-bridge", "id": "ignis-bridge",
"name": "Ignis Bridge", "name": "Ignis Bridge",
"version": "0.7.1", "version": "0.7.2",
"minAppVersion": "1.12.4", "minAppVersion": "1.12.4",
"description": "Additional Ignis specific functionality and ignis plugin management.", "description": "Additional Ignis specific functionality and ignis plugin management.",
"author": "Nystik", "author": "Nystik",

View file

@ -1,11 +1,15 @@
import { ipcRenderer } from "./ipc-renderer.js"; import { ipcRenderer } from "./ipc-renderer.js";
import { webFrame } from "./web-frame.js"; import { webFrame } from "./web-frame.js";
import { remoteShim } from "./remote/index.js"; import { remoteShim } from "./remote/index.js";
import { nativeImageShim } from "./remote/native-image.js";
import { clipboardShim } from "./remote/clipboard.js";
export const electronShim = { export const electronShim = {
ipcRenderer, ipcRenderer,
webFrame, webFrame,
remote: remoteShim, remote: remoteShim,
nativeImage: nativeImageShim,
clipboard: clipboardShim,
safeStorage: { safeStorage: {
isEncryptionAvailable() { isEncryptionAvailable() {

View file

@ -1,4 +1,3 @@
// stub
export const clipboardShim = { export const clipboardShim = {
readText() { readText() {
return ""; return "";
@ -15,7 +14,16 @@ export const clipboardShim = {
}, },
writeHTML(html) { writeHTML(html) {
console.log("[shim:clipboard] writeHTML (stub)"); navigator.clipboard
.write([
new ClipboardItem({
"text/html": new Blob([html], { type: "text/html" }),
"text/plain": new Blob([html], { type: "text/plain" }),
}),
])
.catch((e) => {
console.warn("[shim:clipboard] writeHTML failed:", e);
});
}, },
readImage() { readImage() {
@ -23,7 +31,23 @@ export const clipboardShim = {
}, },
writeImage(image) { writeImage(image) {
console.log("[shim:clipboard] writeImage (stub)"); if (!image || image.isEmpty()) {
return;
}
const pngData = image.toPNG();
if (!pngData || pngData.length === 0) {
return;
}
const blob = new Blob([pngData], { type: "image/png" });
navigator.clipboard
.write([new ClipboardItem({ "image/png": blob })])
.catch((e) => {
console.warn("[shim:clipboard] writeImage failed:", e);
});
}, },
has(format) { has(format) {

View file

@ -1,20 +1,83 @@
export const nativeImageShim = { function createImage(buffer, mimeType) {
createFromBuffer(buffer) {
return { return {
isEmpty: () => !buffer || buffer.length === 0, _buffer: buffer,
getSize: () => ({ width: 0, height: 0 }), _mimeType: mimeType || "image/png",
toPNG: () => buffer || new Uint8Array(0),
toJPEG: (quality) => buffer || new Uint8Array(0), isEmpty() {
toDataURL: () => "", return !buffer || buffer.length === 0;
},
getSize() {
return { width: 0, height: 0 };
},
toPNG() {
return buffer || new Uint8Array(0);
},
toJPEG(quality) {
return buffer || new Uint8Array(0);
},
toDataURL() {
if (!buffer || buffer.length === 0) {
return "";
}
const bytes =
buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
let binary = "";
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return `data:${this._mimeType};base64,${btoa(binary)}`;
},
toBitmap() {
return buffer || new Uint8Array(0);
},
getBitmap() {
return buffer || new Uint8Array(0);
},
}; };
}
export const nativeImageShim = {
createFromBuffer(buffer, options) {
return createImage(buffer, options?.mimeType);
}, },
createFromPath(filePath) { createFromPath(filePath) {
// TODO: could fetch from server and create image return createImage(new Uint8Array(0));
return nativeImageShim.createFromBuffer(new Uint8Array(0));
}, },
createEmpty() { createEmpty() {
return nativeImageShim.createFromBuffer(new Uint8Array(0)); return createImage(new Uint8Array(0));
},
createFromDataURL(dataURL) {
if (!dataURL || !dataURL.startsWith("data:")) {
return createImage(new Uint8Array(0));
}
const parts = dataURL.split(",");
const mimeMatch = parts[0].match(/data:([^;]+)/);
const mimeType = mimeMatch ? mimeMatch[1] : "image/png";
try {
const binary = atob(parts[1]);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return createImage(bytes, mimeType);
} catch {
return createImage(new Uint8Array(0));
}
}, },
}; };

View file

@ -196,10 +196,58 @@ const currentWebContents = {
document.execCommand("copy"); document.execCommand("copy");
}, },
paste() { paste() {
document.execCommand("paste"); navigator.clipboard
.read()
.then(async (items) => {
const dt = new DataTransfer();
let hasFiles = false;
for (const item of items) {
for (const type of item.types) {
const blob = await item.getType(type);
if (type.startsWith("text/")) {
const text = await blob.text();
dt.items.add(text, type);
} else {
const ext = type.split("/")[1] || "bin";
dt.items.add(
new File([blob], `pasted-image.${ext}`, { type }),
);
hasFiles = true;
}
}
}
const pasteEvent = new ClipboardEvent("paste", {
bubbles: true,
cancelable: true,
clipboardData: dt,
});
const target = document.activeElement || document.body;
target.dispatchEvent(pasteEvent);
})
.catch((e) => {
console.warn("[shim:webContents] paste failed:", e);
});
}, },
pasteAndMatchStyle() { pasteAndMatchStyle() {
document.execCommand("paste"); navigator.clipboard
.read()
.then(async (items) => {
for (const item of items) {
if (item.types.includes("text/plain")) {
const blob = await item.getType("text/plain");
const text = await blob.text();
document.execCommand("insertText", false, text);
return;
}
}
})
.catch((e) => {
console.warn("[shim:webContents] pasteAndMatchStyle failed:", e);
});
}, },
replaceMisspelling(word) {}, replaceMisspelling(word) {},