From 70f4ba99d0e3f99a66ab247bb5cee62e37730802 Mon Sep 17 00:00:00 2001 From: BlackHole1 <158blackhole@gmail.com> Date: Tue, 12 Oct 2021 17:35:05 +0800 Subject: [PATCH] feat(desktop): add share screen tip window and support multiple window --- cspell.config.js | 1 + desktop/main-app/src/utils/IPCActions.ts | 5 +- desktop/main-app/src/utils/IPCEmit.ts | 10 +- desktop/main-app/src/utils/WindowEvent.ts | 11 +- desktop/main-app/src/utils/WindowManager.ts | 134 +++++++++++++++++- .../renderer-app/src/assets/image/drag.svg | 11 ++ .../ShareScreen/ShareScreen/index.tsx | 10 +- .../ShareScreen/ShareScreenTip/index.tsx | 59 ++++++++ .../ShareScreen/ShareScreenTip/style.less | 24 ++++ desktop/renderer-app/src/utils/ipc.ts | 20 --- .../src/utils/portalWindowManager.ts | 61 ++++++++ packages/flat-i18n/locales/en.json | 5 +- packages/flat-i18n/locales/zh-CN.json | 5 +- packages/flat-types/src/constants/index.ts | 9 +- packages/flat-types/src/index.ts | 1 + packages/flat-types/src/ipc/index.ts | 2 + packages/flat-types/src/portal/index.ts | 5 + 17 files changed, 332 insertions(+), 41 deletions(-) create mode 100644 desktop/renderer-app/src/assets/image/drag.svg create mode 100644 desktop/renderer-app/src/components/ShareScreen/ShareScreenTip/index.tsx create mode 100644 desktop/renderer-app/src/components/ShareScreen/ShareScreenTip/style.less create mode 100644 desktop/renderer-app/src/utils/portalWindowManager.ts create mode 100644 packages/flat-types/src/portal/index.ts diff --git a/cspell.config.js b/cspell.config.js index 7e22e15d274..1f9a026ba78 100644 --- a/cspell.config.js +++ b/cspell.config.js @@ -105,6 +105,7 @@ module.exports = { "browserslist", "estree", "webstorm", + "Frameless", // CNCF "nindent", diff --git a/desktop/main-app/src/utils/IPCActions.ts b/desktop/main-app/src/utils/IPCActions.ts index 0b328d72985..809814ed883 100644 --- a/desktop/main-app/src/utils/IPCActions.ts +++ b/desktop/main-app/src/utils/IPCActions.ts @@ -1,4 +1,4 @@ -import { CustomSingleWindow } from "./WindowManager"; +import { CustomSingleWindow, windowManager } from "./WindowManager"; import { ipc } from "flat-types"; import { app, ipcMain, powerSaveBlocker } from "electron"; import runtime from "./Runtime"; @@ -105,6 +105,9 @@ export const appActionAsync: ipc.AppActionAsync = { openAsHidden: false, }); }, + "force-close-window": ({ windowName }) => { + windowManager.removeWindow(windowName); + }, }; export const appActionSync: ipc.AppActionSync = { diff --git a/desktop/main-app/src/utils/IPCEmit.ts b/desktop/main-app/src/utils/IPCEmit.ts index 1c2c619af6c..2ca22ab212b 100644 --- a/desktop/main-app/src/utils/IPCEmit.ts +++ b/desktop/main-app/src/utils/IPCEmit.ts @@ -16,19 +16,15 @@ const ipcEmitHandler = (windowName: constants.WindowsName): IPCEmit => { }; export const ipcEmitByMain = ipcEmitHandler(constants.WindowsName.Main); -export const ipcEmitByClass = ipcEmitHandler(constants.WindowsName.Class); -export const ipcEmitByReplay = ipcEmitHandler(constants.WindowsName.Replay); +export const ipcEmitByShareScreenTip = ipcEmitHandler(constants.WindowsName.ShareScreenTip); export const ipcEmit = (windowName: constants.WindowsName): IPCEmit => { switch (windowName) { case constants.WindowsName.Main: { return ipcEmitByMain; } - case constants.WindowsName.Class: { - return ipcEmitByClass; - } - case constants.WindowsName.Replay: { - return ipcEmitByReplay; + case constants.WindowsName.ShareScreenTip: { + return ipcEmitByShareScreenTip; } } }; diff --git a/desktop/main-app/src/utils/WindowEvent.ts b/desktop/main-app/src/utils/WindowEvent.ts index f393c29fa8d..4e8c0d54ef3 100644 --- a/desktop/main-app/src/utils/WindowEvent.ts +++ b/desktop/main-app/src/utils/WindowEvent.ts @@ -18,7 +18,16 @@ export const windowHookClose = (customWindow: CustomSingleWindow): void => { export const windowReadyToShow = (customWindow: CustomSingleWindow): void => { customWindow.window.on("ready-to-show", () => { - customWindow.window.show(); + if (customWindow.options.isPortal) { + // waiting dom load finish + setTimeout(() => { + if (!customWindow.window.isDestroyed()) { + customWindow.window.show(); + } + }, 100); + } else { + customWindow.window.show(); + } }); }; diff --git a/desktop/main-app/src/utils/WindowManager.ts b/desktop/main-app/src/utils/WindowManager.ts index eb8f57a5ed4..1c2c91a8b09 100644 --- a/desktop/main-app/src/utils/WindowManager.ts +++ b/desktop/main-app/src/utils/WindowManager.ts @@ -1,7 +1,14 @@ -import { BrowserWindow, BrowserWindowConstructorOptions, ipcMain, IpcMainEvent } from "electron"; +import { + BrowserWindow, + screen, + BrowserWindowConstructorOptions, + ipcMain, + IpcMainEvent, + Display, +} from "electron"; import { windowHookClose, windowOpenDevTools, windowReadyToShow } from "./WindowEvent"; import runtime from "./Runtime"; -import { constants } from "flat-types"; +import { constants, portal } from "flat-types"; import { Subject, zip } from "rxjs"; import { ignoreElements, mergeMap } from "rxjs/operators"; @@ -11,7 +18,6 @@ const defaultWindowOptions: Pick { + if (!frameName.startsWith(constants.Portal)) { + return; + } + + const customOptions: portal.Options = JSON.parse( + frameName.substring(constants.Portal.length), + ); + + const handler = this.createPortalNewWindow(customOptions.name, options); + + if (handler === null) { + return; + } + + event.preventDefault(); + + event.newGuest = handler().window; + }, + ); + } + + private createPortalNewWindow( + windowName: constants.WindowsName, + options: BrowserWindowConstructorOptions, + ): null | (() => CustomSingleWindow) { + switch (windowName) { + case constants.WindowsName.ShareScreenTip: { + return () => this.createShareScreenTipWindow(options); + } + default: { + return null; + } + } + } + + private getDisplayByMainWindow(): Display { + const mainBounds = this.wins.Main!.window.getBounds(); + + return screen.getDisplayNearestPoint({ + x: mainBounds.x, + y: mainBounds.y, + }); + } + + private static getXCenterPoint(display: Display, windowWidth: number): number { + const { x, width } = display.workArea; + + // see: https://github.com/jenslind/electron-positioner/blob/85bb453453af050dda2479c88c4a24a262f8a2fb/index.js#L74 + return Math.floor(x + (width / 2 - windowWidth / 2)); + } } export const windowManager = new WindowManager(); @@ -153,4 +272,5 @@ interface WindowOptions { name: constants.WindowsName; disableClose?: boolean; isOpenDevTools?: boolean; + isPortal: boolean; } diff --git a/desktop/renderer-app/src/assets/image/drag.svg b/desktop/renderer-app/src/assets/image/drag.svg new file mode 100644 index 00000000000..1e866bfa573 --- /dev/null +++ b/desktop/renderer-app/src/assets/image/drag.svg @@ -0,0 +1,11 @@ + + + 形状结合 + + + + + + + + \ No newline at end of file diff --git a/desktop/renderer-app/src/components/ShareScreen/ShareScreen/index.tsx b/desktop/renderer-app/src/components/ShareScreen/ShareScreen/index.tsx index afe716f1afa..ef319aedc71 100644 --- a/desktop/renderer-app/src/components/ShareScreen/ShareScreen/index.tsx +++ b/desktop/renderer-app/src/components/ShareScreen/ShareScreen/index.tsx @@ -4,6 +4,7 @@ import React, { useEffect, useMemo, useRef } from "react"; import { observer } from "mobx-react-lite"; import classNames from "classnames"; import type { ShareScreenStore } from "../../../stores/ShareScreenStore"; +import { ShareScreenTip } from "../ShareScreenTip"; interface ShareScreenProps { shareScreenStore: ShareScreenStore; @@ -24,5 +25,12 @@ export const ShareScreen = observer(function ShareScreen({ sha }); }, [shareScreenStore.existOtherShareScreen]); - return
; + return ( + <> +
+ {shareScreenStore.enableShareScreenStatus && ( + + )} + + ); }); diff --git a/desktop/renderer-app/src/components/ShareScreen/ShareScreenTip/index.tsx b/desktop/renderer-app/src/components/ShareScreen/ShareScreenTip/index.tsx new file mode 100644 index 00000000000..77b154db61b --- /dev/null +++ b/desktop/renderer-app/src/components/ShareScreen/ShareScreenTip/index.tsx @@ -0,0 +1,59 @@ +import React from "react"; +import { observer } from "mobx-react-lite"; +import { useEffect, useState } from "react"; +import ReactDOM from "react-dom"; +import { constants } from "flat-types"; +import "./style.less"; +import { portalWindowManager } from "../../../utils/portalWindowManager"; +import { ipcAsyncByApp } from "../../../utils/ipc"; +import dragSVG from "../../../assets/image/drag.svg"; +import { Button } from "antd"; +import { ShareScreenStore } from "../../../stores/ShareScreenStore"; +import { useTranslation } from "react-i18next"; + +interface ShareScreenTipProps { + shareScreenStore: ShareScreenStore; +} + +export const ShareScreenTip = observer(function ShareScreenTip({ + shareScreenStore, +}) { + const { t } = useTranslation(); + const [windowInstance, setWindowInstance] = useState(null); + const [containerEl] = useState(() => document.createElement("div")); + + useEffect(() => { + const instance = portalWindowManager.createShareScreenTipPortalWindow(containerEl); + + setWindowInstance(instance); + + return () => { + ipcAsyncByApp("force-close-window", { + windowName: constants.WindowsName.ShareScreenTip, + }); + }; + }, [containerEl, t]); + + useEffect(() => { + if (windowInstance) { + windowInstance.document.title = t("share-screen.tip-window-title"); + } + }, [windowInstance, t]); + + const stopShareScreen = (): void => { + shareScreenStore.close().catch(console.error); + }; + + return ReactDOM.createPortal( +
+
+ drag icon + {t("share-screen.tip-window-body")} +
+ +
, + containerEl, + ); +}); diff --git a/desktop/renderer-app/src/components/ShareScreen/ShareScreenTip/style.less b/desktop/renderer-app/src/components/ShareScreen/ShareScreenTip/style.less new file mode 100644 index 00000000000..db61ea4c756 --- /dev/null +++ b/desktop/renderer-app/src/components/ShareScreen/ShareScreenTip/style.less @@ -0,0 +1,24 @@ +.share-screen-tip { + display: flex; + justify-content: space-between; + padding: 0 8px; + width: 100%; + height: 100%; + align-items: center; + background: rgba(0, 0, 0, 0.75); + color: #FFFFFF; + font-size: 14px; + + & > div { + display: flex; + + & > span { + display: block; + margin-left: 16px; + } + } + + & > button { + border-radius: 8px; + } +} diff --git a/desktop/renderer-app/src/utils/ipc.ts b/desktop/renderer-app/src/utils/ipc.ts index ff22fa2ae8c..a48c8d993de 100644 --- a/desktop/renderer-app/src/utils/ipc.ts +++ b/desktop/renderer-app/src/utils/ipc.ts @@ -24,26 +24,6 @@ export const ipcAsyncByMainWindow = < ipcAsync(constants.WindowsName.Main)(action, args); }; -export const ipcAsyncByMainClass = < - T extends keyof ipc.WindowActionAsync, - U extends Parameters[0], ->( - action: T, - args: U, -): void => { - ipcAsync(constants.WindowsName.Class)(action, args); -}; - -export const ipcAsyncByMainReplay = < - T extends keyof ipc.WindowActionAsync, - U extends Parameters[0], ->( - action: T, - args: U, -): void => { - ipcAsync(constants.WindowsName.Replay)(action, args); -}; - export const ipcAsyncByApp = < T extends keyof ipc.AppActionAsync, U extends Parameters[0], diff --git a/desktop/renderer-app/src/utils/portalWindowManager.ts b/desktop/renderer-app/src/utils/portalWindowManager.ts new file mode 100644 index 00000000000..acb15cf963f --- /dev/null +++ b/desktop/renderer-app/src/utils/portalWindowManager.ts @@ -0,0 +1,61 @@ +import { constants, portal } from "flat-types"; + +class PortalWindowManager { + public createShareScreenTipPortalWindow(containerElement: HTMLDivElement): Window { + const shareScreenTipWindow = this.createWindow(containerElement, { + name: constants.WindowsName.ShareScreenTip, + }); + + PortalWindowManager.patchFramelessStyle(shareScreenTipWindow); + + return shareScreenTipWindow; + } + + private createWindow(containerElement: HTMLDivElement, feature: portal.Options): Window { + const urlHash = new URL(window.location.href).hash; + const featureString = JSON.stringify(feature); + + const portalWindow = window.open( + `about:blank${urlHash}`, + `${constants.Portal}${featureString}`, + )!; + + // avoid being unable to use defined style + const styles = document.querySelectorAll("style"); + const links = document.querySelectorAll("link"); + + styles.forEach((ele: HTMLStyleElement) => { + portalWindow.document.head.appendChild(ele.cloneNode(true)); + }); + + links.forEach((ele: HTMLLinkElement) => { + portalWindow.document.head.appendChild(ele.cloneNode(true)); + }); + + // if don’t do this, the image resource will fail to load + const base = document.createElement("base"); + base.href = window.location.origin; + portalWindow.document.head.appendChild(base); + + portalWindow.document.body.appendChild(containerElement); + + return portalWindow; + } + + // see: https://www.electronjs.org/docs/api/frameless-window#draggable-region + private static patchFramelessStyle(portalWindow: Window): void { + // @ts-ignore + portalWindow.document.body.style.webkitAppRegion = "drag"; + + // see: https://stackoverflow.com/a/66395289/6596777 + portalWindow.document.head.appendChild( + Object.assign(document.createElement("style"), { + textContent: `button { + -webkit-app-region: no-drag; + }`, + }), + ); + } +} + +export const portalWindowManager = new PortalWindowManager(); diff --git a/packages/flat-i18n/locales/en.json b/packages/flat-i18n/locales/en.json index 50002a02aa8..59d5e519397 100644 --- a/packages/flat-i18n/locales/en.json +++ b/packages/flat-i18n/locales/en.json @@ -355,6 +355,9 @@ "share-screen": { "browser-not-permission": "Please grant your browser access to screen recording", "desktop-not-permission": "Please grant Flat access to screen recording", - "choose-share-content": "Choose what to share" + "choose-share-content": "Choose what to share", + "tip-window-title": "Ending screen sharing", + "tip-window-body": "You are sharing the current screen", + "tip-window-button": "End Sharing" } } diff --git a/packages/flat-i18n/locales/zh-CN.json b/packages/flat-i18n/locales/zh-CN.json index c58eb1450b4..bfb3e2bbab5 100644 --- a/packages/flat-i18n/locales/zh-CN.json +++ b/packages/flat-i18n/locales/zh-CN.json @@ -355,6 +355,9 @@ "share-screen": { "browser-not-permission": "请授予浏览器访问屏幕录制的权限", "desktop-not-permission": "请授予 Flat 访问屏幕录制的权限", - "choose-share-content": "选择共享内容" + "choose-share-content": "选择共享内容", + "tip-window-title": "结束屏幕共享", + "tip-window-body": "正在共享屏幕", + "tip-window-button": "结束共享" } } diff --git a/packages/flat-types/src/constants/index.ts b/packages/flat-types/src/constants/index.ts index 68710b58867..dd8c2d81538 100644 --- a/packages/flat-types/src/constants/index.ts +++ b/packages/flat-types/src/constants/index.ts @@ -1,7 +1,6 @@ export enum WindowsName { Main = "Main", - Replay = "Replay", - Class = "Class", + ShareScreenTip = "ShareScreenTip", } export const PageSize = { @@ -25,4 +24,10 @@ export const PageSize = { width: 1280, height: 720, }, + ShareScreenTip: { + width: 320, + height: 48, + }, } as const; + +export const Portal = "portal;"; diff --git a/packages/flat-types/src/index.ts b/packages/flat-types/src/index.ts index 1b5d26742c5..ae265a5cf21 100644 --- a/packages/flat-types/src/index.ts +++ b/packages/flat-types/src/index.ts @@ -2,3 +2,4 @@ export * as ipc from "./ipc"; export * as runtime from "./runtime"; export * as constants from "./constants"; export * as update from "./update"; +export * as portal from "./portal"; diff --git a/packages/flat-types/src/ipc/index.ts b/packages/flat-types/src/ipc/index.ts index d3e51b61f88..d041520739e 100644 --- a/packages/flat-types/src/ipc/index.ts +++ b/packages/flat-types/src/ipc/index.ts @@ -1,5 +1,6 @@ import type { Type as RuntimeType } from "../runtime"; import type { UpdateCheckInfo, PrereleaseTag } from "../update"; +import { WindowsName } from "../constants"; export type WindowActionAsync = { "set-win-size": (args: { @@ -25,6 +26,7 @@ export type WindowActionAsync = { export type AppActionAsync = { "set-open-at-login": (args: { isOpenAtLogin: boolean }) => void; + "force-close-window": (args: { windowName: WindowsName }) => void; }; export type AppActionSync = { diff --git a/packages/flat-types/src/portal/index.ts b/packages/flat-types/src/portal/index.ts new file mode 100644 index 00000000000..df56b282f18 --- /dev/null +++ b/packages/flat-types/src/portal/index.ts @@ -0,0 +1,5 @@ +import { WindowsName } from "../constants"; + +export interface Options { + name: WindowsName; +}