diff --git a/src/main/context.ts b/src/main/context.ts index da3b5dc..05ff78f 100644 --- a/src/main/context.ts +++ b/src/main/context.ts @@ -2,6 +2,8 @@ import { Context as BaseContext } from '../shared/kernel' import { ipcMain } from 'electron' export class MainContext extends BaseContext { + public isQuitting = false + constructor() { super() } diff --git a/src/main/hosting/index.ts b/src/main/hosting/index.ts index 3236dc5..cab2910 100644 --- a/src/main/hosting/index.ts +++ b/src/main/hosting/index.ts @@ -13,7 +13,8 @@ export { EventRepositoryToken, SettlementRepositoryToken, ThemeServiceToken, - WindowManagerToken + WindowManagerToken, + TrayServiceToken } from './tokens' export type { appRuntimeContext, diff --git a/src/main/hosting/tokens.ts b/src/main/hosting/tokens.ts index 6321d8b..a28ad91 100644 --- a/src/main/hosting/tokens.ts +++ b/src/main/hosting/tokens.ts @@ -12,3 +12,4 @@ export const EventRepositoryToken = Symbol.for('secscore.eventRepository') export const SettlementRepositoryToken = Symbol.for('secscore.settlementRepository') export const ThemeServiceToken = Symbol.for('secscore.themeService') export const WindowManagerToken = Symbol.for('secscore.windowManager') +export const TrayServiceToken = Symbol.for('secscore.trayService') diff --git a/src/main/index.ts b/src/main/index.ts index e647fdd..f05cb9e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -14,6 +14,7 @@ import { AuthService } from './services/AuthService' import { DataService } from './services/DataService' import { ThemeService } from './services/ThemeService' import { WindowManager, type windowManagerOptions } from './services/WindowManager' +import { TrayService } from './services/TrayService' import { StudentRepository } from './repos/StudentRepository' import { ReasonRepository } from './repos/ReasonRepository' import { EventRepository } from './repos/EventRepository' @@ -31,7 +32,8 @@ import { SettingsStoreToken, StudentRepositoryToken, ThemeServiceToken, - WindowManagerToken + WindowManagerToken, + TrayServiceToken } from './hosting' type mainAppConfig = { @@ -137,6 +139,10 @@ app.whenReady().then(async () => { WindowManagerToken, (p) => new WindowManager(p.get(MainContext), config.window) ) + services.addSingleton( + TrayServiceToken, + (p) => new TrayService(p.get(MainContext), config.window) + ) }) .configure(async (_builderContext, appCtx) => { const services = appCtx.services @@ -156,6 +162,8 @@ app.whenReady().then(async () => { services.get(SettlementRepositoryToken) services.get(ThemeServiceToken) services.get(WindowManagerToken) + const tray = services.get(TrayServiceToken) as TrayService + tray.initialize() const logLevelSetting = ctx.settings.getValue('log_level') as logLevel if (logLevelSetting) { @@ -254,12 +262,14 @@ app.whenReady().then(async () => { }) const host = await builder.build() + const ctx = host.services.get(MainContext) as MainContext await host.start() let disposing = false const beforeQuitHandler = () => { if (disposing) return disposing = true + ctx.isQuitting = true app.removeListener('before-quit', beforeQuitHandler) void host.dispose() } diff --git a/src/main/services/TrayService.ts b/src/main/services/TrayService.ts new file mode 100644 index 0000000..c732695 --- /dev/null +++ b/src/main/services/TrayService.ts @@ -0,0 +1,58 @@ +import { Service } from '../../shared/kernel' +import { MainContext } from '../context' +import { Tray, Menu, app, nativeImage } from 'electron' +import type { windowManagerOptions } from './WindowManager' + +export class TrayService extends Service { + private tray: Tray | null = null + + constructor( + ctx: MainContext, + private readonly opts: windowManagerOptions + ) { + super(ctx, 'tray') + } + + private get mainCtx() { + return this.ctx as MainContext + } + + public initialize() { + const icon = nativeImage.createFromPath(this.opts.icon) + this.tray = new Tray(icon) + + const contextMenu = Menu.buildFromTemplate([ + { + label: '显示主窗口', + click: () => { + this.showMainWindow() + } + }, + { type: 'separator' }, + { + label: '退出 SecScore', + click: () => { + app.quit() + } + } + ]) + + this.tray.setToolTip('SecScore 积分管理') + this.tray.setContextMenu(contextMenu) + + this.tray.on('double-click', () => { + this.showMainWindow() + }) + } + + private showMainWindow() { + const mainWin = this.mainCtx.windows.get('main') + if (mainWin) { + if (mainWin.isMinimized()) mainWin.restore() + mainWin.show() + mainWin.focus() + } else { + this.mainCtx.windows.open({ key: 'main', title: 'SecScore', route: '/' }) + } + } +} diff --git a/src/main/services/WindowManager.ts b/src/main/services/WindowManager.ts index 4e869d7..32ca28c 100644 --- a/src/main/services/WindowManager.ts +++ b/src/main/services/WindowManager.ts @@ -76,6 +76,14 @@ export class WindowManager extends Service { win.webContents.setZoomFactor(zoom) this.windows.set(input.key, win) + + win.on('close', (event) => { + if (!this.mainCtx.isQuitting && input.key === 'main') { + event.preventDefault() + win.hide() + } + }) + win.on('closed', () => { this.windows.delete(input.key) }) diff --git a/src/preload/types.ts b/src/preload/types.ts index a6f7e69..a92958f 100644 --- a/src/preload/types.ts +++ b/src/preload/types.ts @@ -20,6 +20,7 @@ export interface themeConfig { export type settingsSpec = { is_wizard_completed: boolean log_level: logLevel + window_zoom: number } export type settingsKey = keyof settingsSpec