From e10f214deefcae2198afd1ebab7096d879f4b088 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Fri, 19 Jun 2026 06:31:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(client):=20=E7=B3=BB=E7=BB=9F=E6=89=98?= =?UTF-8?q?=E7=9B=98=E5=B8=B8=E9=A9=BB=20+=20=E5=85=B3=E9=97=AD=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E9=9A=90=E8=97=8F=E5=88=B0=E6=89=98=E7=9B=98(?= =?UTF-8?q?=E6=A1=8C=E9=9D=A2=E7=AB=AF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - system_tray.dart:TrayService 用 tray_manager + window_manager。setPreventClose 拦截关闭 → 隐藏到托盘(常驻保持隧道);托盘图标点击/「显示主界面」重新显示, 「退出」真正退出。 - main() 改 async:桌面端 runApp 前 init 托盘;移动端/web 跳过(isDesktop 守卫)。 - 托盘图标复用 app_icon_32.png → assets/tray_icon.png。 - 新依赖:tray_manager / window_manager。 flutter analyze 0 error;114 tests passed。托盘为运行时/原生功能,需 release .app 验证。 Co-Authored-By: Claude Opus 4.8 --- client/assets/tray_icon.png | Bin 0 -> 1656 bytes client/lib/main.dart | 10 +++++- client/lib/system_tray.dart | 66 ++++++++++++++++++++++++++++++++++++ client/pubspec.yaml | 3 ++ 4 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 client/assets/tray_icon.png create mode 100644 client/lib/system_tray.dart diff --git a/client/assets/tray_icon.png b/client/assets/tray_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2019a15166553722e2f1c539183407bfcb0f2bc2 GIT binary patch literal 1656 zcmV-;28a2HP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$^;z>k7R9FeMS8Z$*MHqf&XZN-r zP}^QhQJ^5Lgvv+dE4BzoM1hbH6U;&Uh=MUh`9qW-{xF*O!x#a7nqXqoKZrI)2%2ak z2BFvnN=_&Q1ISkkv|tl2Er-seCzo8)nJms~T!r1Nz@7R^IV~(w@t8+6~i_Agedn=bpLw=-` zS`;!3(eHSqK!M?T;%KBK5~K^HO9>$wT+jWW_LE-@2DoVghu>YX!8YaR#5$+naWz;l z=|MD$7S(9ok@}wwPq)B**j8 z#`*#9OO79AYs|?m4Kc}dHF!{s40msvl;^^8Qj}N*4AX>1V?}xl)6&m;+V|1}P};%d zjb;$u?{hHx<-;V+^->AsmQFxf^%4|S%!XZ9f>d8GI--Bz`q6s0jsqbjt!FL6)@O{_ zyNqEf^1c!SjwYe|!zE4h+?39f$47?KEkBkRn-N{2x42ZwyZ zTPD5N>^aUH*Yc;=pmNn~$SE2ZV2vnCXVl=)=U&9w{a-?cajdBAG+O3?feFdG85 z@(YlP$Kb^K{P$!<;gl*=t$7P2RSSYONcME%-&0L+Q%U5Pl_TDH4T-KBD40AQ;py{r z5>8$53ffOJpttQjZgCkx1!2T*-Nd!y2g!pW2!r>3KdlEP zD-{D(AS7;{X~vYLFQA01xLfmF7qPZ~P_^!D%5U!k!@(dc_@scsWXsUSW1aj2rG9C zEeydPTZ~8MK25XPiaQ$v@;pId%WNHIj-$7|6+IVE;!4BMdQRqh?x&NG7bm*AaJBh& zofB!c6ezYdJgu6D43^&2V+YW6;S^@A+pI&DshExFXlX>WZWn9{$+CBLp!57G#5!83 zKxEnveiBjwsfGY-sCYIqumk!u zPVW8;=jy-J6=Yiyr+?g~LqJD7I*?F43`Br&#SgF7`r8$+%>)hzkmM>cLpI76u0me8 z3^yoWI$D~DEM++D6XD0IDc-YmB{}ii8l1hLOWJev?kIJL^kF1TfQ=M&8*0)H7y)0K z#T%8I>enoVVM1DTilArA)201_zYo3vf-1-M!RbMU9YgG6rA*M&JD{G%k4E0Tc{(Q= zOP)uc@$`JIbs;T1=J&o%8vd_7pRdX4_=7aqf_}Co?brlvI<9J?ePSYWV91eN_Mo3L zINdi|4YGa*T><>_Ke1qMunV&-8IuU?H0gWTNcRB78%WW?U}Uq$nDEeO#Bx-WEbL>Xz}$+bC!e*qn_T`#ETkbL$)p^1Wu}x{hqgc{qE=?y?O6# zX}`R>>fx(&R$oDP20ADD9GHsE%m*P!hehCIWFqCLP3!kW_p#da*JJPL>wm4DQFes* zRZt+xEjqHYySUuBI!60rN0KfME30U5l=sGl@1lDdoSP1T-}V0^5y;hY-h8Fp8Uo)+ zSG5WL+RRz^*XQdw runApp(const ProviderScope(child: PangolinApp())); +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + // 桌面端:常驻系统托盘,关闭窗口隐藏到托盘而非退出。 + if (isDesktop) { + await TrayService.instance.init(); + } + runApp(const ProviderScope(child: PangolinApp())); +} class PangolinApp extends ConsumerWidget { const PangolinApp({super.key}); diff --git a/client/lib/system_tray.dart b/client/lib/system_tray.dart new file mode 100644 index 0000000..478f2cb --- /dev/null +++ b/client/lib/system_tray.dart @@ -0,0 +1,66 @@ +// system_tray.dart — 桌面端系统托盘 + 关闭即最小化到托盘。 +// +// macOS/Windows/Linux:关闭主窗口不退出,而是隐藏到托盘;托盘图标点击/菜单 +// 可重新显示或真正退出。移动端/web 不涉及。 +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:tray_manager/tray_manager.dart'; +import 'package:window_manager/window_manager.dart'; + +bool get isDesktop => + !kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux); + +class TrayService with TrayListener, WindowListener { + TrayService._(); + static final TrayService instance = TrayService._(); + + /// 在 runApp 前调用(仅桌面端)。设置窗口拦截关闭 + 托盘图标与菜单。 + Future init() async { + await windowManager.ensureInitialized(); + // 关闭窗口转为隐藏到托盘(见 onWindowClose)。 + await windowManager.setPreventClose(true); + windowManager.addListener(this); + + trayManager.addListener(this); + await trayManager.setIcon('assets/tray_icon.png'); + await trayManager.setToolTip('穿山甲 Pangolin'); + await trayManager.setContextMenu(Menu(items: [ + MenuItem(key: 'show', label: '显示主界面'), + MenuItem.separator(), + MenuItem(key: 'quit', label: '退出'), + ])); + } + + Future _show() async { + await windowManager.show(); + await windowManager.focus(); + } + + // ── 托盘事件 ────────────────────────────────────────────────── + @override + void onTrayIconMouseDown() => _show(); + + @override + void onTrayIconRightMouseDown() => trayManager.popUpContextMenu(); + + @override + void onTrayMenuItemClick(MenuItem menuItem) { + switch (menuItem.key) { + case 'show': + _show(); + case 'quit': + windowManager.setPreventClose(false); + windowManager.destroy(); + } + } + + // ── 窗口事件 ────────────────────────────────────────────────── + @override + void onWindowClose() async { + // 点关闭 → 隐藏到托盘而非退出(常驻保持隧道)。 + if (await windowManager.isPreventClose()) { + await windowManager.hide(); + } + } +} diff --git a/client/pubspec.yaml b/client/pubspec.yaml index bf68abb..76d8c34 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -20,6 +20,8 @@ dependencies: shared_preferences: ^2.5.5 package_info_plus: ^9.0.1 launch_at_startup: ^0.5.1 + tray_manager: ^0.5.3 + window_manager: ^0.5.1 dev_dependencies: flutter_test: @@ -42,6 +44,7 @@ flutter: - assets/logo-mark-white.svg - assets/logo-wordmark.svg - assets/app-icon.svg + - assets/tray_icon.png # ── 字体(由 google_fonts 运行时加载,无需本地 ttf)── # 若需离线/打包字体,下载 ttf 放 fonts/,并按 pubspec 模板恢复 fonts: 段。