mirror of
https://github.com/Nystik-gh/ignis.git
synced 2026-06-17 04:35:53 +00:00
move shim to new package
This commit is contained in:
51
packages/shim/src/electron/index.js
Normal file
51
packages/shim/src/electron/index.js
Normal 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;
|
||||
},
|
||||
},
|
||||
};
|
||||
260
packages/shim/src/electron/ipc-renderer.js
Normal file
260
packages/shim/src/electron/ipc-renderer.js
Normal 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);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
43
packages/shim/src/electron/remote/app.js
Normal file
43
packages/shim/src/electron/remote/app.js
Normal 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() {},
|
||||
};
|
||||
64
packages/shim/src/electron/remote/clipboard.js
Normal file
64
packages/shim/src/electron/remote/clipboard.js
Normal 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(() => {});
|
||||
},
|
||||
};
|
||||
259
packages/shim/src/electron/remote/dialog.js
Normal file
259
packages/shim/src/electron/remote/dialog.js
Normal 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);
|
||||
},
|
||||
};
|
||||
50
packages/shim/src/electron/remote/index.js
Normal file
50
packages/shim/src/electron/remote/index.js
Normal 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();
|
||||
},
|
||||
};
|
||||
53
packages/shim/src/electron/remote/menu.js
Normal file
53
packages/shim/src/electron/remote/menu.js
Normal 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 || "";
|
||||
}
|
||||
}
|
||||
83
packages/shim/src/electron/remote/native-image.js
Normal file
83
packages/shim/src/electron/remote/native-image.js
Normal 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));
|
||||
}
|
||||
},
|
||||
};
|
||||
37
packages/shim/src/electron/remote/notification.js
Normal file
37
packages/shim/src/electron/remote/notification.js
Normal 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;
|
||||
}
|
||||
}
|
||||
40
packages/shim/src/electron/remote/screen.js
Normal file
40
packages/shim/src/electron/remote/screen.js
Normal 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() {},
|
||||
};
|
||||
20
packages/shim/src/electron/remote/session.js
Normal file
20
packages/shim/src/electron/remote/session.js
Normal file
@@ -0,0 +1,20 @@
|
||||
export const sessionShim = {
|
||||
defaultSession: {
|
||||
clearCache() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
clearStorageData() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
setSpellCheckerLanguages(langs) {},
|
||||
getSpellCheckerLanguages() {
|
||||
return [];
|
||||
},
|
||||
|
||||
on() {},
|
||||
once() {},
|
||||
removeListener() {},
|
||||
},
|
||||
};
|
||||
15
packages/shim/src/electron/remote/shell.js
Normal file
15
packages/shim/src/electron/remote/shell.js
Normal 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);
|
||||
},
|
||||
};
|
||||
21
packages/shim/src/electron/remote/system-preferences.js
Normal file
21
packages/shim/src/electron/remote/system-preferences.js
Normal 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() {},
|
||||
};
|
||||
64
packages/shim/src/electron/remote/theme.js
Normal file
64
packages/shim/src/electron/remote/theme.js
Normal 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;
|
||||
},
|
||||
};
|
||||
419
packages/shim/src/electron/remote/window.js
Normal file
419
packages/shim/src/electron/remote/window.js
Normal 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;
|
||||
},
|
||||
};
|
||||
24
packages/shim/src/electron/web-frame.js
Normal file
24
packages/shim/src/electron/web-frame.js
Normal 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;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user