feat(tray): electron tray (#635)
This commit is contained in:
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user