perf: 🚀 The main panel supports single instance startup

This commit is contained in:
viarotel 2025-02-17 09:59:01 +08:00
parent 6c4242c40a
commit c49d22cabf
4 changed files with 205 additions and 18 deletions

View File

@ -0,0 +1,10 @@
import { EventEmitter } from 'node:events'
const eventEmitter = new EventEmitter()
export {
EventEmitter,
eventEmitter,
}
export default eventEmitter

157
electron/helpers/single.js Normal file
View File

@ -0,0 +1,157 @@
import { app, BrowserWindow } from 'electron'
/**
* 确保 Electron 应用只运行一个实例的工具函数
* @typedef {Object} SingleInstanceOptions
* @property {Function} [onSecondInstance] - 当第二个实例启动时的回调函数
* @property {boolean} [enableSandbox=false] - 是否启用沙箱模式
* @property {Function} [onSuccess] - 成功获取单例锁后的回调函数
* @property {Function} [onShowWindow] - 主窗口已展示回调
* @property {boolean} [forceFocus=true] - 是否强制聚焦已存在的窗口
* @property {boolean} [silentMode=false] - 静默模式不显示任何提示
* @property {Function} [onError] - 错误处理回调函数
*/
/**
* 第二个实例启动时的回调函数类型
* @callback OnSecondInstanceCallback
* @param {Event} event - Electron 事件对象
* @param {string[]} commandLine - 命令行参数数组
* @param {string} workingDirectory - 工作目录
* @param {BrowserWindow|null} mainWindow - 主窗口实例如果存在的话
*/
/**
* 确保应用程序只运行单个实例
* @param {SingleInstanceOptions} options - 配置选项
* @returns {boolean} 是否成功获取单例锁
*
* @example
* // 基础使用
* ensureSingleInstance({
* onSuccess: () => {
* app.whenReady().then(createWindow)
* }
* });
*
* @example
* // 高级使用
* ensureSingleInstance({
* onSecondInstance: (event, commandLine, workingDirectory, mainWindow) => {
* if (mainWindow) {
* mainWindow.webContents.send('new-instance-launched', commandLine);
* }
* },
* onSuccess: () => {
* console.log('Successfully acquired lock');
* createWindow();
* },
* onError: (error) => {
* console.error('Error in single instance check:', error);
* },
* forceFocus: true,
* silentMode: false
* });
*
* @throws {Error} 如果在非 Electron 环境中调用
*/
function ensureSingleInstance(options = {}) {
// 参数解构与默认值设置
const {
onSecondInstance,
enableSandbox = false,
onSuccess,
onShowWindow,
onError,
forceFocus = true,
silentMode = false,
} = options
// 验证运行环境
if (!app || !BrowserWindow) {
const error = new Error('ensureSingleInstance must be called in Electron environment')
if (onError) {
onError(error)
return false
}
throw error
}
try {
// 沙箱模式检查
if (enableSandbox) {
!silentMode && console.log('Sandbox mode enabled, skipping single instance check')
onSuccess?.()
return true
}
// 请求单例锁
const gotTheLock = app.requestSingleInstanceLock()
// 如果无法获取锁,说明已有实例在运行
if (!gotTheLock) {
!silentMode && console.log('Application instance already running, quitting...')
app.quit()
return false
}
// 监听第二个实例的启动
app.on('second-instance', (event, commandLine, workingDirectory) => {
try {
// 获取所有窗口
const windows = BrowserWindow.getAllWindows()
const mainWindow = windows.length ? windows[0] : null
// 处理窗口焦点
onShowWindow?.(mainWindow, commandLine)
if (mainWindow) {
if (mainWindow.isMinimized() || !mainWindow.isVisible()) {
mainWindow.show()
}
if (forceFocus) {
mainWindow.focus()
}
}
// 调用用户自定义的回调
onSecondInstance?.(event, commandLine, workingDirectory, mainWindow)
}
catch (error) {
!silentMode && console.error('Error handling second instance:', error)
onError?.(error)
}
})
// 调用成功回调
onSuccess?.()
return true
}
catch (error) {
!silentMode && console.error('Error in ensureSingleInstance:', error)
onError?.(error)
return false
}
}
/**
* 检查当前是否为应用程序的主实例
* @returns {boolean} 如果是主实例返回 true否则返回 false
*/
function isMainInstance() {
return app.requestSingleInstanceLock()
}
/**
* 释放单例锁允许其他实例启动
* @returns {void}
*/
function releaseSingleInstanceLock() {
app.releaseSingleInstanceLock()
}
export {
ensureSingleInstance,
isMainInstance,
releaseSingleInstanceLock,
}

View File

@ -1,13 +1,18 @@
import { app, dialog, Menu, Tray } from 'electron'
import { trayPath } from '$electron/configs/index.js'
import { executeI18n } from '$electron/helpers/index.js'
import appStore from '$electron/helpers/store.js'
import { app, dialog, Menu, Tray } from 'electron'
import { eventEmitter } from '$electron/helpers/emitter.js'
export default (mainWindow) => {
const t = value => executeI18n(mainWindow, value)
let tray = null
eventEmitter.on('tray:destroy', () => {
tray?.destroy?.()
})
const showApp = () => {
if (process.platform === 'darwin') {
app.dock.show()

View File

@ -22,6 +22,9 @@ import { loadPage } from './helpers/index.js'
import { Edger } from './helpers/edger/index.js'
import { ensureSingleInstance } from './helpers/single.js'
import { eventEmitter } from './helpers/emitter.js'
const require = createRequire(import.meta.url)
const __dirname = path.dirname(fileURLToPath(import.meta.url))
@ -122,6 +125,7 @@ function createWindow() {
control(mainWindow)
}
function onWhenReady() {
app.whenReady().then(() => {
electronApp.setAppUserModelId('com.viarotel.escrcpy')
@ -142,6 +146,17 @@ app.whenReady().then(() => {
mainWindow.show()
})
})
}
ensureSingleInstance({
onSuccess() {
onWhenReady()
},
onShowWindow() {
eventEmitter.emit('tray:destroy')
}
})
app.on('window-all-closed', () => {
app.isQuiting = true