diff --git a/electron.vite.config.ts b/electron.vite.config.ts index dc5f95daa..f2d8572d9 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -38,6 +38,7 @@ export default defineConfig({ rollupOptions: { input: { index: "emain/preload.ts", + "preload-webview": "emain/preload-webview.ts", }, output: { format: "cjs", diff --git a/emain/emain.ts b/emain/emain.ts index bd217fa05..5f37ca65c 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -582,6 +582,41 @@ electron.ipcMain.on("open-external", (event, url) => { } }); +function getUrlInSession(session: Electron.Session, url: string): Readable { + const request = electron.net.request({ + url, + method: "GET", + session, + }); + const readable = new Readable({ + read() {}, // No-op, we'll push data manually + }); + request.on("response", (response) => { + response.on("data", (chunk) => { + readable.push(chunk); + }); + + response.on("end", () => { + readable.push(null); + }); + }); + request.on("error", (err) => { + console.error("Request error:", err); + readable.destroy(err); + }); + request.end(); + return readable; +} + +electron.ipcMain.on("save-image", (event: electron.IpcMainEvent, payload: { src: string }) => { + console.log("save-image", payload.src); + const wc = event.sender; + if (wc == null) { + return; + } + getUrlInSession(wc.session, payload.src); +}); + electron.ipcMain.on("download", (event, payload) => { const window = electron.BrowserWindow.fromWebContents(event.sender); const streamingUrl = getWebServerEndpoint() + "/wave/stream-file?path=" + encodeURIComponent(payload.filePath); diff --git a/emain/platform.ts b/emain/platform.ts index 22d5e8651..1d6d06076 100644 --- a/emain/platform.ts +++ b/emain/platform.ts @@ -36,6 +36,9 @@ ipcMain.on("get-user-name", (event) => { ipcMain.on("get-host-name", (event) => { event.returnValue = os.hostname(); }); +ipcMain.on("get-webview-preload", (event) => { + event.returnValue = path.join(getElectronAppBasePath(), "preload", "preload-webview.cjs"); +}); // must match golang function getWaveHomeDir() { diff --git a/emain/preload-webview.ts b/emain/preload-webview.ts new file mode 100644 index 000000000..86cc90eec --- /dev/null +++ b/emain/preload-webview.ts @@ -0,0 +1,20 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +const { ipcRenderer } = require("electron"); + +document.addEventListener("contextmenu", (event) => { + console.log("contextmenu event", event); + if (event.target == null) { + return; + } + const targetElement = event.target as HTMLElement; + // Check if the right-click is on an image + if (targetElement.tagName === "IMG") { + const imgElem = targetElement as HTMLImageElement; + const imageUrl = imgElem.src; + ipcRenderer.send("save-image", { src: imageUrl }); + } +}); + +console.log("loaded wave preload-webview.ts"); diff --git a/emain/preload.ts b/emain/preload.ts index 4ad0bb2c4..6368812f3 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -12,6 +12,7 @@ contextBridge.exposeInMainWorld("api", { getHostName: () => ipcRenderer.sendSync("get-host-name"), getAboutModalDetails: () => ipcRenderer.sendSync("get-about-modal-details"), getDocsiteUrl: () => ipcRenderer.sendSync("get-docsite-url"), + getWebviewPreload: () => ipcRenderer.sendSync("get-webview-preload"), openNewWindow: () => ipcRenderer.send("open-new-window"), showContextMenu: (menu, position) => ipcRenderer.send("contextmenu-show", menu, position), onContextMenuClick: (callback) => ipcRenderer.on("contextmenu-click", (_event, id) => callback(id)), diff --git a/frontend/app/view/webview/webview.tsx b/frontend/app/view/webview/webview.tsx index c5aed8af7..6c771fae7 100644 --- a/frontend/app/view/webview/webview.tsx +++ b/frontend/app/view/webview/webview.tsx @@ -16,6 +16,19 @@ import * as jotai from "jotai"; import React, { memo, useEffect, useState } from "react"; import "./webview.less"; +let webviewPreloadUrl = null; + +function getWebviewPreloadUrl() { + if (webviewPreloadUrl == null) { + webviewPreloadUrl = getApi().getWebviewPreload(); + console.log("webviewPreloadUrl", webviewPreloadUrl); + } + if (webviewPreloadUrl == null) { + return null; + } + return "file://" + webviewPreloadUrl; +} + export class WebViewModel implements ViewModel { viewType: string; blockId: string; @@ -501,6 +514,7 @@ const WebView = memo(({ model }: WebViewProps) => { src={metaUrlInitial} data-blockid={model.blockId} data-webcontentsid={webContentsId} // needed for emain + preload={getWebviewPreloadUrl()} // @ts-ignore This is a discrepancy between the React typing and the Chromium impl for webviewTag. Chrome webviewTag expects a string, while React expects a boolean. allowpopups="true" > diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 14ef43570..cc7296747 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -57,6 +57,7 @@ declare global { getEnv: (varName: string) => string; getUserName: () => string; getHostName: () => string; + getWebviewPreload: () => string; getAboutModalDetails: () => AboutModalDetails; getDocsiteUrl: () => string; showContextMenu: (menu?: ElectronContextMenuItem[]) => void;