feat: add electron native webview render support #152

all website can be allow to visit in electron
pull/147/merge
moonrailgun 1 year ago
parent af16ebe47b
commit 146952d4f3

@ -222,7 +222,7 @@
"electronmon": {
"patterns": [
"!**/**",
"src/main/*"
"src/main/**/*"
],
"logLevel": "quiet"
}

@ -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);
});
}

@ -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...');
@ -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) {

@ -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';

@ -2,6 +2,9 @@ import {
regCustomPanel,
regChatInputButton,
postMessageEvent,
sharedEvent,
regPluginSettings,
getCachedUserSettings,
} from '@capital/common';
import { Icon } from '@capital/component';
import React from 'react';
@ -9,8 +12,12 @@ 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`);
@ -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
) => {

@ -0,0 +1,3 @@
.ant-dropdown-menu {
box-shadow: none; /* avoid group detail dropdown's shadow will make dom invisiable */
}

@ -37,4 +37,13 @@ export const Translate = {
'zh-CN': '已经是最新版',
'en-US': 'Already the latest version',
}),
nativeWebviewRender: localTrans({
'zh-CN': '启用原生浏览器内核渲染',
'en-US': 'Use Native Webview Render',
}),
nativeWebviewRenderDesc: localTrans({
'zh-CN': '解除默认网页访问限制允许在Tailchat嵌入任意网站内容',
'en-US':
'Lift default web page access restrictions and allow any website content to be embedded in Tailchat',
}),
};

@ -40,11 +40,11 @@ const DefaultWebviewKernel: React.FC<WebviewKernelProps> = React.memo(
);
DefaultWebviewKernel.displayName = 'DefaultWebviewKernel';
const [getWebviewKernel, setWebviewKernel] = buildRegFn<
const [getWebviewKernel, setWebviewKernel, resetWebviewKernel] = buildRegFn<
() => React.ComponentType<WebviewKernelProps>
>('webviewKernelComponent', () => DefaultWebviewKernel);
export { setWebviewKernel };
export { setWebviewKernel, resetWebviewKernel };
interface WebviewProps {
className?: string;

@ -74,6 +74,7 @@ export {
useUpdateRef,
isDevelopment,
} from 'tailchat-shared';
export { setWebviewKernel, resetWebviewKernel } from '@/components/Webview';
export { navigate } from '@/components/AppRouterApi';
export { useLocation, useNavigate } from 'react-router';

@ -299,7 +299,7 @@ interface PluginUserExtraInfo {
export const [pluginUserExtraInfo, regUserExtraInfo] =
buildRegList<PluginUserExtraInfo>();
type PluginSettings = FullModalFactoryConfig & {
export type PluginSettings = FullModalFactoryConfig & {
position: 'system'; // 后面可能还会有个人设置/群组设置
};

Loading…
Cancel
Save