feat(tray): electron tray (#635)

This commit is contained in:
Sline
2025-10-04 21:54:25 +08:00
committed by GitHub
parent 25ead776b9
commit 22f1e79aca
3 changed files with 91 additions and 8 deletions
+81 -5
View File
@@ -6,18 +6,22 @@ import { fileURLToPath } from 'node:url'
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
import { Format, LogLevel, setGlobalFormat, setGlobalLogLevel } from '@guiiai/logg'
import { defineInvokeHandler } from '@unbird/eventa'
import { defineInvoke, defineInvokeHandler } from '@unbird/eventa'
import { createContext } from '@unbird/eventa/adapters/electron/main'
import { app, BrowserWindow, ipcMain, screen, shell } from 'electron'
import { app, BrowserWindow, ipcMain, Menu, screen, shell, Tray } from 'electron'
import { isMacOS } from 'std-env'
import icon from '../../resources/icon.png?asset'
import { electronCursorPoint, electronStartTrackingCursorPoint } from '../shared/eventa'
import { electronCursorPoint, electronOpenSettings, electronStartTrackingCursorPoint } from '../shared/eventa'
setGlobalFormat(Format.Pretty)
setGlobalLogLevel(LogLevel.Log)
// Store the eventa context and invokers to reuse them
let eventaContext: ReturnType<typeof createContext> | null = null
let openSettingsInvoker: ReturnType<typeof defineInvoke<void, void>> | null = null
if (/^true$/i.test(env.APP_REMOTE_DEBUG || '')) {
const remoteDebugPort = Number(env.APP_REMOTE_DEBUG_PORT || '9222')
if (Number.isNaN(remoteDebugPort) || !Number.isInteger(remoteDebugPort) || remoteDebugPort < 0 || remoteDebugPort > 65535) {
@@ -30,10 +34,12 @@ if (/^true$/i.test(env.APP_REMOTE_DEBUG || '')) {
app.dock?.setIcon(icon)
let mainWindow: BrowserWindow | null = null
let appTray: Tray | null = null
let trackCursorPointInterval: NodeJS.Timeout | undefined
function createWindow(): void {
const mainWindow = new BrowserWindow({
mainWindow = new BrowserWindow({
title: 'AIRI',
width: 916.0,
height: 1245.0,
@@ -49,7 +55,8 @@ function createWindow(): void {
hasShadow: false,
})
const { context } = createContext(ipcMain, mainWindow)
eventaContext = createContext(ipcMain, mainWindow)
const { context } = eventaContext
defineInvokeHandler(context, electronStartTrackingCursorPoint, () => {
trackCursorPointInterval = setInterval(() => {
@@ -60,6 +67,9 @@ function createWindow(): void {
}, 32)
})
// Create the openSettings invoker once and store it for reuse
openSettingsInvoker = defineInvoke<void, void>(context, electronOpenSettings)
mainWindow.setAlwaysOnTop(true)
if (isMacOS) {
mainWindow.setWindowButtonVisibility(false)
@@ -80,6 +90,63 @@ function createWindow(): void {
}
}
function createTray(): void {
if (appTray) {
return
}
const showMainWindow = (): void => {
if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore()
}
mainWindow.show()
mainWindow.focus()
}
}
// Create tray icon
appTray = new Tray(icon)
// Define tray menu
const contextMenu = Menu.buildFromTemplate([
{
label: 'Show Window',
click: showMainWindow,
},
{ type: 'separator' },
{
label: 'Settings',
click: () => {
if (mainWindow) {
showMainWindow()
// Send the open settings command using the pre-created invoker
if (openSettingsInvoker) {
openSettingsInvoker(undefined)
}
}
},
},
{ type: 'separator' },
{
label: 'Quit',
click: () => {
app.quit()
},
},
])
// Set tray properties
appTray.setContextMenu(contextMenu)
appTray.setToolTip('Project AIRI')
appTray.addListener('click', showMainWindow)
// On macOS, there's a special double-click event
if (platform === 'darwin') {
appTray.addListener('double-click', showMainWindow)
}
}
// 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.
@@ -127,6 +194,7 @@ app.whenReady().then(() => {
app.on('browser-window-created', (_, window) => optimizer.watchWindowShortcuts(window))
createWindow()
createTray()
}).catch((err) => {
console.error('Error during app initialization:', err)
})
@@ -143,3 +211,11 @@ app.on('window-all-closed', () => {
app.quit()
}
})
// Clean up tray when app quits
app.on('before-quit', () => {
if (appTray) {
appTray.destroy()
appTray = null
}
})
+9 -3
View File
@@ -2,14 +2,14 @@
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
import { useOnboardingStore } from '@proj-airi/stage-ui/stores/onboarding'
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
import { defineInvoke } from '@unbird/eventa'
import { defineInvoke, defineInvokeHandler } from '@unbird/eventa'
import { createContext } from '@unbird/eventa/adapters/electron/renderer'
import { storeToRefs } from 'pinia'
import { onMounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { RouterView } from 'vue-router'
import { RouterView, useRouter } from 'vue-router'
import { electronStartTrackingCursorPoint } from '../shared/eventa'
import { electronOpenSettings, electronStartTrackingCursorPoint } from '../shared/eventa'
import { useWindowMode } from './stores/window-controls'
useWindowMode()
@@ -18,6 +18,7 @@ const displayModelsStore = useDisplayModelsStore()
const settingsStore = useSettings()
const { language, themeColorsHue, themeColorsHueDynamic } = storeToRefs(settingsStore)
const onboardingStore = useOnboardingStore()
const router = useRouter()
watch(language, () => {
i18n.locale.value = language.value
@@ -33,6 +34,11 @@ onMounted(async () => {
const { context } = createContext(window.electron.ipcRenderer)
const startTrackingCursorPoint = defineInvoke(context, electronStartTrackingCursorPoint)
await startTrackingCursorPoint(undefined)
// Listen for open-settings IPC message from main process
defineInvokeHandler(context, electronOpenSettings, () => {
router.push('/settings')
})
})
watch(themeColorsHue, () => {
@@ -15,3 +15,4 @@ interface Point {
export const electronCursorPoint = defineEventa<Point>('electron:eventa:event:cursor-point')
export const electronStartTrackingCursorPoint = defineInvokeEventa('electron:eventa:invoke:start-tracking-cursor-point')
export const electronOpenSettings = defineInvokeEventa<void, void>('electron:eventa:invoke:open-settings')