diff --git a/client/assets/tray_icon.ico b/client/assets/tray_icon.ico new file mode 100644 index 0000000..a917de1 Binary files /dev/null and b/client/assets/tray_icon.ico differ diff --git a/client/lib/main.dart b/client/lib/main.dart index b03f2c5..55af956 100644 --- a/client/lib/main.dart +++ b/client/lib/main.dart @@ -31,6 +31,8 @@ Future main() async { await windowManager.setMinimumSize(const Size(720, 560)); await windowManager.setSize(const Size(920, 640)); await windowManager.center(); + // 窗口标题用中文品牌名(不带 "VPN" 字眼);英文环境下原生标题为 "Pangolin"。 + await windowManager.setTitle('穿山甲'); } runApp(const ProviderScope(child: PangolinApp())); } @@ -44,17 +46,31 @@ class PangolinApp extends ConsumerStatefulWidget { class _PangolinAppState extends ConsumerState { AppLifecycleListener? _lifecycle; + // 是否正在「真退出」(托盘「退出」菜单触发)。区分:点关闭只隐藏到托盘、保持隧道。 + bool _isQuitting = false; @override void initState() { super.initState(); if (isDesktop) { - // 真退出(Cmd+Q / 托盘退出)前先停内核拆隧道,避免 sing-box 孤儿 + TUN 残留。 + // 关闭 / 退出请求的处理: + // - Windows:点 X / Alt+F4 会触发 onExitRequested,但桌面端要常驻托盘、隧道不断 + // → 取消退出并隐藏到托盘。真正退出只走托盘「退出」菜单(见 onBeforeQuit)。 + // (否则会出现:连接被拆、窗口却只是隐藏,即「关掉就断网但进程还在」。) + // - macOS:Cmd+Q 等真退出 → 先停内核拆隧道再退出(红叉关闭走 onWindowClose 隐藏)。 _lifecycle = AppLifecycleListener(onExitRequested: () async { + if (Platform.isWindows && !_isQuitting) { + await windowManager.hide(); + return ui.AppExitResponse.cancel; + } await _teardownVpn(); return ui.AppExitResponse.exit; }); - TrayService.instance.onBeforeQuit = _teardownVpn; + // 托盘「退出」前置钩子:标记真退出 + 停内核拆隧道(避免 sing-box 孤儿 + TUN 残留)。 + TrayService.instance.onBeforeQuit = () async { + _isQuitting = true; + await _teardownVpn(); + }; } } diff --git a/client/lib/system_tray.dart b/client/lib/system_tray.dart index 67edadb..1eea776 100644 --- a/client/lib/system_tray.dart +++ b/client/lib/system_tray.dart @@ -53,7 +53,11 @@ class TrayService with TrayListener, WindowListener { windowManager.addListener(this); trayManager.addListener(this); - await trayManager.setIcon('assets/tray_icon.png'); + // Windows 托盘走 Win32 Shell_NotifyIcon,只认 .ico(传 PNG 会显示空白); + // macOS/Linux 用 PNG。 + await trayManager.setIcon( + Platform.isWindows ? 'assets/tray_icon.ico' : 'assets/tray_icon.png', + ); await trayManager.setToolTip('穿山甲 Pangolin'); await trayManager.setContextMenu(Menu(items: [ MenuItem(key: 'show', label: '显示主界面'), diff --git a/client/pubspec.yaml b/client/pubspec.yaml index 2f305b2..05fed38 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -1,7 +1,7 @@ name: pangolin_vpn description: 穿山甲 · Pangolin — 极简、稳定、跨平台网络加速客户端。 publish_to: "none" -version: 1.0.0+1 +version: 1.0.2+3 environment: sdk: ^3.5.0 @@ -55,6 +55,7 @@ flutter: - assets/logo-wordmark.svg - assets/app-icon.svg - assets/tray_icon.png + - assets/tray_icon.ico # Windows 托盘专用(Win32 只认 .ico) # ── 字体(本地打包,全平台离线)── # 拉丁全量(Sora/Manrope/JetBrains Mono)+ Noto Sans SC 子集(GB2312 6763 字,单 Regular diff --git a/client/windows/installer/pangolin.iss b/client/windows/installer/pangolin.iss index 72d5015..6dbdb72 100644 --- a/client/windows/installer/pangolin.iss +++ b/client/windows/installer/pangolin.iss @@ -3,7 +3,7 @@ ; 前置: 先在 client/ 跑 `flutter build windows`(Release),产物在 ; ..\..\build\windows\x64\runner\Release #define MyAppName "穿山甲 Pangolin" -#define MyAppVersion "1.0.0" +#define MyAppVersion "1.0.2" #define MyAppPublisher "Pangolin" #define MyAppExeName "pangolin_vpn.exe" #define BuildDir "..\..\build\windows\x64\runner\Release" diff --git a/client/windows/runner/Runner.rc b/client/windows/runner/Runner.rc index 89e9401..e1b3e72 100644 --- a/client/windows/runner/Runner.rc +++ b/client/windows/runner/Runner.rc @@ -90,12 +90,12 @@ BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.pangolin" "\0" - VALUE "FileDescription", "pangolin_vpn" "\0" + VALUE "FileDescription", "Pangolin" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "pangolin_vpn" "\0" + VALUE "InternalName", "Pangolin" "\0" VALUE "LegalCopyright", "Copyright (C) 2026 com.pangolin. All rights reserved." "\0" VALUE "OriginalFilename", "pangolin_vpn.exe" "\0" - VALUE "ProductName", "pangolin_vpn" "\0" + VALUE "ProductName", "Pangolin" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END diff --git a/client/windows/runner/main.cpp b/client/windows/runner/main.cpp index 45dd14e..ffc6f9c 100644 --- a/client/windows/runner/main.cpp +++ b/client/windows/runner/main.cpp @@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.Create(L"pangolin_vpn", origin, size)) { + if (!window.Create(L"Pangolin", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); diff --git a/client/windows/runner/resources/app_icon.ico b/client/windows/runner/resources/app_icon.ico index c04e20c..c1febf4 100644 Binary files a/client/windows/runner/resources/app_icon.ico and b/client/windows/runner/resources/app_icon.ico differ diff --git a/client/windows/runner/win32_window.cpp b/client/windows/runner/win32_window.cpp index 60608d0..c37ab98 100644 --- a/client/windows/runner/win32_window.cpp +++ b/client/windows/runner/win32_window.cpp @@ -144,6 +144,25 @@ bool Win32Window::Create(const std::wstring& title, return false; } + // 显式给窗口设大/小两种图标:任务栏 / Alt-Tab 用 ICON_BIG(32px),标题栏用 + // ICON_SMALL(16px)。仅靠 WNDCLASS.hIcon(LoadIcon 取单一尺寸)时,任务栏可能 + // 拿不到正确尺寸而沿用旧/默认图标 —— 从多尺寸 app_icon.ico 按系统尺寸各取一份。 + HINSTANCE instance = GetModuleHandle(nullptr); + HANDLE big_icon = LoadImage(instance, MAKEINTRESOURCE(IDI_APP_ICON), IMAGE_ICON, + GetSystemMetrics(SM_CXICON), + GetSystemMetrics(SM_CYICON), LR_DEFAULTCOLOR); + HANDLE small_icon = LoadImage(instance, MAKEINTRESOURCE(IDI_APP_ICON), IMAGE_ICON, + GetSystemMetrics(SM_CXSMICON), + GetSystemMetrics(SM_CYSMICON), LR_DEFAULTCOLOR); + if (big_icon) { + SendMessage(window, WM_SETICON, ICON_BIG, + reinterpret_cast(big_icon)); + } + if (small_icon) { + SendMessage(window, WM_SETICON, ICON_SMALL, + reinterpret_cast(small_icon)); + } + UpdateTheme(window); return OnCreate();