move shim to new package

This commit is contained in:
Nystik
2026-05-20 20:49:28 +02:00
parent a0b44bde58
commit fe11f30c01
66 changed files with 46 additions and 23 deletions

View File

@@ -0,0 +1,51 @@
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() {
return false;
},
encryptString(plainText) {
return Buffer.from(plainText);
},
decryptString(encrypted) {
return encrypted.toString();
},
},
webUtils: {
getPathForFile(file) {
return "";
},
},
deprecate: {
function(fn, name) {
return fn;
},
event(emitter, name) {},
removeFunction(fn, name) {
return fn;
},
log(message) {
console.log("[electron:deprecate]", message);
},
warn(oldName, newName) {},
promisify(fn) {
return fn;
},
renameFunction(fn, newName) {
return fn;
},
},
};

View File

@@ -0,0 +1,260 @@
import { showVaultManager } from "../ui-registry.js";
import { vaultService } from "@ignis/services";
const listeners = new Map();
const syncHandlers = {
vault: () => window.__vaultConfig || { id: "default-vault", path: "/" },
version: () => window.__obsidianVersion || "0.0.0",
"is-dev": () => false,
"file-url": () =>
"/vault-files/" + encodeURIComponent(window.__currentVaultId || "") + "/",
"disable-update": () => true,
update: () => "",
"disable-gpu": () => false,
frame: () => null,
"set-icon": () => null,
"get-icon": () => null,
relaunch: () => {
window.location.reload();
return null;
},
starter: () => {
showVaultManager();
return null;
},
help: () => {
window.open("https://help.obsidian.md/", "_blank");
return null;
},
sandbox: () => null,
"copy-asar": () => false,
"check-update": () => null,
"vault-list": () => {
const result = {};
for (const v of window.__vaultList || []) {
result[v.id] = {
path: "/" + v.id,
ts: Date.now(),
open: v.id === vaultService.getCurrentVaultId(),
};
}
return result;
},
"vault-open": (vaultPath, newWindow) => {
const id = (vaultPath || "").replace(/^\/+/, "");
const vault = (window.__vaultList || []).find((v) => v.id === id);
if (!vault && id) {
if (!vaultService.createVaultSync(id)) {
return "Failed to create vault";
}
}
vaultService.openVault(id);
return true;
},
"vault-remove": (vaultPath) => {
const id = (vaultPath || "").replace(/^\/+/, "");
return vaultService.deleteVaultSync(id);
},
"vault-move": (oldPath, newPath) => {
return "Moving vaults is not supported in the web version";
},
"vault-message": () => null,
"get-default-vault-path": () => "/My Vault",
"get-documents-path": () => "/",
"desktop-dir": () => "/desktop",
"documents-dir": () => "/documents",
resources: () => "",
};
function arrayBufferToBase64(buf) {
const bytes = new Uint8Array(buf);
let binary = "";
const chunk = 8192;
for (let i = 0; i < bytes.length; i += chunk) {
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
}
return btoa(binary);
}
function base64ToArrayBuffer(base64) {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
async function handleRequestUrl(requestId, request) {
try {
let body = request.body;
let binary = false;
if (body instanceof ArrayBuffer) {
body = arrayBufferToBase64(body);
binary = true;
}
const res = await fetch("/api/proxy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: request.url,
method: request.method || "GET",
headers: request.headers || {},
contentType: request.contentType,
body,
binary,
}),
});
const proxyResult = await res.json();
if (!res.ok) {
ipcRenderer._emit(requestId, {
error: proxyResult.error || "Proxy request failed",
});
return;
}
// Electron's e.reply(requestId, data) sends on the requestId channel
ipcRenderer._emit(requestId, {
status: proxyResult.status,
headers: proxyResult.headers,
body: base64ToArrayBuffer(proxyResult.body),
});
} catch (e) {
ipcRenderer._emit(requestId, {
error: e.message,
});
}
}
export const ipcRenderer = {
send(channel, ...args) {
console.log("[shim:ipcRenderer] send:", channel, args);
if (channel === "context-menu") {
queueMicrotask(() =>
ipcRenderer._emit("context-menu", {
webContentsId: 1,
editFlags: { canCut: true, canCopy: true, canPaste: true },
}),
);
return;
}
if (channel === "request-url") {
const [requestId, request] = args;
handleRequestUrl(requestId, request);
return;
}
if (channel === "print-to-pdf") {
const iframe = window.__popupIframe;
if (iframe) {
setTimeout(() => {
iframe.contentWindow.print();
setTimeout(() => {
iframe.contentWindow.close();
ipcRenderer._emit("print-to-pdf", { success: true });
}, 500);
}, 200);
} else {
window.print();
queueMicrotask(() => {
ipcRenderer._emit("print-to-pdf", { success: true });
});
}
return;
}
},
sendSync(channel, ...args) {
console.log("[shim:ipcRenderer] sendSync:", channel, args);
if (syncHandlers[channel]) {
return syncHandlers[channel](...args);
}
console.warn("[shim:ipcRenderer] Unhandled sendSync channel:", channel);
return null;
},
on(channel, listener) {
if (!listeners.has(channel)) {
listeners.set(channel, []);
}
listeners.get(channel).push(listener);
return ipcRenderer;
},
once(channel, listener) {
const wrapped = (...args) => {
ipcRenderer.removeListener(channel, wrapped);
listener(...args);
};
return ipcRenderer.on(channel, wrapped);
},
removeListener(channel, listener) {
const arr = listeners.get(channel);
if (arr) {
const idx = arr.indexOf(listener);
if (idx >= 0) {
arr.splice(idx, 1);
}
}
return ipcRenderer;
},
removeAllListeners(channel) {
if (channel) {
listeners.delete(channel);
} else {
listeners.clear();
}
return ipcRenderer;
},
_emit(channel, ...args) {
const arr = listeners.get(channel);
if (arr) {
for (const fn of arr) {
fn({}, ...args);
}
}
},
};

