diff --git a/.metadata b/.metadata index 77d11fd..facf12b 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668" + revision: "c23637390482d4cf9598c3ce3f2be31aa7332daf" channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - - platform: windows - create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + - platform: web + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf # User provided section diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 755ea5e..33473a6 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'dart:io'; // 添加 dart:io 导入以使用 Platform 类 +import 'dart:io'; +import 'package:path/path.dart' as path; +import 'cof.dart'; class AppConfig { static final AppConfig _instance = AppConfig._internal(); - static late SharedPreferences _prefs; + static late ConfigManager _configManager; + static late String _configDirectory; factory AppConfig() { return _instance; @@ -14,112 +16,146 @@ class AppConfig { // 初始化配置 static Future init() async { - _prefs = await SharedPreferences.getInstance(); + // 获取可执行文件所在目录而不是当前工作目录 + _configDirectory = File(Platform.resolvedExecutable).parent.path; + final configPath = path.join(_configDirectory, 'config.yaml'); + + _configManager = ConfigManager( + filePath: configPath, + defaultConfig: { + 'theme': { + 'mode': 'system', + 'seedColor': Colors.blue.value, + }, + 'server': { + 'list': ['public.easytier.cn:11010'], + 'current': 'public.easytier.cn:11010', + }, + 'room': { + 'name': 'kevin', + 'password': 'kevin', + }, + 'user': { + 'name': Platform.localHostname, + }, + 'network': { + 'virtualIP': '', + 'dynamicIP': true, + }, + 'system': { + 'closeToTray': true, + }, + }, + ); + + await _configManager.load(); } // 主题设置 - static const String _keyThemeMode = 'themeMode'; ThemeMode get themeMode { - final String? value = _prefs.getString(_keyThemeMode); + final String? value = _configManager.get('theme.mode'); return ThemeMode.values.firstWhere( - (mode) => mode.toString() == value, + (mode) => mode.toString() == 'ThemeMode.$value', orElse: () => ThemeMode.system, ); } Future setThemeMode(ThemeMode mode) async { - await _prefs.setString(_keyThemeMode, mode.toString()); + final modeString = mode.toString().split('.').last.toLowerCase(); + _configManager.set('theme.mode', modeString); + await _configManager.save(); } // 主题色设置 - static const String _keySeedColor = 'seedColor'; Color get seedColor { - final int? value = _prefs.getInt(_keySeedColor); + final int? value = _configManager.get('theme.seedColor'); return value != null ? Color(value) : Colors.blue; } Future setSeedColor(Color color) async { - await _prefs.setInt(_keySeedColor, color.value); + _configManager.set('theme.seedColor', color.value); + await _configManager.save(); } // 服务器列表设置 - static const String _keyServerList = 'serverList'; List get serverList { - final List? value = _prefs.getStringList(_keyServerList); - return value?.isNotEmpty == true ? value! : ['public.easytier.net:11010']; + final List? value = _configManager.get('server.list'); + return value?.cast() ?? ['public.easytier.cn:11010']; } Future setServerList(List servers) async { - await _prefs.setStringList(_keyServerList, servers); + _configManager.set('server.list', servers); + await _configManager.save(); } // 当前选中的服务器设置 - static const String _keyCurrentServer = 'currentServer'; String get currentServer { - return _prefs.getString(_keyCurrentServer) ?? 'public.easytier.net:11010'; + return _configManager.get('server.current') ?? + 'public.easytier.cn:11010'; } Future setCurrentServer(String server) async { - await _prefs.setString(_keyCurrentServer, server); + _configManager.set('server.current', server); + await _configManager.save(); } // 房间名设置 - static const String _keyRoomName = 'roomName'; String get roomName { - return _prefs.getString(_keyRoomName) ?? 'kevin'; + return _configManager.get('room.name') ?? 'kevin'; } Future setRoomName(String name) async { - await _prefs.setString(_keyRoomName, name); + _configManager.set('room.name', name); + await _configManager.save(); } // 房间密码设置 - static const String _keyRoomPassword = 'roomPassword'; String get roomPassword { - return _prefs.getString(_keyRoomPassword) ?? 'kevin'; + return _configManager.get('room.password') ?? 'kevin'; } Future setRoomPassword(String password) async { - await _prefs.setString(_keyRoomPassword, password); + _configManager.set('room.password', password); + await _configManager.save(); } // 用户名设置 - static const String _keyUsername = 'username'; String get username { - return _prefs.getString(_keyUsername) ?? Platform.localHostname; + return _configManager.get('user.name') ?? Platform.localHostname; } Future setUsername(String name) async { - await _prefs.setString(_keyUsername, name); + _configManager.set('user.name', name); + await _configManager.save(); } // 虚拟IP设置 - static const String _keyVirtualIP = 'virtualIP'; String get virtualIP { - return _prefs.getString(_keyVirtualIP) ?? ''; - } - - // 关闭按钮进入托盘 - static const String _keyCloseToTray = 'closeToTray'; - bool get closeToTray { - return _prefs.getBool(_keyCloseToTray) ?? true; - } - - Future setCloseToTray(bool enabled) async { - await _prefs.setBool(_keyCloseToTray, enabled); + return _configManager.get('network.virtualIP') ?? ''; } Future setVirtualIP(String ip) async { - await _prefs.setString(_keyVirtualIP, ip); + _configManager.set('network.virtualIP', ip); + await _configManager.save(); + } + + // 关闭按钮进入托盘 + bool get closeToTray { + return _configManager.get('system.closeToTray') ?? true; + } + + Future setCloseToTray(bool enabled) async { + _configManager.set('system.closeToTray', enabled); + await _configManager.save(); } // 动态获取IP设置 - static const String _keyDynamicIP = 'dynamicIP'; bool get dynamicIP { - return _prefs.getBool(_keyDynamicIP) ?? true; + return _configManager.get('network.dynamicIP') ?? true; } Future setDynamicIP(bool enabled) async { - await _prefs.setBool(_keyDynamicIP, enabled); + _configManager.set('network.dynamicIP', enabled); + await _configManager.save(); } } diff --git a/lib/config/cof.dart b/lib/config/cof.dart new file mode 100644 index 0000000..bf9f461 --- /dev/null +++ b/lib/config/cof.dart @@ -0,0 +1,159 @@ +import 'dart:io'; +import 'package:yaml/yaml.dart'; + +class ConfigManager { + final String filePath; + final Map defaultConfig; + late Map _config; + + ConfigManager({ + required this.filePath, + required this.defaultConfig, + }) : _config = Map.from(defaultConfig); + + /// 加载配置文件(如果不存在则创建默认配置) + Future load() async { + final file = File(filePath); + + if (!await file.exists()) { + await _createDefaultConfig(); + return; + } + + try { + final content = await file.readAsString(); + final yamlMap = loadYaml(content); + _config = _mergeConfigs(_convertYaml(yamlMap)); + } catch (e) { + throw Exception('配置文件解析失败: $e'); + } + } + + /// 保存当前配置到文件 + Future save() async { + final yamlString = _generateYaml(); + await File(filePath).writeAsString(yamlString); + } + + /// 获取配置值(支持点分隔符访问嵌套字段) + T? get(String keyPath) { + final keys = keyPath.split('.'); + dynamic value = _config; + + for (final key in keys) { + if (value is Map && value.containsKey(key)) { + value = value[key]; + } else { + return null; + } + } + + return value is T ? value : null; + } + + /// 设置配置值(支持点分隔符访问嵌套字段) + void set(String keyPath, T value) { + final keys = keyPath.split('.'); + dynamic current = _config; + + for (int i = 0; i < keys.length - 1; i++) { + final key = keys[i]; + current = current.putIfAbsent(key, () => {}); + } + + current[keys.last] = value; + } + + /// 合并用户配置与默认配置 + Map _mergeConfigs(Map userConfig) { + return _deepMerge(defaultConfig, userConfig); + } + + /// 深度合并两个Map + Map _deepMerge( + Map base, Map override) { + final result = Map.from(base); + + override.forEach((key, value) { + if (value is Map && + result[key] is Map) { + result[key] = _deepMerge(result[key], value); + } else { + result[key] = value; + } + }); + + return result; + } + + /// 转换YAML结构为Dart Map + dynamic _convertYaml(dynamic yaml) { + if (yaml is YamlMap) { + return yaml.map((k, v) => MapEntry(k.toString(), _convertYaml(v))); + } + if (yaml is YamlList) { + return yaml.map((e) => _convertYaml(e)).toList(); + } + return yaml; + } + + /// 生成YAML字符串 + String _generateYaml() { + final buffer = StringBuffer(); + _writeMap(_config, buffer, 0); + return buffer.toString(); + } + + /// 递归写入Map结构 + void _writeMap(Map map, StringBuffer buffer, int indent) { + final prefix = ' ' * indent; + + map.forEach((key, value) { + buffer.write('$prefix$key: '); + + if (value is Map) { + buffer.writeln(); + _writeMap(value, buffer, indent + 1); + } else if (value is List) { + _writeList(value, buffer, indent + 1); + } else { + buffer.writeln(_formatValue(value)); + } + }); + } + + /// 处理列表值 + void _writeList(List list, StringBuffer buffer, int indent) { + final prefix = ' ' * indent; + + if (list.isEmpty) { + buffer.writeln('[]'); + return; + } + + buffer.writeln(); + for (final item in list) { + buffer.write('$prefix- '); + if (item is Map) { + _writeMap(item.cast(), buffer, indent + 1); + } else if (item is List) { + _writeList(item, buffer, indent + 1); + } else { + buffer.writeln(_formatValue(item)); + } + } + } + + /// 格式值处理 + String _formatValue(dynamic value) { + if (value is String) return '"$value"'; + if (value is bool) return value.toString(); + return value.toString(); + } + + /// 创建默认配置文件 + Future _createDefaultConfig() async { + _config = Map.from(defaultConfig); + await save(); + } +} diff --git a/lib/main.dart b/lib/main.dart index 8aab376..4395f05 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,13 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:ASTRAL/src/rust/frb_generated.dart'; +import 'package:astral/src/rust/frb_generated.dart'; import 'package:window_manager/window_manager.dart'; import 'app.dart'; import 'config/windowconfiguration.dart'; import 'config/app_config.dart'; import 'utils/kv_state.dart'; import 'package:provider/provider.dart'; // 添加这一行 -import 'package:tray_manager/tray_manager.dart'; -import 'package:ASTRAL/utils/app_info.dart'; +import 'package:astral/utils/app_info.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/screens/Home.dart b/lib/screens/Home.dart index 717c0ca..e8fc36e 100644 --- a/lib/screens/Home.dart +++ b/lib/screens/Home.dart @@ -33,6 +33,8 @@ class _MainScreenState extends State late List _pages; late List _navItems; late AnimationController _titleAnimationController; + // 添加一个key用于IndexedStack + final GlobalKey _indexedStackKey = GlobalKey(); @override void initState() { @@ -70,8 +72,16 @@ class _MainScreenState extends State currentThemeMode: widget.currentThemeMode, ); - // 预先创建所有页面 - _pages = _navItems.map((item) => item.pageBuilder()).toList(); + // 预先创建所有页面,并包装在AutomaticKeepAlive中 + _pages = _navItems.map((item) { + final page = item.pageBuilder(); + // 如果页面已经实现了AutomaticKeepAliveClientMixin,则不需要包装 + if (page is AutomaticKeepAliveClientMixin) { + return page; + } + // 否则包装在PageKeepAlive中 + return PageKeepAlive(child: page); + }).toList(); } // 将侧边栏构建方法整合到MainScreen类中 @@ -100,8 +110,9 @@ class _MainScreenState extends State // 设置一个阈值,当宽度大于此值时使用侧边栏 final bool useSidebar = screenWidth > 600; - // 创建一个共享的 IndexedStack 实例 + // 创建一个共享的 IndexedStack 实例,使用相同的key final indexedStack = IndexedStack( + key: _indexedStackKey, index: widget.currentIndex, children: _pages, ); @@ -207,3 +218,24 @@ class _MainScreenState extends State ); } } + +// 添加一个包装类来保持页面状态 +class PageKeepAlive extends StatefulWidget { + final Widget child; + + const PageKeepAlive({Key? key, required this.child}) : super(key: key); + + @override + State createState() => _PageKeepAliveState(); +} + +class _PageKeepAliveState extends State with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return widget.child; + } +} diff --git a/lib/screens/about.dart b/lib/screens/about.dart index 01aa2c1..8c20403 100644 --- a/lib/screens/about.dart +++ b/lib/screens/about.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import 'dart:math' as math; -import 'package:ASTRAL/utils/app_info.dart'; +import 'package:astral/utils/app_info.dart'; class InfoPage extends StatefulWidget { const InfoPage({super.key}); diff --git a/lib/screens/front.dart b/lib/screens/front.dart index bc0cb9e..e98deec 100644 --- a/lib/screens/front.dart +++ b/lib/screens/front.dart @@ -1,9 +1,9 @@ // 导入必要的包 import 'dart:convert'; -import 'package:ASTRAL/src/rust/api/simple.dart'; -import 'package:ASTRAL/utils/kv_state.dart'; -import 'package:ASTRAL/utils/app_info.dart'; +import 'package:astral/src/rust/api/simple.dart'; +import 'package:astral/utils/kv_state.dart'; +import 'package:astral/utils/app_info.dart'; import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'dart:async'; @@ -122,6 +122,9 @@ class _HomePageState extends State { severurl: Serverip); // 模拟连接过程,2秒后连接成功 Future.delayed(const Duration(seconds: 2), () { + // 检查组件是否仍然挂载 + if (!mounted) return; + if (isRunning) { // 确保用户没有在连接过程中取消 setState(() { @@ -129,6 +132,12 @@ class _HomePageState extends State { // 连接成功后开始计时 timer = Timer.periodic(const Duration(seconds: 1), (timer) async { + // 检查组件是否仍然挂载 + if (!mounted) { + timer.cancel(); + return; + } + final info = await getRunningInfo(); // 打印运行信息的详细内容 // print("运行信息详情:${info}"); @@ -153,10 +162,13 @@ class _HomePageState extends State { km.virtualIP = ipStr; } } - // print("- 用户名: ${info?.myNodeInfo?.hostname}"); - // print("- 虚拟IPv4: ${info?.myNodeInfo?.virtualIpv4?.address}"); - // print("- version: ${info?.myNodeInfo?.version}"); - // print("- 本地IP: ${info.myNodeInfo.}"); + + // 再次检查组件是否仍然挂载 + if (!mounted) { + timer.cancel(); + return; + } + setState(() { runningTime += const Duration(seconds: 1); }); @@ -203,6 +215,9 @@ class _HomePageState extends State { } } + // 再次检查挂载状态,确保在setState前组件仍然挂载 + if (!mounted) return; + // 计算速度 (字节/秒 转换为 MB/秒) setState(() { _uploadBytes = totalUploadBytes; @@ -228,26 +243,6 @@ class _HomePageState extends State { }); } - // 根据屏幕宽度计算列数 - int _getColumnCount(BuildContext context) { - final width = MediaQuery.of(context).size.width; - if (width < 600) { - return 1; // 手机屏幕显示1列 - } else if (width < 900) { - return 2; // 平板或小屏幕显示2列 - } else { - return 3; // 大屏幕显示3列 - } - } - - String _formatDuration(Duration duration) { - String twoDigits(int n) => n.toString().padLeft(2, '0'); - String hours = twoDigits(duration.inHours); - String minutes = twoDigits(duration.inMinutes.remainder(60)); - String seconds = twoDigits(duration.inSeconds.remainder(60)); - return '$hours:$minutes:$seconds'; - } - @override void didChangeDependencies() { super.didChangeDependencies(); @@ -282,31 +277,37 @@ class _HomePageState extends State { //我的用户名 username = Provider.of(context).username; Serverip = Provider.of(context).serverIP; - // 使用 SliverPadding 包裹 SliverList + + // 使用 LayoutBuilder 来处理布局变化,同时保留状态 return Scaffold( - body: CustomScrollView( - // 添加这个属性来控制滚动行为 - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - // 替换原有的 SliverList 为 SliverPadding + SliverGrid - SliverPadding( - padding: const EdgeInsets.all(16.0), - sliver: SliverMasonryGrid.count( - crossAxisCount: _getColumnCount(context), // 根据屏幕宽度动态设置列数 - mainAxisSpacing: 16, // 主轴间距 - crossAxisSpacing: 16, // 交叉轴间距 - childCount: _cardBuilders.length, - itemBuilder: (context, index) { - // 直接从列表中获取构建函数并调用 - return ConstrainedBox( - constraints: const BoxConstraints(minHeight: 100), - child: _cardBuilders[index](colorScheme), - ); - }, + body: LayoutBuilder(builder: (context, constraints) { + // 根据约束计算列数 + final columnCount = _getColumnCount(constraints.maxWidth); + + return CustomScrollView( + // 添加这个属性来控制滚动行为 + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + // 替换原有的 SliverList 为 SliverPadding + SliverGrid + SliverPadding( + padding: const EdgeInsets.all(16.0), + sliver: SliverMasonryGrid.count( + crossAxisCount: columnCount, // 使用计算出的列数 + mainAxisSpacing: 16, // 主轴间距 + crossAxisSpacing: 16, // 交叉轴间距 + childCount: _cardBuilders.length, + itemBuilder: (context, index) { + // 直接从列表中获取构建函数并调用 + return ConstrainedBox( + constraints: const BoxConstraints(minHeight: 100), + child: _cardBuilders[index](colorScheme), + ); + }, + ), ), - ), - ], - ), + ], + ); + }), floatingActionButton: AnimatedContainer( duration: const Duration(milliseconds: 200), curve: Curves.easeOutCubic, @@ -351,6 +352,25 @@ class _HomePageState extends State { ); } + // 修改为接受宽度参数,而不是使用 MediaQuery + int _getColumnCount(double width) { + if (width < 600) { + return 1; // 手机屏幕显示1列 + } else if (width < 900) { + return 2; // 平板或小屏幕显示2列 + } else { + return 3; // 大屏幕显示3列 + } + } + + String _formatDuration(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, '0'); + String hours = twoDigits(duration.inHours); + String minutes = twoDigits(duration.inMinutes.remainder(60)); + String seconds = twoDigits(duration.inSeconds.remainder(60)); + return '$hours:$minutes:$seconds'; + } + // 根据连接状态获取按钮图标 Widget _getButtonIcon(ConnectionState state) { switch (state) { @@ -920,3 +940,5 @@ Widget _buildVersionItem( ], ); } + + diff --git a/lib/screens/kvroom.dart b/lib/screens/kvroom.dart index f3eaa3b..253c493 100644 --- a/lib/screens/kvroom.dart +++ b/lib/screens/kvroom.dart @@ -1,13 +1,10 @@ // 导入必要的包 -import 'dart:convert'; import 'package:flutter/services.dart'; // 添加这一行导入剪贴板服务 -import 'package:ASTRAL/src/rust/api/simple.dart'; -import 'package:ASTRAL/utils/kv_state.dart'; +import 'package:astral/utils/kv_state.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../widgets/card.dart'; -import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; /// 玩家信息模型类 class PlayerInfo { @@ -48,8 +45,7 @@ class RoomPage extends StatefulWidget { class _RoomPageState extends State { List players = []; bool isLoading = true; - // 添加布局类型状态变量 - bool isGridLayout = true; // 默认使用网格布局 + // 移除布局类型状态变量 @override void initState() { @@ -64,18 +60,7 @@ class _RoomPageState extends State { return Scaffold( appBar: AppBar( title: const Text('房间成员'), - // 添加布局切换按钮 - actions: [ - IconButton( - icon: Icon(isGridLayout ? Icons.view_list : Icons.grid_view), - tooltip: isGridLayout ? '切换到列表视图' : '切换到网格视图', - onPressed: () { - setState(() { - isGridLayout = !isGridLayout; - }); - }, - ), - ], + // 移除布局切换按钮 ), body: Consumer( builder: (context, km, child) { @@ -138,29 +123,18 @@ class _RoomPageState extends State { slivers: [ SliverPadding( padding: const EdgeInsets.all(16.0), - sliver: isGridLayout - ? SliverMasonryGrid.count( - crossAxisCount: _getColumnCount(context), - mainAxisSpacing: 16, - crossAxisSpacing: 16, - childCount: players.length, - itemBuilder: (context, index) { - return _buildPlayerCard( - players[index], colorScheme); - }, - ) - : SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: _buildPlayerListItem( - players[index], colorScheme), - ); - }, - childCount: players.length, - ), - ), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: + _buildPlayerListItem(players[index], colorScheme), + ); + }, + childCount: players.length, + ), + ), ), ], ); @@ -201,8 +175,8 @@ class _RoomPageState extends State { // 如果有连接信息,计算网络统计数据 if (node.connections.isNotEmpty) { for (var conn in node.connections) { - uploadSpeed += (conn.txBytes as BigInt).toInt() ~/ 1024; // 转换为KB - downloadSpeed += (conn.rxBytes as BigInt).toInt() ~/ 1024; // 转换为KB + uploadSpeed += conn.txBytes.toInt() ~/ 1024; // 转换为KB + downloadSpeed += conn.rxBytes.toInt() ~/ 1024; // 转换为KB sentPackets += conn.txPackets.toInt(); receivedPackets += conn.rxPackets.toInt(); } @@ -221,7 +195,7 @@ class _RoomPageState extends State { PlayerInfo( name: node.hostname, ip: node.ipv4, // 临时IP,实际应从节点信息中获取 - latency: (node.latencyMs * 1000).toInt(), // 转换为毫秒 + latency: (node.latencyMs).toInt(), // 转换为毫秒 connectionType: connectionType, uploadSpeed: uploadSpeed, downloadSpeed: downloadSpeed, @@ -419,127 +393,359 @@ class _RoomPageState extends State { ); } -// 构建列表项视图 + // 构建列表项视图 Widget _buildPlayerListItem(PlayerInfo player, ColorScheme colorScheme) { // 根据延迟值确定颜色 Color latencyColor = _getLatencyColor(player.latency); // 根据连接类型选择图标 IconData connectionIcon = _getConnectionIcon(player.connectionType); + // 检测是否为小屏幕设备 + final isSmallScreen = MediaQuery.of(context).size.width < 600; + return FloatingCard( colorScheme: colorScheme, maxWidth: double.infinity, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // 左侧玩家信息 - Expanded( - flex: 3, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 玩家名称和连接类型 - Row( - children: [ - Icon(Icons.person, color: colorScheme.primary, size: 22), - const SizedBox(width: 8), - Expanded( - child: Text( - player.name, - style: const TextStyle( - fontSize: 18, fontWeight: FontWeight.bold), - overflow: TextOverflow.ellipsis, - ), + child: isSmallScreen + ? _buildMobilePlayerListItem( + player, colorScheme, latencyColor, connectionIcon) + : _buildDesktopPlayerListItem( + player, colorScheme, latencyColor, connectionIcon), + ); + } + + // 为移动设备优化的列表项布局 + Widget _buildMobilePlayerListItem(PlayerInfo player, ColorScheme colorScheme, + Color latencyColor, IconData connectionIcon) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 玩家名称和连接类型 + Row( + children: [ + Icon(Icons.person, color: colorScheme.primary, size: 22), + const SizedBox(width: 8), + Expanded( + child: Text( + player.name, + style: + const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: + _getConnectionTypeColor(player.connectionType, colorScheme), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + connectionIcon, + size: 14, + color: Colors.white, + ), + const SizedBox(width: 4), + Text( + player.connectionType, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, ), - ], - ), - const SizedBox(height: 8), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + + // IP地址 + _buildInfoRow( + Icons.lan, + 'IP地址', + player.ip, + colorScheme, + showCopyButton: true, + ), + const SizedBox(height: 8), + + // 延迟信息 + _buildInfoRow( + Icons.speed, + '延迟', + '${player.latency} ms', + colorScheme, + valueColor: latencyColor, + ), + const SizedBox(height: 8), + + // ET版本 + _buildInfoRow( + Icons.memory, + 'ET版本', + player.etVersion, + colorScheme, + ), + const SizedBox(height: 8), + + // 丢包率信息 + _buildInfoRow( + Icons.error_outline, + '丢包率', + '${player.packetLossRate}%', + colorScheme, + valueColor: _getPacketLossColor(player.packetLossRate), + ), - // IP地址 - _buildInfoRow( - Icons.lan, - 'IP地址', - player.ip, - colorScheme, - showCopyButton: true, - ), - const SizedBox(height: 8), + // 网络数据部分 + const Divider(height: 16), - // ET版本 - _buildInfoRow( - Icons.memory, - 'ET版本', - player.etVersion, - colorScheme, - ), - ], - ), + // 网络数据信息 - 移动设备上使用紧凑布局 + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: _buildNetworkDataItem( + '上传', + '${player.uploadSpeed} KB/s', + Icons.upload, + colorScheme.primary, + ), + ), + Expanded( + child: _buildNetworkDataItem( + '下载', + '${player.downloadSpeed} KB/s', + Icons.download, + colorScheme.secondary, + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: _buildNetworkDataItem( + '发送包', + '${player.sentPackets}', + Icons.send, + colorScheme.primary, + ), + ), + Expanded( + child: _buildNetworkDataItem( + '接收包', + '${player.receivedPackets}', + Icons.call_received, + colorScheme.secondary, + ), + ), + ], + ), + ], ), + ), + ], + ); + } - // 右侧网络状态信息 - Expanded( - flex: 2, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 连接类型标签 - Container( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: _getConnectionTypeColor( - player.connectionType, colorScheme), - borderRadius: BorderRadius.circular(12), + // 为桌面设备优化的列表项布局 + Widget _buildDesktopPlayerListItem(PlayerInfo player, ColorScheme colorScheme, + Color latencyColor, IconData connectionIcon) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 左侧玩家基本信息 + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 玩家名称和连接类型 + Row( + children: [ + Icon(Icons.person, color: colorScheme.primary, size: 22), + const SizedBox(width: 8), + Expanded( + child: Text( + player.name, + style: const TextStyle( + fontSize: 18, fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - connectionIcon, - size: 14, - color: Colors.white, - ), - const SizedBox(width: 4), - Text( - player.connectionType, - style: const TextStyle( + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: _getConnectionTypeColor( + player.connectionType, colorScheme), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + connectionIcon, + size: 14, color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.bold, + ), + const SizedBox(width: 4), + Text( + player.connectionType, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), ), - ), - ], + ], + ), + ), + ], + ), + const SizedBox(height: 12), + + // IP地址 + _buildInfoRow( + Icons.lan, + 'IP地址', + player.ip, + colorScheme, + showCopyButton: true, + ), + const SizedBox(height: 8), + + // ET版本 + _buildInfoRow( + Icons.memory, + 'ET版本', + player.etVersion, + colorScheme, + ), + ], + ), + ), + + // 中间网络状态信息 + Expanded( + flex: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 延迟信息 + _buildInfoRow( + Icons.speed, + '延迟', + '${player.latency} ms', + colorScheme, + valueColor: latencyColor, + ), + const SizedBox(height: 8), + + // 丢包率信息 + _buildInfoRow( + Icons.error_outline, + '丢包率', + '${player.packetLossRate}%', + colorScheme, + valueColor: _getPacketLossColor(player.packetLossRate), + ), + const SizedBox(height: 8), + + // 上传下载速度 + Row( + children: [ + Expanded( + child: _buildNetworkDataItem( + '上传', + '${player.uploadSpeed} KB/s', + Icons.upload, + colorScheme.primary, + ), + ), + Expanded( + child: _buildNetworkDataItem( + '下载', + '${player.downloadSpeed} KB/s', + Icons.download, + colorScheme.secondary, + ), ), - ), - const SizedBox(height: 8), + ], + ), + ], + ), + ), - // 延迟信息 - _buildInfoRow( - Icons.speed, - '延迟', - '${player.latency} ms', - colorScheme, - valueColor: latencyColor, - ), - const SizedBox(height: 8), + // 右侧包数据信息 + Expanded( + flex: 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildNetworkDataItem( + '发送包', + '${player.sentPackets}', + Icons.send, + colorScheme.primary, + ), + const SizedBox(height: 8), + _buildNetworkDataItem( + '接收包', + '${player.receivedPackets}', + Icons.call_received, + colorScheme.secondary, + ), + ], + ), + ), + ], + ); + } - // 丢包率信息 - _buildInfoRow( - Icons.error_outline, - '丢包率', - '${player.packetLossRate}%', - colorScheme, - valueColor: _getPacketLossColor(player.packetLossRate), - ), - ], + // 更紧凑的网络数据项 + Widget _buildNetworkDataItem( + String label, + String value, + IconData icon, + Color color, + ) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 18, color: color), + const SizedBox(width: 4), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle(fontSize: 12), ), - ), - ], - ), + Text( + value, + style: TextStyle( + fontWeight: FontWeight.bold, + color: color, + fontSize: 13, + ), + ), + ], + ), + ], ); } - // 构建信息行 // 构建信息行 Widget _buildInfoRow( IconData icon, diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index f1c4260..3be3631 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import '../config/app_config.dart'; import '../utils/ping_util.dart'; -import 'package:ASTRAL/utils/kv_state.dart'; +import 'package:astral/utils/kv_state.dart'; import 'package:provider/provider.dart'; class SettingsPage extends StatefulWidget { @@ -35,37 +35,34 @@ class _SettingsPageState extends State { // 初始化 ping 状态 for (var server in _serverList) { pingResults[server] = null; - isPinging[server] = false; + isPinging[server] = true; // 默认所有服务器都开启ping } - // 开始 ping 当前服务器,并设置为持续 ping - _startPingServer(_currentServer, forceContinuous: true); + // 开始 ping 所有服务器 + for (var server in _serverList) { + _pingServer(server); + } } @override void dispose() { // 停止所有 ping for (var server in _serverList) { - _stopPingServer(server); + isPinging[server] = false; } super.dispose(); } - // 修改开始 ping 方法,添加强制持续 ping 参数 - void _startPingServer(String server, {bool forceContinuous = false}) { + // 简化 ping 方法,移除强制持续 ping 参数 + void _startPingServer(String server) { if (isPinging[server] == true) return; isPinging[server] = true; - if (forceContinuous) { - isPinging[server] = true; // 设置为持续 ping 状态 - } _pingServer(server); } - // 修改停止 ping 方法 + // 修改停止 ping 方法 - 实际上不再需要,但保留以防将来需要 void _stopPingServer(String server) { - // 如果是当前服务器,不允许停止 - if (server == _currentServer) return; isPinging[server] = false; } @@ -119,9 +116,10 @@ class _SettingsPageState extends State { _serverList.add(result); _appConfig.setServerList(_serverList); - // 初始化新服务器的 ping 状态 + // 初始化新服务器的 ping 状态并立即开始 ping pingResults[result] = null; - isPinging[result] = false; + isPinging[result] = true; + _pingServer(result); }); } } @@ -207,21 +205,18 @@ class _SettingsPageState extends State { // 添加构建 ping 显示组件的方法 Widget _buildPingWidget(String server) { final pingResult = pingResults[server]; - if (server == _currentServer || isPinging[server] == true) { - if (pingResult == null) { - return const Text('测试中...', style: TextStyle(color: Colors.grey)); - } else { - return Text( - '${pingResult}ms', - style: TextStyle( - color: pingResult < 100 - ? Colors.green - : (pingResult < 300 ? Colors.orange : Colors.red), - ), - ); - } + if (pingResult == null) { + return const Text('测试中...', style: TextStyle(color: Colors.grey)); + } else { + return Text( + '${pingResult}ms', + style: TextStyle( + color: pingResult < 100 + ? Colors.green + : (pingResult < 300 ? Colors.orange : Colors.red), + ), + ); } - return const Text('点击测试', style: TextStyle(color: Colors.grey)); } @override @@ -248,22 +243,7 @@ class _SettingsPageState extends State { leading: const Icon(Icons.list), title: const Text('服务器列表'), onExpansionChanged: (expanded) { - // 展开/折叠时处理其他服务器的 ping 状态 - setState(() { - for (var server in _serverList) { - if (server != _currentServer) { - if (expanded) { - // 如果之前是手动开启的,则恢复 ping - if (isPinging[server] == true) { - _startPingServer(server); - } - } else { - // 折叠时暂停所有非当前服务器的 ping - _stopPingServer(server); - } - } - } - }); + // 不再需要处理展开折叠时的 ping 状态,所有服务器都持续 ping }, children: [ // 在服务器列表前添加当前服务器的 ping 状态显示 @@ -277,22 +257,17 @@ class _SettingsPageState extends State { // 构建延迟显示组件 Widget pingWidget; - if (isPinging[server] == true) { - if (pingResult == null) { - pingWidget = const Text('测试中...', - style: TextStyle(color: Colors.grey)); - } else { - pingWidget = Text('${pingResult}ms', - style: TextStyle( - color: pingResult < 100 - ? Colors.green - : (pingResult < 300 - ? Colors.orange - : Colors.red))); - } - } else { - pingWidget = const Text('点击测试', + if (pingResult == null) { + pingWidget = const Text('测试中...', style: TextStyle(color: Colors.grey)); + } else { + pingWidget = Text('${pingResult}ms', + style: TextStyle( + color: pingResult < 100 + ? Colors.green + : (pingResult < 300 + ? Colors.orange + : Colors.red))); } return ListTile( @@ -308,26 +283,7 @@ class _SettingsPageState extends State { trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - // 添加 ping 按钮 - IconButton( - icon: Icon( - isPinging[server] == true - ? Icons.pause - : Icons.play_arrow, - color: isPinging[server] == true - ? Colors.blue - : null, - ), - onPressed: () { - setState(() { - if (isPinging[server] == true) { - _stopPingServer(server); - } else { - _startPingServer(server); - } - }); - }, - ), + // 移除 ping 按钮,只保留编辑和删除按钮 IconButton( icon: const Icon(Icons.edit), onPressed: () => _showEditServerDialog(index), @@ -343,8 +299,7 @@ class _SettingsPageState extends State { _currentServer = server; Provider.of(context, listen: false).serverIP = server; - // 开始 ping 新选择的服务器 - _startPingServer(server); + // 不再需要特别开始 ping 新选择的服务器,因为所有服务器都在 ping }); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('已切换到服务器: $server')), @@ -364,7 +319,7 @@ class _SettingsPageState extends State { ), ), const SizedBox(height: 16), - + // 添加应用设置卡片 Card( child: Column( @@ -387,7 +342,7 @@ class _SettingsPageState extends State { ], ), ), - + const SizedBox(height: 16), Card( child: Column( diff --git a/lib/src/rust/api/simple.dart b/lib/src/rust/api/simple.dart index 9d03672..5d71124 100644 --- a/lib/src/rust/api/simple.dart +++ b/lib/src/rust/api/simple.dart @@ -123,6 +123,7 @@ class KVNodeInfo { final String hostname; final String ipv4; final double latencyMs; + final String nat; final List connections; final String version; final int cost; @@ -131,6 +132,7 @@ class KVNodeInfo { required this.hostname, required this.ipv4, required this.latencyMs, + required this.nat, required this.connections, required this.version, required this.cost, @@ -141,6 +143,7 @@ class KVNodeInfo { hostname.hashCode ^ ipv4.hashCode ^ latencyMs.hashCode ^ + nat.hashCode ^ connections.hashCode ^ version.hashCode ^ cost.hashCode; @@ -153,6 +156,7 @@ class KVNodeInfo { hostname == other.hostname && ipv4 == other.ipv4 && latencyMs == other.latencyMs && + nat == other.nat && connections == other.connections && version == other.version && cost == other.cost; diff --git a/lib/src/rust/frb_generated.dart b/lib/src/rust/frb_generated.dart index c702b04..a11ad19 100644 --- a/lib/src/rust/frb_generated.dart +++ b/lib/src/rust/frb_generated.dart @@ -558,15 +558,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { KVNodeInfo dco_decode_kv_node_info(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 6) - throw Exception('unexpected arr length: expect 6 but see ${arr.length}'); + if (arr.length != 7) + throw Exception('unexpected arr length: expect 7 but see ${arr.length}'); return KVNodeInfo( hostname: dco_decode_String(arr[0]), ipv4: dco_decode_String(arr[1]), latencyMs: dco_decode_f_64(arr[2]), - connections: dco_decode_list_kv_node_connection_stats(arr[3]), - version: dco_decode_String(arr[4]), - cost: dco_decode_i_32(arr[5]), + nat: dco_decode_String(arr[3]), + connections: dco_decode_list_kv_node_connection_stats(arr[4]), + version: dco_decode_String(arr[5]), + cost: dco_decode_i_32(arr[6]), ); } @@ -795,6 +796,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_hostname = sse_decode_String(deserializer); var var_ipv4 = sse_decode_String(deserializer); var var_latencyMs = sse_decode_f_64(deserializer); + var var_nat = sse_decode_String(deserializer); var var_connections = sse_decode_list_kv_node_connection_stats(deserializer); var var_version = sse_decode_String(deserializer); @@ -803,6 +805,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { hostname: var_hostname, ipv4: var_ipv4, latencyMs: var_latencyMs, + nat: var_nat, connections: var_connections, version: var_version, cost: var_cost); @@ -1050,6 +1053,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(self.hostname, serializer); sse_encode_String(self.ipv4, serializer); sse_encode_f_64(self.latencyMs, serializer); + sse_encode_String(self.nat, serializer); sse_encode_list_kv_node_connection_stats(self.connections, serializer); sse_encode_String(self.version, serializer); sse_encode_i_32(self.cost, serializer); diff --git a/lib/src/rust/frb_generated.io.dart b/lib/src/rust/frb_generated.io.dart index e516831..fd2c21d 100644 --- a/lib/src/rust/frb_generated.io.dart +++ b/lib/src/rust/frb_generated.io.dart @@ -375,7 +375,7 @@ class RustLibWire implements BaseWire { late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfoPtr = _lookup)>>( - 'frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo'); + 'frbgen_astral_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo'); late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo = _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfoPtr .asFunction)>(); @@ -391,7 +391,7 @@ class RustLibWire implements BaseWire { late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfoPtr = _lookup)>>( - 'frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo'); + 'frbgen_astral_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo'); late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo = _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfoPtr .asFunction)>(); @@ -407,7 +407,7 @@ class RustLibWire implements BaseWire { late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfoPtr = _lookup)>>( - 'frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo'); + 'frbgen_astral_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo'); late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo = _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfoPtr .asFunction)>(); @@ -423,7 +423,7 @@ class RustLibWire implements BaseWire { late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfoPtr = _lookup)>>( - 'frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo'); + 'frbgen_astral_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo'); late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo = _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfoPtr .asFunction)>(); @@ -439,7 +439,7 @@ class RustLibWire implements BaseWire { late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePairPtr = _lookup)>>( - 'frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair'); + 'frbgen_astral_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair'); late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair = _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePairPtr .asFunction)>(); @@ -455,7 +455,7 @@ class RustLibWire implements BaseWire { late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePairPtr = _lookup)>>( - 'frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair'); + 'frbgen_astral_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair'); late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair = _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePairPtr .asFunction)>(); @@ -471,7 +471,7 @@ class RustLibWire implements BaseWire { late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoutePtr = _lookup)>>( - 'frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute'); + 'frbgen_astral_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute'); late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute = _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoutePtr .asFunction)>(); @@ -487,7 +487,7 @@ class RustLibWire implements BaseWire { late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoutePtr = _lookup)>>( - 'frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute'); + 'frbgen_astral_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute'); late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute = _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoutePtr .asFunction)>(); diff --git a/lib/utils/kv_state.dart b/lib/utils/kv_state.dart index 167b9d2..f444571 100644 --- a/lib/utils/kv_state.dart +++ b/lib/utils/kv_state.dart @@ -1,4 +1,4 @@ -import 'package:ASTRAL/src/rust/api/simple.dart'; +import 'package:astral/src/rust/api/simple.dart'; import 'package:flutter/foundation.dart'; import '../config/app_config.dart'; diff --git a/lib/代办 b/lib/代办 new file mode 100644 index 0000000..84a6f20 --- /dev/null +++ b/lib/代办 @@ -0,0 +1,11 @@ +[ ] 更换为官方服务器 +[ ] 添加服务器多选 +[ ] 移除模拟延迟(对其实不用等待2秒的那个是模拟的我给忘了)->改为检测是否成功连接 +[ ] 修复延迟计算:从peer连接获取最小延迟(μs->ms),无效则用路由延迟 +[x] 管理员问题没权限 +[ ] 配置信息改为运行目录 +[ ] 看nat类型 +[x] 缩放尺寸让导航栏变为底部会导致页面状态丢失 +[x] 服务器始终检测延迟,去除暂停和开始反正也不怎么影响性能多此一举还增加复杂度😜 +[x] 增加自动更新检测 和自动更新 +[ ] 那就打包两个版本 一个便携版(配置文件随软件) 一个安装版(配置文件随软件) \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 414cbaa..4c931ff 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -229,10 +229,10 @@ packages: dependency: "direct main" description: name: fl_chart - sha256: c1e26c7e48496be85104c16c040950b0436674cdf0737f3f6e95511b2529b592 + sha256: "5276944c6ffc975ae796569a826c38a62d2abcf264e26b88fa6f482e107f4237" url: "https://pub.flutter-io.cn" source: hosted - version: "0.63.0" + version: "0.70.2" flutter: dependency: "direct main" description: flutter @@ -255,10 +255,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" url: "https://pub.flutter-io.cn" source: hosted - version: "4.0.0" + version: "5.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -416,10 +416,10 @@ packages: dependency: transitive description: name: lints - sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 url: "https://pub.flutter-io.cn" source: hosted - version: "4.0.0" + version: "5.1.1" logging: dependency: transitive description: @@ -985,7 +985,7 @@ packages: source: hosted version: "1.1.0" yaml: - dependency: transitive + dependency: "direct main" description: name: yaml sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce diff --git a/pubspec.yaml b/pubspec.yaml index d7a74ad..ecd6c76 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,4 +1,4 @@ -name: ASTRAL +name: astral description: "ASTRAL" # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. @@ -42,13 +42,14 @@ dependencies: flutter_rust_bridge: 2.9.0 flutter_colorpicker: ^1.1.0 window_manager: ^0.4.3 - fl_chart: ^0.63.0 + fl_chart: ^0.70.2 flutter_staggered_grid_view: ^0.7.0 json_annotation: ^4.8.1 tray_manager: ^0.3.2 system_tray: ^2.0.3 url_launcher: ^6.3.1 package_info_plus: ^8.3.0 # 添加这一行 + yaml: ^3.1.2 dev_dependencies: flutter_test: @@ -60,7 +61,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^4.0.0 + flutter_lints: ^5.0.0 integration_test: sdk: flutter @@ -70,7 +71,7 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: assets: - - assets/dlls/ + # - assets/dlls/ - assets/icon.ico fonts: diff --git a/rust/src/api/simple.rs b/rust/src/api/simple.rs index 76054a5..c31b011 100644 --- a/rust/src/api/simple.rs +++ b/rust/src/api/simple.rs @@ -280,7 +280,7 @@ pub fn is_easytier_running() -> bool { // 定义节点连接统计信息结构体 pub struct KVNodeConnectionStats { - pub conn_type: String, + pub conn_type: String, // 连接类型 pub rx_bytes: u64, pub tx_bytes: u64, pub rx_packets: u64, @@ -291,6 +291,7 @@ pub struct KVNodeInfo { pub hostname: String, pub ipv4: String, pub latency_ms: f64, + pub nat: String, // NAT类型 pub connections: Vec, pub version: String, pub cost: i32, @@ -328,7 +329,33 @@ pub fn get_network_status() -> KVNetworkStatus { let mut node_info = KVNodeInfo { hostname: route.hostname.clone(), ipv4, - latency_ms: f64::from(route.path_latency.max(0)) / 1000.0, + latency_ms: if let Some(peer) = &pair.peer { + // 类似cli.rs中的get_latency_ms方法 + let mut min_latency = u64::MAX; + for conn in &peer.conns { + if let Some(stats) = &conn.stats { + min_latency = min_latency.min(stats.latency_us); + } + } + if min_latency == u64::MAX { + // 如果没有找到有效的连接延迟,则使用路由路径延迟 + f64::from(route.path_latency.max(0)) / 1000.0 + } else { + // 将微秒转换为毫秒 + f64::from(min_latency as u32) / 1000.0 + } + } else { + // 如果没有peer信息,则使用路由路径延迟 + f64::from(route.path_latency.max(0)) / 1000.0 + }, + nat: route.stun_info.as_ref().map_or_else( + || "Unknown".to_string(), + |stun| { + // 使用NatType枚举替代直接匹配数字 + let nat_type = NatType::try_from(stun.udp_nat_type).unwrap_or(NatType::Unknown); + format!("{:?}", nat_type) + } + ), connections: Vec::new(), version: route.version.clone(), cost, diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 2e64d15..1ab7b8a 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -546,6 +546,7 @@ impl SseDecode for crate::api::simple::KVNodeInfo { let mut var_hostname = ::sse_decode(deserializer); let mut var_ipv4 = ::sse_decode(deserializer); let mut var_latencyMs = ::sse_decode(deserializer); + let mut var_nat = ::sse_decode(deserializer); let mut var_connections = >::sse_decode(deserializer); let mut var_version = ::sse_decode(deserializer); @@ -554,6 +555,7 @@ impl SseDecode for crate::api::simple::KVNodeInfo { hostname: var_hostname, ipv4: var_ipv4, latency_ms: var_latencyMs, + nat: var_nat, connections: var_connections, version: var_version, cost: var_cost, @@ -819,6 +821,7 @@ impl flutter_rust_bridge::IntoDart for crate::api::simple::KVNodeInfo { self.hostname.into_into_dart().into_dart(), self.ipv4.into_into_dart().into_dart(), self.latency_ms.into_into_dart().into_dart(), + self.nat.into_into_dart().into_dart(), self.connections.into_into_dart().into_dart(), self.version.into_into_dart().into_dart(), self.cost.into_into_dart().into_dart(), @@ -964,6 +967,7 @@ impl SseEncode for crate::api::simple::KVNodeInfo { ::sse_encode(self.hostname, serializer); ::sse_encode(self.ipv4, serializer); ::sse_encode(self.latency_ms, serializer); + ::sse_encode(self.nat, serializer); >::sse_encode(self.connections, serializer); ::sse_encode(self.version, serializer); ::sse_encode(self.cost, serializer); @@ -1087,56 +1091,56 @@ mod io { flutter_rust_bridge::frb_generated_boilerplate_io!(); #[unsafe(no_mangle)] - pub extern "C" fn frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + pub extern "C" fn frbgen_astral_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( ptr: *const std::ffi::c_void, ) { MoiArc::>::increment_strong_count(ptr as _); } #[unsafe(no_mangle)] - pub extern "C" fn frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + pub extern "C" fn frbgen_astral_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( ptr: *const std::ffi::c_void, ) { MoiArc::>::decrement_strong_count(ptr as _); } #[unsafe(no_mangle)] - pub extern "C" fn frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + pub extern "C" fn frbgen_astral_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( ptr: *const std::ffi::c_void, ) { MoiArc::>::increment_strong_count(ptr as _); } #[unsafe(no_mangle)] - pub extern "C" fn frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + pub extern "C" fn frbgen_astral_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( ptr: *const std::ffi::c_void, ) { MoiArc::>::decrement_strong_count(ptr as _); } #[unsafe(no_mangle)] - pub extern "C" fn frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + pub extern "C" fn frbgen_astral_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( ptr: *const std::ffi::c_void, ) { MoiArc::>::increment_strong_count(ptr as _); } #[unsafe(no_mangle)] - pub extern "C" fn frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + pub extern "C" fn frbgen_astral_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( ptr: *const std::ffi::c_void, ) { MoiArc::>::decrement_strong_count(ptr as _); } #[unsafe(no_mangle)] - pub extern "C" fn frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + pub extern "C" fn frbgen_astral_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( ptr: *const std::ffi::c_void, ) { MoiArc::>::increment_strong_count(ptr as _); } #[unsafe(no_mangle)] - pub extern "C" fn frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + pub extern "C" fn frbgen_astral_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( ptr: *const std::ffi::c_void, ) { MoiArc::>::decrement_strong_count(ptr as _); diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..dbd890c --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:astral/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..b0e834b --- /dev/null +++ b/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + astral + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..56decc4 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "astral", + "short_name": "astral", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 650f187..ca97fa1 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -75,6 +75,11 @@ set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) +# 添加管理员权限要求(修正路径格式) +target_link_options(${BINARY_NAME} PRIVATE + "LINKER:/MANIFESTUAC:level='requireAdministrator' uiAccess='false'" + $<$:"LINKER:/MANIFESTINPUT:${CMAKE_CURRENT_SOURCE_DIR}/runner/runner.exe.manifest">) + install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt index 394917c..f686f62 100644 --- a/windows/runner/CMakeLists.txt +++ b/windows/runner/CMakeLists.txt @@ -33,6 +33,14 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") # Add dependency libraries and include directories. Add any application-specific # dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +# 添加资源复制指令到可执行目标所在目录 +add_custom_command(TARGET ${BINARY_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_CURRENT_SOURCE_DIR}/../../assets/dlls" + "${CMAKE_INSTALL_PREFIX}/" + COMMENT "Copying DLL files to output directory" + VERBATIM +) target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest index 153653e..7bf9e03 100644 --- a/windows/runner/runner.exe.manifest +++ b/windows/runner/runner.exe.manifest @@ -1,5 +1,12 @@ + + + + + + + PerMonitorV2