@@ -6,18 +6,22 @@ import { fileURLToPath } from 'node:url'
66
77import { electronApp , is , optimizer } from '@electron-toolkit/utils'
88import { Format , LogLevel , setGlobalFormat , setGlobalLogLevel } from '@guiiai/logg'
9- import { defineInvokeHandler } from '@unbird/eventa'
9+ import { defineInvoke , defineInvokeHandler } from '@unbird/eventa'
1010import { createContext } from '@unbird/eventa/adapters/electron/main'
11- import { app , BrowserWindow , ipcMain , screen , shell } from 'electron'
11+ import { app , BrowserWindow , ipcMain , Menu , screen , shell , Tray } from 'electron'
1212import { isMacOS } from 'std-env'
1313
1414import icon from '../../resources/icon.png?asset'
1515
16- import { electronCursorPoint , electronStartTrackingCursorPoint } from '../shared/eventa'
16+ import { electronCursorPoint , electronOpenSettings , electronStartTrackingCursorPoint } from '../shared/eventa'
1717
1818setGlobalFormat ( Format . Pretty )
1919setGlobalLogLevel ( LogLevel . Log )
2020
21+ // Store the eventa context and invokers to reuse them
22+ let eventaContext : ReturnType < typeof createContext > | null = null
23+ let openSettingsInvoker : ReturnType < typeof defineInvoke < void , void > > | null = null
24+
2125if ( / ^ t r u e $ / i. test ( env . APP_REMOTE_DEBUG || '' ) ) {
2226 const remoteDebugPort = Number ( env . APP_REMOTE_DEBUG_PORT || '9222' )
2327 if ( Number . isNaN ( remoteDebugPort ) || ! Number . isInteger ( remoteDebugPort ) || remoteDebugPort < 0 || remoteDebugPort > 65535 ) {
@@ -30,10 +34,12 @@ if (/^true$/i.test(env.APP_REMOTE_DEBUG || '')) {
3034
3135app . dock ?. setIcon ( icon )
3236
37+ let mainWindow : BrowserWindow | null = null
38+ let appTray : Tray | null = null
3339let trackCursorPointInterval : NodeJS . Timeout | undefined
3440
3541function createWindow ( ) : void {
36- const mainWindow = new BrowserWindow ( {
42+ mainWindow = new BrowserWindow ( {
3743 title : 'AIRI' ,
3844 width : 916.0 ,
3945 height : 1245.0 ,
@@ -49,7 +55,8 @@ function createWindow(): void {
4955 hasShadow : false ,
5056 } )
5157
52- const { context } = createContext ( ipcMain , mainWindow )
58+ eventaContext = createContext ( ipcMain , mainWindow )
59+ const { context } = eventaContext
5360
5461 defineInvokeHandler ( context , electronStartTrackingCursorPoint , ( ) => {
5562 trackCursorPointInterval = setInterval ( ( ) => {
@@ -60,6 +67,9 @@ function createWindow(): void {
6067 } , 32 )
6168 } )
6269
70+ // Create the openSettings invoker once and store it for reuse
71+ openSettingsInvoker = defineInvoke < void , void > ( context , electronOpenSettings )
72+
6373 mainWindow . setAlwaysOnTop ( true )
6474 if ( isMacOS ) {
6575 mainWindow . setWindowButtonVisibility ( false )
@@ -80,6 +90,63 @@ function createWindow(): void {
8090 }
8191}
8292
93+ function createTray ( ) : void {
94+ if ( appTray ) {
95+ return
96+ }
97+
98+ const showMainWindow = ( ) : void => {
99+ if ( mainWindow ) {
100+ if ( mainWindow . isMinimized ( ) ) {
101+ mainWindow . restore ( )
102+ }
103+ mainWindow . show ( )
104+ mainWindow . focus ( )
105+ }
106+ }
107+
108+ // Create tray icon
109+ appTray = new Tray ( icon )
110+
111+ // Define tray menu
112+ const contextMenu = Menu . buildFromTemplate ( [
113+ {
114+ label : 'Show Window' ,
115+ click : showMainWindow ,
116+ } ,
117+ { type : 'separator' } ,
118+ {
119+ label : 'Settings' ,
120+ click : ( ) => {
121+ if ( mainWindow ) {
122+ showMainWindow ( )
123+ // Send the open settings command using the pre-created invoker
124+ if ( openSettingsInvoker ) {
125+ openSettingsInvoker ( undefined )
126+ }
127+ }
128+ } ,
129+ } ,
130+ { type : 'separator' } ,
131+ {
132+ label : 'Quit' ,
133+ click : ( ) => {
134+ app . quit ( )
135+ } ,
136+ } ,
137+ ] )
138+
139+ // Set tray properties
140+ appTray . setContextMenu ( contextMenu )
141+ appTray . setToolTip ( 'Project AIRI' )
142+ appTray . addListener ( 'click' , showMainWindow )
143+
144+ // On macOS, there's a special double-click event
145+ if ( platform === 'darwin' ) {
146+ appTray . addListener ( 'double-click' , showMainWindow )
147+ }
148+ }
149+
83150// This method will be called when Electron has finished
84151// initialization and is ready to create browser windows.
85152// Some APIs can only be used after this event occurs.
@@ -127,6 +194,7 @@ app.whenReady().then(() => {
127194 app . on ( 'browser-window-created' , ( _ , window ) => optimizer . watchWindowShortcuts ( window ) )
128195
129196 createWindow ( )
197+ createTray ( )
130198} ) . catch ( ( err ) => {
131199 console . error ( 'Error during app initialization:' , err )
132200} )
@@ -143,3 +211,11 @@ app.on('window-all-closed', () => {
143211 app . quit ( )
144212 }
145213} )
214+
215+ // Clean up tray when app quits
216+ app . on ( 'before-quit' , ( ) => {
217+ if ( appTray ) {
218+ appTray . destroy ( )
219+ appTray = null
220+ }
221+ } )
0 commit comments