Merge worktree-windows-icon-tray-fix: Windows 桌面/托盘/任务栏图标 + 关窗不断连接
- 关窗(X/Alt+F4)隐藏到托盘并保持隧道,真退出只走托盘菜单(Platform.isWindows 隔离) - 窗口标题设「穿山甲」(无 VPN 字眼) - Windows 托盘改用 .ico(Win32 只认 ico,PNG 空白),mac/Linux 仍 PNG - 更新 app_icon.ico / tray_icon.ico / Runner.rc / installer Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
+18
-2
@@ -31,6 +31,8 @@ Future<void> main() async {
|
|||||||
await windowManager.setMinimumSize(const Size(720, 560));
|
await windowManager.setMinimumSize(const Size(720, 560));
|
||||||
await windowManager.setSize(const Size(920, 640));
|
await windowManager.setSize(const Size(920, 640));
|
||||||
await windowManager.center();
|
await windowManager.center();
|
||||||
|
// 窗口标题用中文品牌名(不带 "VPN" 字眼);英文环境下原生标题为 "Pangolin"。
|
||||||
|
await windowManager.setTitle('穿山甲');
|
||||||
}
|
}
|
||||||
runApp(const ProviderScope(child: PangolinApp()));
|
runApp(const ProviderScope(child: PangolinApp()));
|
||||||
}
|
}
|
||||||
@@ -44,17 +46,31 @@ class PangolinApp extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _PangolinAppState extends ConsumerState<PangolinApp> {
|
class _PangolinAppState extends ConsumerState<PangolinApp> {
|
||||||
AppLifecycleListener? _lifecycle;
|
AppLifecycleListener? _lifecycle;
|
||||||
|
// 是否正在「真退出」(托盘「退出」菜单触发)。区分:点关闭只隐藏到托盘、保持隧道。
|
||||||
|
bool _isQuitting = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (isDesktop) {
|
if (isDesktop) {
|
||||||
// 真退出(Cmd+Q / 托盘退出)前先停内核拆隧道,避免 sing-box 孤儿 + TUN 残留。
|
// 关闭 / 退出请求的处理:
|
||||||
|
// - Windows:点 X / Alt+F4 会触发 onExitRequested,但桌面端要常驻托盘、隧道不断
|
||||||
|
// → 取消退出并隐藏到托盘。真正退出只走托盘「退出」菜单(见 onBeforeQuit)。
|
||||||
|
// (否则会出现:连接被拆、窗口却只是隐藏,即「关掉就断网但进程还在」。)
|
||||||
|
// - macOS:Cmd+Q 等真退出 → 先停内核拆隧道再退出(红叉关闭走 onWindowClose 隐藏)。
|
||||||
_lifecycle = AppLifecycleListener(onExitRequested: () async {
|
_lifecycle = AppLifecycleListener(onExitRequested: () async {
|
||||||
|
if (Platform.isWindows && !_isQuitting) {
|
||||||
|
await windowManager.hide();
|
||||||
|
return ui.AppExitResponse.cancel;
|
||||||
|
}
|
||||||
await _teardownVpn();
|
await _teardownVpn();
|
||||||
return ui.AppExitResponse.exit;
|
return ui.AppExitResponse.exit;
|
||||||
});
|
});
|
||||||
TrayService.instance.onBeforeQuit = _teardownVpn;
|
// 托盘「退出」前置钩子:标记真退出 + 停内核拆隧道(避免 sing-box 孤儿 + TUN 残留)。
|
||||||
|
TrayService.instance.onBeforeQuit = () async {
|
||||||
|
_isQuitting = true;
|
||||||
|
await _teardownVpn();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,11 @@ class TrayService with TrayListener, WindowListener {
|
|||||||
windowManager.addListener(this);
|
windowManager.addListener(this);
|
||||||
|
|
||||||
trayManager.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.setToolTip('穿山甲 Pangolin');
|
||||||
await trayManager.setContextMenu(Menu(items: [
|
await trayManager.setContextMenu(Menu(items: [
|
||||||
MenuItem(key: 'show', label: '显示主界面'),
|
MenuItem(key: 'show', label: '显示主界面'),
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,7 @@
|
|||||||
name: pangolin_vpn
|
name: pangolin_vpn
|
||||||
description: 穿山甲 · Pangolin — 极简、稳定、跨平台网络加速客户端。
|
description: 穿山甲 · Pangolin — 极简、稳定、跨平台网络加速客户端。
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.0.0+1
|
version: 1.0.2+3
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.0
|
sdk: ^3.5.0
|
||||||
@@ -55,6 +55,7 @@ flutter:
|
|||||||
- assets/logo-wordmark.svg
|
- assets/logo-wordmark.svg
|
||||||
- assets/app-icon.svg
|
- assets/app-icon.svg
|
||||||
- assets/tray_icon.png
|
- assets/tray_icon.png
|
||||||
|
- assets/tray_icon.ico # Windows 托盘专用(Win32 只认 .ico)
|
||||||
|
|
||||||
# ── 字体(本地打包,全平台离线)──
|
# ── 字体(本地打包,全平台离线)──
|
||||||
# 拉丁全量(Sora/Manrope/JetBrains Mono)+ Noto Sans SC 子集(GB2312 6763 字,单 Regular
|
# 拉丁全量(Sora/Manrope/JetBrains Mono)+ Noto Sans SC 子集(GB2312 6763 字,单 Regular
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
; 前置: 先在 client/ 跑 `flutter build windows`(Release),产物在
|
; 前置: 先在 client/ 跑 `flutter build windows`(Release),产物在
|
||||||
; ..\..\build\windows\x64\runner\Release
|
; ..\..\build\windows\x64\runner\Release
|
||||||
#define MyAppName "穿山甲 Pangolin"
|
#define MyAppName "穿山甲 Pangolin"
|
||||||
#define MyAppVersion "1.0.0"
|
#define MyAppVersion "1.0.2"
|
||||||
#define MyAppPublisher "Pangolin"
|
#define MyAppPublisher "Pangolin"
|
||||||
#define MyAppExeName "pangolin_vpn.exe"
|
#define MyAppExeName "pangolin_vpn.exe"
|
||||||
#define BuildDir "..\..\build\windows\x64\runner\Release"
|
#define BuildDir "..\..\build\windows\x64\runner\Release"
|
||||||
|
|||||||
@@ -90,12 +90,12 @@ BEGIN
|
|||||||
BLOCK "040904e4"
|
BLOCK "040904e4"
|
||||||
BEGIN
|
BEGIN
|
||||||
VALUE "CompanyName", "com.pangolin" "\0"
|
VALUE "CompanyName", "com.pangolin" "\0"
|
||||||
VALUE "FileDescription", "pangolin_vpn" "\0"
|
VALUE "FileDescription", "Pangolin" "\0"
|
||||||
VALUE "FileVersion", VERSION_AS_STRING "\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 "LegalCopyright", "Copyright (C) 2026 com.pangolin. All rights reserved." "\0"
|
||||||
VALUE "OriginalFilename", "pangolin_vpn.exe" "\0"
|
VALUE "OriginalFilename", "pangolin_vpn.exe" "\0"
|
||||||
VALUE "ProductName", "pangolin_vpn" "\0"
|
VALUE "ProductName", "Pangolin" "\0"
|
||||||
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
|||||||
FlutterWindow window(project);
|
FlutterWindow window(project);
|
||||||
Win32Window::Point origin(10, 10);
|
Win32Window::Point origin(10, 10);
|
||||||
Win32Window::Size size(1280, 720);
|
Win32Window::Size size(1280, 720);
|
||||||
if (!window.Create(L"pangolin_vpn", origin, size)) {
|
if (!window.Create(L"Pangolin", origin, size)) {
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
window.SetQuitOnClose(true);
|
window.SetQuitOnClose(true);
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
@@ -144,6 +144,25 @@ bool Win32Window::Create(const std::wstring& title,
|
|||||||
return false;
|
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<LPARAM>(big_icon));
|
||||||
|
}
|
||||||
|
if (small_icon) {
|
||||||
|
SendMessage(window, WM_SETICON, ICON_SMALL,
|
||||||
|
reinterpret_cast<LPARAM>(small_icon));
|
||||||
|
}
|
||||||
|
|
||||||
UpdateTheme(window);
|
UpdateTheme(window);
|
||||||
|
|
||||||
return OnCreate();
|
return OnCreate();
|
||||||
|
|||||||
Reference in New Issue
Block a user