diff --git a/client/assets/tray_icon.png b/client/assets/tray_icon.png new file mode 100644 index 0000000..2019a15 Binary files /dev/null and b/client/assets/tray_icon.png differ diff --git a/client/lib/main.dart b/client/lib/main.dart index 1b41297..4ced3fa 100644 --- a/client/lib/main.dart +++ b/client/lib/main.dart @@ -8,11 +8,19 @@ import 'services/token_store.dart'; import 'shell/home_shell.dart'; import 'state/app_providers.dart'; import 'state/auth_provider.dart'; +import 'system_tray.dart'; import 'widgets/auth_screen.dart'; import 'widgets/onboarding_screen.dart'; import 'widgets/pangolin_logo.dart'; -void main() => 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: 段。