From f14cac6490e072dfc5465b2ac803fe332ad3cda7 Mon Sep 17 00:00:00 2001 From: Nystik <236107-Nystik@users.noreply.gitlab.com> Date: Sun, 29 Mar 2026 20:02:22 +0200 Subject: [PATCH] clipboard fixes --- CHANGELOG.md | 11 +++ package.json | 2 +- plugin/manifest.json | 2 +- src/shims/electron/index.js | 4 ++ src/shims/electron/remote/clipboard.js | 30 +++++++- src/shims/electron/remote/native-image.js | 85 ++++++++++++++++++++--- src/shims/electron/remote/window.js | 52 +++++++++++++- 7 files changed, 168 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c5ab87..388a252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ 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) ### Added diff --git a/package.json b/package.json index 557e982..1db8baf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ignis", - "version": "0.7.1", + "version": "0.7.2", "private": true, "description": "An Electron shim and server bridge for running Obsidian in a browser.", "scripts": { diff --git a/plugin/manifest.json b/plugin/manifest.json index 3e63119..dd1ec3f 100644 --- a/plugin/manifest.json +++ b/plugin/manifest.json @@ -1,7 +1,7 @@ { "id": "ignis-bridge", "name": "Ignis Bridge", - "version": "0.7.1", + "version": "0.7.2", "minAppVersion": "1.12.4", "description": "Additional Ignis specific functionality and ignis plugin management.", "author": "Nystik", diff --git a/src/shims/electron/index.js b/src/shims/electron/index.js index 4eea863..22af4e7 100644 --- a/src/shims/electron/index.js +++ b/src/shims/electron/index.js @@ -1,11 +1,15 @@ import { ipcRenderer } from "./ipc-renderer.js"; import { webFrame } from "./web-frame.js"; import { remoteShim } from "./remote/index.js"; +import { nativeImageShim } from "./remote/native-image.js"; +import { clipboardShim } from "./remote/clipboard.js"; export const electronShim = { ipcRenderer, webFrame, remote: remoteShim, + nativeImage: nativeImageShim, + clipboard: clipboardShim, safeStorage: { isEncryptionAvailable() { diff --git a/src/shims/electron/remote/clipboard.js b/src/shims/electron/remote/clipboard.js index 490ff45..d19aab9 100644 --- a/src/shims/electron/remote/clipboard.js +++ b/src/shims/electron/remote/clipboard.js @@ -1,4 +1,3 @@ -// stub export const clipboardShim = { readText() { return ""; @@ -15,7 +14,16 @@ export const clipboardShim = { }, 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() { @@ -23,7 +31,23 @@ export const clipboardShim = { }, 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) { diff --git a/src/shims/electron/remote/native-image.js b/src/shims/electron/remote/native-image.js index 92a4873..9da2290 100644 --- a/src/shims/electron/remote/native-image.js +++ b/src/shims/electron/remote/native-image.js @@ -1,20 +1,83 @@ +function createImage(buffer, mimeType) { + return { + _buffer: buffer, + _mimeType: mimeType || "image/png", + + isEmpty() { + 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) { - return { - isEmpty: () => !buffer || buffer.length === 0, - getSize: () => ({ width: 0, height: 0 }), - toPNG: () => buffer || new Uint8Array(0), - toJPEG: (quality) => buffer || new Uint8Array(0), - toDataURL: () => "", - }; + createFromBuffer(buffer, options) { + return createImage(buffer, options?.mimeType); }, createFromPath(filePath) { - // TODO: could fetch from server and create image - return nativeImageShim.createFromBuffer(new Uint8Array(0)); + return createImage(new Uint8Array(0)); }, 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)); + } }, }; diff --git a/src/shims/electron/remote/window.js b/src/shims/electron/remote/window.js index 3ea79b8..1312d4d 100644 --- a/src/shims/electron/remote/window.js +++ b/src/shims/electron/remote/window.js @@ -196,10 +196,58 @@ const currentWebContents = { document.execCommand("copy"); }, 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() { - 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) {},