This commit is contained in:
会做饭的二哈
2025-03-16 17:53:06 +08:00
parent 769dab91f0
commit 45b6d939da
29 changed files with 949 additions and 366 deletions
+6 -6
View File
@@ -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
+79 -43
View File
@@ -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<void> 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<String>('theme.mode');
return ThemeMode.values.firstWhere(
(mode) => mode.toString() == value,
(mode) => mode.toString() == 'ThemeMode.$value',
orElse: () => ThemeMode.system,
);
}
Future<void> 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<int>('theme.seedColor');
return value != null ? Color(value) : Colors.blue;
}
Future<void> 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<String> get serverList {
final List<String>? value = _prefs.getStringList(_keyServerList);
return value?.isNotEmpty == true ? value! : ['public.easytier.net:11010'];
final List? value = _configManager.get<List>('server.list');
return value?.cast<String>() ?? ['public.easytier.cn:11010'];
}
Future<void> setServerList(List<String> 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<String>('server.current') ??
'public.easytier.cn:11010';
}
Future<void> 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<String>('room.name') ?? 'kevin';
}
Future<void> 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<String>('room.password') ?? 'kevin';
}
Future<void> 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<String>('user.name') ?? Platform.localHostname;
}
Future<void> 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<void> setCloseToTray(bool enabled) async {
await _prefs.setBool(_keyCloseToTray, enabled);
return _configManager.get<String>('network.virtualIP') ?? '';
}
Future<void> setVirtualIP(String ip) async {
await _prefs.setString(_keyVirtualIP, ip);
_configManager.set('network.virtualIP', ip);
await _configManager.save();
}
// 关闭按钮进入托盘
bool get closeToTray {
return _configManager.get<bool>('system.closeToTray') ?? true;
}
Future<void> 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<bool>('network.dynamicIP') ?? true;
}
Future<void> setDynamicIP(bool enabled) async {
await _prefs.setBool(_keyDynamicIP, enabled);
_configManager.set('network.dynamicIP', enabled);
await _configManager.save();
}
}
+159
View File
@@ -0,0 +1,159 @@
import 'dart:io';
import 'package:yaml/yaml.dart';
class ConfigManager {
final String filePath;
final Map<String, dynamic> defaultConfig;
late Map<String, dynamic> _config;
ConfigManager({
required this.filePath,
required this.defaultConfig,
}) : _config = Map.from(defaultConfig);
/// 加载配置文件(如果不存在则创建默认配置)
Future<void> 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<void> save() async {
final yamlString = _generateYaml();
await File(filePath).writeAsString(yamlString);
}
/// 获取配置值(支持点分隔符访问嵌套字段)
T? get<T>(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<T>(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, () => <String, dynamic>{});
}
current[keys.last] = value;
}
/// 合并用户配置与默认配置
Map<String, dynamic> _mergeConfigs(Map<String, dynamic> userConfig) {
return _deepMerge(defaultConfig, userConfig);
}
/// 深度合并两个Map
Map<String, dynamic> _deepMerge(
Map<String, dynamic> base, Map<String, dynamic> override) {
final result = Map<String, dynamic>.from(base);
override.forEach((key, value) {
if (value is Map<String, dynamic> &&
result[key] is Map<String, dynamic>) {
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<String, dynamic> map, StringBuffer buffer, int indent) {
final prefix = ' ' * indent;
map.forEach((key, value) {
buffer.write('$prefix$key: ');
if (value is Map<String, dynamic>) {
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<String, dynamic>(), 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<void> _createDefaultConfig() async {
_config = Map.from(defaultConfig);
await save();
}
}
+2 -3
View File
@@ -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<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
+35 -3
View File
@@ -33,6 +33,8 @@ class _MainScreenState extends State<MainScreen>
late List<Widget> _pages;
late List<NavItem> _navItems;
late AnimationController _titleAnimationController;
// 添加一个key用于IndexedStack
final GlobalKey _indexedStackKey = GlobalKey();
@override
void initState() {
@@ -70,8 +72,16 @@ class _MainScreenState extends State<MainScreen>
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<MainScreen>
// 设置一个阈值,当宽度大于此值时使用侧边栏
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<MainScreen>
);
}
}
// 添加一个包装类来保持页面状态
class PageKeepAlive extends StatefulWidget {
final Widget child;
const PageKeepAlive({Key? key, required this.child}) : super(key: key);
@override
State<PageKeepAlive> createState() => _PageKeepAliveState();
}
class _PageKeepAliveState extends State<PageKeepAlive> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
}
+1 -1
View File
@@ -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});
+53 -31
View File
@@ -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<HomePage> {
severurl: Serverip);
// 模拟连接过程,2秒后连接成功
Future.delayed(const Duration(seconds: 2), () {
// 检查组件是否仍然挂载
if (!mounted) return;
if (isRunning) {
// 确保用户没有在连接过程中取消
setState(() {
@@ -129,6 +132,12 @@ class _HomePageState extends State<HomePage> {
// 连接成功后开始计时
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<HomePage> {
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<HomePage> {
}
}
// 再次检查挂载状态,确保在setState前组件仍然挂载
if (!mounted) return;
// 计算速度 (字节/秒 转换为 MB/秒)
setState(() {
_uploadBytes = totalUploadBytes;
@@ -228,26 +243,6 @@ class _HomePageState extends State<HomePage> {
});
}
// 根据屏幕宽度计算列数
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,9 +277,14 @@ class _HomePageState extends State<HomePage> {
//我的用户名
username = Provider.of<KM>(context).username;
Serverip = Provider.of<KM>(context).serverIP;
// 使用 SliverPadding 包裹 SliverList
// 使用 LayoutBuilder 来处理布局变化,同时保留状态
return Scaffold(
body: CustomScrollView(
body: LayoutBuilder(builder: (context, constraints) {
// 根据约束计算列数
final columnCount = _getColumnCount(constraints.maxWidth);
return CustomScrollView(
// 添加这个属性来控制滚动行为
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
@@ -292,7 +292,7 @@ class _HomePageState extends State<HomePage> {
SliverPadding(
padding: const EdgeInsets.all(16.0),
sliver: SliverMasonryGrid.count(
crossAxisCount: _getColumnCount(context), // 根据屏幕宽度动态设置列数
crossAxisCount: columnCount, // 使用计算出的列数
mainAxisSpacing: 16, // 主轴间距
crossAxisSpacing: 16, // 交叉轴间距
childCount: _cardBuilders.length,
@@ -306,7 +306,8 @@ class _HomePageState extends State<HomePage> {
),
),
],
),
);
}),
floatingActionButton: AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutCubic,
@@ -351,6 +352,25 @@ class _HomePageState extends State<HomePage> {
);
}
// 修改为接受宽度参数,而不是使用 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(
],
);
}
+279 -73
View File
@@ -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<RoomPage> {
List<PlayerInfo> players = [];
bool isLoading = true;
// 添加布局类型状态变量
bool isGridLayout = true; // 默认使用网格布局
// 移除布局类型状态变量
@override
void initState() {
@@ -64,18 +60,7 @@ class _RoomPageState extends State<RoomPage> {
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<KM>(
builder: (context, km, child) {
@@ -138,24 +123,13 @@ class _RoomPageState extends State<RoomPage> {
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(
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: _buildPlayerListItem(
players[index], colorScheme),
child:
_buildPlayerListItem(players[index], colorScheme),
);
},
childCount: players.length,
@@ -201,8 +175,8 @@ class _RoomPageState extends State<RoomPage> {
// 如果有连接信息,计算网络统计数据
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<RoomPage> {
PlayerInfo(
name: node.hostname,
ip: node.ipv4, // 临时IP,实际应从节点信息中获取
latency: (node.latencyMs * 1000).toInt(), // 转换为毫秒
latency: (node.latencyMs).toInt(), // 转换为毫秒
connectionType: connectionType,
uploadSpeed: uploadSpeed,
downloadSpeed: downloadSpeed,
@@ -419,20 +393,178 @@ class _RoomPageState extends State<RoomPage> {
);
}
// 构建列表项视图
// 构建列表项视图
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,
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: 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),
),
// 网络数据部分
const Divider(height: 16),
// 网络数据信息 - 移动设备上使用紧凑布局
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,
),
),
],
),
],
),
),
],
);
}
// 为桌面设备优化的列表项布局
Widget _buildDesktopPlayerListItem(PlayerInfo player, ColorScheme colorScheme,
Color latencyColor, IconData connectionIcon) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 左侧玩家基本信息
Expanded(
flex: 3,
child: Column(
@@ -451,38 +583,6 @@ class _RoomPageState extends State<RoomPage> {
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 8),
// 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: [
// 连接类型标签
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
@@ -511,8 +611,37 @@ class _RoomPageState extends State<RoomPage> {
],
),
),
],
),
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,
@@ -531,15 +660,92 @@ class _RoomPageState extends State<RoomPage> {
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,
),
),
],
),
],
),
),
// 右侧包数据信息
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,
),
],
),
),
],
);
}
// 更紧凑的网络数据项
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,
+16 -61
View File
@@ -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<SettingsPage> {
// 初始化 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<SettingsPage> {
_serverList.add(result);
_appConfig.setServerList(_serverList);
// 初始化新服务器的 ping 状态
// 初始化新服务器的 ping 状态并立即开始 ping
pingResults[result] = null;
isPinging[result] = false;
isPinging[result] = true;
_pingServer(result);
});
}
}
@@ -207,7 +205,6 @@ class _SettingsPageState extends State<SettingsPage> {
// 添加构建 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 {
@@ -221,8 +218,6 @@ class _SettingsPageState extends State<SettingsPage> {
);
}
}
return const Text('点击测试', style: TextStyle(color: Colors.grey));
}
@override
Widget build(BuildContext context) {
@@ -248,22 +243,7 @@ class _SettingsPageState extends State<SettingsPage> {
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,7 +257,6 @@ class _SettingsPageState extends State<SettingsPage> {
// 构建延迟显示组件
Widget pingWidget;
if (isPinging[server] == true) {
if (pingResult == null) {
pingWidget = const Text('测试中...',
style: TextStyle(color: Colors.grey));
@@ -290,10 +269,6 @@ class _SettingsPageState extends State<SettingsPage> {
? Colors.orange
: Colors.red)));
}
} else {
pingWidget = const Text('点击测试',
style: TextStyle(color: Colors.grey));
}
return ListTile(
leading: const Icon(Icons.computer),
@@ -308,26 +283,7 @@ class _SettingsPageState extends State<SettingsPage> {
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<SettingsPage> {
_currentServer = server;
Provider.of<KM>(context, listen: false).serverIP =
server;
// 开始 ping 新选择的服务器
_startPingServer(server);
// 不再需要特别开始 ping 新选择的服务器,因为所有服务器都在 ping
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('已切换到服务器: $server')),
+4
View File
@@ -123,6 +123,7 @@ class KVNodeInfo {
final String hostname;
final String ipv4;
final double latencyMs;
final String nat;
final List<KVNodeConnectionStats> 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;
+9 -5
View File
@@ -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<dynamic>;
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);
+8 -8
View File
@@ -375,7 +375,7 @@ class RustLibWire implements BaseWire {
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfoPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'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<void Function(ffi.Pointer<ffi.Void>)>();
@@ -391,7 +391,7 @@ class RustLibWire implements BaseWire {
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfoPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'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<void Function(ffi.Pointer<ffi.Void>)>();
@@ -407,7 +407,7 @@ class RustLibWire implements BaseWire {
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfoPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'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<void Function(ffi.Pointer<ffi.Void>)>();
@@ -423,7 +423,7 @@ class RustLibWire implements BaseWire {
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfoPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'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<void Function(ffi.Pointer<ffi.Void>)>();
@@ -439,7 +439,7 @@ class RustLibWire implements BaseWire {
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePairPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'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<void Function(ffi.Pointer<ffi.Void>)>();
@@ -455,7 +455,7 @@ class RustLibWire implements BaseWire {
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePairPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'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<void Function(ffi.Pointer<ffi.Void>)>();
@@ -471,7 +471,7 @@ class RustLibWire implements BaseWire {
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoutePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'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<void Function(ffi.Pointer<ffi.Void>)>();
@@ -487,7 +487,7 @@ class RustLibWire implements BaseWire {
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoutePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'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<void Function(ffi.Pointer<ffi.Void>)>();
+1 -1
View File
@@ -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';
+11
View File
@@ -0,0 +1,11 @@
[ ] 更换为官方服务器
[ ] 添加服务器多选
[ ] 移除模拟延迟(对其实不用等待2秒的那个是模拟的我给忘了)->改为检测是否成功连接
[ ] 修复延迟计算:从peer连接获取最小延迟(μs->ms),无效则用路由延迟
[x] 管理员问题没权限
[ ] 配置信息改为运行目录
[ ] 看nat类型
[x] 缩放尺寸让导航栏变为底部会导致页面状态丢失
[x] 服务器始终检测延迟,去除暂停和开始反正也不怎么影响性能多此一举还增加复杂度😜
[x] 增加自动更新检测 和自动更新
[ ] 那就打包两个版本 一个便携版(配置文件随软件) 一个安装版(配置文件随软件)
+7 -7
View File
@@ -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
+5 -4
View File
@@ -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:
+29 -2
View File
@@ -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<KVNodeConnectionStats>,
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,
+12 -8
View File
@@ -546,6 +546,7 @@ impl SseDecode for crate::api::simple::KVNodeInfo {
let mut var_hostname = <String>::sse_decode(deserializer);
let mut var_ipv4 = <String>::sse_decode(deserializer);
let mut var_latencyMs = <f64>::sse_decode(deserializer);
let mut var_nat = <String>::sse_decode(deserializer);
let mut var_connections =
<Vec<crate::api::simple::KVNodeConnectionStats>>::sse_decode(deserializer);
let mut var_version = <String>::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 {
<String>::sse_encode(self.hostname, serializer);
<String>::sse_encode(self.ipv4, serializer);
<f64>::sse_encode(self.latency_ms, serializer);
<String>::sse_encode(self.nat, serializer);
<Vec<crate::api::simple::KVNodeConnectionStats>>::sse_encode(self.connections, serializer);
<String>::sse_encode(self.version, serializer);
<i32>::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::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<MyNodeInfo>>::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::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<MyNodeInfo>>::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::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PeerInfo>>::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::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PeerInfo>>::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::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PeerRoutePair>>::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::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PeerRoutePair>>::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::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<Route>>::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::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<Route>>::decrement_strong_count(ptr as _);
+30
View File
@@ -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);
});
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+38
View File
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="astral">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>astral</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>
+35
View File
@@ -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"
}
]
}
+5
View File
@@ -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'"
$<$<C_COMPILER_ID:MSVC>:"LINKER:/MANIFESTINPUT:${CMAKE_CURRENT_SOURCE_DIR}/runner/runner.exe.manifest">)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
+8
View File
@@ -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}")
+7
View File
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>