返回 oh-my-ppt
tray.ts
根目录 / src / main / app / tray.ts
1 import { app, BrowserWindow, Tray, Menu, nativeImage } from 'electron'
2 import log from 'electron-log/main.js'
3 import { dirname, join } from 'path'
4 import { fileURLToPath } from 'url'
5 import { existsSync } from 'fs'
6
7 let tray: Tray | null = null
8 let hasShownHideBalloon = false
9 const __dirname = dirname(fileURLToPath(import.meta.url))
10 // This module is bundled into the main-process entry, so __dirname is out/main
11 // at runtime even though the source lives in app/.
12 const mainOutputDir = __dirname
13
14 function resolveTrayIconPaths(): string[] {
15 const iconPaths: string[] = []
16 const fileNames = ['16x16.png', '32x32.png', 'icon.ico']
17 const roots = [
18 join(process.resourcesPath, 'icons'),
19 join(process.resourcesPath, 'build/icons'),
20 join(process.resourcesPath, 'app.asar.unpacked/build/icons'),
21 join(process.resourcesPath, 'app.asar.unpacked/resources/icons'),
22 join(mainOutputDir, '../../resources/icons'),
23 join(mainOutputDir, '../../build/icons'),
24 join(process.cwd(), 'build/icons')
25 ]
26
27 for (const root of roots) {
28 for (const fileName of fileNames) {
29 const iconPath = join(root, fileName)
30 if (existsSync(iconPath)) iconPaths.push(iconPath)
31 }
32 }
33
34 if (process.platform === 'win32' && existsSync(process.execPath)) {
35 iconPaths.push(process.execPath)
36 }
37
38 return Array.from(new Set(iconPaths))
39 }
40
41 function showMainWindow(mainWindow: BrowserWindow | null): void {
42 if (!mainWindow || mainWindow.isDestroyed()) return
43 if (mainWindow.isMinimized()) mainWindow.restore()
44 mainWindow.show()
45 mainWindow.focus()
46 }
47
48 export function createTray(mainWindow: BrowserWindow | null): boolean {
49 const iconPaths = resolveTrayIconPaths()
50 if (iconPaths.length === 0) {
51 log.warn('[tray] icon not found')
52 return false
53 }
54
55 let selectedIconPath: string | null = null
56 let trayIcon: Electron.NativeImage | null = null
57 for (const iconPath of iconPaths) {
58 const icon = nativeImage.createFromPath(iconPath)
59 if (icon.isEmpty()) {
60 log.warn('[tray] candidate icon is empty', { iconPath })
61 continue
62 }
63 selectedIconPath = iconPath
64 trayIcon =
65 process.platform === 'win32' && !iconPath.toLowerCase().endsWith('.exe')
66 ? icon.resize({ width: 16, height: 16 })
67 : icon
68 break
69 }
70
71 if (!selectedIconPath || !trayIcon || trayIcon.isEmpty()) {
72 log.warn('[tray] all candidate icons are empty', { iconPaths })
73 return false
74 }
75
76 try {
77 tray = new Tray(trayIcon)
78 } catch (error) {
79 log.warn('[tray] create failed', {
80 iconPath: selectedIconPath,
81 message: error instanceof Error ? error.message : String(error)
82 })
83 return false
84 }
85 tray.setToolTip('Oh My PPT')
86
87 const contextMenu = Menu.buildFromTemplate([
88 {
89 label: '显示主窗口',
90 click: () => {
91 showMainWindow(mainWindow)
92 }
93 },
94 { type: 'separator' },
95 {
96 label: '退出',
97 click: () => {
98 if (mainWindow && !mainWindow.isDestroyed()) {
99 mainWindow.destroy()
100 }
101 app.quit()
102 }
103 }
104 ])
105 tray.setContextMenu(contextMenu)
106
107 tray.on('click', () => {
108 showMainWindow(mainWindow)
109 })
110
111 log.info('[tray] created', { iconPath: selectedIconPath })
112 return true
113 }
114
115 export function showTrayHideBalloon(): void {
116 if (process.platform !== 'win32' || !tray || tray.isDestroyed() || hasShownHideBalloon) return
117 hasShownHideBalloon = true
118 tray.displayBalloon({
119 title: 'Oh My PPT 已最小化到托盘',
120 content: '点击通知区域中的 Oh My PPT 图标可恢复窗口。',
121 iconType: 'info',
122 largeIcon: false,
123 noSound: true
124 })
125 }
126
127 export function destroyTray(): void {
128 if (tray) {
129 tray.destroy()
130 tray = null
131 }
132 }
133
133 lines TYPESCRIPT