|
|
@@ -0,0 +1,360 @@
|
|
|
+import { app, BrowserWindow, Event, Input, ipcMain, Menu, WebContentsView } from 'electron'
|
|
|
+import { fileURLToPath } from 'node:url'
|
|
|
+import path from 'node:path'
|
|
|
+import fs from 'node:fs'
|
|
|
+
|
|
|
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
|
+
|
|
|
+// The built directory structure
|
|
|
+//
|
|
|
+// ├─┬─┬ dist
|
|
|
+// │ │ └── index.html
|
|
|
+// │ │
|
|
|
+// │ ├─┬ dist-electron
|
|
|
+// │ │ ├── main.js
|
|
|
+// │ │ └── preload.mjs
|
|
|
+// │
|
|
|
+process.env.APP_ROOT = path.join(__dirname, '..')
|
|
|
+
|
|
|
+// 🚧 Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x
|
|
|
+export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL']
|
|
|
+export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron')
|
|
|
+export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist')
|
|
|
+
|
|
|
+process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST
|
|
|
+
|
|
|
+let mainWindow: BrowserWindow | null
|
|
|
+let childView: WebContentsView | null
|
|
|
+
|
|
|
+let isSideOpen = true
|
|
|
+let childViewAspectRatio = 0;
|
|
|
+const SIDE_WIDTH = 250
|
|
|
+const EXPAND_VIEW_SIZE = 40
|
|
|
+const LOADING_VIEW_WIDTH = 150
|
|
|
+const LOADING_VIEW_HEIGHT = 100
|
|
|
+
|
|
|
+function loadWindowPage(window: BrowserWindow, subPath: string) {
|
|
|
+ if (VITE_DEV_SERVER_URL) {
|
|
|
+ window.loadURL(VITE_DEV_SERVER_URL + "#" + subPath)
|
|
|
+ } else {
|
|
|
+ window.loadFile(path.join(RENDERER_DIST, 'index.html') + "#" + subPath)
|
|
|
+ }
|
|
|
+}
|
|
|
+function loadViewUrl(view: WebContentsView, subPath: string) {
|
|
|
+ if (!mainWindow) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (VITE_DEV_SERVER_URL) {
|
|
|
+ view.webContents.loadURL(VITE_DEV_SERVER_URL + "#" + subPath)
|
|
|
+ } else {
|
|
|
+ view.webContents.loadFile(path.join(RENDERER_DIST, 'index.html') + "#" + subPath)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+Menu.setApplicationMenu(null)
|
|
|
+
|
|
|
+function createWindow() {
|
|
|
+ mainWindow = new BrowserWindow({
|
|
|
+ icon: path.join(process.env.VITE_PUBLIC, 'icon.ico'),
|
|
|
+ webPreferences: {
|
|
|
+ preload: path.join(__dirname, 'preload.mjs'),
|
|
|
+ contextIsolation: true,
|
|
|
+ allowRunningInsecureContent: true,
|
|
|
+ partition: 'persist:minnan-demo-app',
|
|
|
+ },
|
|
|
+ width: 1200,
|
|
|
+ height: 800,
|
|
|
+ })
|
|
|
+
|
|
|
+ childView = new WebContentsView({
|
|
|
+ webPreferences: {
|
|
|
+ partition: 'persist:minnan-demo-app',
|
|
|
+ allowRunningInsecureContent: true,
|
|
|
+ enableBlinkFeatures: 'PasswordManager',
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const expandButtonView = new WebContentsView({
|
|
|
+ webPreferences: {
|
|
|
+ preload: path.join(__dirname, 'preload.mjs'),
|
|
|
+ contextIsolation: true,
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const loadingView = new BrowserWindow({
|
|
|
+ skipTaskbar: true,
|
|
|
+ width: LOADING_VIEW_WIDTH,
|
|
|
+ height: LOADING_VIEW_HEIGHT,
|
|
|
+ parent: mainWindow,
|
|
|
+ thickFrame: true,
|
|
|
+ titleBarStyle: 'hidden',
|
|
|
+ webPreferences: {
|
|
|
+ preload: path.join(__dirname, 'preload.mjs'),
|
|
|
+ contextIsolation: true,
|
|
|
+ },
|
|
|
+ })
|
|
|
+ mainWindow.contentView.addChildView(childView)
|
|
|
+ mainWindow.contentView.addChildView(expandButtonView)
|
|
|
+ expandButtonView.setVisible(false)
|
|
|
+
|
|
|
+ childView.webContents.on('did-start-loading', () => {
|
|
|
+ loadingView.show()
|
|
|
+ });
|
|
|
+ childView.webContents.on('did-stop-loading', () => {
|
|
|
+ loadingView.hide()
|
|
|
+ });
|
|
|
+ childView.webContents.on('did-fail-load', (_, errorCode, errorDescription) => {
|
|
|
+ loadingView.hide()
|
|
|
+ loadWindowPage(loadingView, '/error?code=' + errorCode + '&message=' + errorDescription);
|
|
|
+ });
|
|
|
+
|
|
|
+ function updateChildWindowBounds() {
|
|
|
+ const bounds = mainWindow!.getBounds();
|
|
|
+ expandButtonView.setBounds({
|
|
|
+ x: 0,
|
|
|
+ y: bounds.height - 100,
|
|
|
+ width: EXPAND_VIEW_SIZE,
|
|
|
+ height: EXPAND_VIEW_SIZE
|
|
|
+ })
|
|
|
+ loadingView.setBounds({
|
|
|
+ x: bounds.x + (bounds.width - LOADING_VIEW_WIDTH) / 2,
|
|
|
+ y: bounds.y + (bounds.height - LOADING_VIEW_HEIGHT) / 2,
|
|
|
+ width: LOADING_VIEW_WIDTH,
|
|
|
+ height: LOADING_VIEW_HEIGHT
|
|
|
+ })
|
|
|
+ if (childViewAspectRatio) {
|
|
|
+ // 保持子页纵横比
|
|
|
+ const rect = {
|
|
|
+ x: isSideOpen ? SIDE_WIDTH : 0,
|
|
|
+ y: 0,
|
|
|
+ width: bounds.width - (isSideOpen ? SIDE_WIDTH : 0),
|
|
|
+ height: bounds.height
|
|
|
+ };
|
|
|
+
|
|
|
+ // 计算子窗口的最佳尺寸(contain模式)
|
|
|
+ const availableRatio = rect.width / rect.height;
|
|
|
+ let childWidth, childHeight;
|
|
|
+
|
|
|
+ if (availableRatio > childViewAspectRatio) {
|
|
|
+ // 高度受限制
|
|
|
+ childHeight = rect.height;
|
|
|
+ childWidth = childHeight * childViewAspectRatio;
|
|
|
+ } else {
|
|
|
+ // 宽度受限制
|
|
|
+ childWidth = rect.width;
|
|
|
+ childHeight = childWidth / childViewAspectRatio;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算居中位置
|
|
|
+ const childX = rect.x + (rect.width - childWidth) / 2;
|
|
|
+ const childY = rect.y + (rect.height - childHeight) / 2;
|
|
|
+
|
|
|
+ // 应用到子窗口
|
|
|
+ childView!.setBounds({
|
|
|
+ x: Math.round(childX),
|
|
|
+ y: Math.round(childY),
|
|
|
+ width: Math.round(childWidth),
|
|
|
+ height: Math.round(childHeight)
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ childView!.setBounds({
|
|
|
+ x: isSideOpen ? SIDE_WIDTH : 0,
|
|
|
+ y: 0,
|
|
|
+ width: bounds.width - (isSideOpen ? SIDE_WIDTH : 0),
|
|
|
+ height: bounds.height
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ loadWindowPage(mainWindow, '/');
|
|
|
+ loadWindowPage(loadingView, '/loading');
|
|
|
+ loadViewUrl(childView, '/hello');
|
|
|
+ loadViewUrl(expandButtonView, '/expand');
|
|
|
+ updateChildWindowBounds();
|
|
|
+
|
|
|
+ mainWindow.on('resize', () => {
|
|
|
+ updateChildWindowBounds()
|
|
|
+ })
|
|
|
+ mainWindow.webContents.on('did-finish-load', () => {
|
|
|
+ mainWindow?.webContents.send('main-process-message', (new Date).toLocaleString())
|
|
|
+ })
|
|
|
+
|
|
|
+ function handleWindowFullScreenKeys(event: Event, input: Input) {
|
|
|
+ if (input.key === 'F11' && input.type === 'keyDown') {
|
|
|
+ event.preventDefault();
|
|
|
+ mainWindow?.setFullScreen(!mainWindow?.isFullScreen())
|
|
|
+ } else if (input.key === 'F12' && input.type === 'keyDown') {
|
|
|
+ event.preventDefault();
|
|
|
+ mainWindow?.webContents.toggleDevTools();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加F11全屏切换功能
|
|
|
+ mainWindow.webContents.on('before-input-event', (event, input) => {
|
|
|
+ handleWindowFullScreenKeys(event, input)
|
|
|
+ })
|
|
|
+ childView.webContents.on('before-input-event', (event, input) => {
|
|
|
+ handleWindowFullScreenKeys(event, input)
|
|
|
+ })
|
|
|
+ // 处理退出应用事件
|
|
|
+ ipcMain.on('exit-app', () => {
|
|
|
+ app.quit()
|
|
|
+ })
|
|
|
+ // 处理全屏切换事件
|
|
|
+ ipcMain.on('toggle-fullscreen', (_event, isFullScreen: boolean) => {
|
|
|
+ if (mainWindow) {
|
|
|
+ if (isFullScreen) {
|
|
|
+ mainWindow.setFullScreen(true)
|
|
|
+ } else {
|
|
|
+ mainWindow.setFullScreen(false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ // 加载子页URL
|
|
|
+ ipcMain.on('load-child-url', (_event, url: string, aspectRatio: number) => {
|
|
|
+ if (childView)
|
|
|
+ childView.webContents.loadURL(url)
|
|
|
+ childViewAspectRatio = aspectRatio
|
|
|
+ updateChildWindowBounds()
|
|
|
+ })
|
|
|
+ // 子页侧边栏开关
|
|
|
+ ipcMain.on('toggle-child-side', (_event, value: boolean) => {
|
|
|
+ isSideOpen = value
|
|
|
+ expandButtonView.setVisible(!isSideOpen)
|
|
|
+ mainWindow?.webContents.send('main-side-state-changed', isSideOpen)
|
|
|
+ updateChildWindowBounds()
|
|
|
+ })
|
|
|
+ // 处理获取应用路径事件
|
|
|
+ ipcMain.handle('get-app-path', () => {
|
|
|
+ return app.getAppPath()
|
|
|
+ })
|
|
|
+ // 处理打开窗口事件
|
|
|
+ ipcMain.on('open-window', (_event, url: string) => {
|
|
|
+ const newWin = new BrowserWindow({
|
|
|
+ icon: path.join(process.env.VITE_PUBLIC, 'icon.ico'),
|
|
|
+ webPreferences: {
|
|
|
+ preload: path.join(__dirname, 'preload.mjs'),
|
|
|
+ contextIsolation: true,
|
|
|
+ allowRunningInsecureContent: true,
|
|
|
+ },
|
|
|
+ fullscreenable: true,
|
|
|
+ maximizable: true,
|
|
|
+ width: 1200,
|
|
|
+ height: 800,
|
|
|
+ })
|
|
|
+ newWin.loadURL(url)
|
|
|
+ newWin.maximize();
|
|
|
+ // 添加F11全屏切换功能
|
|
|
+ newWin.webContents.on('before-input-event', (event, input) => {
|
|
|
+ if (input.key === 'F11' && input.type === 'keyDown') {
|
|
|
+ event.preventDefault();
|
|
|
+ newWin?.setFullScreen(!newWin?.fullScreen)
|
|
|
+ } else if (input.key === 'F12' && input.type === 'keyDown') {
|
|
|
+ event.preventDefault();
|
|
|
+ newWin?.webContents.toggleDevTools();
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+ // 处理加载apps.json事件
|
|
|
+ ipcMain.handle('load-apps-json', async () => {
|
|
|
+ const appPath = process.cwd()
|
|
|
+ const appsJsonPath = path.join(appPath, 'apps.json')
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (fs.existsSync(appsJsonPath)) {
|
|
|
+ const data = fs.readFileSync(appsJsonPath, 'utf8')
|
|
|
+ return JSON.parse(data)
|
|
|
+ } else {
|
|
|
+ // 开发环境下回退到public目录
|
|
|
+ const devAppsJsonPath = path.join(process.env.VITE_PUBLIC || '', 'apps.json')
|
|
|
+ if (fs.existsSync(devAppsJsonPath)) {
|
|
|
+ const data = fs.readFileSync(devAppsJsonPath, 'utf8')
|
|
|
+ return JSON.parse(data)
|
|
|
+ }
|
|
|
+ throw new Error('apps.json not found')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error loading apps.json:', error)
|
|
|
+ throw error
|
|
|
+ }
|
|
|
+ })
|
|
|
+ // 处理加载默认apps.json事件
|
|
|
+ ipcMain.handle('load-default-apps-json', async () => {
|
|
|
+ const devAppsJsonPath = path.join(process.env.VITE_PUBLIC || '', 'apps.json')
|
|
|
+ if (fs.existsSync(devAppsJsonPath)) {
|
|
|
+ const data = fs.readFileSync(devAppsJsonPath, 'utf8')
|
|
|
+ return JSON.parse(data)
|
|
|
+ }
|
|
|
+ throw new Error('apps.json not found')
|
|
|
+ })
|
|
|
+ // 处理显示配置窗口事件
|
|
|
+ ipcMain.on('show-config', () => {
|
|
|
+ const configWindow = new BrowserWindow({
|
|
|
+ icon: path.join(process.env.VITE_PUBLIC, 'icon.ico'),
|
|
|
+ webPreferences: {
|
|
|
+ preload: path.join(__dirname, 'preload.mjs'),
|
|
|
+ contextIsolation: true,
|
|
|
+ allowRunningInsecureContent: true,
|
|
|
+ },
|
|
|
+ title: '列表配置',
|
|
|
+ parent: mainWindow || undefined,
|
|
|
+ skipTaskbar: true,
|
|
|
+ minimizable: false,
|
|
|
+ maximizable: false,
|
|
|
+ modal: true,
|
|
|
+ width: 800,
|
|
|
+ height: 600,
|
|
|
+ })
|
|
|
+ loadWindowPage(configWindow, '/config')
|
|
|
+ });
|
|
|
+ // 处理保存apps.json事件
|
|
|
+ ipcMain.on('save-apps-json', (_event, appsJson: string) => {
|
|
|
+ const appPath = process.cwd()
|
|
|
+ const appsJsonPath = path.join(appPath, 'apps.json')
|
|
|
+ try {
|
|
|
+ fs.writeFileSync(appsJsonPath, appsJson)
|
|
|
+ mainWindow?.webContents.send('main-config-changed')
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error saving apps.json:', error)
|
|
|
+ throw error
|
|
|
+ }
|
|
|
+ })
|
|
|
+ // 处理显示关于窗口事件
|
|
|
+ ipcMain.on('show-about', () => {
|
|
|
+ const aboutWindow = new BrowserWindow({
|
|
|
+ icon: path.join(process.env.VITE_PUBLIC, 'icon.ico'),
|
|
|
+ webPreferences: {
|
|
|
+ preload: path.join(__dirname, 'preload.mjs'),
|
|
|
+ contextIsolation: true,
|
|
|
+ allowRunningInsecureContent: true,
|
|
|
+ },
|
|
|
+ parent: mainWindow || undefined,
|
|
|
+ title: '关于程序',
|
|
|
+ skipTaskbar: true,
|
|
|
+ maximizable: false,
|
|
|
+ minimizable: false,
|
|
|
+ modal: true,
|
|
|
+ width: 450,
|
|
|
+ height: 470,
|
|
|
+ })
|
|
|
+ loadWindowPage(aboutWindow, '/about')
|
|
|
+ });
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// Quit when all mainWindowdows are closed, except on macOS. There, it's common
|
|
|
+// for applications and their menu bar to stay active until the user quits
|
|
|
+// explicitly with Cmd + Q.
|
|
|
+app.on('window-all-closed', () => {
|
|
|
+ if (process.platform !== 'darwin') {
|
|
|
+ app.quit()
|
|
|
+ mainWindow = null
|
|
|
+ }
|
|
|
+})
|
|
|
+app.on('activate', () => {
|
|
|
+ // On OS X it's common to re-create a mainWindowdow in the app when the
|
|
|
+ // dock icon is clicked and there are no other mainWindowdows open.
|
|
|
+ if (BrowserWindow.getAllWindows().length === 0) {
|
|
|
+ createWindow()
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+app.whenReady().then(createWindow)
|