1
1
import { join } from 'node:path'
2
2
import { env , platform } from 'node:process'
3
3
import { electronApp , is , optimizer } from '@electron-toolkit/utils'
4
- import { app , BrowserWindow , dialog , ipcMain , Menu , screen , shell } from 'electron'
5
- import { inertia } from 'popmotion '
4
+ import { nativeImage , shell } from 'electron/common '
5
+ import { app , BrowserWindow , dialog , ipcMain , Tray } from 'electron/main '
6
6
7
+ import trayIconMacos from '../../build/icon-tray-macos.png?asset'
7
8
import icon from '../../build/icon.png?asset'
9
+ import { createI18n } from './locales'
10
+ import { createApplicationMenu , createTrayMenu } from './menu'
8
11
9
- // FIXME: electron i18n
10
-
11
- let globalMouseTracker : ReturnType < typeof setInterval > | null = null
12
12
let mainWindow : BrowserWindow
13
- let currentAnimationX : { stop : ( ) => void } | null = null
14
- let currentAnimationY : { stop : ( ) => void } | null = null
15
- let isDragging = false
16
- let lastMousePosition = { x : 0 , y : 0 }
17
- let lastMouseTime = Date . now ( )
18
- let currentVelocity = { x : 0 , y : 0 }
19
- let dragOffset = { x : 0 , y : 0 }
13
+ let tray : Tray
14
+
15
+ const i18n = createI18n ( )
16
+
17
+ function showQuitDialog ( ) {
18
+ dialog . showMessageBox ( {
19
+ type : 'info' ,
20
+ title : i18n . t ( 'menu.quit' ) ,
21
+ message : i18n . t ( 'quitDialog.message' ) ,
22
+ buttons : [ i18n . t ( 'quitDialog.buttons.quit' ) , i18n . t ( 'quitDialog.buttons.cancel' ) ] ,
23
+ } ) . then ( ( result ) => {
24
+ if ( result . response === 0 ) {
25
+ mainWindow . webContents . send ( 'before-quit' )
26
+ setTimeout ( ( ) => {
27
+ app . quit ( )
28
+ } , 2000 )
29
+ }
30
+ } )
31
+ }
32
+
33
+ function rebuildTrayMenu ( ) {
34
+ if ( mainWindow . isVisible ( ) ) {
35
+ tray . setContextMenu ( createTrayMenu ( i18n , mainWindow . isVisible ( ) , ( ) => {
36
+ mainWindow . webContents . send ( 'before-hide' )
37
+ setTimeout ( ( ) => {
38
+ mainWindow . hide ( )
39
+ rebuildTrayMenu ( )
40
+ } , 2000 )
41
+ } , createSettingsWindow , showQuitDialog ) )
42
+ return
43
+ }
44
+ tray . setContextMenu ( createTrayMenu ( i18n , mainWindow . isVisible ( ) , ( ) => {
45
+ mainWindow . show ( )
46
+ mainWindow . webContents . send ( 'after-show' )
47
+ rebuildTrayMenu ( )
48
+ } , createSettingsWindow , showQuitDialog ) )
49
+ }
20
50
21
- function createWindow ( ) : void {
51
+ function createWindow ( ) {
22
52
// Create the browser window.
23
53
mainWindow = new BrowserWindow ( {
24
54
width : 300 * 1.5 ,
@@ -57,115 +87,11 @@ function createWindow(): void {
57
87
else {
58
88
mainWindow . loadFile ( join ( import . meta. dirname , '..' , '..' , 'out' , 'renderer' , 'index.html' ) )
59
89
}
60
-
61
- ipcMain . on ( 'start-window-drag' , ( _ ) => {
62
- isDragging = true
63
- const mousePos = screen . getCursorScreenPoint ( )
64
- const [ windowX , windowY ] = mainWindow . getPosition ( )
65
-
66
- // Calculate the offset between cursor and window position
67
- dragOffset = {
68
- x : mousePos . x - windowX ,
69
- y : mousePos . y - windowY ,
70
- }
71
-
72
- // Stop any existing animations
73
- if ( currentAnimationX ) {
74
- currentAnimationX . stop ( )
75
- currentAnimationX = null
76
- }
77
- if ( currentAnimationY ) {
78
- currentAnimationY . stop ( )
79
- currentAnimationY = null
80
- }
81
-
82
- // Initialize last position for velocity tracking
83
- lastMousePosition = { x : mousePos . x , y : mousePos . y }
84
- lastMouseTime = Date . now ( )
85
- currentVelocity = { x : 0 , y : 0 }
86
-
87
- // Start global mouse tracking
88
- if ( ! globalMouseTracker ) {
89
- globalMouseTracker = setInterval ( ( ) => {
90
- const mousePos = screen . getCursorScreenPoint ( )
91
- if ( isDragging ) {
92
- handleWindowMove ( mousePos . x , mousePos . y )
93
- }
94
- } , 16 ) // ~60fps
95
- }
96
- } )
97
-
98
- ipcMain . on ( 'end-window-drag' , ( ) => {
99
- isDragging = false
100
- if ( globalMouseTracker ) {
101
- clearInterval ( globalMouseTracker )
102
- globalMouseTracker = null
103
- }
104
-
105
- // Apply inertia animation when drag ends
106
- const [ currentX , currentY ] = mainWindow . getPosition ( )
107
- let latestX = currentX
108
- let latestY = currentY
109
-
110
- const inertiaConfig = {
111
- power : 0.4 , // Reduced from 0.6 for stronger resistance
112
- timeConstant : 250 , // Reduced from 400 for quicker deceleration
113
- modifyTarget : ( v : number ) => v ,
114
- min : 0 ,
115
- max : Infinity ,
116
- }
117
-
118
- // Clamp velocity to reasonable values
119
- const clampVelocity = ( v : number ) => {
120
- const maxVelocity = 500 // Reduced from 800 for less momentum
121
- const minVelocity = - 500
122
- return Math . min ( Math . max ( v , minVelocity ) , maxVelocity )
123
- }
124
-
125
- // Reduce velocity amplification and clamp values
126
- const amplifiedVelocity = {
127
- x : clampVelocity ( currentVelocity . x * 0.2 ) , // Reduced from 0.3 for less momentum
128
- y : clampVelocity ( currentVelocity . y * 0.2 ) ,
129
- }
130
-
131
- // Ignore very small movements
132
- if ( Math . abs ( amplifiedVelocity . x ) > 35 || Math . abs ( amplifiedVelocity . y ) > 35 ) {
133
- currentAnimationX = inertia ( {
134
- from : currentX ,
135
- velocity : amplifiedVelocity . x ,
136
- ...inertiaConfig ,
137
- onUpdate : ( x ) => {
138
- latestX = Math . round ( x )
139
- mainWindow . setPosition ( latestX , Math . round ( latestY ) )
140
- } ,
141
- onComplete : ( ) => {
142
- currentAnimationX = null
143
- } ,
144
- } )
145
-
146
- currentAnimationY = inertia ( {
147
- from : currentY ,
148
- velocity : amplifiedVelocity . y ,
149
- ...inertiaConfig ,
150
- onUpdate : ( y ) => {
151
- latestY = Math . round ( y )
152
- mainWindow . setPosition ( Math . round ( latestX ) , latestY )
153
- } ,
154
- onComplete : ( ) => {
155
- currentAnimationY = null
156
- } ,
157
- } )
158
- }
159
- } )
160
-
161
- ipcMain . on ( 'move-window' , ( _ , cursorX : number , cursorY : number ) => {
162
- handleWindowMove ( cursorX , cursorY )
163
- } )
164
90
}
165
91
166
92
let settingsWindow : BrowserWindow | null = null
167
93
168
- function createSettingsWindow ( ) : void {
94
+ function createSettingsWindow ( ) {
169
95
if ( settingsWindow ) {
170
96
settingsWindow . show ( )
171
97
return
@@ -210,30 +136,7 @@ function createSettingsWindow(): void {
210
136
// initialization and is ready to create browser windows.
211
137
// Some APIs can only be used after this event occurs.
212
138
app . whenReady ( ) . then ( ( ) => {
213
- // Menu
214
- const menu = Menu . buildFromTemplate ( [
215
- {
216
- label : 'airi' ,
217
- role : 'appMenu' ,
218
- submenu : [
219
- {
220
- role : 'about' ,
221
- } ,
222
- {
223
- role : 'toggleDevTools' ,
224
- } ,
225
- {
226
- label : 'Settings' ,
227
- click : ( ) => createSettingsWindow ( ) ,
228
- } ,
229
- {
230
- label : 'Quit' ,
231
- click : ( ) => app . quit ( ) ,
232
- } ,
233
- ] ,
234
- } ,
235
- ] )
236
- Menu . setApplicationMenu ( menu )
139
+ createApplicationMenu ( i18n , showQuitDialog , createSettingsWindow )
237
140
238
141
// Set app user model id for windows
239
142
electronApp . setAppUserModelId ( 'com.github.moeru-ai.airi-tamagotchi' )
@@ -246,19 +149,7 @@ app.whenReady().then(() => {
246
149
} )
247
150
248
151
// IPC test
249
- // TODO: i18n
250
- ipcMain . on ( 'quit' , ( ) => {
251
- dialog . showMessageBox ( {
252
- type : 'info' ,
253
- title : 'Quit' ,
254
- message : 'Are you sure you want to quit?' ,
255
- buttons : [ 'Quit' , 'Cancel' ] ,
256
- } ) . then ( ( result ) => {
257
- if ( result . response === 0 ) {
258
- app . quit ( )
259
- }
260
- } )
261
- } )
152
+ ipcMain . on ( 'quit' , showQuitDialog )
262
153
263
154
ipcMain . on ( 'open-settings' , ( ) => createSettingsWindow ( ) )
264
155
@@ -271,44 +162,19 @@ app.whenReady().then(() => {
271
162
createWindow ( )
272
163
}
273
164
} )
274
- } )
275
165
276
- // Quit when all windows are closed, except on macOS. There, it's common
277
- // for applications and their menu bar to stay active until the user quits
278
- // explicitly with Cmd + Q.
279
- app . on ( 'window-all-closed' , ( ) => {
280
- if ( platform !== 'darwin' ) {
281
- app . quit ( )
282
- }
283
- } )
166
+ const trayIcon = platform === 'darwin'
167
+ ? nativeImage . createFromPath ( trayIconMacos ) . resize ( { width : 16 , height : 16 } )
168
+ : nativeImage . createFromPath ( icon ) . resize ( { width : 16 , height : 16 } )
169
+ tray = new Tray ( trayIcon )
170
+ tray . setToolTip ( 'Airi' )
171
+ rebuildTrayMenu ( )
284
172
285
- // In this file you can include the rest of your app"s specific main process
286
- // code. You can also put them in separate files and require them here.
287
-
288
- function handleWindowMove ( cursorX : number , cursorY : number ) {
289
- if ( ! isDragging )
290
- return
173
+ app . dock . hide ( )
291
174
292
- // Calculate actual velocity based on mouse movement
293
- const currentTime = Date . now ( )
294
- const deltaTime = currentTime - lastMouseTime
295
-
296
- if ( deltaTime > 0 ) {
297
- // Smooth out velocity calculation with some averaging
298
- const newVelocityX = ( cursorX - lastMousePosition . x ) / deltaTime * 1000
299
- const newVelocityY = ( cursorY - lastMousePosition . y ) / deltaTime * 1000
300
-
301
- currentVelocity = {
302
- x : currentVelocity . x * 0.8 + newVelocityX * 0.2 , // Smooth velocity
303
- y : currentVelocity . y * 0.8 + newVelocityY * 0.2 ,
304
- }
305
- }
306
-
307
- // Update window position based on cursor position and offset
308
- const newX = cursorX - dragOffset . x
309
- const newY = cursorY - dragOffset . y
310
- mainWindow . setPosition ( Math . round ( newX ) , Math . round ( newY ) )
311
-
312
- lastMousePosition = { x : cursorX , y : cursorY }
313
- lastMouseTime = currentTime
314
- }
175
+ ipcMain . on ( 'locale-changed' , ( _ , language : string ) => {
176
+ i18n . setLocale ( language )
177
+ rebuildTrayMenu ( )
178
+ createApplicationMenu ( i18n , showQuitDialog , createSettingsWindow )
179
+ } )
180
+ } )
0 commit comments