dev-sidecar/packages/gui/src/background.js

563 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

'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)
}