mirror of
https://github.com/EasyTier/astral.git
synced 2025-05-19 10:30:24 +00:00
22
This commit is contained in:
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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});
|
||||
|
||||
+72
-50
@@ -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,31 +277,37 @@ class _HomePageState extends State<HomePage> {
|
||||
//我的用户名
|
||||
username = Provider.of<KM>(context).username;
|
||||
Serverip = Provider.of<KM>(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<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(
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+347
-141
@@ -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,29 +123,18 @@ 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(
|
||||
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<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,127 +393,359 @@ 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,
|
||||
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,
|
||||
|
||||
+39
-84
@@ -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,21 +205,18 @@ 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 {
|
||||
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<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,22 +257,17 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
|
||||
// 构建延迟显示组件
|
||||
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<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')),
|
||||
@@ -364,7 +319,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
|
||||
// 添加应用设置卡片
|
||||
Card(
|
||||
child: Column(
|
||||
@@ -387,7 +342,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: Column(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
[ ] 更换为官方服务器
|
||||
[ ] 添加服务器多选
|
||||
[ ] 移除模拟延迟(对其实不用等待2秒的那个是模拟的我给忘了)->改为检测是否成功连接
|
||||
[ ] 修复延迟计算:从peer连接获取最小延迟(μs->ms),无效则用路由延迟
|
||||
[x] 管理员问题没权限
|
||||
[ ] 配置信息改为运行目录
|
||||
[ ] 看nat类型
|
||||
[x] 缩放尺寸让导航栏变为底部会导致页面状态丢失
|
||||
[x] 服务器始终检测延迟,去除暂停和开始反正也不怎么影响性能多此一举还增加复杂度😜
|
||||
[x] 增加自动更新检测 和自动更新
|
||||
[ ] 那就打包两个版本 一个便携版(配置文件随软件) 一个安装版(配置文件随软件)
|
||||
+7
-7
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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 _);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
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 |
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user