hi-client/lib/app/utils/kr_window_manager.dart

311 lines
10 KiB
Dart
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import 'package:kaer_with_panels/singbox/model/singbox_status.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:window_manager/window_manager.dart';
import 'package:tray_manager/tray_manager.dart';
import 'dart:io' show Platform;
import '../services/singbox_imp/kr_sing_box_imp.dart';
class KRWindowManager with WindowListener, TrayListener {
static final KRWindowManager _instance = KRWindowManager._internal();
factory KRWindowManager() => _instance;
KRWindowManager._internal();
/// 初始化窗口管理器
Future<void> kr_initWindowManager() async {
KRLogUtil.kr_i('kr_initWindowManager: 开始初始化窗口管理器');
await windowManager.ensureInitialized();
KRLogUtil.kr_i('kr_initWindowManager: 窗口管理器已初始化');
const WindowOptions windowOptions = WindowOptions(
size: Size(375, 800),
minimumSize: Size(375, 600),
maximumSize: Size(420, 900),
center: true,
backgroundColor: Colors.white,
skipTaskbar: false,
title: 'Hi快VPN',
titleBarStyle: TitleBarStyle.normal,
windowButtonVisibility: true,
);
await windowManager.waitUntilReadyToShow(windowOptions);
KRLogUtil.kr_i('kr_initWindowManager: 窗口准备就绪');
// 先添加监听器
windowManager.addListener(this);
KRLogUtil.kr_i('kr_initWindowManager: 已添加窗口监听器');
// 确保在 Windows 下正确设置窗口属性
if (Platform.isWindows) {
await windowManager.setTitleBarStyle(TitleBarStyle.normal);
await windowManager.setTitle('HiFastVPN');
await windowManager.setSize(const Size(375, 800));
await windowManager.setMinimumSize(const Size(375, 600));
await windowManager.setMaximumSize(const Size(420, 900));
await windowManager.setMaximizable(false);
await windowManager.setResizable(true);
await windowManager.center();
await windowManager.show();
// 阻止窗口关闭
await windowManager.setPreventClose(true);
} else {
await windowManager.setTitle('HiFastVPN');
await windowManager.setSize(const Size(375, 800));
await windowManager.setMinimumSize(const Size(375, 600));
await windowManager.setMaximumSize(const Size(420, 900));
await windowManager.setResizable(true);
await windowManager.center();
await windowManager.show(); // macOS 也需要显式显示窗口
}
// 初始化托盘
await _initTray();
// 初始化平台通道
_initPlatformChannel();
KRLogUtil.kr_i('kr_initWindowManager: 初始化完成');
ever(KRLanguageUtils.kr_language, (_) {
final Menu menu = Menu(
items: [
MenuItem(
label: AppTranslations.kr_tray.openDashboard,
onClick: (_) => _showWindow(),
),
if (Platform.isMacOS) MenuItem.separator(),
if (Platform.isMacOS)
MenuItem(
label: AppTranslations.kr_tray.copyToTerminal,
onClick: (_) => _copyToTerminal(),
),
MenuItem.separator(),
MenuItem(
label: AppTranslations.kr_tray.exitApp,
onClick: (_) => _exitApp(),
),
],
);
trayManager.setContextMenu(menu);
});
}
/// 初始化平台通道
void _initPlatformChannel() {
if (Platform.isMacOS) {
const platform = MethodChannel('hifast_vpn/terminate');
platform.setMethodCallHandler((call) async {
if (call.method == 'onTerminate') {
KRLogUtil.kr_i('收到应用终止通知');
await _handleTerminate();
}
});
}
}
/// 初始化托盘
Future<void> _initTray() async {
KRLogUtil.kr_i('_initTray: 开始初始化托盘');
trayManager.addListener(this);
final String iconPath = Platform.isMacOS
? 'assets/images/tray_icon.png'
: 'assets/images/tray_icon.ico';
await trayManager.setIcon(iconPath);
// 初始化托盘
Future.delayed(const Duration(seconds: 1), () async {
final Menu menu = Menu(
items: [
MenuItem(
label: AppTranslations.kr_tray.openDashboard,
onClick: (_) => _showWindow(),
),
if (Platform.isMacOS) MenuItem.separator(),
if (Platform.isMacOS)
MenuItem(
label: AppTranslations.kr_tray.copyToTerminal,
onClick: (_) => _copyToTerminal(),
),
MenuItem.separator(),
MenuItem(
label: AppTranslations.kr_tray.exitApp,
onClick: (_) => _exitApp(),
),
],
);
await trayManager.setContextMenu(menu);
});
}
/// 复制到终端
Future<void> _copyToTerminal() async {
final String kr_port = KRSingBoxImp.instance.kr_port.toString();
final String proxyText =
'export https_proxy=http://127.0.0.1:$kr_port http_proxy=http://127.0.0.1:$kr_port all_proxy=socks5://127.0.0.1:$kr_port';
await Clipboard.setData(ClipboardData(text: proxyText));
}
/// 退出应用
/// ✅ 改进:先恢复窗口(如果最小化),再显示对话框
Future<void> _exitApp() async {
KRLogUtil.kr_i('_exitApp: 退出应用');
// ✅ 关键修复:先恢复窗口(从最小化状态)
// 这样可以确保对话框可见
try {
await windowManager.show();
await windowManager.focus();
await windowManager.setAlwaysOnTop(true);
KRLogUtil.kr_i('✅ 窗口已恢复,准备显示对话框', tag: 'WindowManager');
} catch (e) {
KRLogUtil.kr_w('⚠️ 恢复窗口失败(可能已显示): $e', tag: 'WindowManager');
}
// 🔧 修复:检查 VPN 是否在运行,如果运行则弹窗提醒用户
if (KRSingBoxImp.instance.kr_status.value is! SingboxStopped) {
KRLogUtil.kr_w('⚠️ VPN 正在运行,询问用户是否关闭', tag: 'WindowManager');
// 显示确认对话框
final shouldExit = await Get.dialog<bool>(
AlertDialog(
title: Text('关闭 VPN'),
content: Text("VPN 代理正在运行。\n\n是否现在关闭 VPN 并退出应用?\n\n(应用将等待 VPN 优雅关闭,预计 3-5 秒)"),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: Text('取消'),
),
TextButton(
onPressed: () => Get.back(result: true),
child: Text('关闭并退出', style: const TextStyle(color: Colors.red)),
),
],
),
barrierDismissible: false,
) ?? false;
// ✅ 关键修复:对话框关闭后,恢复窗口的 AlwaysOnTop 状态
try {
await windowManager.setAlwaysOnTop(false);
} catch (e) {
KRLogUtil.kr_w('⚠️ 恢复 AlwaysOnTop 失败: $e', tag: 'WindowManager');
}
if (!shouldExit) {
KRLogUtil.kr_i('_exitApp: 用户取消退出');
return;
}
KRLogUtil.kr_i('_exitApp: 用户确认关闭 VPN 并退出');
} else {
// ✅ VPN 未运行,也要恢复 AlwaysOnTop 状态
try {
await windowManager.setAlwaysOnTop(false);
} catch (e) {
KRLogUtil.kr_w('⚠️ 恢复 AlwaysOnTop 失败: $e', tag: 'WindowManager');
}
}
await _handleTerminate();
await windowManager.destroy();
}
/// 显示窗口
Future<void> _showWindow() async {
KRLogUtil.kr_i('_showWindow: 开始显示窗口');
try {
await windowManager.setSkipTaskbar(false);
await windowManager.show();
await windowManager.focus();
await windowManager.setAlwaysOnTop(true);
await Future.delayed(const Duration(milliseconds: 100));
await windowManager.setAlwaysOnTop(false);
KRLogUtil.kr_i('_showWindow: 窗口显示成功');
} catch (e) {
KRLogUtil.kr_e('_showWindow: 显示窗口失败 - $e');
}
}
@override
void onWindowEvent(String eventName) {
KRLogUtil.kr_i('onWindowEvent: 收到窗口事件 - $eventName');
// 移除 Windows 下自动显示窗口的逻辑
}
@override
void onWindowClose() async {
if (Platform.isWindows) {
await windowManager.setSkipTaskbar(true);
await windowManager.hide();
} else if (Platform.isMacOS) {
await windowManager.hide();
}
}
@override
void onTrayIconMouseDown() {
// 左键点击只显示菜单
trayManager.popUpContextMenu();
}
@override
void onTrayIconRightMouseDown() {
// 右键点击只显示菜单
trayManager.popUpContextMenu();
}
@override
void onWindowFocus() {
// 移除自动显示窗口的逻辑
}
@override
void onWindowBlur() {
// 当窗口失去焦点时,保持窗口可见
}
/// 设置窗口背景颜色
Future<void> kr_setBackgroundColor(Color color) async {
await windowManager.setBackgroundColor(color);
}
/// 处理应用终止
Future<void> _handleTerminate() async {
KRLogUtil.kr_i('_handleTerminate: 处理应用终止');
// 🔧 修复 BUG正确检查 VPN 状态而不是直接比较 Rx 对象
// 之前的代码if (KRSingBoxImp.instance.kr_status == SingboxStatus.started())
// 问题kr_status 是 Rx<SingboxStatus> 对象,不能直接与 SingboxStatus.started() 比较
// 结果:该条件总是 false导致 kr_stop() 从不被调用VPN 不会关闭
if (KRSingBoxImp.instance.kr_status.value is SingboxStarted) {
KRLogUtil.kr_i('🛑 VPN 正在运行,开始关闭...', tag: 'WindowManager');
try {
await KRSingBoxImp.instance.kr_stop();
KRLogUtil.kr_i('✅ VPN 已关闭', tag: 'WindowManager');
} catch (e) {
KRLogUtil.kr_e('❌ VPN 关闭出错: $e', tag: 'WindowManager');
}
} else {
KRLogUtil.kr_i('✅ VPN 未运行,无需关闭', tag: 'WindowManager');
}
// 销毁托盘
try {
await trayManager.destroy();
KRLogUtil.kr_i('✅ 托盘已销毁', tag: 'WindowManager');
} catch (e) {
KRLogUtil.kr_w('⚠️ 销毁托盘出错: $e', tag: 'WindowManager');
}
}
}