Skip to content

Commit

Permalink
feat: add electron native webview render support #152
Browse files Browse the repository at this point in the history
all website can be allow to visit in electron
  • Loading branch information
moonrailgun committed Oct 11, 2023
1 parent af16ebe commit 146952d
Show file tree
Hide file tree
Showing 10 changed files with 357 additions and 4 deletions.
2 changes: 1 addition & 1 deletion client/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@
"electronmon": {
"patterns": [
"!**/**",
"src/main/*"
"src/main/**/*"
],
"logLevel": "quiet"
}
Expand Down
186 changes: 186 additions & 0 deletions client/desktop/src/main/lib/webview-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/**
* Fork from https://github.com/msgbyte/webbox/blob/main/src/main/webviewManager.ts
*/

import { BrowserView, BrowserWindow, ipcMain, Rectangle } from 'electron';
import os from 'os';
import log from 'electron-log';

interface WebviewInfo {
view: BrowserView;
url: string;
hidden: boolean;
}

const webviewMap = new Map<string, WebviewInfo>();

/**
* fix rect into correct size
*/
function fixRect(rect: Rectangle, isFullScreen: boolean): Rectangle {
const xOffset = 1;
const yOffset = !isFullScreen && os.platform() === 'darwin' ? 28 : 0; // add y axis offset in mac os if is not fullScreen

return {
x: Math.round(rect.x) + xOffset,
y: Math.round(rect.y) + yOffset,
width: Math.round(rect.width) - xOffset,
height: Math.round(rect.height),
};
}

export function initWebviewManager(win: BrowserWindow) {
ipcMain.on('$mount-webview', (e, info) => {
if (!win) {
log.info('[mount-webview]', 'cannot get mainWindow');
return;
}

log.info('[mount-webview] info:', info);

const { key, url } = info;
if (!url) {
return;
}

if (webviewMap.has(key)) {
const webview = webviewMap.get(key)!;
win.setTopBrowserView(webview.view);
webview.view.setBounds(fixRect(info.rect, win.isFullScreen()));
if (webview.url !== url) {
// url has been change.
webview.view.webContents.loadURL(url);
}
return;
}

// hideAllWebview();
const view = new BrowserView({
webPreferences: {
nodeIntegration: false,
},
});
view.setBackgroundColor('#fff');
view.setBounds(fixRect(info.rect, win.isFullScreen()));
view.webContents.loadURL(url);
win.addBrowserView(view);
webviewMap.set(key, { view, url, hidden: false });
});

ipcMain.on('$unmount-webview', (e, info) => {
if (!win) {
log.info('[unmount-webview]', 'cannot get mainWindow');
return;
}

log.info('[unmount-webview] info:', info);

const { key } = info;
const webview = webviewMap.get(key);
if (webview) {
win.removeBrowserView(webview.view);
webviewMap.delete(key);
}
});

ipcMain.on('$update-webview-rect', (e, info) => {
if (!win) {
log.info('[update-webview-rect]', 'cannot get mainWindow');
return;
}

log.info('[update-webview-rect] info:', info);

// Change All View to avoid under view display on resize.
// webviewMap.forEach((webview) => {
// webview.hidden = false;
// webview.view.setBounds(fixRect(info.rect, win.isFullScreen()));
// });

// Change Single View
const webview = webviewMap.get(info.key);
if (webview) {
webview.hidden = false;
webview.view.setBounds(fixRect(info.rect, win.isFullScreen()));
}
});

ipcMain.on('$show-webview', (e, info) => {
log.info('[show-webview] info:', info);

const webview = webviewMap.get(info.key);
if (webview) {
showWebView(webview);
}
});

ipcMain.on('$hide-webview', (e, info) => {
log.info('[hide-webview] info:', info);

const webview = webviewMap.get(info.key);
if (webview) {
hideWebView(webview);
}
});

ipcMain.on('$hide-all-webview', () => {
log.info('[hide-all-webview]');

hideAllWebview();
});

ipcMain.on('$clear-all-webview', () => {
if (!win) {
log.info('[clear-all-webview]', 'cannot get mainWindow');
return;
}

log.info('[clear-all-webview]');

win.getBrowserViews().forEach((view) => {
win.removeBrowserView(view);
});

webviewMap.clear();
});
}

const HIDDEN_OFFSET = 3000;

/**
* Show webview with remove offset in y
*/
function showWebView(webview: WebviewInfo) {
if (webview.hidden === false) {
return;
}

webview.hidden = false;
const oldBounds = webview.view.getBounds();
webview.view.setBounds({
...oldBounds,
y: oldBounds.y - HIDDEN_OFFSET,
});
}

/**
* Hide webview with append offset in y
*/
function hideWebView(webview: WebviewInfo) {
if (webview.hidden === true) {
return;
}

webview.hidden = true;
const oldBounds = webview.view.getBounds();
webview.view.setBounds({
...oldBounds,
y: oldBounds.y + HIDDEN_OFFSET,
});
}

