311 lines
10 KiB
Dart
Executable File
311 lines
10 KiB
Dart
Executable File
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');
|
||
}
|
||
}
|
||
}
|