View File

@@ -0,0 +1,43 @@
export const appShim = {
getPath(name) {
const paths = {
userData: "/.obsidian",
home: "/",
documents: "/documents",
desktop: "/desktop",
temp: "/tmp",
appData: "/.obsidian",
};
return paths[name] || "/";
},
getVersion() {
return window.__obsidianVersion || "0.0.0";
},
getName() {
return "Obsidian";
},
getLocale() {
return navigator.language || "en-US";
},
isPackaged: true,
quit() {
console.log("[shim:app] quit (stub)");
},
relaunch() {
window.location.reload();
},
whenReady() {
return Promise.resolve();
},
on() {},
once() {},
removeListener() {},
};

View File

@@ -0,0 +1,64 @@
export const clipboardShim = {
readText() {
return "";
},
writeText(text) {
navigator.clipboard.writeText(text).catch((e) => {
console.warn("[shim:clipboard] writeText failed:", e);
});
},
readHTML() {
return "";
},
writeHTML(html) {
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() {
return { isEmpty: () => true, toPNG: () => new Uint8Array(0) };
},
writeImage(image) {
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) {
return false;
},
read(format) {
return "";
},
clear() {
navigator.clipboard.writeText("").catch(() => {});
},
};

View File

@@ -0,0 +1,259 @@
import {
showMessageDialog,
showConfirmDialog,
showPromptDialog,
} from "../../ui-registry.js";
import { inputCacheSet, inputCacheDelete } from "../../fs/input-cache.js";
const IMPORTS_DIR = ".obsidian/imports";
const STAGED_TTL_MS = 120_000; // 2 minutes
let staged = { paths: [], fingerprint: null, timestamp: 0 };
function getCallerFingerprint() {
const stack = new Error().stack || "";
const frames = stack
.split("\n")
.filter((l) => !l.includes("shim-loader") && !l.includes("dialog.js"));
return frames.slice(0, 3).join("|");
}
function clearStagedFiles() {
if (staged.paths.length === 0) return;
console.log("[shim:dialog] Clearing expired staged files");
for (const p of staged.paths) {
inputCacheDelete(p.replace(/^\//, ""));
}
staged = { paths: [], fingerprint: null, timestamp: 0 };
}
function buildAcceptString(filters) {
if (!filters || filters.length === 0) {
return "";
}
const extensions = filters.flatMap((f) => f.extensions || []);
if (extensions.includes("*")) {
return "";
}
return extensions.map((ext) => "." + ext).join(",");
}
function pickFiles(accept, multiple) {
return new Promise((resolve) => {
const input = document.createElement("input");
input.type = "file";
input.multiple = multiple;
input.style.display = "none";
if (accept) {
input.accept = accept;
}
input.addEventListener("change", () => {
const files = Array.from(input.files || []);
input.remove();
resolve(files);
});
// User closed the picker without selecting
input.addEventListener("cancel", () => {
input.remove();
resolve([]);
});
document.body.appendChild(input);
input.click();
});
}
async function cacheToImports(file) {
const arrayBuffer = await file.arrayBuffer();
const bytes = new Uint8Array(arrayBuffer);
const targetPath = IMPORTS_DIR + "/" + file.name;
inputCacheSet(targetPath, bytes);
return "/" + targetPath;
}
async function startWorkaroundFlow(options, fingerprint) {
const properties = options?.properties || [];
const multiple = properties.includes("multiSelections");
const accept = buildAcceptString(options?.filters);
const files = await pickFiles(accept, multiple);
if (files.length === 0) {
return;
}
const paths = [];
for (const file of files) {
const vaultPath = await cacheToImports(file);
paths.push(vaultPath);
}
staged = { paths, fingerprint, timestamp: Date.now() };
const names = paths.map((p) => p.split("/").pop()).join(", ");
console.log("[shim:dialog] Files staged for caller:", fingerprint);
await showMessageDialog(
"Files Ready",
`Staged: ${names}\n\nPlease retry the action that brought you here. ` +
"The files will be provided automatically.",
);
}
export const dialogShim = {
async showOpenDialog(browserWindow, options) {
if (typeof browserWindow === "object" && !options) {
options = browserWindow;
}
const properties = options?.properties || [];
const multiple = properties.includes("multiSelections");
const accept = buildAcceptString(options?.filters);
console.log("[shim:dialog] showOpenDialog - opening browser file picker");
const files = await pickFiles(accept, multiple);
if (files.length === 0) {
return { canceled: true, filePaths: [] };
}
const filePaths = [];
for (const file of files) {
const vaultPath = await cacheToImports(file);
filePaths.push(vaultPath);
}
console.log("[shim:dialog] showOpenDialog - cached:", filePaths);
return { canceled: false, filePaths };
},
showOpenDialogSync(browserWindow, options) {
if (typeof browserWindow === "object" && !options) {
options = browserWindow;
}
// If files were staged from a previous workaround, validate and return them
if (staged.paths.length > 0) {
const elapsed = Date.now() - staged.timestamp;
const fingerprint = getCallerFingerprint();
const fingerprintMatch = fingerprint === staged.fingerprint;
const expired = elapsed > STAGED_TTL_MS;
if (expired) {
console.warn("[shim:dialog] Staged files expired after", elapsed, "ms");
clearStagedFiles();
} else if (!fingerprintMatch) {
console.warn(
"[shim:dialog] Staged files caller mismatch - ignoring",
"\n expected:",
staged.fingerprint,
"\n got:",
fingerprint,
);
} else {
const paths = staged.paths;
staged = { paths: [], fingerprint: null, timestamp: 0 };
console.log(
"[shim:dialog] showOpenDialogSync - returning staged files:",
paths,
);
return paths;
}
}
console.warn(
"[shim:dialog] showOpenDialogSync requires workaround in browser context",
);
// Capture fingerprint here where the plugin's call stack is still visible
const callerFingerprint = getCallerFingerprint();
// Fire-and-forget: show warning, then optionally start workaround flow
showConfirmDialog(
"Feature Not Available",
"This action requires a native file picker which is not available in the browser.",
"A workaround is available: select your files first, then retry the action. " +
"They will be provided automatically.\n\n" +
"Note: individual files must be under 200 MB.",
"Select Files",
).then((confirmed) => {
if (confirmed) {
startWorkaroundFlow(options, callerFingerprint);
}
});
return undefined;
},
async showSaveDialog(browserWindow, options) {
if (typeof browserWindow === "object" && !options) {
options = browserWindow;
}
const defaultName =
options?.defaultPath?.split(/[\/\\]/).pop() || "download";
const name = await showPromptDialog(
"Save File",
"Save as:",
"filename",
defaultName,
"Save",
);
if (!name) {
return { canceled: true, filePath: undefined };
}
return { canceled: false, filePath: "/downloads/" + name };
},
async showMessageBox(browserWindow, options) {
if (typeof browserWindow === "object" && !options) {
options = browserWindow;
}
console.log("[shim:dialog] showMessageBox:", options);
const message = options.message || "";
const detail = options.detail || "";
const buttons = options.buttons || ["OK"];
const fullMessage = message + (detail ? "\n\n" + detail : "");
if (buttons.length <= 1) {
await showMessageDialog(options.title || "Message", fullMessage);
return { response: 0, checkboxChecked: false };
}
const result = await showConfirmDialog(
options.title || "Confirm",
message,
detail,
buttons[0],
);
return {
response: result ? 0 : 1,
checkboxChecked: false,
};
},
showErrorBox(title, content) {
console.error("[shim:dialog] Error:", title, content);
showMessageDialog(title, content);
},
};

View File

@@ -0,0 +1,50 @@
import { clipboardShim } from "./clipboard.js";
import { shellShim } from "./shell.js";
import { dialogShim } from "./dialog.js";
import { menuShim, menuItemShim } from "./menu.js";
import { appShim } from "./app.js";
import { windowShim, webContentsShim } from "./window.js";
import { themeShim } from "./theme.js";
import { sessionShim } from "./session.js";
import { systemPreferencesShim } from "./system-preferences.js";
import { screenShim } from "./screen.js";
import { nativeImageShim } from "./native-image.js";
import { notificationShim } from "./notification.js";
export const remoteShim = {
clipboard: clipboardShim,
shell: shellShim,
dialog: dialogShim,
Menu: menuShim,
MenuItem: menuItemShim,
app: appShim,
BrowserWindow: windowShim,
nativeTheme: themeShim,
session: sessionShim,
systemPreferences: systemPreferencesShim,
screen: screenShim,
nativeImage: nativeImageShim,
Notification: notificationShim,
safeStorage: {
isEncryptionAvailable() {
return false;
},
encryptString(plainText) {
return Buffer.from(plainText);
},
decryptString(encrypted) {
return encrypted.toString();
},
},
getCurrentWindow() {
return windowShim._current();
},
webContents: webContentsShim,
getCurrentWebContents() {
return webContentsShim._current();
},
};

View File

@@ -0,0 +1,53 @@
export class menuShim {
constructor() {
this.items = [];
}
static buildFromTemplate(template) {
const menu = new menuShim();
menu.items = (template || []).map((item) => new menuItemShim(item));
return menu;
}
static setApplicationMenu(menu) {
console.log("[shim:Menu] setApplicationMenu (stub)");
}
static getApplicationMenu() {
return null;
}
popup(options) {
console.log("[shim:Menu] popup (stub)", options);
}
append(menuItem) {
this.items.push(menuItem);
}
insert(pos, menuItem) {
this.items.splice(pos, 0, menuItem);
}
closePopup() {}
}
export class menuItemShim {
constructor(options = {}) {
this.label = options.label || "";
this.type = options.type || "normal";
this.click = options.click || null;
this.role = options.role || null;
this.accelerator = options.accelerator || "";
this.enabled = options.enabled !== false;
this.visible = options.visible !== false;
this.checked = !!options.checked;
this.submenu = options.submenu
? menuShim.buildFromTemplate(
Array.isArray(options.submenu) ? options.submenu : [],
)
: null;
this.id = options.id || "";
}
}

View File

@@ -0,0 +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, options) {
return createImage(buffer, options?.mimeType);
},
createFromPath(filePath) {
return createImage(new Uint8Array(0));
},
createEmpty() {
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

@@ -0,0 +1,37 @@
export class notificationShim {
constructor(options = {}) {
this.title = options.title || "";
this.body = options.body || "";
this.silent = options.silent || false;
this._handlers = {};
}
show() {
if ("Notification" in window && Notification.permission === "granted") {
new Notification(this.title, { body: this.body, silent: this.silent });
} else if (
"Notification" in window &&
Notification.permission !== "denied"
) {
Notification.requestPermission().then((perm) => {
if (perm === "granted") {
new Notification(this.title, {
body: this.body,
silent: this.silent,
});
}
});
}
}
close() {}
on(event, handler) {
this._handlers[event] = handler;
return this;
}
static isSupported() {
return "Notification" in window;
}
}

View File

@@ -0,0 +1,40 @@
export const screenShim = {
getPrimaryDisplay() {
return {
workAreaSize: {
width: window.screen.availWidth,
height: window.screen.availHeight,
},
size: { width: window.screen.width, height: window.screen.height },
scaleFactor: window.devicePixelRatio || 1,
bounds: {
x: 0,
y: 0,
width: window.screen.width,
height: window.screen.height,
},
workArea: {
x: 0,
y: 0,
width: window.screen.availWidth,
height: window.screen.availHeight,
},
};
},
getAllDisplays() {
return [screenShim.getPrimaryDisplay()];
},
getDisplayNearestPoint(point) {
return screenShim.getPrimaryDisplay();
},
getCursorScreenPoint() {
return { x: 0, y: 0 };
},
on() {},
once() {},
removeListener() {},
};

View File

@@ -0,0 +1,20 @@
export const sessionShim = {
defaultSession: {
clearCache() {
return Promise.resolve();
},
clearStorageData() {
return Promise.resolve();
},
setSpellCheckerLanguages(langs) {},
getSpellCheckerLanguages() {
return [];
},
on() {},
once() {},
removeListener() {},
},
};

View File

@@ -0,0 +1,15 @@
export const shellShim = {
openExternal(url) {
window.open(url, "_blank");
return Promise.resolve();
},
openPath(filePath) {
console.log("[shim:shell] openPath (stub):", filePath);
return Promise.resolve("");
},
showItemInFolder(filePath) {
console.log("[shim:shell] showItemInFolder (stub):", filePath);
},
};

View File

@@ -0,0 +1,21 @@
export const systemPreferencesShim = {
getAccentColor() {
return "0078d4"; // Default Windows accent blue
},
isAeroGlassEnabled() {
return false;
},
getMediaAccessStatus(mediaType) {
return "granted";
},
askForMediaAccess(mediaType) {
return Promise.resolve(true);
},
on() {},
once() {},
removeListener() {},
};

View File

@@ -0,0 +1,64 @@
const listeners = [];
const darkQuery =
typeof window !== "undefined"
? window.matchMedia("(prefers-color-scheme: dark)")
: null;
if (darkQuery?.addEventListener) {
darkQuery.addEventListener("change", () => {
for (const fn of listeners) {
fn();
}
});
}
export const themeShim = {
get shouldUseDarkColors() {
return darkQuery ? darkQuery.matches : true;
},
get themeSource() {
return "system";
},
set themeSource(val) {
// No-op in browser; theme is controlled by OS
},
on(event, callback) {
if (event === "updated") {
listeners.push(callback);
}
return themeShim;
},
once(event, callback) {
if (event === "updated") {
const wrapped = () => {
const idx = listeners.indexOf(wrapped);
if (idx >= 0) {
listeners.splice(idx, 1);
}
callback();
};
listeners.push(wrapped);
}
return themeShim;
},
removeListener(event, callback) {
const idx = listeners.indexOf(callback);
if (idx >= 0) {
listeners.splice(idx, 1);
}
return themeShim;
},
removeAllListeners() {
listeners.length = 0;
return themeShim;
},
};

View File

@@ -0,0 +1,419 @@
const currentWindowState = {
title: "Obsidian",
isMaximized: false,
isMinimized: false,
isFullScreen: false,
isAlwaysOnTop: false,
bounds: { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight },
focusTime: Date.now(),
};
const currentWindow = {
isMaximized: () => currentWindowState.isMaximized,
isMinimized: () => currentWindowState.isMinimized,
isFullScreen: () => !!document.fullscreenElement,
isAlwaysOnTop: () => currentWindowState.isAlwaysOnTop,
isFocused: () => document.hasFocus(),
isVisible: () => true,
isDestroyed: () => false,
minimize() {
console.log("[shim:window] minimize (stub)");
},
maximize() {
currentWindowState.isMaximized = true;
},
unmaximize() {
currentWindowState.isMaximized = false;
},
restore() {
currentWindowState.isMinimized = false;
},
close() {
console.log("[shim:window] close (stub)");
},
focus() {
window.focus();
},
show() {},
hide() {},
setTitle(title) {
currentWindowState.title = title;
document.title = title;
},
getTitle() {
return currentWindowState.title;
},
setAlwaysOnTop(flag) {
currentWindowState.isAlwaysOnTop = flag;
},
setFullScreen(flag) {
if (flag) {
document.documentElement.requestFullscreen?.();
} else {
document.exitFullscreen?.();
}
},
getBounds() {
return {
x: window.screenX,
y: window.screenY,
width: window.innerWidth,
height: window.innerHeight,
};
},
setBounds(bounds) {
console.log("[shim:window] setBounds (stub):", bounds);
},
setSize(width, height) {},
setPosition(x, y) {},
center() {},
setTrafficLightPosition() {},
setWindowButtonPosition() {},
get webContents() {
return webContentsShim._current();
},
get menuBarVisible() {
return false;
},
set menuBarVisible(v) {},
get loaded() {
return true;
},
set loaded(v) {},
get focusTime() {
return currentWindowState.focusTime;
},
set focusTime(v) {
currentWindowState.focusTime = v;
},
on(event, handler) {
if (event === "focus") {
window.addEventListener("focus", handler);
} else if (event === "blur") {
window.addEventListener("blur", handler);
} else if (event === "resize") {
window.addEventListener("resize", handler);
}
return currentWindow;
},
once(event, handler) {
if (event === "focus") {
window.addEventListener("focus", handler, { once: true });
}
return currentWindow;
},
removeListener() {
return currentWindow;
},
removeAllListeners() {
return currentWindow;
},
};
const currentWebContents = {
id: 1,
_zoomLevel: 0,
get zoomLevel() {
return this._zoomLevel;
},
set zoomLevel(v) {
this._zoomLevel = v;
},
executeJavaScript(code) {
try {
return Promise.resolve(eval(code));
} catch (e) {
return Promise.reject(e);
}
},
getZoomFactor() {
return Math.pow(1.2, this._zoomLevel);
},
getZoomLevel() {
return this._zoomLevel;
},
setZoomLevel(v) {
this._zoomLevel = v;
},
isDevToolsOpened() {
return false;
},
openDevTools() {},
setWindowOpenHandler(handler) {
this._windowOpenHandler = handler;
},
printToPDF(options) {
return new Promise((resolve) => {
window.print();
resolve(Buffer.from([]));
});
},
capturePage(rect) {
// TODO: could use html2canvas
console.log("[shim:webContents] capturePage (stub)");
return Promise.resolve({
toPNG: () => new Uint8Array(0),
toJPEG: () => new Uint8Array(0),
});
},
undo() {},
redo() {},
cut() {
document.execCommand("cut");
},
copy() {
document.execCommand("copy");
},
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() {
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) {},
session: {
availableSpellCheckerLanguages: [],
setSpellCheckerLanguages(langs) {},
addWordToSpellCheckerDictionary(word) {},
},
setSpellCheckerLanguages(langs) {},
on(event, handler) {
return currentWebContents;
},
once(event, handler) {
return currentWebContents;
},
removeListener() {
return currentWebContents;
},
get isSecured() {
return true;
},
set isSecured(v) {},
};
// Popup tracking for PDF export etc.
let _popupWindow = null;
let _popupWebContents = null;
export function registerPopupWindow() {
_popupWebContents = {
id: 2,
_zoomLevel: 0,
getZoomFactor() {
return 1;
},
getZoomLevel() {
return 0;
},
setZoomLevel() {},
printToPDF(options) {
return Promise.resolve(Buffer.from([]));
},
executeJavaScript(code) {
try {
return Promise.resolve(eval(code));
} catch (e) {
return Promise.reject(e);
}
},
on() {
return _popupWebContents;
},
once() {
return _popupWebContents;
},
removeListener() {
return _popupWebContents;
},
isDestroyed() {
return false;
},
isFocused() {
return false;
},
};
_popupWindow = {
id: 2,
webContents: _popupWebContents,
isDestroyed() {
return false;
},
isFocused() {
return false;
},
isVisible() {
return false;
},
close() {
_popupWindow = null;
_popupWebContents = null;
},
destroy() {
_popupWindow = null;
_popupWebContents = null;
},
on() {
return _popupWindow;
},
once() {
return _popupWindow;
},
removeListener() {
return _popupWindow;
},
};
return _popupWindow;
}
export function unregisterPopupWindow() {
_popupWindow = null;
_popupWebContents = null;
}
export const windowShim = {
_current: () => currentWindow,
getFocusedWindow() {
return currentWindow;
},
getAllWindows() {
const wins = [currentWindow];
if (_popupWindow) {
wins.push(_popupWindow);
}
return wins;
},
fromId(id) {
if (id === currentWindow.id) {
return currentWindow;
}
if (_popupWindow && id === _popupWindow.id) {
return _popupWindow;
}
return null;
},
fromWebContents(wc) {
if (wc === currentWebContents) {
return currentWindow;
}
if (_popupWebContents && wc === _popupWebContents) {
return _popupWindow;
}
return null;
},
};
export const webContentsShim = {
_current: () => currentWebContents,
fromId(id) {
if (id === currentWebContents.id) {
return currentWebContents;
}
if (_popupWebContents && id === _popupWebContents.id) {
return _popupWebContents;
}
return null;
},
getAllWebContents() {
const wcs = [currentWebContents];
if (_popupWebContents) {
wcs.push(_popupWebContents);
}
return wcs;
},
};

View File

@@ -0,0 +1,24 @@
let currentZoom = 0;
export const webFrame = {
getZoomLevel() {
return currentZoom;
},
setZoomLevel(level) {
currentZoom = level;
// Approximate Electron's zoom behavior via CSS zoom
// Electron zoom level 0 = 100%, each step is ~20%
const scale = Math.pow(1.2, level);
document.body.style.zoom = scale;
},
getZoomFactor() {
return Math.pow(1.2, currentZoom);
},
setZoomFactor(factor) {
currentZoom = Math.log(factor) / Math.log(1.2);
document.body.style.zoom = factor;
},
};