function hideAllWebview() {
Array.from(webviewMap.values()).forEach((webview) => {
hideWebView(webview);
});
}
3 changes: 3 additions & 0 deletions client/desktop/src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import is from 'electron-is';
import { initScreenshots } from './screenshots';
import { generateInjectedScript } from './inject';
import { handleTailchatMessage } from './inject/message-handler';
import { initWebviewManager } from './lib/webview-manager';

log.info('Start...');

Expand Down Expand Up @@ -277,6 +278,8 @@ const createMainWindow = async (url: string) => {

mainWindowState.manage(mainWindow);

initWebviewManager(mainWindow);

// Remove this if your app does not use auto updates
new AppUpdater();
} catch (err) {
Expand Down
114 changes: 114 additions & 0 deletions client/web/plugins/com.msgbyte.env.electron/src/ElectronWebview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React, { useEffect, useRef } from 'react';

interface ElectronWebviewProps {
className?: string;
src: string;
}
export const ElectronWebview: React.FC<ElectronWebviewProps> = React.memo(
(props) => {
const containerRef = useRef<HTMLDivElement>(null);
const key = props.src;
const url = props.src;

useEffect(() => {
if (!containerRef.current) {
return;
}

const rect = containerRef.current.getBoundingClientRect();

(window as any).electron.ipcRenderer.sendMessage('$mount-webview', {
key,
url,
rect: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
},
});

return () => {
(window as any).electron.ipcRenderer.sendMessage('$unmount-webview', {
key,
});
};
}, [key, url]);

useEffect(() => {
if (!containerRef.current) {
return;
}

const intersectionObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry: any) => {
if (entry.isVisible === true) {
// 完全可见,显示
(window as any).electron.ipcRenderer.sendMessage(
'$show-webview',
{
key: key,
}
);
} else {
(window as any).electron.ipcRenderer.sendMessage(
'$hide-webview',
{
key: key,
}
);
}
});
},
{
trackVisibility: true,
delay: 200,
} as any
);

const resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const { target } = entry;
if (!target.parentElement) {
return;
}

const rect = target.getBoundingClientRect();

(window as any).electron.ipcRenderer.sendMessage(
'$update-webview-rect',
{
key: key,
rect: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
},
}
);
});
});

intersectionObserver.observe(containerRef.current);
resizeObserver.observe(containerRef.current);

return () => {
if (containerRef.current) {
intersectionObserver.unobserve(containerRef.current);
resizeObserver.unobserve(containerRef.current);
}
};
}, [key]);

return (
<div
ref={containerRef}
className={props.className}
style={{ width: '100%', height: '100%' }}
/>
);
}
);
ElectronWebview.displayName = 'ElectronWebview';
37 changes: 37 additions & 0 deletions client/web/plugins/com.msgbyte.env.electron/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@ import {
regCustomPanel,
regChatInputButton,
postMessageEvent,
sharedEvent,
regPluginSettings,
getCachedUserSettings,
} from '@capital/common';
import { Icon } from '@capital/component';
import React from 'react';
import { DeviceInfoPanel } from './DeviceInfoPanel';
import { Translate } from './translate';
import { forwardSharedEvent } from './utils';
import { checkUpdate } from './checkUpdate';
import { setWebviewKernel, resetWebviewKernel } from '@capital/common';
import { ElectronWebview } from './ElectronWebview';
import './overwrite.css';

const PLUGIN_NAME = 'Electron Support';
const WEBVIEW_CONFIG = 'electron:nativeWebviewRender';

console.log(`Plugin ${PLUGIN_NAME} is loaded`);

Expand All @@ -37,12 +44,42 @@ regChatInputButton({
},
});

regPluginSettings({
position: 'system',
type: 'boolean',
name: WEBVIEW_CONFIG,
label: Translate.nativeWebviewRender,
desc: Translate.nativeWebviewRenderDesc,
});

forwardSharedEvent('receiveUnmutedMessage');

setTimeout(() => {
checkUpdate();
}, 1000);

let changedWithElectron = false;

const checkSettingConfig = (settings: Record<string, any>) => {
if (settings[WEBVIEW_CONFIG] === true) {
setWebviewKernel(() => ElectronWebview);
changedWithElectron = true;
} else if (changedWithElectron === true) {
// 如果关闭了配置且仅当之前用electron设置了webview,则重置
resetWebviewKernel();
}
};

sharedEvent.on('loginSuccess', () => {
getCachedUserSettings().then((settings) => {
checkSettingConfig(settings);
});
});

sharedEvent.on('userSettingsUpdate', (settings) => {
checkSettingConfig(settings);
});

navigator.mediaDevices.getDisplayMedia = async (
options: DisplayMediaStreamOptions
) => {
Expand Down
3 changes: 3 additions & 0 deletions client/web/plugins/com.msgbyte.env.electron/src/overwrite.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.ant-dropdown-menu {
box-shadow: none; /* avoid group detail dropdown's shadow will make dom invisiable */
}
Loading

0 comments on commit 146952d

Please sign in to comment.