diff --git a/home_monitor/assets/app_icon.ico b/home_monitor/assets/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/home_monitor/assets/app_icon.ico differ diff --git a/home_monitor/lib/main.dart b/home_monitor/lib/main.dart index 9477fb7..ea4e2e6 100644 --- a/home_monitor/lib/main.dart +++ b/home_monitor/lib/main.dart @@ -1,9 +1,19 @@ +import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'providers/providers.dart'; import 'screens/screens.dart'; +import 'services/services.dart'; -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // 初始化窗口管理(仅桌面端) + if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) { + await WindowService().init(); + } + runApp(const HomeMonitorApp()); } diff --git a/home_monitor/lib/screens/home_screen.dart b/home_monitor/lib/screens/home_screen.dart index 93c9b0e..cc89a51 100644 --- a/home_monitor/lib/screens/home_screen.dart +++ b/home_monitor/lib/screens/home_screen.dart @@ -1,6 +1,9 @@ +import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/providers.dart'; +import '../services/services.dart'; import '../widgets/widgets.dart'; /// 主页面 @@ -12,6 +15,9 @@ class HomeScreen extends StatefulWidget { } class _HomeScreenState extends State { + final TrayService _trayService = TrayService(); + final WindowService _windowService = WindowService(); + @override void initState() { super.initState(); @@ -21,9 +27,71 @@ class _HomeScreenState extends State { provider.initDefaultDevices(); provider.refreshAll(); provider.startAutoRefresh(intervalSeconds: 30); + + // 初始化系统托盘 + _initSystemTray(provider); }); } + /// 初始化系统托盘 + Future _initSystemTray(MonitorProvider provider) async { + if (!_trayService.isSupported) return; + + await _trayService.init( + onShow: () => _windowService.showWindow(), + onHide: () => _windowService.hideWindow(), + onExit: () => _exitApp(), + onRefresh: () => provider.refreshAll(), + ); + + // 设置窗口关闭事件(最小化到托盘) + _windowService.onCloseRequested = () => _handleCloseRequest(); + } + + /// 处理窗口关闭请求 + void _handleCloseRequest() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('关闭窗口'), + content: const Text('请选择操作:'), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + _windowService.hideWindow(); + }, + child: const Text('最小化到托盘'), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + _exitApp(); + }, + child: const Text('退出程序'), + ), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + ], + ), + ); + } + + /// 退出应用 + Future _exitApp() async { + await _trayService.destroy(); + await _windowService.closeWindow(); + } + + @override + void dispose() { + _trayService.destroy(); + _windowService.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -60,6 +128,13 @@ class _HomeScreenState extends State { }, tooltip: '设置', ), + // 最小化到托盘按钮(仅桌面端) + if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) + IconButton( + icon: const Icon(Icons.minimize), + onPressed: () => _windowService.hideWindow(), + tooltip: '最小化到托盘', + ), ], ); } diff --git a/home_monitor/lib/services/services.dart b/home_monitor/lib/services/services.dart index bc5aab9..c572865 100644 --- a/home_monitor/lib/services/services.dart +++ b/home_monitor/lib/services/services.dart @@ -1,3 +1,4 @@ // 服务层导出文件 export 'network_service.dart'; export 'speed_test_service.dart'; +export 'tray_service.dart'; diff --git a/home_monitor/lib/services/tray_service.dart b/home_monitor/lib/services/tray_service.dart new file mode 100644 index 0000000..be5803a --- /dev/null +++ b/home_monitor/lib/services/tray_service.dart @@ -0,0 +1,218 @@ +import 'dart:io'; +import 'dart:ui'; +import 'package:flutter/foundation.dart'; +import 'package:system_tray/system_tray.dart'; +import 'package:window_manager/window_manager.dart'; + +/// 系统托盘服务 +class TrayService { + static final TrayService _instance = TrayService._internal(); + factory TrayService() => _instance; + TrayService._internal(); + + final SystemTray _systemTray = SystemTray(); + bool _isInitialized = false; + + // 回调函数 + VoidCallback? onShow; + VoidCallback? onHide; + VoidCallback? onExit; + VoidCallback? onRefresh; + + /// 是否支持系统托盘(仅桌面平台) + bool get isSupported => !kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS); + + /// 初始化系统托盘 + Future init({ + VoidCallback? onShow, + VoidCallback? onHide, + VoidCallback? onExit, + VoidCallback? onRefresh, + }) async { + if (!isSupported || _isInitialized) return; + + this.onShow = onShow; + this.onHide = onHide; + this.onExit = onExit; + this.onRefresh = onRefresh; + + try { + // 初始化托盘图标 + String iconPath = Platform.isWindows + ? 'assets/app_icon.ico' + : 'assets/app_icon.png'; + + await _systemTray.initSystemTray( + title: '家庭网络监控', + iconPath: iconPath, + toolTip: '家庭网络监控 - 运行中', + ); + + // 创建右键菜单 + final menu = Menu(); + await menu.buildFrom([ + MenuItemLabel( + label: '显示窗口', + onClicked: (menuItem) => _handleShow(), + ), + MenuItemLabel( + label: '隐藏窗口', + onClicked: (menuItem) => _handleHide(), + ), + MenuSeparator(), + MenuItemLabel( + label: '刷新状态', + onClicked: (menuItem) => _handleRefresh(), + ), + MenuSeparator(), + MenuItemLabel( + label: '退出程序', + onClicked: (menuItem) => _handleExit(), + ), + ]); + + await _systemTray.setContextMenu(menu); + + // 注册托盘事件 + _systemTray.registerSystemTrayEventHandler((eventName) { + if (eventName == kSystemTrayEventClick) { + // 单击托盘图标显示窗口 + _handleShow(); + } else if (eventName == kSystemTrayEventRightClick) { + // 右键显示菜单 + _systemTray.popUpContextMenu(); + } + }); + + _isInitialized = true; + } catch (e) { + debugPrint('系统托盘初始化失败: $e'); + } + } + + /// 更新托盘提示文字 + Future updateToolTip(String message) async { + if (!_isInitialized) return; + await _systemTray.setToolTip('家庭网络监控 - $message'); + } + + /// 更新托盘标题 + Future updateTitle(String title) async { + if (!_isInitialized) return; + await _systemTray.setTitle(title); + } + + void _handleShow() { + onShow?.call(); + } + + void _handleHide() { + onHide?.call(); + } + + void _handleExit() { + onExit?.call(); + } + + void _handleRefresh() { + onRefresh?.call(); + } + + /// 销毁托盘 + Future destroy() async { + if (!_isInitialized) return; + await _systemTray.destroy(); + _isInitialized = false; + } +} + +/// 窗口管理服务 +class WindowService with WindowListener { + static final WindowService _instance = WindowService._internal(); + factory WindowService() => _instance; + WindowService._internal(); + + bool _isInitialized = false; + VoidCallback? onCloseRequested; + + /// 是否支持窗口管理 + bool get isSupported => !kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS); + + /// 初始化窗口管理 + Future init({VoidCallback? onCloseRequested}) async { + if (!isSupported || _isInitialized) return; + + this.onCloseRequested = onCloseRequested; + + await windowManager.ensureInitialized(); + + // 配置窗口选项 + const windowOptions = WindowOptions( + size: Size(420, 700), + minimumSize: Size(350, 500), + center: true, + backgroundColor: Color(0x00000000), + skipTaskbar: false, + titleBarStyle: TitleBarStyle.normal, + title: '家庭网络监控', + ); + + await windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.show(); + await windowManager.focus(); + }); + + // 添加窗口监听器 + windowManager.addListener(this); + + // 阻止默认关闭行为 + await windowManager.setPreventClose(true); + + _isInitialized = true; + } + + /// 显示窗口 + Future showWindow() async { + if (!isSupported) return; + await windowManager.show(); + await windowManager.focus(); + } + + /// 隐藏窗口(最小化到托盘) + Future hideWindow() async { + if (!isSupported) return; + await windowManager.hide(); + } + + /// 最小化窗口 + Future minimizeWindow() async { + if (!isSupported) return; + await windowManager.minimize(); + } + + /// 关闭窗口 + Future closeWindow() async { + if (!isSupported) return; + await windowManager.destroy(); + } + + /// 设置窗口始终置顶 + Future setAlwaysOnTop(bool isAlwaysOnTop) async { + if (!isSupported) return; + await windowManager.setAlwaysOnTop(isAlwaysOnTop); + } + + /// 窗口关闭事件 + @override + void onWindowClose() { + onCloseRequested?.call(); + } + + /// 销毁 + void dispose() { + if (_isInitialized) { + windowManager.removeListener(this); + } + } +} + diff --git a/home_monitor/pubspec.yaml b/home_monitor/pubspec.yaml index 2677d7b..1297a66 100644 --- a/home_monitor/pubspec.yaml +++ b/home_monitor/pubspec.yaml @@ -80,10 +80,9 @@ flutter: # the material Icons class. uses-material-design: true - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg + # 资源文件 + assets: + - assets/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images