'use strict' /* global __static */ import path from 'node:path' import DevSidecar from '@docmirror/dev-sidecar' import { app, BrowserWindow, dialog, globalShortcut, ipcMain, Menu, nativeImage, nativeTheme, powerMonitor, protocol, Tray } from 'electron' import minimist from 'minimist' import { createProtocol } from 'vue-cli-plugin-electron-builder/lib' import backend from './bridge/backend' import jsonApi from '@docmirror/mitmproxy/src/json' import log from './utils/util.log.gui' log.info(`background.js start, platform is ${process.platform}`) const isWindows = process.platform === 'win32' const isLinux = process.platform === 'linux' const isMac = process.platform === 'darwin' const isDevelopment = process.env.NODE_ENV !== 'production' // 避免其他系统出现异常,只有 Windows 使用 './background/powerMonitor' let _powerMonitor = powerMonitor if (isWindows) { try { _powerMonitor = require('./background/powerMonitor').powerMonitor } catch (e) { log.error(`加载 './background/powerMonitor' 失败,现捕获异常并使用默认的 powerMonitor。\r\n目前,启动着DS重启电脑时,将无法正常关闭系统代理,届时请自行关闭系统代理!\r\n捕获的异常信息:`, e) } } // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. let win let winIsHidden = false let tray // 防止被内存清理 let forceClose = false try { DevSidecar.api.config.reload() } catch (e) { log.error('配置加载失败:', e) } let hideDockWhenWinClose = DevSidecar.api.config.get().app.dock.hideWhenWinClose || false // Scheme must be registered before the app is ready protocol.registerSchemesAsPrivileged([ { scheme: 'app', privileges: { secure: true, standard: true } }, ]) function openDevTools () { try { log.debug('尝试打开 `开发者工具`') win.webContents.openDevTools() log.debug('打开 `开发者工具` 成功') } catch (e) { log.error('打开 `开发者工具` 失败:', e) } } function closeDevTools () { try { log.debug('尝试关闭 `开发者工具`') win.webContents.closeDevTools() log.debug('关闭 `开发者工具` 成功') } catch (e) { log.error('关闭 `开发者工具` 失败:', e) } } function switchDevTools () { if (!win || !win.webContents) { return } if (win.webContents.isDevToolsOpened()) { closeDevTools() } else { openDevTools() } } // 隐藏主窗口,并创建托盘,绑定关闭事件 function setTray () { // const topMenu = Menu.buildFromTemplate({}) // Menu.setApplicationMenu(topMenu) // 用一个 Tray 来表示一个图标,这个图标处于正在运行的系统的通知区 // 通常被添加到一个 context menu 上. // 系统托盘右键菜单 const trayMenuTemplate = [ { // 系统托盘图标目录 label: 'DevTools (F12)', click: switchDevTools, }, { // 系统托盘图标目录 label: '退出', click: () => { log.info('force quit') forceClose = true quit('系统托盘图标-退出') }, }, ] // 设置系统托盘图标 const iconRootPath = path.join(__dirname, '../extra/icons/tray') let iconPath = path.join(iconRootPath, 'icon.png') const iconWhitePath = path.join(iconRootPath, 'icon-white.png') const iconBlackPath = path.join(iconRootPath, 'icon-black.png') if (isMac) { iconPath = nativeTheme.shouldUseDarkColors ? iconWhitePath : iconBlackPath } const trayIcon = nativeImage.createFromPath(iconPath) const appTray = new Tray(trayIcon) // 当桌面主题更新时 if (isMac) { nativeTheme.on('updated', () => { log.info('i am changed') if (nativeTheme.shouldUseDarkColors) { log.info('i am dark.') tray.setImage(iconWhitePath) } else { log.info('i am light.') tray.setImage(iconBlackPath) // tray.setPressedImage(iconWhitePath) } }) } // 图标的上下文菜单 const contextMenu = Menu.buildFromTemplate(trayMenuTemplate) // 设置托盘悬浮提示 appTray.setToolTip('DevSidecar-开发者边车辅助工具') // 单击托盘小图标显示应用 appTray.on('click', () => { // 显示主程序 showWin() }) appTray.on('right-click', () => { setTimeout(() => { appTray.popUpContextMenu(contextMenu) }, 200) }) return appTray } function checkHideWin () { const config = DevSidecar.api.config.get() // 配置为false时,不需要校验 if (!config.app.needCheckHideWindow) { return true } // 如果是linux,且没有设置快捷键,则提示先设置快捷键 if (isLinux && !hasShortcut(config.app.showHideShortcut)) { dialog.showMessageBox({ type: 'info', title: '提示:请先设置快捷键', message: '由于大部分 Linux 系统没有系统托盘,所以需使用快捷键呼出窗口。\n但您还未设置快捷键,请先到 “设置” 页面中设置好快捷键,再关闭窗口。', buttons: ['确定'], }) return false } return true } function hideWin (reason = '', needCheck = false) { if (win) { if (needCheck && !checkHideWin()) { return } win.hide() if (isMac && hideDockWhenWinClose) { app.dock.hide() } winIsHidden = true } else { log.warn(`win is null, do not hide win, reason: ${reason}`) } } function showWin () { if (win) { win.show() } else { log.warn('win is null, do not show win') } if (app.dock) { app.dock.show() } winIsHidden = false } function changeAppConfig (config) { if (config.hideDockWhenWinClose != null) { hideDockWhenWinClose = config.hideDockWhenWinClose } } function createWindow (startHideWindow, autoQuitIfError = true) { // Create the browser window. const windowSize = DevSidecar.api.config.get().app.windowSize || {} try { win = new BrowserWindow({ width: windowSize.width || 900, height: windowSize.height || 750, title: 'DevSidecar', webPreferences: { enableRemoteModule: true, contextIsolation: false, nativeWindowOpen: true, // ADD THIS // Use pluginOptions.nodeIntegration, leave this alone // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info nodeIntegration: true, // process.env.ELECTRON_NODE_INTEGRATION }, show: !startHideWindow, icon: path.join(__static, 'icon.png'), }) } catch (e) { log.error('创建窗口失败:', e) dialog.showErrorBox('错误', `创建窗口失败: ${e.message}`) if (autoQuitIfError) { quit('创建窗口失败') } return false } winIsHidden = !!startHideWindow Menu.setApplicationMenu(null) win.setMenu(null) // !!IMPORTANT if (isWindows && typeof _powerMonitor.setupMainWindow === 'function') { _powerMonitor.setupMainWindow(win) } if (process.env.WEBPACK_DEV_SERVER_URL) { // Load the url of the dev server if in development mode win.loadURL(process.env.WEBPACK_DEV_SERVER_URL) if (!process.env.IS_TEST) { setTimeout(openDevTools, 2000) } } else { createProtocol('app') // Load the index.html when not in development win.loadURL('app://./index.html') } if (startHideWindow) { hideWin('startHideWindow') } win.on('closed', async (...args) => { log.info('win closed:', ...args) win = null tray = null }) ipcMain.on('close', async (event, message) => { if (message.value === 1) { quit('ipc receive "close"') } else { hideWin('ipc receive "close"', true) } }) win.on('close', (e, ...args) => { log.info('win close:', e, ...args) if (forceClose) { return } e.preventDefault() const config = DevSidecar.api.config.get() const closeStrategy = config.app.closeStrategy if (closeStrategy === 1) { // 直接退出 quit('win close') } else if (closeStrategy === 2) { // 隐藏窗口 hideWin('win close', true) } else { // 弹窗提示,选择关闭策略 win.webContents.send('close.showTip', { closeStrategy, showHideShortcut: config.app.showHideShortcut }) } }) win.on('session-end', async (e, ...args) => { log.info('win session-end:', e, ...args) await quit('win session-end') }) const shortcut = (event, input) => { if (input.key === 'F12' && input.type === 'keyUp' && !input.control && !input.shift && !input.alt && !input.meta) { // 按 F12,打开/关闭 开发者工具 event.preventDefault() switchDevTools() } else if (input.key === 'F5' && input.type === 'keyUp' && !input.control && !input.shift && !input.alt && !input.meta) { // 按 F5,刷新页面 event.preventDefault() win.webContents.reload() } else { // 全文检索框(SearchBar)相关快捷键 if ((input.key === 'F' || input.key === 'f') && input.type === 'keyDown' && input.control && !input.shift && !input.alt && !input.meta) { // 按 Ctrl + F,显示或隐藏全文检索框(SearchBar) event.preventDefault() win.webContents.send('search-bar', { key: 'show-hide' }) } else if (input.key === 'Escape' && input.type === 'keyUp' && !input.control && !input.shift && !input.alt && !input.meta) { // 按 ESC,隐藏全文检索框(SearchBar) event.preventDefault() win.webContents.send('search-bar', { key: 'hide' }) } else if (input.key === 'F3' && input.type === 'keyDown' && !input.control && !input.shift && !input.alt && !input.meta) { // 按 F3,全文检索框(SearchBar)定位到下一个 event.preventDefault() win.webContents.send('search-bar', { key: 'next' }) } else if (input.key === 'F3' && input.type === 'keyDown' && !input.control && input.shift && !input.alt && !input.meta) { // 按 Shift + F3,全文检索框(SearchBar)定位到上一个 event.preventDefault() win.webContents.send('search-bar', { key: 'previous' }) } } } // 监听键盘事件 win.webContents.on('before-input-event', (event, input) => { win.webContents.executeJavaScript('config') .then((value) => { console.info('window.config:', value, ', key:', input.key) if (!value || (value.disableBeforeInputEvent !== true && value.disableBeforeInputEvent !== 'true')) { shortcut(event, input) } }) .catch(() => { shortcut(event, input) }) }) // 监听渲染进程发送过来的消息 win.webContents.on('ipc-message', (event, channel, message, ...args) => { console.info('win ipc-message:', event, channel, message, ...args) // 记录日志 if (channel && channel.startsWith('[ERROR]')) { log.error('win ipc-message:', channel.substring(7), message, ...args) } else { log.info('win ipc-message:', channel, message, ...args) } if (channel === 'change-showHideShortcut') { registerShowHideShortcut(message) } }) return true } async function beforeQuit () { log.info('before quit') return DevSidecar.api.shutdown() } async function quit (reason) { log.info('app quit:', reason) if (tray) { tray.displayBalloon({ title: '正在关闭', content: '关闭中,请稍候。。。' }) } await beforeQuit() forceClose = true app.quit() } function hasShortcut (showHideShortcut) { return showHideShortcut && showHideShortcut.length > 1 } function registerShowHideShortcut (showHideShortcut) { globalShortcut.unregisterAll() if (hasShortcut(showHideShortcut)) { try { const registerSuccess = globalShortcut.register(DevSidecar.api.config.get().app.showHideShortcut, () => { if (winIsHidden) { showWin() } else { if (!win.isFocused()) { win.focus() // 如果窗口打开着,但没有获取焦点,则获取焦点,而不是hide } else { hideWin('shortcut') } } }) if (registerSuccess) { log.info('注册快捷键成功:', DevSidecar.api.config.get().app.showHideShortcut) } else { log.error('注册快捷键失败:', DevSidecar.api.config.get().app.showHideShortcut) } } catch (e) { log.error('注册快捷键异常:', DevSidecar.api.config.get().app.showHideShortcut, ', error:', e) } } } function initApp () { if (isMac) { app.whenReady().then(() => { app.dock.setIcon(path.join(__dirname, '../extra/icons/512x512.png')) }) } // 全局监听快捷键,用于 显示/隐藏 窗口 app.whenReady().then(async () => { registerShowHideShortcut(DevSidecar.api.config.get().app.showHideShortcut) }) } // -------------执行开始--------------- try { app.disableHardwareAcceleration() // 禁用gpu // 开启后是否默认隐藏window let startHideWindow = !DevSidecar.api.config.get().app.startShowWindow if (app.getLoginItemSettings().wasOpenedAsHidden) { startHideWindow = true } else if (process.argv) { const args = minimist(process.argv) log.info('start args:', args) // 通过启动参数,判断是否隐藏窗口 const hideWindowArg = `${args.hideWindow}` if (hideWindowArg === 'true' || hideWindowArg === '1') { startHideWindow = true } else if (hideWindowArg === 'false' || hideWindowArg === '0') { startHideWindow = false } } log.info('startHideWindow = ', startHideWindow, ', app.getLoginItemSettings() = ', jsonApi.stringify2(app.getLoginItemSettings())) // 禁止双开 const isFirstInstance = app.requestSingleInstanceLock() if (!isFirstInstance) { log.info('app quit: is second instance(禁止双开)') setTimeout(() => { app.quit() }, 1000) } else { app.on('before-quit', async () => { log.info('before-quit') if (process.platform === 'darwin') { quit('before quit') } }) app.on('will-quit', () => { log.info('应用关闭,注销所有快捷键') globalShortcut.unregisterAll() }) app.on('second-instance', (event, commandLine) => { log.info('new app started, command:', commandLine) if (win) { showWin() win.focus() } }) // Quit when all windows are closed. app.on('window-all-closed', () => { log.info('window-all-closed') // On macOS it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q if (process.platform !== 'darwin') { quit('window-all-closed') } }) app.on('activate', () => { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (win == null) { createWindow(false, false) } else { showWin() } }) // initApp() // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.on('ready', async () => { // if (isDevelopment && !process.env.IS_TEST) { // // Install Vue Devtools // try { // await installExtension(VUEJS_DEVTOOLS) // } catch (e) { // log.error('Vue Devtools failed to install:', e.toString()) // } // } try { if (!createWindow(startHideWindow)) { return // 创建窗口失败,应用将关闭 } } catch (err) { log.error('createWindow error:', err) } try { const context = { win, app, beforeQuit, quit, ipcMain, dialog, log, api: DevSidecar.api, changeAppConfig } backend.install(context) // 模块安装 } catch (err) { log.error('install modules error:', err) } try { // 最小化到托盘 tray = setTray() } catch (err) { log.error('setTray error:', err) } _powerMonitor.on('shutdown', async (e) => { if (e) { e.preventDefault() } log.info('系统关机,恢复代理设置') await quit('系统关机') }) }) } initApp() // Exit cleanly on request from parent process in development mode. if (isDevelopment) { if (process.platform === 'win32') { process.on('message', (data) => { if (data === 'graceful-exit') { quit('graceful-exit') } }) } else { process.on('SIGINT', () => { quit('SIGINT') }) } } // 系统关机和重启时的操作 process.on('exit', () => { quit('进程结束,退出app') }) log.info('background.js finished') } catch (e) { log.error('应用启动过程中,出现未知异常:', e) }