Skip to content

Commit 22f1e79

Browse files
authored
feat(tray): electron tray (#635)
1 parent 25ead77 commit 22f1e79

File tree

3 files changed

+91
-8
lines changed

3 files changed

+91
-8
lines changed

apps/stage-tamagotchi/src/main/index.ts

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,22 @@ import { fileURLToPath } from 'node:url'
66

77
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
88
import { Format, LogLevel, setGlobalFormat, setGlobalLogLevel } from '@guiiai/logg'
9-
import { defineInvokeHandler } from '@unbird/eventa'
9+
import { defineInvoke, defineInvokeHandler } from '@unbird/eventa'
1010
import { 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'
1212
import { isMacOS } from 'std-env'
1313

1414
import icon from '../../resources/icon.png?asset'
1515

16-
import { electronCursorPoint, electronStartTrackingCursorPoint } from '../shared/eventa'
16+
import { electronCursorPoint, electronOpenSettings, electronStartTrackingCursorPoint } from '../shared/eventa'
1717

1818
setGlobalFormat(Format.Pretty)
1919
setGlobalLogLevel(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+
2125
if (/^true$/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

3135
app.dock?.setIcon(icon)
3236

37+
let mainWindow: BrowserWindow | null = null
38+
let appTray: Tray | null = null
3339
let trackCursorPointInterval: NodeJS.Timeout | undefined
3440

3541
function 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+
})

apps/stage-tamagotchi/src/renderer/App.vue

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
33
import { useOnboardingStore } from '@proj-airi/stage-ui/stores/onboarding'
44
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
5-
import { defineInvoke } from '@unbird/eventa'
5+
import { defineInvoke, defineInvokeHandler } from '@unbird/eventa'
66
import { createContext } from '@unbird/eventa/adapters/electron/renderer'
77
import { storeToRefs } from 'pinia'
88
import { onMounted, watch } from 'vue'
99
import { useI18n } from 'vue-i18n'
10-
import { RouterView } from 'vue-router'
10+
import { RouterView, useRouter } from 'vue-router'
1111
12-
import { electronStartTrackingCursorPoint } from '../shared/eventa'
12+
import { electronOpenSettings, electronStartTrackingCursorPoint } from '../shared/eventa'
1313
import { useWindowMode } from './stores/window-controls'
1414
1515
useWindowMode()
@@ -18,6 +18,7 @@ const displayModelsStore = useDisplayModelsStore()
1818
const settingsStore = useSettings()
1919
const { language, themeColorsHue, themeColorsHueDynamic } = storeToRefs(settingsStore)
2020
const onboardingStore = useOnboardingStore()
21+
const router = useRouter()
2122
2223
watch(language, () => {
2324
i18n.locale.value = language.value
@@ -33,6 +34,11 @@ onMounted(async () => {
3334
const { context } = createContext(window.electron.ipcRenderer)
3435
const startTrackingCursorPoint = defineInvoke(context, electronStartTrackingCursorPoint)
3536
await startTrackingCursorPoint(undefined)
37+
38+
// Listen for open-settings IPC message from main process
39+
defineInvokeHandler(context, electronOpenSettings, () => {
40+
router.push('/settings')
41+
})
3642
})
3743
3844
watch(themeColorsHue, () => {

apps/stage-tamagotchi/src/shared/eventa.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ interface Point {
1515

1616
export const electronCursorPoint = defineEventa<Point>('electron:eventa:event:cursor-point')
1717
export const electronStartTrackingCursorPoint = defineInvokeEventa('electron:eventa:invoke:start-tracking-cursor-point')
18+
export const electronOpenSettings = defineInvokeEventa<void, void>('electron:eventa:invoke:open-settings')

0 commit comments

Comments
 (0)