diff --git a/.gitignore b/.gitignore index 4104ff8..c597a77 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ node_modules/ jspm_packages/ build/ out/ +bundle/ # TypeScript v1 declaration files typings/ diff --git a/assets/favicon.ico b/assets/favicon.ico index 7a2af28..1037109 100644 Binary files a/assets/favicon.ico and b/assets/favicon.ico differ diff --git a/assets/living-together.icns b/assets/living-together.icns new file mode 100644 index 0000000..3b1c04c Binary files /dev/null and b/assets/living-together.icns differ diff --git a/assets/living-together.ico b/assets/living-together.ico new file mode 100644 index 0000000..1c24b13 Binary files /dev/null and b/assets/living-together.ico differ diff --git a/config/electron.forge.config.js b/config/electron.forge.config.js new file mode 100644 index 0000000..bc8e4c2 --- /dev/null +++ b/config/electron.forge.config.js @@ -0,0 +1,68 @@ +const FS = require("fs"); +const Path = require("path"); +const minimist = require("minimist"); + +const args = minimist(process.argv.slice(2)); + +const PackageJSON = JSON.parse( + FS.readFileSync(Path.join(__dirname, "../package.json")) +); + +const Config = { + "name": PackageJSON.name, + "productName": PackageJSON.name, + "version": PackageJSON.version, + "description": PackageJSON.description, + "main": "./Electron.js", + "scripts": { + "start": "electron-forge start", + "package": "electron-forge package", + "make": "electron-forge make", + "publish": "electron-forge publish", + "lint": "echo \"No linting configured\"" + }, + "keywords": PackageJSON.keywords, + "author": { + "name": PackageJSON.author, + "email": "mrkbear@qq.com" + }, + "license": PackageJSON.license, + "config": { + "forge": { + "packagerConfig": { + "appBundleId": "com.mrkbear.living-together", + "appCopyright": "2021-2022 © copyright MrKBear", + "download": { + "rejectUnauthorized": false, + "executableName": "LivingTogether", + "mirrorOptions": { + "mirror": 'https://npmmirror.com/mirrors/electron/', + "customDir": '{{ version }}', + } + }, + "asar": true, + "icon": "./living-together" + }, + "makers": [ + { + "name": "@electron-forge/maker-zip", + "platforms": [ + "darwin" + ] + } + ] + } + }, + "dependencies": { + "electron-squirrel-startup": "^1.0.0", + "detect-port": PackageJSON.dependencies["detect-port"], + "express": PackageJSON.dependencies["express"], + }, + "devDependencies": { + "@electron-forge/cli": "^6.0.0-beta.63", + "@electron-forge/maker-zip": "^6.0.0-beta.63", + "electron": PackageJSON.devDependencies.electron + } +} + +FS.writeFileSync(Path.join(Path.resolve("./"), args.out ?? "./", "./package.json"), JSON.stringify(Config, null, 4)); \ No newline at end of file diff --git a/config/webpack.common.js b/config/webpack.common.js index 036b1c3..907e2b4 100644 --- a/config/webpack.common.js +++ b/config/webpack.common.js @@ -86,6 +86,10 @@ const Entry = () => ({ import: source("./Electron/Electron.ts"), dependOn: ["Service"] }, + + SimulatorWindow: { + import: source("./Electron/SimulatorWindow.ts"), + } }); /** diff --git a/config/webpack.electron.js b/config/webpack.electron.js index de970fd..a691723 100644 --- a/config/webpack.electron.js +++ b/config/webpack.electron.js @@ -10,6 +10,7 @@ module.exports = (env) => { entry: { Service: AllEntry.Service, Electron: AllEntry.Electron, + SimulatorWindow: AllEntry.SimulatorWindow }, output: Output("[name].js"), @@ -24,6 +25,7 @@ module.exports = (env) => { } }, + // externals: [nodeExternals({ allowlist: [/^(((?!electron).)*)$/] })], externals: [nodeExternals()], module: { diff --git a/package.json b/package.json index b5390c7..3ba3002 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "build-lab": "npm run clean & webpack --mode development --config ./config/webpack.lab.js", "release-lab": "npm run clean & webpack --mode production --no-devtool --config ./config/webpack.lab.js", "hmr-web": "webpack serve --open --config ./config/webpack.web.js", + "hmr-desktop": "webpack serve --open --config ./config/webpack.desktop.js", "build-web": "npm run clean & webpack --mode development --config ./config/webpack.web.js", "release-web": "npm run clean & webpack --mode production --no-devtool --config ./config/webpack.web.js", "build-desktop-web": "npm run clean & webpack --mode development --config ./config/webpack.desktop.js", @@ -18,16 +19,22 @@ "run-service": "node ./build/ServiceRunner.js --run --path ./build --port 12000", "build-run-web": "npm run build-web & npm run build-service & npm run run-service", "release-run-web": "npm run release-web & npm run release-service & npm run run-service", - "copy-fluent-icon": "fse mkdirp ./build/font-icon/ && fse emptyDir ./build/font-icon/ && fse copy ./node_modules/@fluentui/font-icons-mdl2/fonts/ ./build/font-icon/", + "copy-fluent-icon": "fse mkdirp ./build/font-icon/ & fse emptyDir ./build/font-icon/ & fse copy ./node_modules/@fluentui/font-icons-mdl2/fonts/ ./build/font-icon/", "build-run-desktop-web": "npm run build-desktop-web & npm run copy-fluent-icon & npm run build-service & npm run run-service", "release-run-desktop-web": "npm run release-desktop-web & npm run copy-fluent-icon & npm run release-service & npm run run-service", "skip-electron-ci": "set ELECTRON_SKIP_BINARY_DOWNLOAD=1& npm ci", "build-electron": "webpack --mode development --config ./config/webpack.electron.js", "release-electron": "webpack --mode production --no-devtool --config ./config/webpack.electron.js", "electron-cache": "set ELECTRON_SKIP_BINARY_DOWNLOAD=& set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/& set ELECTRON_CUSTOM_DIR={{ version }}& node ./node_modules/electron/install.js", - "electron": "set LIVING_TOGETHER_BASE_PATH=./build& npx electron ./build/Electron.js", + "electron": "set LIVING_TOGETHER_BASE_PATH=./build& set LIVING_TOGETHER_WEB_PATH=/& npx electron ./build/Electron.js", + "hmr-electron": "npm run build-electron & set LIVING_TOGETHER_SERVICE=http://127.0.0.1:12000& npm run electron", "build-run-electron": "npm run build-desktop-web & npm run copy-fluent-icon & npm run build-electron & npm run electron", - "release-run-electron": "npm run release-desktop-web & npm run copy-fluent-icon & npm run release-electron & npm run electron" + "release-run-electron": "npm run release-desktop-web & npm run copy-fluent-icon & npm run release-electron & npm run electron", + "copy-package-json": "fse mkdirp ./bundle/ & node ./config/electron.forge.config.js --out ./bundle", + "copy-build-result": "fse mkdirp ./bundle/ & fse mkdirp ./build/ & fse copy ./build/ ./bundle/", + "copy-electron-icon": "fse mkdirp ./bundle/ & fse copy ./assets/living-together.ico ./bundle/living-together.ico & fse copy ./assets/living-together.icns ./bundle/living-together.icns", + "electron-app-ci": "cd ./bundle & npm install & cd ../", + "gen-bundle": "fse emptyDir ./bundle/ & npm run copy-package-json & npm run copy-electron-icon & npm run electron-app-ci" }, "keywords": [ "artwork", diff --git a/source/Component/HeaderBar/HeaderBar.scss b/source/Component/HeaderBar/HeaderBar.scss index 5f284ed..1894c59 100644 --- a/source/Component/HeaderBar/HeaderBar.scss +++ b/source/Component/HeaderBar/HeaderBar.scss @@ -35,20 +35,20 @@ div.header-bar { div.header-windows-action { height: 100%; - width: 135px; - min-width: 135px; + width: 145px; + min-width: 145px; display: flex; // 在 Electron 中用于拖拽窗口 -webkit-app-region: no-drag; div.action-button { - width: 45px; - height: 45px; + width: 100%; + height: 100%; display: flex; justify-content: center; align-items: center; - font-size: .5em; + font-size: .8em; } div.action-button:hover { @@ -74,4 +74,21 @@ div.header-bar { background-color: rgba($color: #FFFFFF, $alpha: .1); } } +} + +div.header-bar.desktop-header-bar { + font-size: .9em; + + div.title > i, div.fps-view > i { + font-size: 18px; + } + + div.title > span { + display: inline-block; + padding-left: 5px; + } + + div.title { + padding-left: 5px; + } } \ No newline at end of file diff --git a/source/Component/HeaderBar/HeaderBar.tsx b/source/Component/HeaderBar/HeaderBar.tsx index 2be43d2..b287681 100644 --- a/source/Component/HeaderBar/HeaderBar.tsx +++ b/source/Component/HeaderBar/HeaderBar.tsx @@ -4,6 +4,7 @@ import { useStatusWithEvent, useStatus, IMixinStatusProps } from "@Context/Statu import { useSettingWithEvent, IMixinSettingProps, Platform } from "@Context/Setting"; import { Theme, BackgroundLevel, FontLevel } from "@Component/Theme/Theme"; import { LocalizationTooltipHost } from "@Component/Localization/LocalizationTooltipHost"; +import { useElectronWithEvent, IMixinElectronProps } from "@Context/Electron"; import { I18N } from "@Component/Localization/Localization"; import "./HeaderBar.scss"; @@ -78,18 +79,41 @@ class HeaderFpsView extends Component { public render() { + + const isMaxSize = this.props.electron?.isMaximized(); + return -
- +
{ + this.props.electron?.minimize(); + }} + > +
-
- +
{ + if (isMaxSize) { + this.props.electron?.unMaximize(); + } else { + this.props.electron?.maximize(); + } + }} + > +
-
- +
{ + this.props.electron?.close() + }} + > +
} @@ -115,8 +139,13 @@ class HeaderBar extends Component, E extends Record>(consumer: Consumer, keyName: string) { +function superConnect(consumer: Consumer, keyName: string) { return (components: R): R => { return ((props: any) => { diff --git a/source/Context/Electron.ts b/source/Context/Electron.ts new file mode 100644 index 0000000..ad7787a --- /dev/null +++ b/source/Context/Electron.ts @@ -0,0 +1,22 @@ +import { createContext } from "react"; +import { superConnect, superConnectWithEvent } from "@Context/Context"; +import { ISimulatorAPI, IApiEmitterEvent } from "@Electron/SimulatorAPI"; + +interface IMixinElectronProps { + electron?: ISimulatorAPI; +} + +const ElectronContext = createContext((window as any).API ?? {} as ISimulatorAPI); + +ElectronContext.displayName = "Electron"; +const ElectronProvider = ElectronContext.Provider; +const ElectronConsumer = ElectronContext.Consumer; + +/** + * 修饰器 + */ +const useElectron = superConnect(ElectronConsumer, "electron"); + +const useElectronWithEvent = superConnectWithEvent(ElectronConsumer, "electron"); + +export { useElectron, ElectronProvider, IMixinElectronProps, ISimulatorAPI, useElectronWithEvent }; \ No newline at end of file diff --git a/source/Electron/Electron.ts b/source/Electron/Electron.ts index e274c75..e62db7a 100644 --- a/source/Electron/Electron.ts +++ b/source/Electron/Electron.ts @@ -1,5 +1,6 @@ -import { app, BrowserWindow } from "electron"; +import { app, BrowserWindow, ipcMain } from "electron"; import { Service } from "@Service/Service"; +import { join as pathJoin } from "path"; const ENV = process.env ?? {}; class ElectronApp { @@ -14,6 +15,11 @@ class ElectronApp { public async runService() { + if (ENV.LIVING_TOGETHER_SERVICE) { + this.serviceUrl = ENV.LIVING_TOGETHER_SERVICE; + return; + } + let defaultPort: number | undefined = parseInt(ENV.LIVING_TOGETHER_DEFAULT_PORT ?? ""); if (isNaN(defaultPort)) defaultPort = undefined; @@ -22,7 +28,7 @@ class ElectronApp { ); } - public mainWindows?: BrowserWindow; + public simulatorWindow?: BrowserWindow; public async runMainThread() { @@ -30,14 +36,59 @@ class ElectronApp { await this.runService(); - this.mainWindows = new BrowserWindow({ + let preload = pathJoin(__dirname, "./SimulatorWindow.js"); + + // if (ENV.LIVING_TOGETHER_BASE_PATH) { + // preload = pathJoin(__dirname, ENV.LIVING_TOGETHER_BASE_PATH, "./SimulatorWindow.js"); + // } + + this.simulatorWindow = new BrowserWindow({ width: 800, height: 600, titleBarStyle: 'hidden', frame: false, + minWidth: 460, + minHeight: 300, + webPreferences: { preload } }); - this.mainWindows.loadURL(this.serviceUrl); + this.simulatorWindow.loadURL(this.serviceUrl + (ENV.LIVING_TOGETHER_WEB_PATH ?? "/resources/app.asar/")); + + this.handelSimulatorWindowBehavior(); + + app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit() + }); + } + + private handelSimulatorWindowBehavior() { + + ipcMain.on("windows.close", () => { + this.simulatorWindow?.close(); + }); + + ipcMain.on("windows.maximize", () => { + this.simulatorWindow?.maximize(); + }); + + ipcMain.on("windows.unMaximize", () => { + this.simulatorWindow?.unmaximize(); + }); + + ipcMain.on("windows.isMaximized", (event) => { + event.returnValue = this.simulatorWindow?.isMaximized(); + }); + + ipcMain.on("windows.minimize", (event) => { + this.simulatorWindow?.minimize(); + }); + + const sendWindowsChangeMessage = () => { + this.simulatorWindow?.webContents.send("windows.windowsSizeStateChange"); + } + + this.simulatorWindow?.on("maximize", sendWindowsChangeMessage); + this.simulatorWindow?.on("unmaximize", sendWindowsChangeMessage); } } diff --git a/source/Electron/SimulatorAPI.ts b/source/Electron/SimulatorAPI.ts new file mode 100644 index 0000000..6581821 --- /dev/null +++ b/source/Electron/SimulatorAPI.ts @@ -0,0 +1,35 @@ +import { Emitter } from "@Model/Emitter"; + +type IApiEmitterEvent = { + windowsSizeStateChange: void; +} + +interface ISimulatorAPI extends Emitter { + + /** + * 关闭窗口 + */ + close: () => void; + + /** + * 最大化窗口 + */ + maximize: () => void; + + /** + * 取消最大化 + */ + unMaximize: () => void; + + /** + * 是否处于最大化状态 + */ + isMaximized: () => boolean; + + /** + * 是否处于最大化状态 + */ + minimize: () => void; +} + +export { ISimulatorAPI, IApiEmitterEvent } \ No newline at end of file diff --git a/source/Electron/SimulatorWindow.ts b/source/Electron/SimulatorWindow.ts new file mode 100644 index 0000000..4012366 --- /dev/null +++ b/source/Electron/SimulatorWindow.ts @@ -0,0 +1,64 @@ +import { contextBridge, ipcRenderer } from "electron"; +import { ISimulatorAPI } from "@Electron/SimulatorAPI" + +const emitterMap: Array<[key: string, value: Function[]]> = []; +const queryEmitter = (key: string) => { + let res: (typeof emitterMap)[0] | undefined; + emitterMap.forEach((item) => { + if (item[0] === key) res = item; + }); + + if (res) { + if (Array.isArray(res[1])) return res[1]; + res[1] = []; + return res[1]; + } + + else { + res = [key, []]; + emitterMap.push(res); + return res[1]; + } +} + +const API: ISimulatorAPI = { + + close() { + ipcRenderer.send("windows.close"); + }, + + maximize() { + ipcRenderer.send("windows.maximize"); + }, + + unMaximize() { + ipcRenderer.send("windows.unMaximize"); + }, + + isMaximized() { + return ipcRenderer.sendSync("windows.isMaximized"); + }, + + minimize() { + ipcRenderer.send("windows.minimize"); + }, + + all: new Map() as any, + + resetAll: () => emitterMap.splice(0), + reset: (type) => queryEmitter(type).splice(0), + on: (type, handler) => queryEmitter(type).push(handler), + off: (type, handler) => { + const handlers = queryEmitter(type); + handlers.splice(handlers.indexOf(handler!) >>> 0, 1); + }, + emit: ((type: string, evt: any) => { + queryEmitter(type).slice().map((handler: any) => { handler(evt) }); + }) as any, +} + +ipcRenderer.on("windows.windowsSizeStateChange", () => { + API.emit("windowsSizeStateChange"); +}); + +contextBridge.exposeInMainWorld("API", API); \ No newline at end of file diff --git a/source/Page/SimulatorDesktop/SimulatorDesktop.tsx b/source/Page/SimulatorDesktop/SimulatorDesktop.tsx index 08d4c68..55a6a50 100644 --- a/source/Page/SimulatorDesktop/SimulatorDesktop.tsx +++ b/source/Page/SimulatorDesktop/SimulatorDesktop.tsx @@ -1,7 +1,9 @@ import { Component, ReactNode } from "react"; import { SettingProvider, Setting, Platform } from "@Context/Setting"; import { Theme, BackgroundLevel, FontLevel } from "@Component/Theme/Theme"; +import { ISimulatorAPI } from "@Electron/SimulatorAPI"; import { StatusProvider, Status } from "@Context/Status"; +import { ElectronProvider } from "@Context/Electron"; import { ClassicRenderer } from "@GLRender/ClassicRenderer"; import { initializeIcons } from '@fluentui/font-icons-mdl2'; import { RootContainer } from "@Component/Container/RootContainer"; @@ -16,6 +18,11 @@ import "./SimulatorDesktop.scss"; initializeIcons("./font-icon/"); class SimulatorDesktop extends Component { + + /** + * Electron API + */ + public electron: ISimulatorAPI; /** * 全局设置 @@ -47,6 +54,16 @@ class SimulatorDesktop extends Component { individual.position[2] = (Math.random() - .5) * 2; }) }; + + (window as any).setting = this.setting; + (window as any).status = this.status; + + this.electron = {} as ISimulatorAPI; + if ((window as any).API) { + this.electron = (window as any).API; + } else { + console.error("SimulatorDesktop: Can't find electron API"); + } } public componentDidMount() { @@ -82,7 +99,9 @@ class SimulatorDesktop extends Component { public render(): ReactNode { return - {this.renderContent()} + + {this.renderContent()} + } @@ -94,9 +113,9 @@ class SimulatorDesktop extends Component { fontLevel={FontLevel.Level3} > - +
diff --git a/source/Service/Service.ts b/source/Service/Service.ts index c79be00..ceba384 100644 --- a/source/Service/Service.ts +++ b/source/Service/Service.ts @@ -42,7 +42,7 @@ class Service { console.log("Service: service run in port " + this.servicePort); - return "http://127.0.0.1:" + this.servicePort + "/"; + return "http://127.0.0.1:" + this.servicePort; } }