add desktop tray and updater support (#1193)

This commit is contained in:
ekko
2026-06-01 11:03:18 +08:00
committed by GitHub
parent 1fbbfdfad6
commit 4516502705
15 changed files with 689 additions and 50 deletions
+123 -12
View File
@@ -1,13 +1,106 @@
import { app, BrowserWindow, Menu, shell, ipcMain } from 'electron'
import { app, BrowserWindow, Menu, Tray, shell, ipcMain, nativeImage } from 'electron'
import { join } from 'node:path'
import { startWebUiServer, stopWebUiServer, getToken } from './webui-server'
import { desktopIcon, hermesBinExists, hermesBin } from './paths'
import { initAutoUpdater } from './updater'
import { desktopIcon, desktopTrayTemplateIcon, hermesBinExists, hermesBin } from './paths'
import { checkForDesktopUpdates, initAutoUpdater } from './updater'
import { t } from './desktop-i18n'
const PORT = Number(process.env.HERMES_DESKTOP_PORT) || 8748
const START_HIDDEN = process.argv.includes('--hidden')
let mainWindow: BrowserWindow | null = null
let serverUrl: string | null = null
let tray: Tray | null = null
let isQuitting = false
function showMainWindow() {
if (!mainWindow) {
createWindow()
}
if (!mainWindow) return
if (mainWindow.isMinimized()) mainWindow.restore()
mainWindow.show()
mainWindow.focus()
}
function quitApp() {
isQuitting = true
app.quit()
}
function getOpenAtLogin(): boolean {
return app.getLoginItemSettings().openAtLogin
}
function setOpenAtLogin(openAtLogin: boolean) {
app.setLoginItemSettings({
openAtLogin,
openAsHidden: true,
path: process.execPath,
args: ['--hidden'],
})
}
function updateTrayMenu() {
if (!tray) return
const isVisible = !!mainWindow && mainWindow.isVisible()
const menu = Menu.buildFromTemplate([
{
label: isVisible ? t('tray.hide') : t('tray.show'),
click: () => {
if (mainWindow?.isVisible()) {
mainWindow.hide()
} else {
showMainWindow()
}
updateTrayMenu()
},
},
{
label: t('tray.checkForUpdates'),
click: () => {
checkForDesktopUpdates(true).catch(err => {
console.error('[tray] update check failed:', err)
})
},
},
{
label: t('tray.openAtLogin'),
type: 'checkbox',
checked: getOpenAtLogin(),
click: (item) => {
setOpenAtLogin(item.checked)
updateTrayMenu()
},
},
{ type: 'separator' },
{
label: t('tray.quit'),
click: quitApp,
},
])
tray.setContextMenu(menu)
}
function createTray() {
if (tray) return
const source = process.platform === 'darwin' ? desktopTrayTemplateIcon() : desktopIcon()
const icon = nativeImage.createFromPath(source).resize({
width: process.platform === 'darwin' ? 18 : 16,
height: process.platform === 'darwin' ? 18 : 16,
quality: 'best',
})
if (process.platform === 'darwin') {
icon.setTemplateImage(true)
}
tray = new Tray(icon)
tray.setToolTip('Hermes Studio')
tray.on('click', () => {
showMainWindow()
updateTrayMenu()
})
updateTrayMenu()
}
function createWindow() {
mainWindow = new BrowserWindow({
@@ -18,6 +111,7 @@ function createWindow() {
title: 'Hermes Studio',
backgroundColor: '#1a1a1a',
autoHideMenuBar: true,
show: !START_HIDDEN,
...(process.platform === 'linux' ? { icon: desktopIcon() } : {}),
webPreferences: {
preload: join(__dirname, '..', 'preload', 'index.js'),
@@ -27,6 +121,16 @@ function createWindow() {
},
})
mainWindow.on('close', (event) => {
if (isQuitting) return
event.preventDefault()
mainWindow?.hide()
updateTrayMenu()
})
mainWindow.on('show', updateTrayMenu)
mainWindow.on('hide', updateTrayMenu)
// External links → system browser
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith('http://127.0.0.1') || url.startsWith('http://localhost')) {
@@ -44,6 +148,7 @@ function createWindow() {
} else {
mainWindow.loadURL(splashHtml())
}
updateTrayMenu()
}
function splashHtml(): string {
@@ -95,10 +200,7 @@ if (!gotLock) {
app.quit()
} else {
app.on('second-instance', () => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore()
mainWindow.focus()
}
showMainWindow()
})
app.whenReady().then(() => {
@@ -107,25 +209,34 @@ if (!gotLock) {
// visual clutter. macOS keeps a menu (system requirement) but Electron's
// default is fine there.
if (process.platform !== 'darwin') Menu.setApplicationMenu(null)
createTray()
createWindow()
bootstrap()
initAutoUpdater()
initAutoUpdater({
beforeQuitAndInstall: () => {
isQuitting = true
},
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
} else if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore()
mainWindow.show()
mainWindow.focus()
showMainWindow()
}
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
if (isQuitting && process.platform !== 'darwin') app.quit()
})
app.on('before-quit', async (e) => {
if (!isQuitting && process.platform !== 'darwin') {
e.preventDefault()
mainWindow?.hide()
updateTrayMenu()
return
}
e.preventDefault()
await stopWebUiServer().catch(() => undefined)
app.exit(0)