mirror of https://github.com/msgbyte/tailchat
				
				
				
			feat: add electron native webview render support #152
all website can be allow to visit in electronpull/147/merge
							parent
							
								
									af16ebe47b
								
							
						
					
					
						commit
						146952d4f3
					
				@ -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);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@ -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';
 | 
			
		||||
@ -0,0 +1,3 @@
 | 
			
		||||
.ant-dropdown-menu {
 | 
			
		||||
  box-shadow: none; /* avoid group detail dropdown's shadow will make dom invisiable */
 | 
			
		||||
}
 | 
			
		||||
					Loading…
					
					
				
		Reference in New Issue