mirror of
https://github.com/EasyTier/astral.git
synced 2025-05-19 10:30:24 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca49264ad6 | ||
|
|
99ed31f992 | ||
|
|
f6de2f3849 | ||
|
|
3e8fae1c3e | ||
|
|
c72617aaf0 | ||
|
|
b1e48999c3 | ||
|
|
2f6195ecd9 | ||
|
|
4fb5387f5b | ||
|
|
7d52dc475d | ||
|
|
567b3518e8 | ||
|
|
45b6d939da | ||
|
|
769dab91f0 | ||
|
|
9192ee2cc3 |
@@ -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,8 @@ 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
|
||||
|
||||
# User provided section
|
||||
|
||||
|
||||
Vendored
+6
@@ -4,6 +4,12 @@
|
||||
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Flutter",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "Flutter Run",
|
||||
"request": "launch",
|
||||
|
||||
@@ -1,16 +1,43 @@
|
||||
# fltier
|
||||
# AstralET 游戏联机工具
|
||||
|
||||
A new Flutter project.
|
||||
[](https://github.com/ldoubil/astral)
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
## 预览
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||
## 使用教程
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
|
||||
1. **下载并安装**
|
||||
前往 [GitHub 项目页面](https://github.com/ldoubil/astral) 下载最新版本的 AstralET。
|
||||
|
||||
2. **启动应用**
|
||||
安装完成后,启动 AstralET。
|
||||
|
||||
3. **开始联机**
|
||||
- 修改你喜欢的名字和房间密码,然后点击开始即可(如果无法获取到IP尝试管理员启动或者看看防火墙-后期更新会解决这个问题)。
|
||||
|
||||
4. **没有第四步了**
|
||||
- 剩下的功能我相信你一看就懂.
|
||||
|
||||
|
||||
## 介绍
|
||||
|
||||
AstralET 是一款基于 **Flutter** 和 **easytier** 开发的轻量级游戏联机工具,旨在为玩家提供简单、高效的联机体验。
|
||||
|
||||
## 特性
|
||||
|
||||
- **内置 easytier**:将 easytier 直接编译到 AstralET 中,无需额外安装,也不会保留任何后台进程。
|
||||
- **即开即用**:联机时启动应用,结束后关闭即可,操作简单便捷。
|
||||
- **活跃维护**:作者积极更新,随时修复问题并优化功能(上班摸鱼成果)。
|
||||
|
||||
## 联系我们 & 功能建议
|
||||
|
||||
- **QQ 群**: [点击加入 QQ 群](https://qm.qq.com/q/r4VsExDDt6)
|
||||
- **GitHub Issues**: [提交问题或建议](https://github.com/ldoubil/astral/issues)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.2 KiB |
+6
-6
@@ -3,8 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:system_tray/system_tray.dart';
|
||||
import 'dart:io';
|
||||
import 'screens/主屏幕.dart';
|
||||
import 'config/主题配置.dart';
|
||||
import 'screens/Home.dart';
|
||||
import 'config/themeconfiguration.dart';
|
||||
import 'config/app_config.dart';
|
||||
|
||||
// 定义应用程序的主要StatefulWidget
|
||||
@@ -88,10 +88,10 @@ class _MyAppState extends State<MyApp> {
|
||||
// 更改主题色的方法
|
||||
void changeSeedColor(Color color) {
|
||||
// 使用 Future.microtask 延迟状态更新,避免在当前帧中触发重建
|
||||
setState(() {
|
||||
_seedColor = color;
|
||||
AppConfig().setSeedColor(color);
|
||||
});
|
||||
setState(() {
|
||||
_seedColor = color;
|
||||
AppConfig().setSeedColor(color);
|
||||
});
|
||||
}
|
||||
|
||||
// 更改底部导航栏选中索引的方法
|
||||
|
||||
+224
-52
@@ -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,282 @@ 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': [
|
||||
{
|
||||
'url': 'public.easytier.cn:11010',
|
||||
'name': '公共服务器',
|
||||
'selected': true,
|
||||
}
|
||||
],
|
||||
},
|
||||
'room': {
|
||||
'name': 'kevin',
|
||||
'password': 'kevin',
|
||||
},
|
||||
'user': {
|
||||
'name': Platform.localHostname,
|
||||
},
|
||||
'network': {
|
||||
'virtualIP': '',
|
||||
'dynamicIP': true,
|
||||
},
|
||||
'system': {
|
||||
'closeToTray': true,
|
||||
'enablePing': true, // 添加全局ping开关默认配置
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await _configManager.load();
|
||||
|
||||
// 验证配置格式,如果不正确则使用默认值覆盖
|
||||
validateAndFixConfig();
|
||||
}
|
||||
|
||||
// 验证配置格式并修复不正确的配置
|
||||
static void validateAndFixConfig() {
|
||||
// 验证主题设置
|
||||
_validateThemeConfig();
|
||||
|
||||
// 验证服务器列表
|
||||
_validateServerListConfig();
|
||||
|
||||
// 验证房间设置
|
||||
_validateRoomConfig();
|
||||
|
||||
// 验证用户设置
|
||||
_validateUserConfig();
|
||||
|
||||
// 验证网络设置
|
||||
_validateNetworkConfig();
|
||||
|
||||
// 验证系统设置
|
||||
_validateSystemConfig();
|
||||
|
||||
// 保存修复后的配置
|
||||
_configManager.save();
|
||||
}
|
||||
|
||||
// 验证主题配置
|
||||
static void _validateThemeConfig() {
|
||||
// 验证主题模式
|
||||
final String? themeMode = _configManager.get<String>('theme.mode');
|
||||
if (themeMode == null ||
|
||||
!['system', 'light', 'dark'].contains(themeMode.toLowerCase())) {
|
||||
_configManager.set('theme.mode', 'system');
|
||||
}
|
||||
|
||||
// 验证主题色
|
||||
final int? seedColor = _configManager.get<int>('theme.seedColor');
|
||||
if (seedColor == null) {
|
||||
_configManager.set('theme.seedColor', Colors.blue.value);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证服务器列表配置
|
||||
static void _validateServerListConfig() {
|
||||
final dynamic serverList = _configManager.get('server.list');
|
||||
bool isValid = true;
|
||||
|
||||
if (serverList is! List) {
|
||||
isValid = false;
|
||||
} else {
|
||||
for (var server in serverList) {
|
||||
if (server is! Map ||
|
||||
!server.containsKey('url') ||
|
||||
!server.containsKey('name') ||
|
||||
!server.containsKey('selected')) {
|
||||
isValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
_configManager.set('server.list', [
|
||||
{
|
||||
'url': 'public.easytier.cn:11010',
|
||||
'name': '公共服务器',
|
||||
'selected': true,
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证房间配置
|
||||
static void _validateRoomConfig() {
|
||||
final String? roomName = _configManager.get<String>('room.name');
|
||||
if (roomName == null || roomName.isEmpty) {
|
||||
_configManager.set('room.name', 'kevin');
|
||||
}
|
||||
|
||||
final String? roomPassword = _configManager.get<String>('room.password');
|
||||
if (roomPassword == null || roomPassword.isEmpty) {
|
||||
_configManager.set('room.password', 'kevin');
|
||||
}
|
||||
}
|
||||
|
||||
// 验证用户配置
|
||||
static void _validateUserConfig() {
|
||||
final String? username = _configManager.get<String>('user.name');
|
||||
if (username == null || username.isEmpty) {
|
||||
_configManager.set('user.name', Platform.localHostname);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证网络配置
|
||||
static void _validateNetworkConfig() {
|
||||
final String? virtualIP = _configManager.get<String>('network.virtualIP');
|
||||
if (virtualIP == null) {
|
||||
_configManager.set('network.virtualIP', '');
|
||||
}
|
||||
|
||||
final bool? dynamicIP = _configManager.get<bool>('network.dynamicIP');
|
||||
if (dynamicIP == null) {
|
||||
_configManager.set('network.dynamicIP', true);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证系统配置
|
||||
static void _validateSystemConfig() {
|
||||
final bool? closeToTray = _configManager.get<bool>('system.closeToTray');
|
||||
if (closeToTray == null) {
|
||||
_configManager.set('system.closeToTray', true);
|
||||
}
|
||||
|
||||
final bool? enablePing = _configManager.get<bool>('system.enablePing');
|
||||
if (enablePing == null) {
|
||||
_configManager.set('system.enablePing', true);
|
||||
}
|
||||
}
|
||||
|
||||
// 主题设置
|
||||
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'];
|
||||
List<Map<String, dynamic>> get serverList {
|
||||
final dynamic value = _configManager.get('server.list');
|
||||
if (value is List) {
|
||||
return List<Map<String, dynamic>>.from(
|
||||
value.map((item) => Map<String, dynamic>.from(item as Map)),
|
||||
);
|
||||
}
|
||||
return [
|
||||
{
|
||||
'url': 'public.easytier.cn:11010',
|
||||
'name': '公共服务器',
|
||||
'selected': true,
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> setServerList(List<String> servers) async {
|
||||
await _prefs.setStringList(_keyServerList, servers);
|
||||
}
|
||||
|
||||
// 当前选中的服务器设置
|
||||
static const String _keyCurrentServer = 'currentServer';
|
||||
String get currentServer {
|
||||
return _prefs.getString(_keyCurrentServer) ?? 'public.easytier.net:11010';
|
||||
}
|
||||
|
||||
Future<void> setCurrentServer(String server) async {
|
||||
await _prefs.setString(_keyCurrentServer, server);
|
||||
Future<void> setServerList(List<Map<String, dynamic>> servers) async {
|
||||
_configManager.set('server.list', servers);
|
||||
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();
|
||||
}
|
||||
|
||||
// 全局ping开关设置
|
||||
bool get enablePing {
|
||||
return _configManager.get<bool>('system.enablePing') ?? true;
|
||||
}
|
||||
|
||||
Future<void> setEnablePing(bool enabled) async {
|
||||
_configManager.set('system.enablePing', enabled);
|
||||
await _configManager.save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
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) {
|
||||
// 提供更详细的错误信息并使用默认配置
|
||||
print('配置文件解析失败: $e');
|
||||
print('将使用默认配置继续运行。请检查配置文件格式是否正确。');
|
||||
_config = Map.from(defaultConfig);
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存当前配置到文件
|
||||
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>{});
|
||||
}
|
||||
|
||||
// 类型转换确保值类型兼容性
|
||||
if (value is Map<String, dynamic>) {
|
||||
current[keys.last] = Map<String, Object>.from(value);
|
||||
} else if (value is List<Map<String, dynamic>>) {
|
||||
current[keys.last] =
|
||||
value.map((e) => Map<String, Object>.from(e)).toList();
|
||||
} else {
|
||||
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) {
|
||||
if (item.isEmpty) {
|
||||
buffer.writeln('{}');
|
||||
} else {
|
||||
buffer.writeln();
|
||||
// 注意这里使用了正确的缩进级别
|
||||
_writeMapInList(item.cast<String, dynamic>(), buffer, indent + 1);
|
||||
}
|
||||
} else if (item is List) {
|
||||
_writeList(item, buffer, indent + 1);
|
||||
} else {
|
||||
buffer.writeln(_formatValue(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 专门用于处理列表中的Map项,确保正确缩进
|
||||
void _writeMapInList(
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 格式值处理
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../screens/首页.dart';
|
||||
import '../screens/设置.dart';
|
||||
import '../screens/关于.dart';
|
||||
import '../screens/房间.dart';
|
||||
import '../screens/front.dart';
|
||||
import '../screens/settings.dart';
|
||||
import '../screens/about.dart';
|
||||
import '../screens/kvroom.dart';
|
||||
|
||||
class NavItem {
|
||||
final String label;
|
||||
+4
-5
@@ -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/窗口配置.dart';
|
||||
import 'config/windowconfiguration.dart';
|
||||
import 'config/app_config.dart';
|
||||
import 'utils/状态.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();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:astral/utils/up.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import '../widgets/窗口控制按钮.dart';
|
||||
import '../widgets/主题选择器.dart';
|
||||
import '../utils/主题工具.dart';
|
||||
import '../config/导航配置.dart';
|
||||
import '../widgets/window_control_buttons.dart';
|
||||
import '../widgets/theme_selector.dart';
|
||||
import '../utils/theme_tools.dart';
|
||||
import '../config/navigationconfiguration.dart';
|
||||
|
||||
class MainScreen extends StatefulWidget {
|
||||
final Function toggleThemeMode;
|
||||
@@ -33,6 +34,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() {
|
||||
@@ -44,6 +47,17 @@ class _MainScreenState extends State<MainScreen>
|
||||
duration: const Duration(seconds: 3),
|
||||
vsync: this,
|
||||
)..repeat();
|
||||
|
||||
// 添加异步更新检查(推荐方式)
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final updateChecker = UpdateChecker(
|
||||
owner: 'ldoubil',
|
||||
repo: 'astral',
|
||||
);
|
||||
if (mounted) {
|
||||
updateChecker.scheckForUpdates(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -70,8 +84,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 +122,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 +230,25 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// ignore_for_file: file_names
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:astral/utils/app_info.dart';
|
||||
|
||||
class InfoPage extends StatelessWidget {
|
||||
const InfoPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 静态图标
|
||||
Container(
|
||||
height: 110,
|
||||
width: 110,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.5),
|
||||
blurRadius: 15,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.games,
|
||||
size: 50,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// 应用名称
|
||||
Text(
|
||||
'ASTRAL',
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 2.0,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 版本号
|
||||
Text(
|
||||
AppInfoUtil.getVersion(),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// 静态卡片
|
||||
_buildCard(
|
||||
context,
|
||||
'特别鸣谢',
|
||||
'特别感谢EasyTier作者所做的工作和帮助,为本项目提供了重要的技术支持。如果您有功能需求或遇到bug,欢迎加入我们的QQ群获取帮助和了解最新动态。',
|
||||
Icons.favorite,
|
||||
),
|
||||
// 合并后的卡片
|
||||
|
||||
const SizedBox(height: 30),
|
||||
// 静态按钮
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.group_add),
|
||||
label: const Text('加入QQ群'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
elevation: 5,
|
||||
),
|
||||
onPressed: () async {
|
||||
final url = 'https://qm.qq.com/q/ErscyNPTzO';
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
await launchUrl(Uri.parse(url));
|
||||
} else {
|
||||
// 无法打开链接时显示提示
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('无法打开QQ群链接')),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// 版权信息
|
||||
Text(
|
||||
'© ${DateTime.now().year} ASTRAL Team',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCard(
|
||||
BuildContext context,
|
||||
String title,
|
||||
String content,
|
||||
IconData icon,
|
||||
) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
content,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
// 导入必要的包
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:ASTRAL/src/rust/api/simple.dart';
|
||||
import 'package:ASTRAL/utils/%E7%8A%B6%E6%80%81.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';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../widgets/卡片.dart';
|
||||
import '../widgets/card.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import '../utils/runin.dart';
|
||||
|
||||
@@ -52,8 +52,7 @@ class HomePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
// 定义状态枚举
|
||||
|
||||
int connectionTimeoutCounter = 0;
|
||||
// 当前连接状态
|
||||
ConnectionState _connectionState = ConnectionState.notStarted;
|
||||
|
||||
@@ -74,7 +73,7 @@ class _HomePageState extends State<HomePage> {
|
||||
|
||||
bool _isAutoIP = true; // 只用于控制IP自动/手动模式
|
||||
|
||||
String Serverip = "";
|
||||
List<String> Serverip = [""];
|
||||
int _uploadBytes = 0;
|
||||
int _downloadBytes = 0;
|
||||
int _lastUploadBytes = 0;
|
||||
@@ -87,25 +86,105 @@ class _HomePageState extends State<HomePage> {
|
||||
late final TextEditingController _roomPasswordController;
|
||||
late final TextEditingController _usernameController;
|
||||
late final TextEditingController _virtualIPController;
|
||||
// 添加FocusNode来监听焦点变化
|
||||
late final FocusNode _virtualIPFocusNode;
|
||||
late final FocusNode _usernameControllerFocusNode;
|
||||
late final FocusNode _roomNameControllerFocusNode;
|
||||
late final FocusNode _roomPasswordControllerFocusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 初始化 TextEditingController
|
||||
// 初始化所有TextEditingController
|
||||
_roomNameController = TextEditingController(text: roomName);
|
||||
_roomPasswordController = TextEditingController(text: roomPassword);
|
||||
_usernameController = TextEditingController(text: username);
|
||||
_virtualIPController = TextEditingController(text: publicIP);
|
||||
_virtualIPController = TextEditingController(text: publicIP); // 添加缺失的初始化
|
||||
|
||||
// 修改卡片构建器列表,添加版本信息卡片
|
||||
// 初始化FocusNode并添加监听器
|
||||
_virtualIPFocusNode = FocusNode();
|
||||
_virtualIPFocusNode.addListener(_onVirtualIPFocusChange);
|
||||
|
||||
// 初始化用户名、房间名和密码的FocusNode
|
||||
_usernameControllerFocusNode = FocusNode();
|
||||
_usernameControllerFocusNode.addListener(_onUsernameFocusChange);
|
||||
|
||||
_roomNameControllerFocusNode = FocusNode();
|
||||
_roomNameControllerFocusNode.addListener(_onRoomNameFocusChange);
|
||||
|
||||
_roomPasswordControllerFocusNode = FocusNode();
|
||||
_roomPasswordControllerFocusNode.addListener(_onRoomPasswordFocusChange);
|
||||
|
||||
// 修改卡片构建器列表,添加服务器列表卡片
|
||||
_cardBuilders = [
|
||||
_buildNetworkStatusCard, // 网络状态卡片
|
||||
_buildUserInfoCard, // 用户信息卡片
|
||||
_buildRoomInfoCard, // 房间信息卡片
|
||||
_buildVersionInfoCard, // 新增版本信息卡片
|
||||
_buildNetworkStatusCard,
|
||||
_buildUserInfoCard,
|
||||
_buildRoomInfoCard,
|
||||
_buildServerListCard,
|
||||
_buildVersionInfoCard,
|
||||
];
|
||||
}
|
||||
|
||||
// 添加焦点变化监听方法
|
||||
void _onVirtualIPFocusChange() {
|
||||
if (!_virtualIPFocusNode.hasFocus && !_isAutoIP) {
|
||||
// 当失去焦点且不是自动IP模式时更新值
|
||||
Provider.of<KM>(context, listen: false).virtualIP =
|
||||
_virtualIPController.text;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加用户名焦点变化监听方法
|
||||
void _onUsernameFocusChange() {
|
||||
if (!_usernameControllerFocusNode.hasFocus) {
|
||||
Provider.of<KM>(context, listen: false).username =
|
||||
_usernameController.text;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加房间名焦点变化监听方法
|
||||
void _onRoomNameFocusChange() {
|
||||
if (!_roomNameControllerFocusNode.hasFocus) {
|
||||
Provider.of<KM>(context, listen: false).roomName =
|
||||
_roomNameController.text;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加房间密码焦点变化监听方法
|
||||
void _onRoomPasswordFocusChange() {
|
||||
if (!_roomPasswordControllerFocusNode.hasFocus) {
|
||||
Provider.of<KM>(context, listen: false).roomPassword =
|
||||
_roomPasswordController.text;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// 取消所有计时器
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
|
||||
// 释放所有控制器和焦点节点
|
||||
_roomNameController.dispose();
|
||||
_roomPasswordController.dispose();
|
||||
_usernameController.dispose();
|
||||
_virtualIPController.dispose();
|
||||
|
||||
_virtualIPFocusNode.removeListener(_onVirtualIPFocusChange);
|
||||
_virtualIPFocusNode.dispose();
|
||||
|
||||
_usernameControllerFocusNode.removeListener(_onUsernameFocusChange);
|
||||
_usernameControllerFocusNode.dispose();
|
||||
|
||||
_roomNameControllerFocusNode.removeListener(_onRoomNameFocusChange);
|
||||
_roomNameControllerFocusNode.dispose();
|
||||
|
||||
_roomPasswordControllerFocusNode.removeListener(_onRoomPasswordFocusChange);
|
||||
_roomPasswordControllerFocusNode.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void toggleRunning() {
|
||||
final km = Provider.of<KM>(context, listen: false);
|
||||
setState(() {
|
||||
@@ -120,54 +199,83 @@ class _HomePageState extends State<HomePage> {
|
||||
roomName: roomName,
|
||||
roomPassword: roomPassword,
|
||||
severurl: Serverip);
|
||||
// 模拟连接过程,2秒后连接成功
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (isRunning) {
|
||||
// 确保用户没有在连接过程中取消
|
||||
setState(() {
|
||||
_connectionState = ConnectionState.connected;
|
||||
// 连接成功后开始计时
|
||||
|
||||
timer = Timer.periodic(const Duration(seconds: 1), (timer) async {
|
||||
final info = await getRunningInfo();
|
||||
// 打印运行信息的详细内容
|
||||
// print("运行信息详情:${info}");
|
||||
Runin runin = parseRunin(info);
|
||||
// 获取网络状态
|
||||
// 获取网络状态
|
||||
final networkStatus = await getNetworkStatus();
|
||||
km.nodes = networkStatus.nodes;
|
||||
|
||||
// 更新网络流量数据
|
||||
_updateNetworkStats(networkStatus.nodes);
|
||||
|
||||
// print('设备名称: ${runin.devName}');
|
||||
// print(
|
||||
// '设备ID: ${_intToIpv4String(runin.myNodeInfo?.virtualIpv4?.address?.addr ?? 0)}');
|
||||
final int? version =
|
||||
runin.myNodeInfo?.virtualIpv4?.address?.addr;
|
||||
if (version != null) {
|
||||
String ipStr = _intToIpv4String(version);
|
||||
// 检查IP不为0.0.0.0且与当前IP不同时才更新
|
||||
if (ipStr != "0.0.0.0" && publicIP != ipStr) {
|
||||
km.virtualIP = ipStr;
|
||||
}
|
||||
}
|
||||
// print("- 用户名: ${info?.myNodeInfo?.hostname}");
|
||||
// print("- 虚拟IPv4: ${info?.myNodeInfo?.virtualIpv4?.address}");
|
||||
// print("- version: ${info?.myNodeInfo?.version}");
|
||||
// print("- 本地IP: ${info.myNodeInfo.}");
|
||||
setState(() {
|
||||
runningTime += const Duration(seconds: 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
// 不再使用固定延迟模拟连接成功,而是通过定时检查IP来确定连接状态
|
||||
timer = Timer.periodic(const Duration(seconds: 1), (timer) async {
|
||||
// 检查组件是否仍然挂载
|
||||
if (!mounted) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
final info = await getRunningInfo();
|
||||
Runin runin = parseRunin(info);
|
||||
|
||||
// 获取网络状态
|
||||
final networkStatus = await getNetworkStatus();
|
||||
final km = Provider.of<KM>(context, listen: false);
|
||||
km.nodes = networkStatus.nodes;
|
||||
|
||||
// 更新网络流量数据
|
||||
_updateNetworkStats(networkStatus.nodes);
|
||||
|
||||
final int? version = runin.myNodeInfo?.virtualIpv4?.address?.addr;
|
||||
if (version != null) {
|
||||
String ipStr = _intToIpv4String(version);
|
||||
if (publicIP != ipStr) {
|
||||
km.virtualIP = ipStr;
|
||||
}
|
||||
|
||||
// 如果当前状态还是连接中,则更新为已连接
|
||||
if (_connectionState == ConnectionState.connecting) {
|
||||
setState(() {
|
||||
_connectionState = ConnectionState.connected;
|
||||
});
|
||||
}
|
||||
connectionTimeoutCounter = 0;
|
||||
} else if (_connectionState == ConnectionState.connecting) {
|
||||
connectionTimeoutCounter++;
|
||||
if (connectionTimeoutCounter >= 10) {
|
||||
connectionTimeoutCounter = 0;
|
||||
timer.cancel();
|
||||
setState(() {
|
||||
isRunning = false;
|
||||
_connectionState = ConnectionState.notStarted;
|
||||
runningTime = Duration.zero;
|
||||
});
|
||||
|
||||
closeAllServer();
|
||||
// 清空玩家列表数据
|
||||
Provider.of<KM>(context, listen: false).nodes = [];
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('连接失败,未能获取节点信息'),
|
||||
duration: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 再次检查组件是否仍然挂载
|
||||
if (!mounted) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
runningTime += const Duration(seconds: 1);
|
||||
});
|
||||
});
|
||||
// 移除原来的Future.delayed模拟连接成功的代码
|
||||
} else {
|
||||
// 停止时重置状态
|
||||
_connectionState = ConnectionState.notStarted;
|
||||
closeAllServer();
|
||||
// 清空玩家列表数据
|
||||
Provider.of<KM>(context, listen: false).nodes = [];
|
||||
timer?.cancel();
|
||||
runningTime = Duration.zero;
|
||||
// 重置网络统计数据
|
||||
@@ -177,6 +285,7 @@ class _HomePageState extends State<HomePage> {
|
||||
_lastDownloadBytes = 0;
|
||||
uploadSpeed = 0;
|
||||
downloadSpeed = 0;
|
||||
//connectionTimeoutCounter
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -203,6 +312,9 @@ class _HomePageState extends State<HomePage> {
|
||||
}
|
||||
}
|
||||
|
||||
// 再次检查挂载状态,确保在setState前组件仍然挂载
|
||||
if (!mounted) return;
|
||||
|
||||
// 计算速度 (字节/秒 转换为 MB/秒)
|
||||
setState(() {
|
||||
_uploadBytes = totalUploadBytes;
|
||||
@@ -228,26 +340,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();
|
||||
@@ -264,6 +356,7 @@ class _HomePageState extends State<HomePage> {
|
||||
text: km.username,
|
||||
selection: _usernameController.selection,
|
||||
);
|
||||
// 添加虚拟IP控制器的值同步
|
||||
_virtualIPController.value = TextEditingValue(
|
||||
text: km.virtualIP,
|
||||
selection: _virtualIPController.selection,
|
||||
@@ -282,31 +375,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 +450,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) {
|
||||
@@ -431,88 +549,6 @@ class _HomePageState extends State<HomePage> {
|
||||
}
|
||||
}
|
||||
|
||||
// 修改卡片构建方法,移除多余的内边距
|
||||
Widget _buildDashboardCard(ColorScheme colorScheme) {
|
||||
return FloatingCard(
|
||||
colorScheme: colorScheme,
|
||||
maxWidth: 600, // 设置最大宽度
|
||||
height: 200,
|
||||
child: SizedBox(
|
||||
child: PieChart(
|
||||
PieChartData(
|
||||
sections: [
|
||||
PieChartSectionData(
|
||||
value: uploadSpeed,
|
||||
title: '上传',
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
PieChartSectionData(
|
||||
value: downloadSpeed,
|
||||
title: '下载',
|
||||
color: colorScheme.secondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// 修改流量统计卡片,移除多余的内边距
|
||||
Widget _buildTrafficCard(ColorScheme colorScheme) {
|
||||
return FloatingCard(
|
||||
colorScheme: colorScheme,
|
||||
maxWidth: 600,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 修改标题为图标+文字组合
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.data_usage, color: colorScheme.primary, size: 22),
|
||||
const SizedBox(width: 8),
|
||||
const Text('流量统计',
|
||||
style:
|
||||
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildTrafficInfo('上传速度', '$uploadSpeed MB/s', Icons.upload,
|
||||
colorScheme.primary),
|
||||
_buildTrafficInfo('下载速度', '$downloadSpeed MB/s', Icons.download,
|
||||
colorScheme.secondary),
|
||||
],
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
// 修改IP地址卡片,移除多余的内边距
|
||||
Widget _buildIPCard(ColorScheme colorScheme) {
|
||||
return FloatingCard(
|
||||
colorScheme: colorScheme,
|
||||
maxWidth: 600,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 修改标题为图标+文字组合
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.wifi, color: colorScheme.primary, size: 22),
|
||||
const SizedBox(width: 8),
|
||||
const Text('网络信息',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildIPInfo('虚拟 IP', publicIP, Icons.public, colorScheme),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTrafficInfo(
|
||||
String label, String value, IconData icon, Color color) {
|
||||
return Column(
|
||||
@@ -538,7 +574,60 @@ class _HomePageState extends State<HomePage> {
|
||||
);
|
||||
}
|
||||
|
||||
// 添加服务器列表卡片
|
||||
Widget _buildServerListCard(ColorScheme colorScheme) {
|
||||
final km = Provider.of<KM>(context);
|
||||
// 直接使用 serverIP 列表,而不是尝试访问 serverList
|
||||
final serverUrls = km.serverIP;
|
||||
|
||||
return FloatingCard(
|
||||
colorScheme: colorScheme,
|
||||
maxWidth: 600,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题栏
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.dns, color: colorScheme.primary, size: 22),
|
||||
const SizedBox(width: 8),
|
||||
const Text('当前服务器',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 当前选中的服务器
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: serverUrls.isEmpty ||
|
||||
(serverUrls.length == 1 && serverUrls[0].isEmpty)
|
||||
? [
|
||||
Chip(
|
||||
avatar: Icon(Icons.info_outline,
|
||||
size: 16, color: colorScheme.error),
|
||||
label: const Text('未选择服务器'),
|
||||
backgroundColor:
|
||||
colorScheme.errorContainer.withOpacity(0.3),
|
||||
)
|
||||
]
|
||||
: serverUrls
|
||||
.map((url) => Chip(
|
||||
avatar: Icon(Icons.dns,
|
||||
size: 16, color: colorScheme.primary),
|
||||
label: Text(url),
|
||||
backgroundColor: colorScheme.surfaceVariant,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 新增合并后的网络状态卡片(合并了流量统计和IP信息)
|
||||
// 修改网络状态卡片,去除流量统计部分
|
||||
Widget _buildNetworkStatusCard(ColorScheme colorScheme) {
|
||||
return FloatingCard(
|
||||
colorScheme: colorScheme,
|
||||
@@ -576,33 +665,6 @@ class _HomePageState extends State<HomePage> {
|
||||
|
||||
// IP信息部分
|
||||
_buildIPInfo('虚拟 IP', publicIP, Icons.public, colorScheme),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 流量统计部分
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.data_usage, color: colorScheme.primary, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text('流量统计',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colorScheme.primary,
|
||||
)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildTrafficInfo('上传速度', '$uploadSpeed MB/s', Icons.upload,
|
||||
colorScheme.primary),
|
||||
_buildTrafficInfo('下载速度', '$downloadSpeed MB/s', Icons.download,
|
||||
colorScheme.secondary),
|
||||
],
|
||||
),
|
||||
|
||||
// 添加运行时间显示
|
||||
if (_connectionState == ConnectionState.connected) ...[
|
||||
@@ -696,9 +758,12 @@ class _HomePageState extends State<HomePage> {
|
||||
// 用户名输入框
|
||||
TextField(
|
||||
controller: _usernameController,
|
||||
focusNode: _usernameControllerFocusNode, // 添加焦点节点
|
||||
enabled: _connectionState != ConnectionState.connected,
|
||||
onChanged: (value) {
|
||||
km.username = value;
|
||||
onEditingComplete: () {
|
||||
// 改为完成编辑时更新
|
||||
Provider.of<KM>(context, listen: false).username =
|
||||
_usernameController.text;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: '用户名',
|
||||
@@ -715,12 +780,17 @@ class _HomePageState extends State<HomePage> {
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _virtualIPController,
|
||||
focusNode: _virtualIPFocusNode, // 添加焦点节点
|
||||
enabled: !_isAutoIP &&
|
||||
_connectionState != ConnectionState.connected,
|
||||
onChanged: (value) {
|
||||
// 保留空回调以避免实时更新
|
||||
},
|
||||
onEditingComplete: () {
|
||||
// 添加完成编辑回调
|
||||
if (!_isAutoIP) {
|
||||
setState(() {});
|
||||
km.virtualIP = value;
|
||||
Provider.of<KM>(context, listen: false).virtualIP =
|
||||
_virtualIPController.text;
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
@@ -743,6 +813,10 @@ class _HomePageState extends State<HomePage> {
|
||||
_isAutoIP = value;
|
||||
});
|
||||
km.dynamicIP = value;
|
||||
// 切换模式时同步最新值
|
||||
if (!value) {
|
||||
km.virtualIP = _virtualIPController.text;
|
||||
}
|
||||
}
|
||||
: null,
|
||||
),
|
||||
@@ -812,9 +886,12 @@ class _HomePageState extends State<HomePage> {
|
||||
// 房间名称输入框
|
||||
TextField(
|
||||
controller: _roomNameController,
|
||||
focusNode: _roomNameControllerFocusNode, // 添加焦点节点
|
||||
enabled: _connectionState != ConnectionState.connected,
|
||||
onChanged: (value) {
|
||||
km.roomName = value;
|
||||
onEditingComplete: () {
|
||||
// 改为完成编辑时更新
|
||||
Provider.of<KM>(context, listen: false).roomName =
|
||||
_roomNameController.text;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: '房间名称',
|
||||
@@ -828,9 +905,12 @@ class _HomePageState extends State<HomePage> {
|
||||
// 房间密码输入框
|
||||
TextField(
|
||||
controller: _roomPasswordController,
|
||||
focusNode: _roomPasswordControllerFocusNode, // 添加焦点节点
|
||||
enabled: _connectionState != ConnectionState.connected,
|
||||
onChanged: (value) {
|
||||
km.roomPassword = value;
|
||||
onEditingComplete: () {
|
||||
// 改为完成编辑时更新
|
||||
Provider.of<KM>(context, listen: false).roomPassword =
|
||||
_roomPasswordController.text;
|
||||
},
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
@@ -861,7 +941,7 @@ class _HomePageState extends State<HomePage> {
|
||||
// 添加版本信息卡片
|
||||
Widget _buildVersionInfoCard(ColorScheme colorScheme) {
|
||||
// 这里可以从配置或API获取实际版本号
|
||||
final String appVersion = AppInfoUtil.getFullVersion();
|
||||
final String appVersion = AppInfoUtil.getVersion();
|
||||
|
||||
return FloatingCard(
|
||||
colorScheme: colorScheme,
|
||||
@@ -0,0 +1,949 @@
|
||||
// 导入必要的包
|
||||
import 'package:flutter/services.dart'; // 添加这一行导入剪贴板服务
|
||||
|
||||
import 'package:astral/utils/kv_state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../widgets/card.dart';
|
||||
|
||||
/// 玩家信息模型类
|
||||
class PlayerInfo {
|
||||
final String name;
|
||||
final String ip;
|
||||
final int latency; // 延迟(ms)
|
||||
final String connectionType; // 连接类型:直链、中转、本机
|
||||
final int uploadSpeed; // 上传速度(KB/s)
|
||||
final int downloadSpeed; // 下载速度(KB/s)
|
||||
final int sentPackets; // 发送包数量
|
||||
final int receivedPackets; // 接收包数量
|
||||
final double packetLossRate; // 丢包率(%)
|
||||
final String etVersion; // ET版本
|
||||
final String natType; // 添加NAT类型
|
||||
|
||||
PlayerInfo({
|
||||
required this.name,
|
||||
required this.ip,
|
||||
required this.latency,
|
||||
required this.connectionType,
|
||||
required this.uploadSpeed,
|
||||
required this.downloadSpeed,
|
||||
required this.sentPackets,
|
||||
required this.receivedPackets,
|
||||
required this.packetLossRate,
|
||||
required this.etVersion,
|
||||
required this.natType, // 添加NAT类型参数
|
||||
});
|
||||
}
|
||||
|
||||
/// 房间页面组件
|
||||
/// 用于显示所有玩家的信息
|
||||
class RoomPage extends StatefulWidget {
|
||||
const RoomPage({super.key});
|
||||
|
||||
@override
|
||||
State<RoomPage> createState() => _RoomPageState();
|
||||
}
|
||||
|
||||
class _RoomPageState extends State<RoomPage> {
|
||||
List<PlayerInfo> players = [];
|
||||
List<PlayerInfo> filteredPlayers = []; // 添加过滤后的玩家列表
|
||||
bool isLoading = true;
|
||||
String searchQuery = ''; // 添加搜索查询字符串
|
||||
TextEditingController searchController = TextEditingController(); // 添加搜索控制器
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
isLoading = true;
|
||||
searchController.addListener(_onSearchChanged); // 添加搜索监听器
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
searchController.removeListener(_onSearchChanged); // 移除搜索监听器
|
||||
searchController.dispose(); // 释放控制器资源
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// 搜索变化处理函数
|
||||
void _onSearchChanged() {
|
||||
setState(() {
|
||||
searchQuery = searchController.text;
|
||||
_filterPlayers(); // 过滤玩家列表
|
||||
});
|
||||
}
|
||||
|
||||
// 过滤玩家列表
|
||||
void _filterPlayers() {
|
||||
if (searchQuery.isEmpty) {
|
||||
filteredPlayers = List.from(players); // 如果搜索为空,显示所有玩家
|
||||
} else {
|
||||
filteredPlayers = players
|
||||
.where((player) =>
|
||||
player.name.toLowerCase().contains(searchQuery.toLowerCase()))
|
||||
.toList(); // 根据名称过滤玩家
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('房间成员'),
|
||||
actions: [
|
||||
// 添加搜索按钮
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: () {
|
||||
showSearch(
|
||||
context: context,
|
||||
delegate: _PlayerSearchDelegate(
|
||||
players: players,
|
||||
colorScheme: colorScheme,
|
||||
buildPlayerListItem: _buildPlayerListItem,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Consumer<KM>(
|
||||
builder: (context, km, child) {
|
||||
// 当 KM 更新时,这个 builder 会被重新调用
|
||||
// 异步处理数据
|
||||
_processNodeData(km);
|
||||
|
||||
if (isLoading) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'加载玩家信息...',
|
||||
style: TextStyle(
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else if (players.isEmpty) {
|
||||
// 添加空数据状态显示
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.people_outline,
|
||||
size: 64,
|
||||
color: colorScheme.primary.withOpacity(0.6),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'暂无房间成员',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'当前没有其他玩家连接到房间',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child:
|
||||
_buildPlayerListItem(players[index], colorScheme),
|
||||
);
|
||||
},
|
||||
childCount: players.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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列
|
||||
}
|
||||
}
|
||||
|
||||
// 处理节点数据 - 合并了原来的两个相似方法
|
||||
Future<void> _processNodeData(KM km) async {
|
||||
try {
|
||||
final nodes = await km.nodes; // 获取最新的节点信息
|
||||
|
||||
// 将节点数据转换为PlayerInfo对象
|
||||
List<PlayerInfo> nodePlayerInfos = [];
|
||||
|
||||
for (var node in nodes) {
|
||||
// 计算上传下载速度和包数量总和
|
||||
int uploadSpeed = 0;
|
||||
int downloadSpeed = 0;
|
||||
int sentPackets = 0;
|
||||
int receivedPackets = 0;
|
||||
String connectionType =
|
||||
_mapConnectionType(node.cost, node.ipv4, km.virtualIP);
|
||||
|
||||
// 如果有连接信息,计算网络统计数据
|
||||
if (node.connections.isNotEmpty) {
|
||||
for (var conn in node.connections) {
|
||||
uploadSpeed += conn.txBytes.toInt() ~/ 1024; // 转换为KB
|
||||
downloadSpeed += conn.rxBytes.toInt() ~/ 1024; // 转换为KB
|
||||
sentPackets += conn.txPackets.toInt();
|
||||
receivedPackets += conn.rxPackets.toInt();
|
||||
}
|
||||
}
|
||||
|
||||
// 计算丢包率 (简单估算)
|
||||
double packetLossRate = node.lossRate;
|
||||
// 获取NAT类型
|
||||
String natType = _mapNatType(node.nat);
|
||||
|
||||
// 创建PlayerInfo对象
|
||||
nodePlayerInfos.add(
|
||||
PlayerInfo(
|
||||
name: node.hostname,
|
||||
ip: node.ipv4, // 临时IP,实际应从节点信息中获取
|
||||
latency: (node.latencyMs).toInt(), // 转换为毫秒
|
||||
connectionType: connectionType,
|
||||
uploadSpeed: uploadSpeed,
|
||||
downloadSpeed: downloadSpeed,
|
||||
sentPackets: sentPackets,
|
||||
receivedPackets: receivedPackets,
|
||||
packetLossRate: packetLossRate,
|
||||
etVersion: node.version, // 获取版本信息
|
||||
natType: natType, // 添加NAT类型
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!mounted) return; // 检查组件是否仍然挂载
|
||||
|
||||
setState(() {
|
||||
players = nodePlayerInfos;
|
||||
_filterPlayers(); // 更新过滤后的玩家列表
|
||||
isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
print("加载节点数据失败: $e");
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建列表项视图
|
||||
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: 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),
|
||||
// NAT类型
|
||||
_buildInfoRow(
|
||||
_getNatTypeIcon(player.natType),
|
||||
'NAT类型',
|
||||
player.natType,
|
||||
colorScheme,
|
||||
valueColor: _getNatTypeColor(player.natType),
|
||||
),
|
||||
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(
|
||||
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),
|
||||
|
||||
// ET版本
|
||||
_buildInfoRow(
|
||||
Icons.memory,
|
||||
'ET版本',
|
||||
player.etVersion,
|
||||
colorScheme,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// NAT类型
|
||||
_buildInfoRow(
|
||||
_getNatTypeIcon(player.natType),
|
||||
'NAT类型',
|
||||
player.natType,
|
||||
colorScheme,
|
||||
valueColor: _getNatTypeColor(player.natType),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 中间网络状态信息
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 右侧包数据信息
|
||||
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,
|
||||
String label,
|
||||
String value,
|
||||
ColorScheme colorScheme, {
|
||||
Color? valueColor,
|
||||
bool showCopyButton = false,
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 20, color: colorScheme.primary),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'$label:',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 添加复制按钮到标签和值之间
|
||||
if (showCopyButton)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy, size: 18),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
tooltip: '复制$label',
|
||||
onPressed: () {
|
||||
// 复制到剪贴板
|
||||
Clipboard.setData(ClipboardData(text: value));
|
||||
// 显示提示
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('已复制: $value'),
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: valueColor ?? colorScheme.secondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 根据延迟值获取颜色
|
||||
Color _getLatencyColor(int latency) {
|
||||
if (latency < 50) {
|
||||
return Colors.green;
|
||||
} else if (latency < 100) {
|
||||
return Colors.orange;
|
||||
} else {
|
||||
return Colors.red;
|
||||
}
|
||||
}
|
||||
|
||||
// 根据丢包率获取颜色
|
||||
Color _getPacketLossColor(double lossRate) {
|
||||
if (lossRate < 1.0) {
|
||||
return Colors.green;
|
||||
} else if (lossRate < 5.0) {
|
||||
return Colors.orange;
|
||||
} else {
|
||||
return Colors.red;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果传入数值=1就是p2p 否则是relay 最后判断是不是等于本机IP如果等于就是direct 本机ip传入
|
||||
String _mapConnectionType(int connType, String ip, String thisip) {
|
||||
// 新增服务器IP判断
|
||||
if (ip == "0.0.0.0") {
|
||||
return '服务器';
|
||||
}
|
||||
// 如果是本机IP,返回direct
|
||||
if (ip == thisip) {
|
||||
return '本机';
|
||||
}
|
||||
// 根据连接成本判断连接类型
|
||||
if (connType == 1) {
|
||||
return '直链';
|
||||
} else if (connType >= 2) {
|
||||
return '中转';
|
||||
}
|
||||
return '未知';
|
||||
}
|
||||
|
||||
// 根据连接类型获取图标
|
||||
IconData _getConnectionIcon(String connectionType) {
|
||||
// 将连接类型转为小写并进行匹配
|
||||
String lowerType = connectionType.toLowerCase();
|
||||
// 新增服务器图标
|
||||
if (lowerType.contains('server') || lowerType.contains('服务器')) {
|
||||
return Icons.dns;
|
||||
} else if (lowerType.contains('p2p') || lowerType.contains('直链')) {
|
||||
return Icons.link;
|
||||
} else if (lowerType.contains('relay') || lowerType.contains('中转')) {
|
||||
return Icons.swap_horiz;
|
||||
} else if (lowerType.contains('direct') || lowerType.contains('本机')) {
|
||||
return Icons.computer;
|
||||
} else {
|
||||
return Icons.device_unknown;
|
||||
}
|
||||
}
|
||||
|
||||
// 根据连接类型获取颜色
|
||||
Color _getConnectionTypeColor(
|
||||
String connectionType, ColorScheme colorScheme) {
|
||||
// 将连接类型转为小写并进行匹配
|
||||
String lowerType = connectionType.toLowerCase();
|
||||
if (lowerType.contains('server') || lowerType.contains('服务器')) {
|
||||
return Colors.deepPurple;
|
||||
} else if (lowerType.contains('p2p') || lowerType.contains('直链')) {
|
||||
return Colors.green;
|
||||
} else if (lowerType.contains('relay') || lowerType.contains('中转')) {
|
||||
return Colors.orange;
|
||||
} else if (lowerType.contains('direct') || lowerType.contains('本机')) {
|
||||
return colorScheme.primary;
|
||||
} else {
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
// 将NAT类型转换为中文
|
||||
String _mapNatType(String natType) {
|
||||
switch (natType) {
|
||||
case 'Unknown':
|
||||
return '未知';
|
||||
case 'OpenInternet':
|
||||
return '开放网络';
|
||||
case 'NoPat':
|
||||
return '无PAT';
|
||||
case 'FullCone':
|
||||
return '全锥形';
|
||||
case 'Restricted':
|
||||
return '受限锥形';
|
||||
case 'PortRestricted':
|
||||
return '端口受限锥形';
|
||||
case 'Symmetric':
|
||||
return '对称型';
|
||||
case 'SymUdpFirewall':
|
||||
return '对称UDP防火墙';
|
||||
case 'SymmetricEasyInc':
|
||||
return '对称递增型';
|
||||
case 'SymmetricEasyDec':
|
||||
return '对称递减型';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
// 根据NAT类型获取图标
|
||||
IconData _getNatTypeIcon(String natType) {
|
||||
if (natType.contains('开放') || natType.contains('全锥形')) {
|
||||
return Icons.public;
|
||||
} else if (natType.contains('受限')) {
|
||||
return Icons.shield;
|
||||
} else if (natType.contains('端口受限')) {
|
||||
return Icons.security;
|
||||
} else if (natType.contains('对称')) {
|
||||
return Icons.sync_alt;
|
||||
} else if (natType.contains('防火墙')) {
|
||||
return Icons.fireplace;
|
||||
} else if (natType.contains('递增')) {
|
||||
return Icons.trending_up;
|
||||
} else if (natType.contains('递减')) {
|
||||
return Icons.trending_down;
|
||||
} else if (natType.contains('无PAT')) {
|
||||
return Icons.router;
|
||||
} else {
|
||||
return Icons.help_outline;
|
||||
}
|
||||
}
|
||||
|
||||
// 根据NAT类型获取颜色
|
||||
Color _getNatTypeColor(String natType) {
|
||||
if (natType.contains('开放') ||
|
||||
natType.contains('全锥形') ||
|
||||
natType.contains('无PAT')) {
|
||||
return Colors.green;
|
||||
} else if (natType.contains('受限') || natType.contains('端口受限')) {
|
||||
return Colors.orange;
|
||||
} else if (natType.contains('对称') || natType.contains('防火墙')) {
|
||||
return Colors.red;
|
||||
} else {
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 玩家搜索委托类
|
||||
class _PlayerSearchDelegate extends SearchDelegate<String> {
|
||||
final List<PlayerInfo> players;
|
||||
final ColorScheme colorScheme;
|
||||
final Function(PlayerInfo, ColorScheme) buildPlayerListItem;
|
||||
|
||||
_PlayerSearchDelegate({
|
||||
required this.players,
|
||||
required this.colorScheme,
|
||||
required this.buildPlayerListItem,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Widget> buildActions(BuildContext context) {
|
||||
return [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
// 如果搜索框为空,直接返回
|
||||
if (query.isEmpty) {
|
||||
close(context, '');
|
||||
} else {
|
||||
// 否则清空搜索内容
|
||||
query = '';
|
||||
showSuggestions(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildLeading(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
close(context, '');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildResults(BuildContext context) {
|
||||
return _buildSearchResults(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildSuggestions(BuildContext context) {
|
||||
return _buildSearchResults(context);
|
||||
}
|
||||
|
||||
// 修改方法签名,添加 BuildContext 参数
|
||||
Widget _buildSearchResults(BuildContext context) {
|
||||
final filteredPlayers = query.isEmpty
|
||||
? players
|
||||
: players
|
||||
.where((player) =>
|
||||
player.name.toLowerCase().contains(query.toLowerCase()))
|
||||
.toList();
|
||||
|
||||
if (filteredPlayers.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search_off,
|
||||
size: 64,
|
||||
color: colorScheme.primary.withOpacity(0.6),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'未找到匹配的玩家',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'尝试使用其他搜索关键词',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 添加对屏幕宽度的检测
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// 使用约束条件获取当前宽度
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
itemCount: filteredPlayers.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: buildPlayerListItem(filteredPlayers[index], colorScheme),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
import 'dart:async';
|
||||
import 'package:astral/utils/app_info.dart';
|
||||
import 'package:astral/utils/up.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../config/app_config.dart';
|
||||
import '../utils/ping_util.dart';
|
||||
import 'package:astral/utils/kv_state.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class SettingsPage extends StatefulWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsPage> createState() => _SettingsPageState();
|
||||
}
|
||||
|
||||
final updateChecker = UpdateChecker(
|
||||
owner: 'ldoubil',
|
||||
repo: 'astral',
|
||||
);
|
||||
|
||||
class _SettingsPageState extends State<SettingsPage> {
|
||||
late List<Map<String, dynamic>> _serverList;
|
||||
final _appConfig = AppConfig();
|
||||
bool _closeToTray = false; // 添加关闭进入托盘变量
|
||||
bool _pingEnabled = true; // 添加全局ping开关
|
||||
|
||||
String serverIP = "";
|
||||
// 添加 ping 相关状态
|
||||
Map<String, int?> pingResults = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_serverList = _appConfig.serverList;
|
||||
serverIP = _getSelectedServersString();
|
||||
_closeToTray = _appConfig.closeToTray; // 初始化托盘设置
|
||||
// 初始化全局ping开关
|
||||
_pingEnabled = _appConfig.enablePing;
|
||||
|
||||
// 初始化 ping 状态
|
||||
for (var server in _serverList) {
|
||||
pingResults[server['url']] = null;
|
||||
}
|
||||
|
||||
// 开始 ping 所有服务器
|
||||
_startPingAllServers();
|
||||
}
|
||||
|
||||
// 获取选中的服务器字符串
|
||||
String _getSelectedServersString() {
|
||||
final selected =
|
||||
_serverList.where((server) => server['selected'] == true).toList();
|
||||
if (selected.isEmpty && _serverList.isNotEmpty) {
|
||||
return _serverList.first['url'];
|
||||
}
|
||||
return selected.map((server) => server['url']).join(', ');
|
||||
}
|
||||
|
||||
// 添加一个计时器变量来控制 ping 操作
|
||||
Timer? _pingTimer;
|
||||
|
||||
// 新增方法:开始 ping 所有服务器
|
||||
void _startPingAllServers() {
|
||||
// 取消之前的计时器(如果存在)
|
||||
_pingTimer?.cancel();
|
||||
|
||||
// 创建新的计时器,使用设置的间隔时间
|
||||
_pingTimer = Timer.periodic(Duration(milliseconds: 1000), (timer) {
|
||||
if (!mounted) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_pingEnabled) {
|
||||
for (var server in _serverList) {
|
||||
_pingServerOnce(server['url']);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// 取消计时器
|
||||
_pingTimer?.cancel();
|
||||
_pingTimer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// 执行单次 ping 操作
|
||||
Future<void> _pingServerOnce(String server) async {
|
||||
final pingResult = await PingUtil.ping(server);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
pingResults[server] = pingResult;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 切换全局ping状态
|
||||
void _togglePingStatus(bool value) {
|
||||
setState(() {
|
||||
_pingEnabled = value;
|
||||
_appConfig.setEnablePing(_pingEnabled);
|
||||
if (!_pingEnabled) {
|
||||
// 如果关闭ping,清空所有结果
|
||||
for (var server in _serverList) {
|
||||
pingResults[server['url']] = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 添加服务器对话框
|
||||
Future<void> _showAddServerDialog() async {
|
||||
final urlController = TextEditingController();
|
||||
final nameController = TextEditingController();
|
||||
|
||||
final result = await showDialog<Map<String, dynamic>>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('添加服务器'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '服务器名称',
|
||||
hintText: '自定义名称',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: urlController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '服务器地址',
|
||||
hintText: 'example.com:port',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (urlController.text.isNotEmpty) {
|
||||
Navigator.pop(context, {
|
||||
'url': urlController.text,
|
||||
'name': nameController.text.isNotEmpty
|
||||
? nameController.text
|
||||
: urlController.text,
|
||||
});
|
||||
}
|
||||
},
|
||||
child: const Text('添加'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
setState(() {
|
||||
// 添加新服务器,默认不选中
|
||||
_serverList.add({
|
||||
'url': result['url'],
|
||||
'name': result['name'],
|
||||
'selected': false,
|
||||
});
|
||||
_appConfig.setServerList(_serverList);
|
||||
|
||||
// 初始化新服务器的 ping 状态
|
||||
pingResults[result['url']] = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑服务器对话框
|
||||
Future<void> _showEditServerDialog(int index) async {
|
||||
final server = _serverList[index];
|
||||
final urlController = TextEditingController(text: server['url']);
|
||||
final nameController = TextEditingController(text: server['name']);
|
||||
|
||||
final result = await showDialog<Map<String, dynamic>>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('编辑服务器'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '服务器名称',
|
||||
hintText: '自定义名称',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: urlController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '服务器地址',
|
||||
hintText: 'example.com:port',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (urlController.text.isNotEmpty) {
|
||||
Navigator.pop(context, {
|
||||
'url': urlController.text,
|
||||
'name': nameController.text.isNotEmpty
|
||||
? nameController.text
|
||||
: urlController.text,
|
||||
});
|
||||
}
|
||||
},
|
||||
child: const Text('保存'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
final oldUrl = server['url'];
|
||||
setState(() {
|
||||
_serverList[index] = {
|
||||
'url': result['url'],
|
||||
'name': result['name'],
|
||||
'selected': server['selected'],
|
||||
};
|
||||
_appConfig.setServerList(_serverList);
|
||||
|
||||
// 更新 ping 状态
|
||||
if (oldUrl != result['url']) {
|
||||
pingResults[result['url']] = pingResults[oldUrl];
|
||||
pingResults.remove(oldUrl);
|
||||
}
|
||||
|
||||
// 更新当前选中的服务器显示
|
||||
serverIP = _getSelectedServersString();
|
||||
// 使用新的 serverList 方法替代直接设置 serverIP
|
||||
Provider.of<KM>(context, listen: false).serverList = _serverList;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 删除服务器
|
||||
Future<void> _deleteServer(int index) async {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('确认删除'),
|
||||
content: const Text('确定要删除这个服务器吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('删除'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirm == true) {
|
||||
final server = _serverList[index];
|
||||
|
||||
setState(() {
|
||||
_serverList.removeAt(index);
|
||||
_appConfig.setServerList(_serverList);
|
||||
|
||||
// 移除 ping 状态
|
||||
pingResults.remove(server['url']);
|
||||
|
||||
// 确保至少有一个服务器被选中
|
||||
if (_serverList.isNotEmpty &&
|
||||
!_serverList.any((server) => server['selected'] == true)) {
|
||||
_serverList.first['selected'] = true;
|
||||
}
|
||||
|
||||
// 更新当前选中的服务器显示
|
||||
serverIP = _getSelectedServersString();
|
||||
// 使用新的 serverList 方法替代直接设置 serverIP
|
||||
Provider.of<KM>(context, listen: false).serverList = _serverList;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 切换服务器选中状态
|
||||
void _toggleServerSelection(int index) {
|
||||
setState(() {
|
||||
_serverList[index]['selected'] =
|
||||
!(_serverList[index]['selected'] ?? false);
|
||||
_appConfig.setServerList(_serverList);
|
||||
|
||||
// 更新当前选中的服务器显示
|
||||
serverIP = _getSelectedServersString();
|
||||
// 使用新的 serverList 方法替代直接设置 serverIP
|
||||
Provider.of<KM>(context, listen: false).serverList = _serverList;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_serverList[index]['selected']
|
||||
? '已选中服务器: ${_serverList[index]['name']}'
|
||||
: '已取消选中: ${_serverList[index]['name']}')),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 添加构建 ping 显示组件的方法
|
||||
Widget _buildPingWidget(String url) {
|
||||
final pingResult = pingResults[url];
|
||||
if (!_pingEnabled) {
|
||||
return const Text('Ping已关闭', style: TextStyle(color: Colors.grey));
|
||||
} else 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// updateChecker.checkForUpdates(context);
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: [
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.dns),
|
||||
title: const Text('已选中的服务器'),
|
||||
subtitle: Text(serverIP),
|
||||
),
|
||||
ExpansionTile(
|
||||
leading: const Icon(Icons.list),
|
||||
title: const Text('服务器列表'),
|
||||
children: [
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: _serverList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final server = _serverList[index];
|
||||
final isSelected = server['selected'] == true;
|
||||
|
||||
return ListTile(
|
||||
leading: Icon(
|
||||
isSelected
|
||||
? Icons.check_circle
|
||||
: Icons.circle_outlined,
|
||||
color: isSelected ? Colors.green : null,
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Text(server['name']),
|
||||
const SizedBox(width: 8),
|
||||
_buildPingWidget(server['url']),
|
||||
],
|
||||
),
|
||||
subtitle: Text(server['url']),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () => _showEditServerDialog(index),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () => _deleteServer(index),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () => _toggleServerSelection(index),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add),
|
||||
title: const Text('添加新服务器'),
|
||||
onTap: _showAddServerDialog,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 添加应用设置卡片
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
const ListTile(
|
||||
leading: Icon(Icons.settings),
|
||||
title: Text('应用设置'),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('关闭时最小化到托盘'),
|
||||
subtitle: const Text('关闭窗口时应用将继续在后台运行'),
|
||||
value: _closeToTray,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_closeToTray = value;
|
||||
_appConfig.setCloseToTray(value);
|
||||
});
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('启用服务器Ping测试'),
|
||||
subtitle: const Text('定期测试所有服务器的网络延迟'),
|
||||
value: _pingEnabled,
|
||||
onChanged: _togglePingStatus,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: Icon(Icons.info),
|
||||
title: Text('应用版本'),
|
||||
subtitle: Text(AppInfoUtil.getVersion()),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.update),
|
||||
title: const Text('检查更新'),
|
||||
onTap: () {
|
||||
updateChecker.checkForUpdates(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
// ignore_for_file: file_names
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'package:ASTRAL/utils/app_info.dart';
|
||||
|
||||
class InfoPage extends StatefulWidget {
|
||||
const InfoPage({super.key});
|
||||
|
||||
@override
|
||||
State<InfoPage> createState() => _InfoPageState();
|
||||
}
|
||||
|
||||
class _InfoPageState extends State<InfoPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 10),
|
||||
vsync: this,
|
||||
)..repeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 旋转的图标效果
|
||||
AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (_, child) {
|
||||
return Transform.rotate(
|
||||
angle: _controller.value * 2 * math.pi,
|
||||
child: Container(
|
||||
height: 110,
|
||||
width: 110,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: SweepGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.primary,
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
Theme.of(context).colorScheme.tertiary,
|
||||
Theme.of(context).colorScheme.primary,
|
||||
],
|
||||
stops: const [0.0, 0.3, 0.6, 1.0],
|
||||
transform:
|
||||
GradientRotation(_controller.value * 2 * math.pi),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.5),
|
||||
blurRadius: 15,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.games,
|
||||
size: 50,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// 应用名称添加渐变效果
|
||||
ShaderMask(
|
||||
shaderCallback: (bounds) => LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.primary,
|
||||
Theme.of(context).colorScheme.tertiary,
|
||||
],
|
||||
).createShader(bounds),
|
||||
child: Text(
|
||||
'ASTRAL',
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 2.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 版本号添加动画效果
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0, end: 1),
|
||||
duration: const Duration(milliseconds: 800),
|
||||
builder: (context, value, child) {
|
||||
return Opacity(
|
||||
opacity: value,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, 20 * (1 - value)),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
AppInfoUtil.getFullVersion(),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// 卡片添加动画和阴影效果
|
||||
_buildAnimatedCard(
|
||||
context,
|
||||
'特别鸣谢',
|
||||
'特别感谢EasyTier作者所做的工作和帮助,为本项目提供了重要的技术支持。如果您有功能需求或遇到bug,欢迎加入我们的QQ群获取帮助和了解最新动态。',
|
||||
Icons.favorite,
|
||||
Colors.red,
|
||||
delay: 200,
|
||||
),
|
||||
// 合并后的卡片
|
||||
|
||||
const SizedBox(height: 30),
|
||||
// 按钮添加动画效果
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0, end: 1),
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
curve: Curves.elasticOut,
|
||||
builder: (context, value, child) {
|
||||
return Transform.scale(
|
||||
scale: value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.group_add),
|
||||
label: const Text('加入QQ群'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
elevation: 5,
|
||||
),
|
||||
onPressed: () async {
|
||||
final url = 'https://qm.qq.com/q/ErscyNPTzO';
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
await launchUrl(Uri.parse(url));
|
||||
} else {
|
||||
// 无法打开链接时显示提示
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('无法打开QQ群链接')),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// 添加版权信息
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0, end: 1),
|
||||
duration: const Duration(milliseconds: 1200),
|
||||
builder: (context, value, child) {
|
||||
return Opacity(
|
||||
opacity: value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'© ${DateTime.now().year} ASTRAL Team',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAnimatedCard(BuildContext context, String title, String content,
|
||||
IconData icon, Color iconColor,
|
||||
{int delay = 0}) {
|
||||
// 根据当前主题调整颜色
|
||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||
final cardColor = Theme.of(context).cardColor;
|
||||
final textColor = Theme.of(context).textTheme.bodyMedium?.color;
|
||||
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0, end: 1),
|
||||
duration: Duration(milliseconds: 800 + delay),
|
||||
curve: Curves.easeOutBack,
|
||||
builder: (context, value, child) {
|
||||
// 确保 opacity 值在有效范围内 (0.0 到 1.0)
|
||||
final safeOpacity = value.clamp(0.0, 1.0);
|
||||
return Opacity(
|
||||
opacity: safeOpacity,
|
||||
child: Transform.translate(
|
||||
offset: Offset(100 * (1 - value), 0),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Card(
|
||||
elevation: 8,
|
||||
shadowColor: iconColor.withOpacity(isDarkMode ? 0.3 : 0.4),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
cardColor,
|
||||
isDarkMode
|
||||
? cardColor.withOpacity(0.9).withBlue(cardColor.blue + 5)
|
||||
: iconColor.withOpacity(0.05),
|
||||
cardColor,
|
||||
],
|
||||
stops: const [0.0, 0.5, 1.0],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: iconColor.withOpacity(isDarkMode ? 0.05 : 0.1),
|
||||
blurRadius: 10,
|
||||
spreadRadius: -5,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 左侧图标 - 适配深色模式
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: iconColor.withOpacity(isDarkMode ? 0.2 : 0.15),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: iconColor.withOpacity(isDarkMode ? 0.15 : 0.2),
|
||||
blurRadius: isDarkMode ? 6 : 8,
|
||||
spreadRadius: isDarkMode ? 0 : 1,
|
||||
),
|
||||
],
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
iconColor.withOpacity(isDarkMode ? 0.8 : 0.7),
|
||||
iconColor.withOpacity(isDarkMode ? 0.2 : 0.1),
|
||||
],
|
||||
stops: const [0.0, 1.0],
|
||||
radius: 0.8,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// 右侧内容 - 适配深色模式
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
color:
|
||||
iconColor.withOpacity(isDarkMode ? 0.9 : 0.8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Divider(
|
||||
color: iconColor.withOpacity(isDarkMode ? 0.4 : 0.3),
|
||||
thickness: 1.5,
|
||||
endIndent: 60,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
content,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
height: 1.5,
|
||||
letterSpacing: 0.5,
|
||||
color: textColor
|
||||
?.withOpacity(isDarkMode ? 0.9 : 1.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,704 +0,0 @@
|
||||
// 导入必要的包
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/services.dart'; // 添加这一行导入剪贴板服务
|
||||
|
||||
import 'package:ASTRAL/src/rust/api/simple.dart';
|
||||
import 'package:ASTRAL/utils/%E7%8A%B6%E6%80%81.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../widgets/卡片.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
|
||||
/// 玩家信息模型类
|
||||
class PlayerInfo {
|
||||
final String name;
|
||||
final String ip;
|
||||
final int latency; // 延迟(ms)
|
||||
final String connectionType; // 连接类型:直链、中转、本机
|
||||
final int uploadSpeed; // 上传速度(KB/s)
|
||||
final int downloadSpeed; // 下载速度(KB/s)
|
||||
final int sentPackets; // 发送包数量
|
||||
final int receivedPackets; // 接收包数量
|
||||
final double packetLossRate; // 丢包率(%)
|
||||
final String etVersion; // ET版本
|
||||
|
||||
PlayerInfo({
|
||||
required this.name,
|
||||
required this.ip,
|
||||
required this.latency,
|
||||
required this.connectionType,
|
||||
required this.uploadSpeed,
|
||||
required this.downloadSpeed,
|
||||
required this.sentPackets,
|
||||
required this.receivedPackets,
|
||||
required this.packetLossRate,
|
||||
required this.etVersion,
|
||||
});
|
||||
}
|
||||
|
||||
/// 房间页面组件
|
||||
/// 用于显示所有玩家的信息
|
||||
class RoomPage extends StatefulWidget {
|
||||
const RoomPage({super.key});
|
||||
|
||||
@override
|
||||
State<RoomPage> createState() => _RoomPageState();
|
||||
}
|
||||
|
||||
class _RoomPageState extends State<RoomPage> {
|
||||
List<PlayerInfo> players = [];
|
||||
bool isLoading = true;
|
||||
// 添加布局类型状态变量
|
||||
bool isGridLayout = true; // 默认使用网格布局
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
isLoading = true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
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) {
|
||||
// 当 KM 更新时,这个 builder 会被重新调用
|
||||
// 异步处理数据
|
||||
_processNodeData(km);
|
||||
|
||||
if (isLoading) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'加载玩家信息...',
|
||||
style: TextStyle(
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else if (players.isEmpty) {
|
||||
// 添加空数据状态显示
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.people_outline,
|
||||
size: 64,
|
||||
color: colorScheme.primary.withOpacity(0.6),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'暂无房间成员',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'当前没有其他玩家连接到房间',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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列
|
||||
}
|
||||
}
|
||||
|
||||
// 处理节点数据 - 合并了原来的两个相似方法
|
||||
Future<void> _processNodeData(KM km) async {
|
||||
try {
|
||||
final nodes = await km.nodes; // 获取最新的节点信息
|
||||
|
||||
// 将节点数据转换为PlayerInfo对象
|
||||
List<PlayerInfo> nodePlayerInfos = [];
|
||||
|
||||
for (var node in nodes) {
|
||||
// 计算上传下载速度和包数量总和
|
||||
int uploadSpeed = 0;
|
||||
int downloadSpeed = 0;
|
||||
int sentPackets = 0;
|
||||
int receivedPackets = 0;
|
||||
String connectionType =
|
||||
_mapConnectionType(node.cost, node.ipv4, km.virtualIP);
|
||||
|
||||
// 如果有连接信息,计算网络统计数据
|
||||
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
|
||||
sentPackets += conn.txPackets.toInt();
|
||||
receivedPackets += conn.rxPackets.toInt();
|
||||
}
|
||||
}
|
||||
|
||||
// 计算丢包率 (简单估算)
|
||||
double packetLossRate = 0.0;
|
||||
if (sentPackets > 0 && receivedPackets > 0) {
|
||||
packetLossRate = (1.0 - (receivedPackets / sentPackets)).abs() * 100;
|
||||
if (packetLossRate > 100) packetLossRate = 100.0;
|
||||
packetLossRate = double.parse(packetLossRate.toStringAsFixed(1));
|
||||
}
|
||||
|
||||
// 创建PlayerInfo对象
|
||||
nodePlayerInfos.add(
|
||||
PlayerInfo(
|
||||
name: node.hostname,
|
||||
ip: node.ipv4, // 临时IP,实际应从节点信息中获取
|
||||
latency: (node.latencyMs * 1000).toInt(), // 转换为毫秒
|
||||
connectionType: connectionType,
|
||||
uploadSpeed: uploadSpeed,
|
||||
downloadSpeed: downloadSpeed,
|
||||
sentPackets: sentPackets,
|
||||
receivedPackets: receivedPackets,
|
||||
packetLossRate: packetLossRate,
|
||||
etVersion: node.version, // 获取版本信息
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!mounted) return; // 检查组件是否仍然挂载
|
||||
|
||||
setState(() {
|
||||
players = nodePlayerInfos;
|
||||
isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
print("加载节点数据失败: $e");
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建玩家信息卡片
|
||||
Widget _buildPlayerCard(PlayerInfo player, ColorScheme colorScheme) {
|
||||
// 根据延迟值确定颜色
|
||||
Color latencyColor = _getLatencyColor(player.latency);
|
||||
|
||||
// 根据连接类型选择图标
|
||||
IconData connectionIcon = _getConnectionIcon(player.connectionType);
|
||||
|
||||
return FloatingCard(
|
||||
colorScheme: colorScheme,
|
||||
maxWidth: 600,
|
||||
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,
|
||||
),
|
||||
),
|
||||
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: 16),
|
||||
|
||||
// IP地址
|
||||
_buildInfoRow(
|
||||
Icons.lan,
|
||||
'IP地址',
|
||||
player.ip,
|
||||
colorScheme,
|
||||
showCopyButton: true,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 延迟信息
|
||||
_buildInfoRow(
|
||||
Icons.speed,
|
||||
'延迟',
|
||||
'${player.latency} ms',
|
||||
colorScheme,
|
||||
valueColor: latencyColor,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// ET版本
|
||||
_buildInfoRow(
|
||||
Icons.memory,
|
||||
'ET版本',
|
||||
player.etVersion,
|
||||
colorScheme,
|
||||
),
|
||||
|
||||
const Divider(height: 24),
|
||||
|
||||
// 网络数据部分标题
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.data_usage, color: colorScheme.primary, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'网络数据',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 网络数据信息 - 优化对齐方式
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildNetworkDataItemAligned(
|
||||
'上传',
|
||||
'${player.uploadSpeed} KB/s',
|
||||
Icons.upload,
|
||||
colorScheme.primary,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildNetworkDataItemAligned(
|
||||
'下载',
|
||||
'${player.downloadSpeed} KB/s',
|
||||
Icons.download,
|
||||
colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildNetworkDataItemAligned(
|
||||
'发送包',
|
||||
'${player.sentPackets}',
|
||||
Icons.send,
|
||||
colorScheme.primary,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildNetworkDataItemAligned(
|
||||
'接收包',
|
||||
'${player.receivedPackets}',
|
||||
Icons.call_received,
|
||||
colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(height: 24),
|
||||
|
||||
// 丢包率信息
|
||||
_buildInfoRow(
|
||||
Icons.error_outline,
|
||||
'丢包率',
|
||||
'${player.packetLossRate}%',
|
||||
colorScheme,
|
||||
valueColor: _getPacketLossColor(player.packetLossRate),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 构建列表项视图
|
||||
Widget _buildPlayerListItem(PlayerInfo player, ColorScheme colorScheme) {
|
||||
// 根据延迟值确定颜色
|
||||
Color latencyColor = _getLatencyColor(player.latency);
|
||||
// 根据连接类型选择图标
|
||||
IconData connectionIcon = _getConnectionIcon(player.connectionType);
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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),
|
||||
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),
|
||||
|
||||
// 延迟信息
|
||||
_buildInfoRow(
|
||||
Icons.speed,
|
||||
'延迟',
|
||||
'${player.latency} ms',
|
||||
colorScheme,
|
||||
valueColor: latencyColor,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 丢包率信息
|
||||
_buildInfoRow(
|
||||
Icons.error_outline,
|
||||
'丢包率',
|
||||
'${player.packetLossRate}%',
|
||||
colorScheme,
|
||||
valueColor: _getPacketLossColor(player.packetLossRate),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 构建信息行
|
||||
// 构建信息行
|
||||
Widget _buildInfoRow(
|
||||
IconData icon,
|
||||
String label,
|
||||
String value,
|
||||
ColorScheme colorScheme, {
|
||||
Color? valueColor,
|
||||
bool showCopyButton = false,
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 20, color: colorScheme.primary),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'$label:',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 添加复制按钮到标签和值之间
|
||||
if (showCopyButton)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy, size: 18),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
tooltip: '复制$label',
|
||||
onPressed: () {
|
||||
// 复制到剪贴板
|
||||
Clipboard.setData(ClipboardData(text: value));
|
||||
// 显示提示
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('已复制: $value'),
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: valueColor ?? colorScheme.secondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 构建对齐的网络数据项
|
||||
Widget _buildNetworkDataItemAligned(
|
||||
String label,
|
||||
String value,
|
||||
IconData icon,
|
||||
Color color,
|
||||
) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 22, color: color),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 根据延迟值获取颜色
|
||||
Color _getLatencyColor(int latency) {
|
||||
if (latency < 50) {
|
||||
return Colors.green;
|
||||
} else if (latency < 100) {
|
||||
return Colors.orange;
|
||||
} else {
|
||||
return Colors.red;
|
||||
}
|
||||
}
|
||||
|
||||
// 根据丢包率获取颜色
|
||||
Color _getPacketLossColor(double lossRate) {
|
||||
if (lossRate < 1.0) {
|
||||
return Colors.green;
|
||||
} else if (lossRate < 5.0) {
|
||||
return Colors.orange;
|
||||
} else {
|
||||
return Colors.red;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果传入数值=1就是p2p 否则是relay 最后判断是不是等于本机IP如果等于就是direct 本机ip传入
|
||||
String _mapConnectionType(int connType, String ip, String thisip) {
|
||||
// 新增服务器IP判断
|
||||
if (ip == "0.0.0.0") {
|
||||
return '服务器';
|
||||
}
|
||||
// 如果是本机IP,返回direct
|
||||
if (ip == thisip) {
|
||||
return '本机';
|
||||
}
|
||||
// 根据连接成本判断连接类型
|
||||
if (connType == 1) {
|
||||
return '直链';
|
||||
} else if (connType >= 2) {
|
||||
return '中转';
|
||||
}
|
||||
return '未知';
|
||||
}
|
||||
|
||||
// 根据连接类型获取图标
|
||||
IconData _getConnectionIcon(String connectionType) {
|
||||
// 将连接类型转为小写并进行匹配
|
||||
String lowerType = connectionType.toLowerCase();
|
||||
// 新增服务器图标
|
||||
if (lowerType.contains('server') || lowerType.contains('服务器')) {
|
||||
return Icons.dns;
|
||||
} else if (lowerType.contains('p2p') || lowerType.contains('直链')) {
|
||||
return Icons.link;
|
||||
} else if (lowerType.contains('relay') || lowerType.contains('中转')) {
|
||||
return Icons.swap_horiz;
|
||||
} else if (lowerType.contains('direct') || lowerType.contains('本机')) {
|
||||
return Icons.computer;
|
||||
} else {
|
||||
return Icons.device_unknown;
|
||||
}
|
||||
}
|
||||
|
||||
// 根据连接类型获取颜色
|
||||
Color _getConnectionTypeColor(
|
||||
String connectionType, ColorScheme colorScheme) {
|
||||
// 将连接类型转为小写并进行匹配
|
||||
String lowerType = connectionType.toLowerCase();
|
||||
if (lowerType.contains('server') || lowerType.contains('服务器')) {
|
||||
return Colors.deepPurple;
|
||||
} else if (lowerType.contains('p2p') || lowerType.contains('直链')) {
|
||||
return Colors.green;
|
||||
} else if (lowerType.contains('relay') || lowerType.contains('中转')) {
|
||||
return Colors.orange;
|
||||
} else if (lowerType.contains('direct') || lowerType.contains('本机')) {
|
||||
return colorScheme.primary;
|
||||
} else {
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,415 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../config/app_config.dart';
|
||||
import '../utils/ping_util.dart';
|
||||
import 'package:ASTRAL/utils/状态.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class SettingsPage extends StatefulWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsPage> createState() => _SettingsPageState();
|
||||
}
|
||||
|
||||
class _SettingsPageState extends State<SettingsPage> {
|
||||
bool _notificationsEnabled = true;
|
||||
double _fontSize = 16.0;
|
||||
late List<String> _serverList;
|
||||
late String _currentServer;
|
||||
final _appConfig = AppConfig();
|
||||
bool _closeToTray = false; // 添加关闭进入托盘变量
|
||||
|
||||
String serverIP = "";
|
||||
// 添加 ping 相关状态
|
||||
Map<String, int?> pingResults = {};
|
||||
Map<String, bool> isPinging = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_serverList = _appConfig.serverList;
|
||||
_currentServer = _appConfig.currentServer;
|
||||
serverIP = _appConfig.currentServer;
|
||||
_closeToTray = _appConfig.closeToTray; // 初始化托盘设置
|
||||
|
||||
// 初始化 ping 状态
|
||||
for (var server in _serverList) {
|
||||
pingResults[server] = null;
|
||||
isPinging[server] = false;
|
||||
}
|
||||
|
||||
// 开始 ping 当前服务器,并设置为持续 ping
|
||||
_startPingServer(_currentServer, forceContinuous: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// 停止所有 ping
|
||||
for (var server in _serverList) {
|
||||
_stopPingServer(server);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// 修改开始 ping 方法,添加强制持续 ping 参数
|
||||
void _startPingServer(String server, {bool forceContinuous = false}) {
|
||||
if (isPinging[server] == true) return;
|
||||
|
||||
isPinging[server] = true;
|
||||
if (forceContinuous) {
|
||||
isPinging[server] = true; // 设置为持续 ping 状态
|
||||
}
|
||||
_pingServer(server);
|
||||
}
|
||||
|
||||
// 修改停止 ping 方法
|
||||
void _stopPingServer(String server) {
|
||||
// 如果是当前服务器,不允许停止
|
||||
if (server == _currentServer) return;
|
||||
isPinging[server] = false;
|
||||
}
|
||||
|
||||
// 执行 ping 操作
|
||||
Future<void> _pingServer(String server) async {
|
||||
if (isPinging[server] != true) return;
|
||||
|
||||
final pingResult = await PingUtil.ping(server);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
pingResults[server] = pingResult;
|
||||
});
|
||||
|
||||
// 1秒后再次 ping
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
_pingServer(server);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 添加服务器对话框
|
||||
Future<void> _showAddServerDialog() async {
|
||||
final controller = TextEditingController();
|
||||
final result = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('添加服务器'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '服务器地址',
|
||||
hintText: 'example.com:port',
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, controller.text),
|
||||
child: const Text('添加'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null && result.isNotEmpty) {
|
||||
setState(() {
|
||||
_serverList.add(result);
|
||||
_appConfig.setServerList(_serverList);
|
||||
|
||||
// 初始化新服务器的 ping 状态
|
||||
pingResults[result] = null;
|
||||
isPinging[result] = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑服务器对话框
|
||||
Future<void> _showEditServerDialog(int index) async {
|
||||
final controller = TextEditingController(text: _serverList[index]);
|
||||
final result = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('编辑服务器'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '服务器地址',
|
||||
hintText: 'example.com:port',
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, controller.text),
|
||||
child: const Text('保存'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null && result.isNotEmpty) {
|
||||
setState(() {
|
||||
_serverList[index] = result;
|
||||
_appConfig.setServerList(_serverList);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 删除服务器
|
||||
Future<void> _deleteServer(int index) async {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('确认删除'),
|
||||
content: const Text('确定要删除这个服务器吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('删除'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirm == true) {
|
||||
final server = _serverList[index];
|
||||
|
||||
// 停止 ping
|
||||
_stopPingServer(server);
|
||||
|
||||
setState(() {
|
||||
_serverList.removeAt(index);
|
||||
_appConfig.setServerList(_serverList);
|
||||
|
||||
// 移除 ping 状态
|
||||
pingResults.remove(server);
|
||||
isPinging.remove(server);
|
||||
|
||||
if (_currentServer == server && _serverList.isNotEmpty) {
|
||||
_currentServer = _serverList[0];
|
||||
_appConfig.setCurrentServer(_currentServer);
|
||||
_startPingServer(_currentServer);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 添加构建 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return const Text('点击测试', style: TextStyle(color: Colors.grey));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
serverIP = Provider.of<KM>(context).virtualIP;
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: [
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.dns),
|
||||
title: Row(
|
||||
children: [
|
||||
const Text('当前服务器'),
|
||||
const SizedBox(width: 8),
|
||||
_buildPingWidget(_currentServer),
|
||||
],
|
||||
),
|
||||
subtitle: Text(_currentServer),
|
||||
),
|
||||
ExpansionTile(
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
children: [
|
||||
// 在服务器列表前添加当前服务器的 ping 状态显示
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: _serverList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final server = _serverList[index];
|
||||
final pingResult = pingResults[server];
|
||||
|
||||
// 构建延迟显示组件
|
||||
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('点击测试',
|
||||
style: TextStyle(color: Colors.grey));
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.computer),
|
||||
title: Row(
|
||||
children: [
|
||||
Text('服务器 ${index + 1}'),
|
||||
const SizedBox(width: 8),
|
||||
pingWidget,
|
||||
],
|
||||
),
|
||||
subtitle: Text(server),
|
||||
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);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () => _showEditServerDialog(index),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () => _deleteServer(index),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_currentServer = server;
|
||||
Provider.of<KM>(context, listen: false).serverIP =
|
||||
server;
|
||||
// 开始 ping 新选择的服务器
|
||||
_startPingServer(server);
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('已切换到服务器: $server')),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add),
|
||||
title: const Text('添加新服务器'),
|
||||
onTap: _showAddServerDialog,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 添加应用设置卡片
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
const ListTile(
|
||||
leading: Icon(Icons.settings),
|
||||
title: Text('应用设置'),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('关闭时最小化到托盘'),
|
||||
subtitle: const Text('关闭窗口时应用将继续在后台运行'),
|
||||
value: _closeToTray,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_closeToTray = value;
|
||||
_appConfig.setCloseToTray(value);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
const ListTile(
|
||||
leading: Icon(Icons.info),
|
||||
title: Text('应用版本'),
|
||||
subtitle: Text('灰度版本'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.update),
|
||||
title: const Text('检查更新'),
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('灰度版本不支持更新')),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ Future<void> createServer(
|
||||
required String specifiedIp,
|
||||
required String roomName,
|
||||
required String roomPassword,
|
||||
required String severurl}) =>
|
||||
required List<String> severurl}) =>
|
||||
RustLib.instance.api.crateApiSimpleCreateServer(
|
||||
username: username,
|
||||
enableDhcp: enableDhcp,
|
||||
@@ -123,6 +123,8 @@ class KVNodeInfo {
|
||||
final String hostname;
|
||||
final String ipv4;
|
||||
final double latencyMs;
|
||||
final String nat;
|
||||
final double lossRate;
|
||||
final List<KVNodeConnectionStats> connections;
|
||||
final String version;
|
||||
final int cost;
|
||||
@@ -131,6 +133,8 @@ class KVNodeInfo {
|
||||
required this.hostname,
|
||||
required this.ipv4,
|
||||
required this.latencyMs,
|
||||
required this.nat,
|
||||
required this.lossRate,
|
||||
required this.connections,
|
||||
required this.version,
|
||||
required this.cost,
|
||||
@@ -141,6 +145,8 @@ class KVNodeInfo {
|
||||
hostname.hashCode ^
|
||||
ipv4.hashCode ^
|
||||
latencyMs.hashCode ^
|
||||
nat.hashCode ^
|
||||
lossRate.hashCode ^
|
||||
connections.hashCode ^
|
||||
version.hashCode ^
|
||||
cost.hashCode;
|
||||
@@ -153,6 +159,8 @@ class KVNodeInfo {
|
||||
hostname == other.hostname &&
|
||||
ipv4 == other.ipv4 &&
|
||||
latencyMs == other.latencyMs &&
|
||||
nat == other.nat &&
|
||||
lossRate == other.lossRate &&
|
||||
connections == other.connections &&
|
||||
version == other.version &&
|
||||
cost == other.cost;
|
||||
|
||||
@@ -85,7 +85,7 @@ abstract class RustLibApi extends BaseApi {
|
||||
required String specifiedIp,
|
||||
required String roomName,
|
||||
required String roomPassword,
|
||||
required String severurl});
|
||||
required List<String> severurl});
|
||||
|
||||
Future<String> crateApiSimpleEasytierVersion();
|
||||
|
||||
@@ -174,7 +174,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
required String specifiedIp,
|
||||
required String roomName,
|
||||
required String roomPassword,
|
||||
required String severurl}) {
|
||||
required List<String> severurl}) {
|
||||
return handler.executeNormal(NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
@@ -183,7 +183,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
sse_encode_String(specifiedIp, serializer);
|
||||
sse_encode_String(roomName, serializer);
|
||||
sse_encode_String(roomPassword, serializer);
|
||||
sse_encode_String(severurl, serializer);
|
||||
sse_encode_list_String(severurl, serializer);
|
||||
pdeCallFfi(generalizedFrbRustBinding, serializer,
|
||||
funcId: 2, port: port_);
|
||||
},
|
||||
@@ -515,6 +515,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
return raw as bool;
|
||||
}
|
||||
|
||||
@protected
|
||||
double dco_decode_f_32(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
return raw as double;
|
||||
}
|
||||
|
||||
@protected
|
||||
double dco_decode_f_64(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
@@ -558,15 +564,17 @@ 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 != 8)
|
||||
throw Exception('unexpected arr length: expect 8 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]),
|
||||
lossRate: dco_decode_f_32(arr[4]),
|
||||
connections: dco_decode_list_kv_node_connection_stats(arr[5]),
|
||||
version: dco_decode_String(arr[6]),
|
||||
cost: dco_decode_i_32(arr[7]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -603,6 +611,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
.toList();
|
||||
}
|
||||
|
||||
@protected
|
||||
List<String> dco_decode_list_String(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
return (raw as List<dynamic>).map(dco_decode_String).toList();
|
||||
}
|
||||
|
||||
@protected
|
||||
List<KVNodeConnectionStats> dco_decode_list_kv_node_connection_stats(
|
||||
dynamic raw) {
|
||||
@@ -752,6 +766,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
return deserializer.buffer.getUint8() != 0;
|
||||
}
|
||||
|
||||
@protected
|
||||
double sse_decode_f_32(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
return deserializer.buffer.getFloat32();
|
||||
}
|
||||
|
||||
@protected
|
||||
double sse_decode_f_64(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
@@ -795,6 +815,8 @@ 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_lossRate = sse_decode_f_32(deserializer);
|
||||
var var_connections =
|
||||
sse_decode_list_kv_node_connection_stats(deserializer);
|
||||
var var_version = sse_decode_String(deserializer);
|
||||
@@ -803,6 +825,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
hostname: var_hostname,
|
||||
ipv4: var_ipv4,
|
||||
latencyMs: var_latencyMs,
|
||||
nat: var_nat,
|
||||
lossRate: var_lossRate,
|
||||
connections: var_connections,
|
||||
version: var_version,
|
||||
cost: var_cost);
|
||||
@@ -856,6 +880,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
return ans_;
|
||||
}
|
||||
|
||||
@protected
|
||||
List<String> sse_decode_list_String(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
|
||||
var len_ = sse_decode_i_32(deserializer);
|
||||
var ans_ = <String>[];
|
||||
for (var idx_ = 0; idx_ < len_; ++idx_) {
|
||||
ans_.add(sse_decode_String(deserializer));
|
||||
}
|
||||
return ans_;
|
||||
}
|
||||
|
||||
@protected
|
||||
List<KVNodeConnectionStats> sse_decode_list_kv_node_connection_stats(
|
||||
SseDeserializer deserializer) {
|
||||
@@ -1013,6 +1049,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
serializer.buffer.putUint8(self ? 1 : 0);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_f_32(double self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
serializer.buffer.putFloat32(self);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_f_64(double self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
@@ -1050,6 +1092,8 @@ 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_f_32(self.lossRate, serializer);
|
||||
sse_encode_list_kv_node_connection_stats(self.connections, serializer);
|
||||
sse_encode_String(self.version, serializer);
|
||||
sse_encode_i_32(self.cost, serializer);
|
||||
@@ -1091,6 +1135,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_list_String(List<String> self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
sse_encode_i_32(self.length, serializer);
|
||||
for (final item in self) {
|
||||
sse_encode_String(item, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_list_kv_node_connection_stats(
|
||||
List<KVNodeConnectionStats> self, SseSerializer serializer) {
|
||||
|
||||
@@ -77,6 +77,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
bool dco_decode_bool(dynamic raw);
|
||||
|
||||
@protected
|
||||
double dco_decode_f_32(dynamic raw);
|
||||
|
||||
@protected
|
||||
double dco_decode_f_64(dynamic raw);
|
||||
|
||||
@@ -107,6 +110,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
List<String> dco_decode_list_String(dynamic raw);
|
||||
|
||||
@protected
|
||||
List<KVNodeConnectionStats> dco_decode_list_kv_node_connection_stats(
|
||||
dynamic raw);
|
||||
@@ -182,6 +188,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
bool sse_decode_bool(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
double sse_decode_f_32(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
double sse_decode_f_64(SseDeserializer deserializer);
|
||||
|
||||
@@ -213,6 +222,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<String> sse_decode_list_String(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<KVNodeConnectionStats> sse_decode_list_kv_node_connection_stats(
|
||||
SseDeserializer deserializer);
|
||||
@@ -288,6 +300,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
void sse_encode_bool(bool self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_f_32(double self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_f_64(double self, SseSerializer serializer);
|
||||
|
||||
@@ -320,6 +335,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
List<Route> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_String(List<String> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_kv_node_connection_stats(
|
||||
List<KVNodeConnectionStats> self, SseSerializer serializer);
|
||||
@@ -375,7 +393,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 +409,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 +425,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 +441,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 +457,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 +473,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 +489,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 +505,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>)>();
|
||||
|
||||
@@ -79,6 +79,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
bool dco_decode_bool(dynamic raw);
|
||||
|
||||
@protected
|
||||
double dco_decode_f_32(dynamic raw);
|
||||
|
||||
@protected
|
||||
double dco_decode_f_64(dynamic raw);
|
||||
|
||||
@@ -109,6 +112,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
List<String> dco_decode_list_String(dynamic raw);
|
||||
|
||||
@protected
|
||||
List<KVNodeConnectionStats> dco_decode_list_kv_node_connection_stats(
|
||||
dynamic raw);
|
||||
@@ -184,6 +190,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
bool sse_decode_bool(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
double sse_decode_f_32(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
double sse_decode_f_64(SseDeserializer deserializer);
|
||||
|
||||
@@ -215,6 +224,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<String> sse_decode_list_String(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<KVNodeConnectionStats> sse_decode_list_kv_node_connection_stats(
|
||||
SseDeserializer deserializer);
|
||||
@@ -290,6 +302,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
void sse_encode_bool(bool self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_f_32(double self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_f_64(double self, SseSerializer serializer);
|
||||
|
||||
@@ -322,6 +337,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
List<Route> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_String(List<String> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_kv_node_connection_stats(
|
||||
List<KVNodeConnectionStats> self, SseSerializer serializer);
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import 'package:astral/src/rust/api/simple.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../config/app_config.dart';
|
||||
|
||||
class KM extends ChangeNotifier {
|
||||
final _config = AppConfig();
|
||||
int _count = 0;
|
||||
int get count => _count;
|
||||
|
||||
void increment() {
|
||||
_count++;
|
||||
notifyListeners(); // 通知监听器重建UI
|
||||
}
|
||||
|
||||
// 房间名
|
||||
String get roomName => _config.roomName;
|
||||
set roomName(String value) {
|
||||
_config.setRoomName(value);
|
||||
notifyListeners(); // 通知监听器重建UI
|
||||
}
|
||||
|
||||
//房间密码设置
|
||||
String get roomPassword => _config.roomPassword;
|
||||
set roomPassword(String value) {
|
||||
_config.setRoomPassword(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
//用户名设置
|
||||
String get username => _config.username;
|
||||
set username(String value) {
|
||||
_config.setUsername(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
//虚拟IP设置
|
||||
String get virtualIP => _config.virtualIP;
|
||||
set virtualIP(String value) {
|
||||
_config.setVirtualIP(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
//动态获取IP设置
|
||||
bool get dynamicIP => _config.dynamicIP;
|
||||
set dynamicIP(bool value) {
|
||||
_config.setDynamicIP(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
//服务器列表
|
||||
List<Map<String, dynamic>> get serverList => _config.serverList;
|
||||
set serverList(List<Map<String, dynamic>> value) {
|
||||
List<Map<String, dynamic>> convertedList = value.map((item) {
|
||||
return Map<String, dynamic>.from(item);
|
||||
}).toList();
|
||||
_config.setServerList(convertedList);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
//获取选中的服务器
|
||||
//获取选中的服务器
|
||||
List<String> get serverIP {
|
||||
try {
|
||||
final selected =
|
||||
serverList.where((server) => server['selected'] == true).toList();
|
||||
|
||||
if (selected.isEmpty && serverList.isNotEmpty) {
|
||||
final firstServer = serverList.first;
|
||||
if (firstServer['url'] is String) {
|
||||
return [firstServer['url'] as String];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
return selected
|
||||
.where((server) => server['url'] is String)
|
||||
.map((server) => server['url'] as String)
|
||||
.toList();
|
||||
} catch (e) {
|
||||
debugPrint('获取服务器IP时出错: $e');
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
//设置服务器选中状态
|
||||
void setServerSelected(String url, bool selected) {
|
||||
final servers = serverList;
|
||||
for (var i = 0; i < servers.length; i++) {
|
||||
if (servers[i]['url'] == url) {
|
||||
servers[i]['selected'] = selected;
|
||||
}
|
||||
}
|
||||
serverList = servers;
|
||||
}
|
||||
|
||||
// 节点列表
|
||||
List<KVNodeInfo> _nodes = [];
|
||||
List<KVNodeInfo> get nodes => _nodes;
|
||||
set nodes(List<KVNodeInfo> value) {
|
||||
_nodes = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -3,25 +3,28 @@ import 'dart:async';
|
||||
|
||||
class PingUtil {
|
||||
static Future<int?> ping(String host) async {
|
||||
Socket? socket;
|
||||
try {
|
||||
// 从 host:port 格式中提取主机名
|
||||
final hostname = host.split(':')[0];
|
||||
final post = host.split(':')[1];
|
||||
final port = host.split(':')[1]; // 修正变量名 post -> port
|
||||
|
||||
// 使用 Socket 连接来测量实际网络延迟
|
||||
final startTime = DateTime.now();
|
||||
final socket = await Socket.connect(hostname, int.parse(post),
|
||||
socket = await Socket.connect(hostname, int.parse(port),
|
||||
timeout: const Duration(seconds: 2));
|
||||
final endTime = DateTime.now();
|
||||
|
||||
// 关闭连接
|
||||
await socket.close();
|
||||
|
||||
// 计算延迟时间
|
||||
return endTime.difference(startTime).inMilliseconds;
|
||||
} on SocketException {
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
} finally {
|
||||
// 确保在所有情况下都关闭 socket 连接
|
||||
socket?.destroy();
|
||||
socket = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:astral/utils/app_info.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class UpdateChecker {
|
||||
/// GitHub 仓库所有者
|
||||
final String owner;
|
||||
|
||||
/// GitHub 仓库名称
|
||||
final String repo;
|
||||
|
||||
/// 可选:指定检查的分支名称,默认为 'main'
|
||||
final String branch;
|
||||
|
||||
UpdateChecker({
|
||||
required this.owner,
|
||||
required this.repo,
|
||||
this.branch = 'main',
|
||||
});
|
||||
|
||||
/// 检查更新
|
||||
Future<void> scheckForUpdates(BuildContext context) async {
|
||||
try {
|
||||
final releaseInfo = await _fetchLatestRelease();
|
||||
if (releaseInfo == null) {
|
||||
_showUpdateDialog(
|
||||
// 添加空值处理
|
||||
context,
|
||||
'检查更新失败',
|
||||
'无法获取最新版本信息',
|
||||
'https://github.com/$owner/$repo/releases',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取当前应用版本
|
||||
final currentVersion = await _getCurrentVersion();
|
||||
|
||||
// 比较版本号,如果有新版本则显示更新弹窗
|
||||
if (_shouldUpdate(currentVersion, releaseInfo['tag_name'])) {
|
||||
_showUpdateDialog(
|
||||
context,
|
||||
releaseInfo['tag_name'],
|
||||
releaseInfo['body'] ?? '新版本已发布',
|
||||
releaseInfo['html_url'],
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_showUpdateDialog(
|
||||
context,
|
||||
'更新检查失败',
|
||||
'检查更新时发生错误: $e',
|
||||
'https://github.com/$owner/$repo/releases',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> checkForUpdates(BuildContext context) async {
|
||||
try {
|
||||
final releaseInfo = await _fetchLatestRelease();
|
||||
if (releaseInfo == null) {
|
||||
_showUpdateDialog(
|
||||
// 添加空值处理
|
||||
context,
|
||||
'检查更新失败',
|
||||
'无法获取最新版本信息',
|
||||
'https://github.com/$owner/$repo/releases',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取当前应用版本
|
||||
final currentVersion = await _getCurrentVersion();
|
||||
|
||||
// 比较版本号,如果有新版本则显示更新弹窗
|
||||
if (_shouldUpdate(currentVersion, releaseInfo['tag_name'])) {
|
||||
_showUpdateDialog(
|
||||
context,
|
||||
releaseInfo['tag_name'],
|
||||
releaseInfo['body'] ?? '新版本已发布',
|
||||
releaseInfo['html_url'],
|
||||
);
|
||||
} else {
|
||||
_showUpdateDialog(
|
||||
context,
|
||||
'当前已是最新版本',
|
||||
'当前版本为: $currentVersion',
|
||||
'https://github.com/$owner/$repo/releases',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_showUpdateDialog(
|
||||
context,
|
||||
'更新检查失败',
|
||||
'检查更新时发生错误: $e',
|
||||
'https://github.com/$owner/$repo/releases',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取最新发布版本信息
|
||||
Future<Map<String, dynamic>?> _fetchLatestRelease() async {
|
||||
try {
|
||||
// 添加异常捕获
|
||||
final response = await http.get(
|
||||
Uri.parse('https://api.github.com/repos/$owner/$repo/releases'),
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'astral',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> releases = json.decode(response.body);
|
||||
if (releases.isEmpty) return null;
|
||||
|
||||
// 获取第一个发布版本(最新版本)
|
||||
return releases[0];
|
||||
} else {
|
||||
debugPrint('获取最新版本失败: ${response.statusCode}');
|
||||
return {
|
||||
// 返回错误信息
|
||||
'tag_name': '错误 ${response.statusCode}',
|
||||
'body': '请求GitHub API失败',
|
||||
'html_url': 'https://github.com/$owner/$repo/releases'
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('网络请求异常: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取当前应用版本
|
||||
Future<String> _getCurrentVersion() async {
|
||||
try {
|
||||
return AppInfoUtil.getVersion();
|
||||
} catch (e) {
|
||||
debugPrint('获取版本信息失败: $e');
|
||||
return "0.0.0"; // 返回默认版本号避免后续比较崩溃
|
||||
}
|
||||
}
|
||||
|
||||
/// 比较版本号,判断是否需要更新
|
||||
bool _shouldUpdate(String currentVersion, String latestVersion) {
|
||||
// 确保版本号格式正确(添加v前缀如果没有)
|
||||
final current = currentVersion.startsWith('v')
|
||||
? currentVersion.substring(1)
|
||||
: currentVersion;
|
||||
final latest = latestVersion.startsWith('v')
|
||||
? latestVersion.substring(1)
|
||||
: latestVersion;
|
||||
|
||||
// 处理预发布版本标签(如 -alpha, -beta 等)
|
||||
String currentClean = current;
|
||||
String latestClean = latest;
|
||||
|
||||
if (current.contains('-')) {
|
||||
currentClean = current.split('-')[0];
|
||||
}
|
||||
|
||||
if (latest.contains('-')) {
|
||||
latestClean = latest.split('-')[0];
|
||||
}
|
||||
|
||||
// 分割版本号为数组
|
||||
final currentParts = currentClean.split('.');
|
||||
final latestParts = latestClean.split('.');
|
||||
|
||||
// 比较主版本号、次版本号和修订号
|
||||
for (int i = 0; i < 3; i++) {
|
||||
final currentPart =
|
||||
i < currentParts.length ? int.parse(currentParts[i]) : 0;
|
||||
final latestPart = i < latestParts.length ? int.parse(latestParts[i]) : 0;
|
||||
|
||||
if (latestPart > currentPart) {
|
||||
return true;
|
||||
} else if (latestPart < currentPart) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 版本号相同,检查预发布标签
|
||||
if (current.contains('-') && !latest.contains('-')) {
|
||||
// 当前是预发布版本,而最新是正式版本
|
||||
return true;
|
||||
} else if (!current.contains('-') && latest.contains('-')) {
|
||||
// 当前是正式版本,而最新是预发布版本
|
||||
return false;
|
||||
} else if (current.contains('-') && latest.contains('-')) {
|
||||
// 两者都是预发布版本,比较预发布标签
|
||||
final currentPreRelease = current.split('-')[1];
|
||||
final latestPreRelease = latest.split('-')[1];
|
||||
|
||||
// 简单比较预发布标签(alpha < beta < rc)
|
||||
if (currentPreRelease.startsWith('alpha') &&
|
||||
(latestPreRelease.startsWith('beta') ||
|
||||
latestPreRelease.startsWith('rc'))) {
|
||||
return true;
|
||||
} else if (currentPreRelease.startsWith('beta') &&
|
||||
latestPreRelease.startsWith('rc')) {
|
||||
return true;
|
||||
} else if (currentPreRelease == latestPreRelease) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果预发布标签包含数字(如 beta.1, beta.2),则比较数字部分
|
||||
if (currentPreRelease.contains('.') &&
|
||||
latestPreRelease.contains('.') &&
|
||||
currentPreRelease.split('.')[0] == latestPreRelease.split('.')[0]) {
|
||||
try {
|
||||
final currentNum = int.parse(currentPreRelease.split('.')[1]);
|
||||
final latestNum = int.parse(latestPreRelease.split('.')[1]);
|
||||
return latestNum > currentNum;
|
||||
} catch (e) {
|
||||
// 解析失败,返回简单比较结果
|
||||
return latestPreRelease.compareTo(currentPreRelease) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 默认比较预发布标签的字符串
|
||||
return latestPreRelease.compareTo(currentPreRelease) > 0;
|
||||
}
|
||||
|
||||
return false; // 版本相同,不需要更新
|
||||
}
|
||||
|
||||
/// 显示更新弹窗
|
||||
void _showUpdateDialog(
|
||||
BuildContext context,
|
||||
String version,
|
||||
String releaseNotes,
|
||||
String downloadUrl,
|
||||
) {
|
||||
final isLatestVersion = version.contains("当前已是最新版本");
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(isLatestVersion ? version : '发现新版本: $version'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!isLatestVersion) Text('更新内容:'),
|
||||
if (!isLatestVersion) const SizedBox(height: 8),
|
||||
Text(
|
||||
releaseNotes,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('稍后再说'),
|
||||
),
|
||||
if (!isLatestVersion) // 仅在新版本弹窗显示更新按钮
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_launchUrl(downloadUrl);
|
||||
},
|
||||
child: const Text('立即更新'),
|
||||
),
|
||||
if (isLatestVersion) // 最新版本显示确认按钮
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 打开浏览器跳转到下载链接
|
||||
Future<void> _launchUrl(String url) async {
|
||||
final uri = Uri.parse(url);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} else {
|
||||
debugPrint('无法打开链接: $url');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import 'package:ASTRAL/src/rust/api/simple.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../config/app_config.dart';
|
||||
|
||||
class KM extends ChangeNotifier {
|
||||
final _config = AppConfig();
|
||||
int _count = 0;
|
||||
int get count => _count;
|
||||
|
||||
void increment() {
|
||||
_count++;
|
||||
notifyListeners(); // 通知监听器重建UI
|
||||
}
|
||||
|
||||
// 房间名
|
||||
String get roomName => _config.roomName;
|
||||
set roomName(String value) {
|
||||
_config.setRoomName(value);
|
||||
notifyListeners(); // 通知监听器重建UI
|
||||
}
|
||||
|
||||
//房间密码设置
|
||||
String get roomPassword => _config.roomPassword;
|
||||
set roomPassword(String value) {
|
||||
_config.setRoomPassword(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
//用户名设置
|
||||
String get username => _config.username;
|
||||
set username(String value) {
|
||||
_config.setUsername(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
//虚拟IP设置
|
||||
String get virtualIP => _config.virtualIP;
|
||||
set virtualIP(String value) {
|
||||
_config.setVirtualIP(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
//动态获取IP设置
|
||||
bool get dynamicIP => _config.dynamicIP;
|
||||
set dynamicIP(bool value) {
|
||||
_config.setDynamicIP(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
//当前服务器IP
|
||||
String get serverIP => _config.currentServer;
|
||||
set serverIP(String value) {
|
||||
_config.setCurrentServer(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 节点列表
|
||||
List<KVNodeInfo> _nodes = [];
|
||||
List<KVNodeInfo> get nodes => _nodes;
|
||||
set nodes(List<KVNodeInfo> value) {
|
||||
_nodes = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FloatingCard extends StatefulWidget {
|
||||
final ColorScheme colorScheme;
|
||||
final Widget child;
|
||||
final double elevation;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final Duration duration;
|
||||
final double hoverElevation;
|
||||
final double? maxWidth;
|
||||
final double? height;
|
||||
|
||||
const FloatingCard({
|
||||
super.key,
|
||||
required this.colorScheme,
|
||||
required this.child,
|
||||
this.elevation = 4,
|
||||
this.padding = const EdgeInsets.all(16.0),
|
||||
this.duration = const Duration(milliseconds: 200),
|
||||
this.hoverElevation = 8,
|
||||
this.maxWidth,
|
||||
this.height,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FloatingCard> createState() => _FloatingCardState();
|
||||
}
|
||||
|
||||
class _FloatingCardState extends State<FloatingCard> {
|
||||
bool isHovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => isHovered = true),
|
||||
onExit: (_) => setState(() => isHovered = false),
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: widget.maxWidth ?? double.infinity,
|
||||
),
|
||||
child: AnimatedContainer(
|
||||
duration: widget.duration,
|
||||
transformAlignment: Alignment.center,
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: isHovered ? widget.hoverElevation : widget.elevation,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
splashColor: widget.colorScheme.primary.withOpacity(0.1),
|
||||
hoverColor: widget.colorScheme.primary.withOpacity(0.05),
|
||||
onTap: () {
|
||||
// 可以添加点击事件处理
|
||||
},
|
||||
child: Padding(
|
||||
padding: widget.padding,
|
||||
child: SizedBox(
|
||||
height: widget.height,
|
||||
child: widget.child,
|
||||
)),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import '../config/app_config.dart';
|
||||
import 'package:windows_notification/notification_message.dart';
|
||||
import 'package:windows_notification/windows_notification.dart';
|
||||
|
||||
// Create an instance of Windows Notification with your application name
|
||||
// application id must be null in packaged mode
|
||||
final _winNotifyPlugin = WindowsNotification(applicationId: 'Astral');
|
||||
// create new NotificationMessage instance with id, title, body, and images
|
||||
NotificationMessage message = NotificationMessage.fromPluginTemplate(
|
||||
"astral_minimized",
|
||||
"Astral 已最小化到托盘",
|
||||
"应用程序正在后台运行,点击托盘图标可以恢复窗口",
|
||||
// largeImage: "assets/images/icon.ico",
|
||||
// image: file_path
|
||||
);
|
||||
|
||||
class WindowControls extends StatelessWidget {
|
||||
const WindowControls({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
onPressed: () => windowManager.minimize(),
|
||||
tooltip: '最小化',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.crop_square),
|
||||
onPressed: () async {
|
||||
if (await windowManager.isMaximized()) {
|
||||
windowManager.unmaximize();
|
||||
} else {
|
||||
windowManager.maximize();
|
||||
}
|
||||
},
|
||||
tooltip: '最大化/还原',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () async {
|
||||
if (AppConfig().closeToTray) {
|
||||
await windowManager.hide(); // 隐藏主窗口
|
||||
// 替换托盘提示为系统通知
|
||||
_winNotifyPlugin.showNotificationPluginTemplate(message);
|
||||
} else {
|
||||
windowManager.close();
|
||||
}
|
||||
},
|
||||
tooltip: '关闭',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
class FloatingCard extends StatefulWidget {
|
||||
final ColorScheme colorScheme;
|
||||
final Widget child;
|
||||
final double elevation;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final Duration duration;
|
||||
final double hoverElevation;
|
||||
final double? maxWidth; // 添加最大宽度参数
|
||||
final double? height; // 添加最大宽度参数
|
||||
final bool enable3DEffect; // 是否启用3D效果
|
||||
final double maxRotationDegree; // 最大旋转角度
|
||||
final bool enableTranslateEffect; // 是否启用偏移效果
|
||||
final double maxTranslateDistance; // 最大偏移距离
|
||||
final double zTranslation; // Z轴偏移距离
|
||||
final bool riseOnHover; // 控制悬浮时是升起还是降下
|
||||
|
||||
const FloatingCard({
|
||||
super.key,
|
||||
required this.colorScheme,
|
||||
required this.child,
|
||||
this.elevation = 4,
|
||||
this.padding = const EdgeInsets.all(16.0),
|
||||
this.duration = const Duration(milliseconds: 200),
|
||||
this.hoverElevation = 8,
|
||||
this.maxWidth,
|
||||
this.height,
|
||||
this.enable3DEffect = false, // 默认启用3D效果
|
||||
this.maxRotationDegree = 10, // 默认最大旋转角度为10度
|
||||
this.enableTranslateEffect = false, // 默认启用偏移效果
|
||||
this.maxTranslateDistance = 0.1, // 默认最大偏移距离为5
|
||||
this.zTranslation = 10, // 默认Z轴偏移距离为20
|
||||
this.riseOnHover = true, // 默认悬浮时升起
|
||||
});
|
||||
|
||||
@override
|
||||
State<FloatingCard> createState() => _FloatingCardState();
|
||||
}
|
||||
|
||||
class _FloatingCardState extends State<FloatingCard> {
|
||||
bool isHovered = false;
|
||||
Offset mousePosition = Offset.zero;
|
||||
final GlobalKey _cardKey = GlobalKey();
|
||||
|
||||
// 获取卡片的尺寸和位置
|
||||
Rect? _getCardRect() {
|
||||
final RenderBox? renderBox =
|
||||
_cardKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (renderBox == null) return null;
|
||||
final position = renderBox.localToGlobal(Offset.zero);
|
||||
return Rect.fromLTWH(
|
||||
position.dx, position.dy, renderBox.size.width, renderBox.size.height);
|
||||
}
|
||||
|
||||
// 计算旋转角度
|
||||
(double, double) _calculateRotation() {
|
||||
final rect = _getCardRect();
|
||||
if (rect == null) return (0, 0);
|
||||
|
||||
// 计算鼠标相对于卡片中心的位置
|
||||
final centerX = rect.width / 2;
|
||||
final centerY = rect.height / 2;
|
||||
final deltaX = (mousePosition.dx - centerX) / centerX;
|
||||
final deltaY = (mousePosition.dy - centerY) / centerY;
|
||||
|
||||
// 计算旋转角度,鼠标在右侧时向左倾斜(Y轴正向旋转),鼠标在下方时向上倾斜(X轴负向旋转)
|
||||
final rotateY = deltaX * widget.maxRotationDegree;
|
||||
final rotateX = -deltaY * widget.maxRotationDegree;
|
||||
|
||||
return (rotateX, rotateY);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 根据是否启用3D效果计算旋转角度
|
||||
final (rotateX, rotateY) =
|
||||
isHovered && widget.enable3DEffect ? _calculateRotation() : (0.0, 0.0);
|
||||
|
||||
// 根据是否启用偏移效果计算偏移距离
|
||||
final translateX = isHovered && widget.enableTranslateEffect
|
||||
? rotateY * widget.maxTranslateDistance
|
||||
: 0.0;
|
||||
final translateY = isHovered && widget.enableTranslateEffect
|
||||
? rotateX * widget.maxTranslateDistance
|
||||
: 0.0;
|
||||
|
||||
// 根据riseOnHover决定Z轴偏移方向
|
||||
final zDirection = widget.riseOnHover ? 1.0 : -1.0;
|
||||
final translateZ = isHovered && widget.enableTranslateEffect
|
||||
? widget.zTranslation * zDirection
|
||||
: 0.0;
|
||||
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => isHovered = true),
|
||||
onExit: (_) => setState(() => isHovered = false),
|
||||
onHover: (event) {
|
||||
if (widget.enable3DEffect || widget.enableTranslateEffect) {
|
||||
setState(() {
|
||||
mousePosition = event.localPosition;
|
||||
});
|
||||
}
|
||||
},
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: widget.maxWidth ?? double.infinity,
|
||||
),
|
||||
child: AnimatedContainer(
|
||||
key: _cardKey,
|
||||
duration: widget.duration,
|
||||
transform: Matrix4.identity()
|
||||
..setEntry(3, 2, 0.001) // 透视效果
|
||||
// 仅当启用3D效果时应用旋转
|
||||
..rotateX(widget.enable3DEffect ? rotateX * math.pi / 180 : 0)
|
||||
..rotateY(widget.enable3DEffect ? rotateY * math.pi / 180 : 0)
|
||||
// 仅当启用偏移效果时应用偏移
|
||||
..translate(translateX, translateY, translateZ),
|
||||
transformAlignment: Alignment.center,
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: isHovered ? widget.hoverElevation : widget.elevation,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
splashColor: widget.colorScheme.primary.withOpacity(0.1),
|
||||
hoverColor: widget.colorScheme.primary.withOpacity(0.05),
|
||||
onTap: () {
|
||||
// 可以添加点击事件处理
|
||||
},
|
||||
child: Padding(
|
||||
padding: widget.padding,
|
||||
child: SizedBox(
|
||||
height: widget.height,
|
||||
child: widget.child,
|
||||
)),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import '../config/app_config.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
class WindowControls extends StatelessWidget {
|
||||
const WindowControls({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
onPressed: () => windowManager.minimize(),
|
||||
tooltip: '最小化',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.crop_square),
|
||||
onPressed: () async {
|
||||
if (await windowManager.isMaximized()) {
|
||||
windowManager.unmaximize();
|
||||
} else {
|
||||
windowManager.maximize();
|
||||
}
|
||||
},
|
||||
tooltip: '最大化/还原',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () async {
|
||||
if (AppConfig().closeToTray) {
|
||||
await windowManager.hide(); // 隐藏主窗口
|
||||
await trayManager.setToolTip('FLN2N 正在后台运行'); // 设置托盘提示
|
||||
} else {
|
||||
windowManager.close();
|
||||
}
|
||||
},
|
||||
tooltip: '关闭',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
[x] 更换为官方服务器
|
||||
[x] 移除模拟延迟(对其实不用等待2秒的那个是模拟的我给忘了)->改为检测是否成功连接
|
||||
[x] 修复延迟计算:从peer连接获取最小延迟(μs->ms),无效则用路由延迟
|
||||
[x] 管理员问题没权限
|
||||
[x] 配置信息改为运行目录
|
||||
[x] 看nat类型
|
||||
[x] 缩放尺寸让导航栏变为底部会导致页面状态丢失
|
||||
[x] 服务器始终检测延迟,去除暂停和开始反正也不怎么影响性能多此一举还增加复杂度😜
|
||||
[x] 最小化提供通知
|
||||
[x] 可以直接搜索玩家
|
||||
[ ] 增加自动更新检测 和自动更新
|
||||
[ ] 那就打包两个版本 一个便携版(配置文件随软件) 一个安装版(配置文件随软件)
|
||||
+79
-7
@@ -185,6 +185,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dbus
|
||||
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
equatable:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -229,10 +237,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 +263,42 @@ 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_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
sha256: d59eeafd6df92174b1d5f68fc9d66634c97ce2e7cfe2293476236547bb19bbbd
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "19.0.0"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_linux
|
||||
sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_local_notifications_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_platform_interface
|
||||
sha256: "2569b973fc9d1f63a37410a9f7c1c552081226c597190cb359ef5d5762d1631c"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.0.0"
|
||||
flutter_local_notifications_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_windows
|
||||
sha256: f8fc0652a601f83419d623c85723a3e82ad81f92b33eaa9bcc21ea1b94773e6e
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -416,10 +456,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:
|
||||
@@ -532,6 +572,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -808,6 +856,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timezone
|
||||
sha256: ffc9d5f4d1193534ef051f9254063fa53d588609418c84299956c3db9383587d
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.10.0"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -976,6 +1032,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.4.3"
|
||||
windows_notification:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: windows_notification
|
||||
sha256: be3e650874615f315402c9b9f3656e29af156709c4b5cc272cb4ca0ab7ba94a8
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -984,8 +1048,16 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
yaml:
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
yaml:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
|
||||
+8
-5
@@ -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.
|
||||
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.0+1
|
||||
version: 1.0.5
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
@@ -42,13 +42,16 @@ dependencies:
|
||||
flutter_rust_bridge: 2.9.0
|
||||
flutter_colorpicker: ^1.1.0
|
||||
window_manager: ^0.4.3
|
||||
fl_chart: ^0.63.0
|
||||
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
|
||||
flutter_local_notifications: ^19.0.0
|
||||
windows_notification: ^1.3.0
|
||||
fl_chart: ^0.70.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -60,7 +63,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 +73,7 @@ dev_dependencies:
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
assets:
|
||||
- assets/dlls/
|
||||
# - assets/dlls/
|
||||
- assets/icon.ico
|
||||
|
||||
fonts:
|
||||
|
||||
Generated
+11
-1
@@ -389,6 +389,15 @@ dependencies = [
|
||||
"x25519-dalek",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bounded_join_set"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae18fd8f4a623bcf416b5bc8f1e0905534d9911597ed17cc57ab9b6eed65454d"
|
||||
dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.8.0"
|
||||
@@ -954,7 +963,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "easytier"
|
||||
version = "2.2.2"
|
||||
version = "2.2.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
@@ -967,6 +976,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.9.0",
|
||||
"boringtun-easytier",
|
||||
"bounded_join_set",
|
||||
"bytecodec",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
|
||||
Generated
-5274
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ name = "easytier"
|
||||
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
|
||||
homepage = "https://github.com/EasyTier/EasyTier"
|
||||
repository = "https://github.com/EasyTier/EasyTier"
|
||||
version = "2.2.2"
|
||||
version = "2.2.3"
|
||||
edition = "2021"
|
||||
authors = ["kkrainbow"]
|
||||
keywords = ["vpn", "p2p", "network", "easytier"]
|
||||
@@ -192,6 +192,8 @@ http_req = { git = "https://github.com/EasyTier/http_req.git", default-features
|
||||
# for dns connector
|
||||
hickory-resolver = "0.24.4"
|
||||
|
||||
bounded_join_set = "0.3.0"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies]
|
||||
machine-uid = "0.5.3"
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ pub fn gen_default_flags() -> Flags {
|
||||
disable_p2p: false,
|
||||
relay_all_peer_rpc: false,
|
||||
disable_udp_hole_punching: false,
|
||||
ipv6_listener: "udp://[::]:0".to_string(),
|
||||
multi_thread: true,
|
||||
data_compress_algo: CompressionAlgoPb::None.into(),
|
||||
bind_device: true,
|
||||
|
||||
@@ -29,7 +29,7 @@ pub const WIN_SERVICE_WORK_DIR_REG_KEY: &str = "SOFTWARE\\EasyTier\\Service\\Wor
|
||||
|
||||
pub const EASYTIER_VERSION: &str = git_version::git_version!(
|
||||
args = ["--abbrev=8", "--always", "--dirty=~"],
|
||||
prefix = concat!(env!("CARGO_PKG_VERSION"), "-@astral "),
|
||||
prefix = concat!(env!("CARGO_PKG_VERSION"), "-"),
|
||||
suffix = "",
|
||||
fallback = env!("CARGO_PKG_VERSION")
|
||||
fallback = "astral"
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::net::{IpAddr, Ipv6Addr, SocketAddr};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -8,6 +8,8 @@ use crate::proto::common::{NatType, StunInfo};
|
||||
use anyhow::Context;
|
||||
use chrono::Local;
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use hickory_resolver::config::{NameServerConfig, Protocol, ResolverConfig, ResolverOpts};
|
||||
use hickory_resolver::TokioAsyncResolver;
|
||||
use rand::seq::IteratorRandom;
|
||||
use tokio::net::{lookup_host, UdpSocket};
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
@@ -22,21 +24,68 @@ use crate::common::error::Error;
|
||||
|
||||
use super::stun_codec_ext::*;
|
||||
|
||||
pub fn get_default_resolver_config() -> ResolverConfig {
|
||||
let mut default_resolve_config = ResolverConfig::new();
|
||||
default_resolve_config.add_name_server(NameServerConfig::new(
|
||||
"223.5.5.5:53".parse().unwrap(),
|
||||
Protocol::Udp,
|
||||
));
|
||||
default_resolve_config.add_name_server(NameServerConfig::new(
|
||||
"180.184.1.1:53".parse().unwrap(),
|
||||
Protocol::Udp,
|
||||
));
|
||||
default_resolve_config
|
||||
}
|
||||
|
||||
pub async fn resolve_txt_record(
|
||||
domain_name: &str,
|
||||
resolver: &TokioAsyncResolver,
|
||||
) -> Result<String, Error> {
|
||||
let response = resolver.txt_lookup(domain_name).await.with_context(|| {
|
||||
format!(
|
||||
"txt_lookup failed, domain_name: {}",
|
||||
domain_name.to_string()
|
||||
)
|
||||
})?;
|
||||
|
||||
let txt_record = response.iter().next().with_context(|| {
|
||||
format!(
|
||||
"no txt record found, domain_name: {}",
|
||||
domain_name.to_string()
|
||||
)
|
||||
})?;
|
||||
|
||||
let txt_data = String::from_utf8_lossy(&txt_record.txt_data()[0]);
|
||||
tracing::info!(?txt_data, ?domain_name, "get txt record");
|
||||
|
||||
Ok(txt_data.to_string())
|
||||
}
|
||||
|
||||
struct HostResolverIter {
|
||||
hostnames: Vec<String>,
|
||||
ips: Vec<SocketAddr>,
|
||||
max_ip_per_domain: u32,
|
||||
use_ipv6: bool,
|
||||
}
|
||||
|
||||
impl HostResolverIter {
|
||||
fn new(hostnames: Vec<String>, max_ip_per_domain: u32) -> Self {
|
||||
fn new(hostnames: Vec<String>, max_ip_per_domain: u32, use_ipv6: bool) -> Self {
|
||||
Self {
|
||||
hostnames,
|
||||
ips: vec![],
|
||||
max_ip_per_domain,
|
||||
use_ipv6,
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_txt_record(domain_name: &str) -> Result<Vec<String>, Error> {
|
||||
let resolver = TokioAsyncResolver::tokio_from_system_conf().unwrap_or(
|
||||
TokioAsyncResolver::tokio(get_default_resolver_config(), ResolverOpts::default()),
|
||||
);
|
||||
let txt_data = resolve_txt_record(domain_name, &resolver).await?;
|
||||
Ok(txt_data.split(" ").map(|x| x.to_string()).collect())
|
||||
}
|
||||
|
||||
#[async_recursion::async_recursion]
|
||||
async fn next(&mut self) -> Option<SocketAddr> {
|
||||
if self.ips.is_empty() {
|
||||
@@ -51,10 +100,35 @@ impl HostResolverIter {
|
||||
format!("{}:3478", host)
|
||||
};
|
||||
|
||||
if host.starts_with("txt:") {
|
||||
let domain_name = host.trim_start_matches("txt:");
|
||||
match Self::get_txt_record(domain_name).await {
|
||||
Ok(hosts) => {
|
||||
tracing::info!(
|
||||
?domain_name,
|
||||
?hosts,
|
||||
"get txt record success when resolve stun server"
|
||||
);
|
||||
// insert hosts to the head of hostnames
|
||||
self.hostnames.splice(0..0, hosts.into_iter());
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
?domain_name,
|
||||
?e,
|
||||
"get txt record failed when resolve stun server"
|
||||
);
|
||||
}
|
||||
}
|
||||
return self.next().await;
|
||||
}
|
||||
|
||||
let use_ipv6 = self.use_ipv6;
|
||||
|
||||
match lookup_host(&host).await {
|
||||
Ok(ips) => {
|
||||
self.ips = ips
|
||||
.filter(|x| x.is_ipv4())
|
||||
.filter(|x| if use_ipv6 { x.is_ipv6() } else { x.is_ipv4() })
|
||||
.choose_multiple(&mut rand::thread_rng(), self.max_ip_per_domain as usize);
|
||||
|
||||
if self.ips.is_empty() {
|
||||
@@ -400,7 +474,7 @@ impl UdpNatTypeDetectResult {
|
||||
// find resp with distinct stun server
|
||||
self.stun_resps
|
||||
.iter()
|
||||
.map(|x| x.stun_server_addr)
|
||||
.map(|x| x.recv_from_addr)
|
||||
.collect::<BTreeSet<_>>()
|
||||
.len()
|
||||
}
|
||||
@@ -555,8 +629,11 @@ impl UdpNatTypeDetector {
|
||||
udp: Arc<UdpSocket>,
|
||||
) -> Result<UdpNatTypeDetectResult, Error> {
|
||||
let mut stun_servers = vec![];
|
||||
let mut host_resolver =
|
||||
HostResolverIter::new(self.stun_server_hosts.clone(), self.max_ip_per_domain);
|
||||
let mut host_resolver = HostResolverIter::new(
|
||||
self.stun_server_hosts.clone(),
|
||||
self.max_ip_per_domain,
|
||||
false,
|
||||
);
|
||||
while let Some(addr) = host_resolver.next().await {
|
||||
stun_servers.push(addr);
|
||||
}
|
||||
@@ -602,7 +679,9 @@ pub trait StunInfoCollectorTrait: Send + Sync {
|
||||
|
||||
pub struct StunInfoCollector {
|
||||
stun_servers: Arc<RwLock<Vec<String>>>,
|
||||
stun_servers_v6: Arc<RwLock<Vec<String>>>,
|
||||
udp_nat_test_result: Arc<RwLock<Option<UdpNatTypeDetectResult>>>,
|
||||
public_ipv6: Arc<AtomicCell<Option<Ipv6Addr>>>,
|
||||
nat_test_result_time: Arc<AtomicCell<chrono::DateTime<Local>>>,
|
||||
redetect_notify: Arc<tokio::sync::Notify>,
|
||||
tasks: std::sync::Mutex<JoinSet<()>>,
|
||||
@@ -621,7 +700,12 @@ impl StunInfoCollectorTrait for StunInfoCollector {
|
||||
udp_nat_type: result.nat_type() as i32,
|
||||
tcp_nat_type: 0,
|
||||
last_update_time: self.nat_test_result_time.load().timestamp(),
|
||||
public_ip: result.public_ips().iter().map(|x| x.to_string()).collect(),
|
||||
public_ip: result
|
||||
.public_ips()
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.chain(self.public_ipv6.load().map(|x| x.to_string()))
|
||||
.collect(),
|
||||
min_port: result.min_port() as u32,
|
||||
max_port: result.max_port() as u32,
|
||||
}
|
||||
@@ -640,7 +724,7 @@ impl StunInfoCollectorTrait for StunInfoCollector {
|
||||
|
||||
if stun_servers.is_empty() {
|
||||
let mut host_resolver =
|
||||
HostResolverIter::new(self.stun_servers.read().unwrap().clone(), 2);
|
||||
HostResolverIter::new(self.stun_servers.read().unwrap().clone(), 2, false);
|
||||
while let Some(addr) = host_resolver.next().await {
|
||||
stun_servers.push(addr);
|
||||
if stun_servers.len() >= 2 {
|
||||
@@ -680,7 +764,9 @@ impl StunInfoCollector {
|
||||
pub fn new(stun_servers: Vec<String>) -> Self {
|
||||
Self {
|
||||
stun_servers: Arc::new(RwLock::new(stun_servers)),
|
||||
stun_servers_v6: Arc::new(RwLock::new(Self::get_default_servers_v6())),
|
||||
udp_nat_test_result: Arc::new(RwLock::new(None)),
|
||||
public_ipv6: Arc::new(AtomicCell::new(None)),
|
||||
nat_test_result_time: Arc::new(AtomicCell::new(Local::now())),
|
||||
redetect_notify: Arc::new(tokio::sync::Notify::new()),
|
||||
tasks: std::sync::Mutex::new(JoinSet::new()),
|
||||
@@ -696,28 +782,42 @@ impl StunInfoCollector {
|
||||
// NOTICE: we may need to choose stun stun server based on geo location
|
||||
// stun server cross nation may return a external ip address with high latency and loss rate
|
||||
vec![
|
||||
"txt:stun.easytier.cn",
|
||||
"stun.miwifi.com",
|
||||
"stun.chat.bilibili.com",
|
||||
"stun.hitv.com",
|
||||
"stun.cdnbye.com",
|
||||
"stun.douyucdn.cn:18000",
|
||||
"fwa.lifesizecloud.com",
|
||||
"global.turn.twilio.com",
|
||||
"turn.cloudflare.com",
|
||||
"stun.isp.net.au",
|
||||
"stun.nextcloud.com",
|
||||
"stun.freeswitch.org",
|
||||
"stun.voip.blackberry.com",
|
||||
"stunserver.stunprotocol.org",
|
||||
"stun.sipnet.com",
|
||||
"stun.radiojar.com",
|
||||
"stun.sonetel.com",
|
||||
]
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_default_servers_v6() -> Vec<String> {
|
||||
vec!["txt:stun-v6.easytier.cn"]
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn get_public_ipv6(servers: &Vec<String>) -> Option<Ipv6Addr> {
|
||||
let mut ips = HostResolverIter::new(servers.to_vec(), 10, true);
|
||||
while let Some(ip) = ips.next().await {
|
||||
let udp = Arc::new(UdpSocket::bind(format!("[::]:0")).await.unwrap());
|
||||
let ret = StunClientBuilder::new(udp.clone())
|
||||
.new_stun_client(ip)
|
||||
.bind_request(false, false)
|
||||
.await;
|
||||
tracing::debug!(?ret, "finish ipv6 udp nat type detect");
|
||||
match ret.map(|x| x.mapped_socket_addr.map(|x| x.ip())) {
|
||||
Ok(Some(IpAddr::V6(v6))) => {
|
||||
return Some(v6);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn start_stun_routine(&self) {
|
||||
if self.started.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
return;
|
||||
@@ -784,6 +884,30 @@ impl StunInfoCollector {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// for ipv6
|
||||
let stun_servers = self.stun_servers_v6.clone();
|
||||
let stored_ipv6 = self.public_ipv6.clone();
|
||||
let redetect_notify = self.redetect_notify.clone();
|
||||
self.tasks.lock().unwrap().spawn(async move {
|
||||
loop {
|
||||
let servers = stun_servers.read().unwrap().clone();
|
||||
Self::get_public_ipv6(&servers)
|
||||
.await
|
||||
.map(|x| stored_ipv6.store(Some(x)));
|
||||
|
||||
let sleep_sec = if stored_ipv6.load().is_none() {
|
||||
60
|
||||
} else {
|
||||
360
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
_ = redetect_notify.notified() => {}
|
||||
_ = tokio::time::sleep(Duration::from_secs(sleep_sec)) => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn update_stun_info(&self) {
|
||||
@@ -862,6 +986,48 @@ mod tests {
|
||||
let detector = UdpNatTypeDetector::new(stun_servers, 1);
|
||||
let ret = detector.detect_nat_type(0).await;
|
||||
println!("{:#?}, {:?}", ret, ret.as_ref().unwrap().nat_type());
|
||||
assert_eq!(ret.unwrap().nat_type(), NatType::PortRestricted);
|
||||
assert_eq!(ret.unwrap().nat_type(), NatType::Restricted);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_txt_public_stun_server() {
|
||||
let stun_servers = vec!["txt:stun.easytier.cn".to_string()];
|
||||
let detector = UdpNatTypeDetector::new(stun_servers, 1);
|
||||
let ret = detector.detect_nat_type(0).await;
|
||||
println!("{:#?}, {:?}", ret, ret.as_ref().unwrap().nat_type());
|
||||
assert!(!ret.unwrap().stun_resps.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_v4_stun() {
|
||||
let mut udp_server = UdpTunnelListener::new("udp://0.0.0.0:55355".parse().unwrap());
|
||||
let mut tasks = JoinSet::new();
|
||||
tasks.spawn(async move {
|
||||
udp_server.listen().await.unwrap();
|
||||
loop {
|
||||
udp_server.accept().await.unwrap();
|
||||
}
|
||||
});
|
||||
let stun_servers = vec!["127.0.0.1:55355".to_string()];
|
||||
|
||||
let detector = UdpNatTypeDetector::new(stun_servers, 1);
|
||||
let ret = detector.detect_nat_type(0).await;
|
||||
println!("{:#?}, {:?}", ret, ret.as_ref().unwrap().nat_type());
|
||||
assert_eq!(ret.unwrap().nat_type(), NatType::Restricted);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_v6_stun() {
|
||||
let mut udp_server = UdpTunnelListener::new("udp://[::]:55355".parse().unwrap());
|
||||
let mut tasks = JoinSet::new();
|
||||
tasks.spawn(async move {
|
||||
udp_server.listen().await.unwrap();
|
||||
loop {
|
||||
udp_server.accept().await.unwrap();
|
||||
}
|
||||
});
|
||||
let stun_servers = vec!["::1:55355".to_string()];
|
||||
let ret = StunInfoCollector::get_public_ipv6(&stun_servers).await;
|
||||
println!("{:#?}", ret);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// try connect peers directly, with either its public ip or lan ip
|
||||
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
collections::HashSet,
|
||||
net::{Ipv6Addr, SocketAddr},
|
||||
str::FromStr,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
@@ -79,7 +81,6 @@ struct DstListenerUrlBlackListItem(PeerId, url::Url);
|
||||
struct DirectConnectorManagerData {
|
||||
global_ctx: ArcGlobalCtx,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
dst_blacklist: timedmap::TimedMap<DstBlackListItem, ()>,
|
||||
dst_listener_blacklist: timedmap::TimedMap<DstListenerUrlBlackListItem, ()>,
|
||||
}
|
||||
|
||||
@@ -88,7 +89,6 @@ impl DirectConnectorManagerData {
|
||||
Self {
|
||||
global_ctx,
|
||||
peer_manager,
|
||||
dst_blacklist: timedmap::TimedMap::new(),
|
||||
dst_listener_blacklist: timedmap::TimedMap::new(),
|
||||
}
|
||||
}
|
||||
@@ -150,7 +150,9 @@ impl DirectConnectorManager {
|
||||
let peers = data.peer_manager.list_peers().await;
|
||||
let mut tasks = JoinSet::new();
|
||||
for peer_id in peers {
|
||||
if peer_id == my_peer_id {
|
||||
if peer_id == my_peer_id
|
||||
|| data.peer_manager.has_directly_connected_conn(peer_id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
tasks.spawn(Self::do_try_direct_connect(data.clone(), peer_id));
|
||||
@@ -173,24 +175,13 @@ impl DirectConnectorManager {
|
||||
dst_peer_id: PeerId,
|
||||
addr: String,
|
||||
) -> Result<(), Error> {
|
||||
data.dst_blacklist.cleanup();
|
||||
if data
|
||||
.dst_blacklist
|
||||
.contains(&DstBlackListItem(dst_peer_id.clone(), addr.clone()))
|
||||
{
|
||||
tracing::debug!("try_connect_to_ip failed, addr in blacklist: {}", addr);
|
||||
return Err(Error::UrlInBlacklist);
|
||||
}
|
||||
|
||||
let connector = create_connector_by_url(&addr, &data.global_ctx).await?;
|
||||
let (peer_id, conn_id) = timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
data.peer_manager.try_connect(connector),
|
||||
std::time::Duration::from_secs(3),
|
||||
data.peer_manager.try_direct_connect(connector),
|
||||
)
|
||||
.await??;
|
||||
|
||||
// let (peer_id, conn_id) = data.peer_manager.try_connect(connector).await?;
|
||||
|
||||
if peer_id != dst_peer_id && !TESTING.load(Ordering::Relaxed) {
|
||||
tracing::info!(
|
||||
"connect to ip succ: {}, but peer id mismatch, expect: {}, actual: {}",
|
||||
@@ -204,6 +195,7 @@ impl DirectConnectorManager {
|
||||
.await?;
|
||||
return Err(Error::InvalidUrl(addr));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -214,7 +206,7 @@ impl DirectConnectorManager {
|
||||
addr: String,
|
||||
) -> Result<(), Error> {
|
||||
let mut rand_gen = rand::rngs::OsRng::default();
|
||||
let backoff_ms = vec![1000, 2000, 4000];
|
||||
let backoff_ms = vec![1000, 2000];
|
||||
let mut backoff_idx = 0;
|
||||
|
||||
loop {
|
||||
@@ -237,12 +229,6 @@ impl DirectConnectorManager {
|
||||
backoff_idx += 1;
|
||||
continue;
|
||||
} else {
|
||||
data.dst_blacklist.insert(
|
||||
DstBlackListItem(dst_peer_id.clone(), addr.clone()),
|
||||
(),
|
||||
std::time::Duration::from_secs(DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC),
|
||||
);
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@@ -273,61 +259,43 @@ impl DirectConnectorManager {
|
||||
|
||||
tracing::debug!(?available_listeners, "got available listeners");
|
||||
|
||||
let mut listener = available_listeners.get(0).ok_or(anyhow::anyhow!(
|
||||
"peer {} have no valid listener",
|
||||
dst_peer_id
|
||||
))?;
|
||||
if available_listeners.is_empty() {
|
||||
return Err(anyhow::anyhow!("peer {} have no valid listener", dst_peer_id).into());
|
||||
}
|
||||
|
||||
// if have default listener, use it first
|
||||
listener = available_listeners
|
||||
let listener = available_listeners
|
||||
.iter()
|
||||
.find(|l| l.scheme() == data.global_ctx.get_flags().default_protocol)
|
||||
.unwrap_or(listener);
|
||||
.unwrap_or(available_listeners.get(0).unwrap());
|
||||
|
||||
let mut tasks = JoinSet::new();
|
||||
let mut tasks = bounded_join_set::JoinSet::new(2);
|
||||
|
||||
let listener_host = listener.socket_addrs(|| None).unwrap().pop();
|
||||
match listener_host {
|
||||
Some(SocketAddr::V4(s_addr)) => {
|
||||
if s_addr.ip().is_unspecified() {
|
||||
ip_list.interface_ipv4s.iter().for_each(|ip| {
|
||||
let mut addr = (*listener).clone();
|
||||
if addr.set_host(Some(ip.to_string().as_str())).is_ok() {
|
||||
tasks.spawn(Self::try_connect_to_ip(
|
||||
data.clone(),
|
||||
dst_peer_id.clone(),
|
||||
addr.to_string(),
|
||||
));
|
||||
} else {
|
||||
tracing::error!(
|
||||
?ip,
|
||||
?listener,
|
||||
?dst_peer_id,
|
||||
"failed to set host for interface ipv4"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(public_ipv4) = ip_list.public_ipv4 {
|
||||
let mut addr = (*listener).clone();
|
||||
if addr
|
||||
.set_host(Some(public_ipv4.to_string().as_str()))
|
||||
.is_ok()
|
||||
{
|
||||
tasks.spawn(Self::try_connect_to_ip(
|
||||
data.clone(),
|
||||
dst_peer_id.clone(),
|
||||
addr.to_string(),
|
||||
));
|
||||
} else {
|
||||
tracing::error!(
|
||||
?public_ipv4,
|
||||
?listener,
|
||||
?dst_peer_id,
|
||||
"failed to set host for public ipv4"
|
||||
);
|
||||
}
|
||||
}
|
||||
ip_list
|
||||
.interface_ipv4s
|
||||
.iter()
|
||||
.chain(ip_list.public_ipv4.iter())
|
||||
.for_each(|ip| {
|
||||
let mut addr = (*listener).clone();
|
||||
if addr.set_host(Some(ip.to_string().as_str())).is_ok() {
|
||||
tasks.spawn(Self::try_connect_to_ip(
|
||||
data.clone(),
|
||||
dst_peer_id.clone(),
|
||||
addr.to_string(),
|
||||
));
|
||||
} else {
|
||||
tracing::error!(
|
||||
?ip,
|
||||
?listener,
|
||||
?dst_peer_id,
|
||||
"failed to set host for interface ipv4"
|
||||
);
|
||||
}
|
||||
});
|
||||
} else if !s_addr.ip().is_loopback() || TESTING.load(Ordering::Relaxed) {
|
||||
tasks.spawn(Self::try_connect_to_ip(
|
||||
data.clone(),
|
||||
@@ -338,47 +306,42 @@ impl DirectConnectorManager {
|
||||
}
|
||||
Some(SocketAddr::V6(s_addr)) => {
|
||||
if s_addr.ip().is_unspecified() {
|
||||
ip_list.interface_ipv6s.iter().for_each(|ip| {
|
||||
let mut addr = (*listener).clone();
|
||||
if addr
|
||||
.set_host(Some(format!("[{}]", ip.to_string()).as_str()))
|
||||
.is_ok()
|
||||
{
|
||||
tasks.spawn(Self::try_connect_to_ip(
|
||||
data.clone(),
|
||||
dst_peer_id.clone(),
|
||||
addr.to_string(),
|
||||
));
|
||||
} else {
|
||||
tracing::error!(
|
||||
?ip,
|
||||
?listener,
|
||||
?dst_peer_id,
|
||||
"failed to set host for interface ipv6"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(public_ipv6) = ip_list.public_ipv6 {
|
||||
let mut addr = (*listener).clone();
|
||||
if addr
|
||||
.set_host(Some(format!("[{}]", public_ipv6.to_string()).as_str()))
|
||||
.is_ok()
|
||||
{
|
||||
tasks.spawn(Self::try_connect_to_ip(
|
||||
data.clone(),
|
||||
dst_peer_id.clone(),
|
||||
addr.to_string(),
|
||||
));
|
||||
} else {
|
||||
tracing::error!(
|
||||
?public_ipv6,
|
||||
?listener,
|
||||
?dst_peer_id,
|
||||
"failed to set host for public ipv6"
|
||||
);
|
||||
}
|
||||
}
|
||||
// for ipv6, only try public ip
|
||||
ip_list
|
||||
.interface_ipv6s
|
||||
.iter()
|
||||
.chain(ip_list.public_ipv6.iter())
|
||||
.filter_map(|x| Ipv6Addr::from_str(&x.to_string()).ok())
|
||||
.filter(|x| {
|
||||
TESTING.load(Ordering::Relaxed)
|
||||
|| (!x.is_loopback()
|
||||
&& !x.is_unspecified()
|
||||
&& !x.is_unique_local()
|
||||
&& !x.is_unicast_link_local()
|
||||
&& !x.is_multicast())
|
||||
})
|
||||
.collect::<HashSet<_>>()
|
||||
.iter()
|
||||
.for_each(|ip| {
|
||||
let mut addr = (*listener).clone();
|
||||
if addr
|
||||
.set_host(Some(format!("[{}]", ip.to_string()).as_str()))
|
||||
.is_ok()
|
||||
{
|
||||
tasks.spawn(Self::try_connect_to_ip(
|
||||
data.clone(),
|
||||
dst_peer_id.clone(),
|
||||
addr.to_string(),
|
||||
));
|
||||
} else {
|
||||
tracing::error!(
|
||||
?ip,
|
||||
?listener,
|
||||
?dst_peer_id,
|
||||
"failed to set host for public ipv6"
|
||||
);
|
||||
}
|
||||
});
|
||||
} else if !s_addr.ip().is_loopback() || TESTING.load(Ordering::Relaxed) {
|
||||
tasks.spawn(Self::try_connect_to_ip(
|
||||
data.clone(),
|
||||
@@ -430,14 +393,6 @@ impl DirectConnectorManager {
|
||||
dst_peer_id: PeerId,
|
||||
) -> Result<(), Error> {
|
||||
let peer_manager = data.peer_manager.clone();
|
||||
// check if we have direct connection with dst_peer_id
|
||||
if let Some(c) = peer_manager.list_peer_conns(dst_peer_id).await {
|
||||
// currently if we have any type of direct connection (udp or tcp), we will not try to connect
|
||||
if !c.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!("try direct connect to peer: {}", dst_peer_id);
|
||||
|
||||
let rpc_stub = peer_manager
|
||||
@@ -466,8 +421,7 @@ mod tests {
|
||||
|
||||
use crate::{
|
||||
connector::direct::{
|
||||
DirectConnectorManager, DirectConnectorManagerData, DstBlackListItem,
|
||||
DstListenerUrlBlackListItem,
|
||||
DirectConnectorManager, DirectConnectorManagerData, DstListenerUrlBlackListItem,
|
||||
},
|
||||
instance::listeners::ListenerManager,
|
||||
peers::tests::{
|
||||
@@ -526,9 +480,7 @@ mod tests {
|
||||
#[values("tcp", "udp", "wg")] proto: &str,
|
||||
#[values("true", "false")] ipv6: bool,
|
||||
) {
|
||||
if ipv6 && proto != "udp" {
|
||||
return;
|
||||
}
|
||||
TESTING.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
let p_a = create_mock_peer_manager().await;
|
||||
let p_b = create_mock_peer_manager().await;
|
||||
@@ -544,14 +496,18 @@ mod tests {
|
||||
dm_a.run_as_client();
|
||||
dm_c.run_as_server();
|
||||
|
||||
let port = if proto == "wg" { 11040 } else { 11041 };
|
||||
if !ipv6 {
|
||||
let port = if proto == "wg" { 11040 } else { 11041 };
|
||||
p_c.get_global_ctx().config.set_listeners(vec![format!(
|
||||
"{}://0.0.0.0:{}",
|
||||
proto, port
|
||||
)
|
||||
.parse()
|
||||
.unwrap()]);
|
||||
} else {
|
||||
p_c.get_global_ctx()
|
||||
.config
|
||||
.set_listeners(vec![format!("{}://[::]:{}", proto, port).parse().unwrap()]);
|
||||
}
|
||||
let mut f = p_c.get_global_ctx().config.get_flags();
|
||||
f.enable_ipv6 = ipv6;
|
||||
@@ -592,9 +548,5 @@ mod tests {
|
||||
1,
|
||||
"tcp://127.0.0.1:10222".parse().unwrap()
|
||||
)));
|
||||
|
||||
assert!(data
|
||||
.dst_blacklist
|
||||
.contains(&DstBlackListItem(1, ip_list.listeners[0].to_string())));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
common::{error::Error, global_ctx::ArcGlobalCtx},
|
||||
tunnel::{Tunnel, TunnelConnector, TunnelError, PROTO_PORT_OFFSET},
|
||||
common::{
|
||||
error::Error,
|
||||
global_ctx::ArcGlobalCtx,
|
||||
stun::{get_default_resolver_config, resolve_txt_record},
|
||||
},
|
||||
tunnel::{IpVersion, Tunnel, TunnelConnector, TunnelError, PROTO_PORT_OFFSET},
|
||||
};
|
||||
use anyhow::Context;
|
||||
use dashmap::DashSet;
|
||||
use hickory_resolver::{
|
||||
config::{NameServerConfig, Protocol, ResolverConfig, ResolverOpts},
|
||||
config::{ResolverConfig, ResolverOpts},
|
||||
proto::rr::rdata::SRV,
|
||||
TokioAsyncResolver,
|
||||
};
|
||||
@@ -38,6 +42,7 @@ pub struct DNSTunnelConnector {
|
||||
addr: url::Url,
|
||||
bind_addrs: Vec<SocketAddr>,
|
||||
global_ctx: ArcGlobalCtx,
|
||||
ip_version: IpVersion,
|
||||
|
||||
default_resolve_config: ResolverConfig,
|
||||
default_resolve_opts: ResolverOpts,
|
||||
@@ -45,21 +50,13 @@ pub struct DNSTunnelConnector {
|
||||
|
||||
impl DNSTunnelConnector {
|
||||
pub fn new(addr: url::Url, global_ctx: ArcGlobalCtx) -> Self {
|
||||
let mut default_resolve_config = ResolverConfig::new();
|
||||
default_resolve_config.add_name_server(NameServerConfig::new(
|
||||
"223.5.5.5:53".parse().unwrap(),
|
||||
Protocol::Udp,
|
||||
));
|
||||
default_resolve_config.add_name_server(NameServerConfig::new(
|
||||
"180.184.1.1:53".parse().unwrap(),
|
||||
Protocol::Udp,
|
||||
));
|
||||
Self {
|
||||
addr,
|
||||
bind_addrs: Vec::new(),
|
||||
global_ctx,
|
||||
ip_version: IpVersion::Both,
|
||||
|
||||
default_resolve_config,
|
||||
default_resolve_config: get_default_resolver_config(),
|
||||
default_resolve_opts: ResolverOpts::default(),
|
||||
}
|
||||
}
|
||||
@@ -69,26 +66,14 @@ impl DNSTunnelConnector {
|
||||
&self,
|
||||
domain_name: &str,
|
||||
) -> Result<Box<dyn TunnelConnector>, Error> {
|
||||
let resolver = TokioAsyncResolver::tokio_from_system_conf().unwrap_or(
|
||||
TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default()),
|
||||
);
|
||||
|
||||
let response = resolver.txt_lookup(domain_name).await.with_context(|| {
|
||||
format!(
|
||||
"txt_lookup failed, domain_name: {}",
|
||||
domain_name.to_string()
|
||||
)
|
||||
})?;
|
||||
|
||||
let txt_record = response.iter().next().with_context(|| {
|
||||
format!(
|
||||
"no txt record found, domain_name: {}",
|
||||
domain_name.to_string()
|
||||
)
|
||||
})?;
|
||||
|
||||
let txt_data = String::from_utf8_lossy(&txt_record.txt_data()[0]);
|
||||
tracing::info!(?txt_data, ?domain_name, "get txt record");
|
||||
let resolver =
|
||||
TokioAsyncResolver::tokio_from_system_conf().unwrap_or(TokioAsyncResolver::tokio(
|
||||
self.default_resolve_config.clone(),
|
||||
self.default_resolve_opts.clone(),
|
||||
));
|
||||
let txt_data = resolve_txt_record(domain_name, &resolver)
|
||||
.await
|
||||
.with_context(|| format!("resolve txt record failed, domain_name: {}", domain_name))?;
|
||||
|
||||
let candidate_urls = txt_data
|
||||
.split(" ")
|
||||
@@ -106,9 +91,9 @@ impl DNSTunnelConnector {
|
||||
)
|
||||
})?;
|
||||
|
||||
let connector = create_connector_by_url(url.as_str(), &self.global_ctx).await;
|
||||
|
||||
connector
|
||||
let mut connector = create_connector_by_url(url.as_str(), &self.global_ctx).await?;
|
||||
connector.set_ip_version(self.ip_version);
|
||||
Ok(connector)
|
||||
}
|
||||
|
||||
fn handle_one_srv_record(record: &SRV, protocol: &str) -> Result<(url::Url, u64), Error> {
|
||||
@@ -141,9 +126,11 @@ impl DNSTunnelConnector {
|
||||
) -> Result<Box<dyn TunnelConnector>, Error> {
|
||||
tracing::info!("handle_srv_record: {}", domain_name);
|
||||
|
||||
let resolver = TokioAsyncResolver::tokio_from_system_conf().unwrap_or(
|
||||
TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default()),
|
||||
);
|
||||
let resolver =
|
||||
TokioAsyncResolver::tokio_from_system_conf().unwrap_or(TokioAsyncResolver::tokio(
|
||||
self.default_resolve_config.clone(),
|
||||
self.default_resolve_opts.clone(),
|
||||
));
|
||||
|
||||
let srv_domains = PROTO_PORT_OFFSET
|
||||
.iter()
|
||||
@@ -192,8 +179,9 @@ impl DNSTunnelConnector {
|
||||
)
|
||||
})?;
|
||||
|
||||
let connector = create_connector_by_url(url.as_str(), &self.global_ctx).await;
|
||||
connector
|
||||
let mut connector = create_connector_by_url(url.as_str(), &self.global_ctx).await?;
|
||||
connector.set_ip_version(self.ip_version);
|
||||
Ok(connector)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +226,10 @@ impl super::TunnelConnector for DNSTunnelConnector {
|
||||
fn set_bind_addrs(&mut self, addrs: Vec<SocketAddr>) {
|
||||
self.bind_addrs = addrs;
|
||||
}
|
||||
|
||||
fn set_ip_version(&mut self, ip_version: IpVersion) {
|
||||
self.ip_version = ip_version;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -293,7 +293,6 @@ impl ManualConnectorManager {
|
||||
ip_version: IpVersion,
|
||||
) -> Result<ReconnResult, Error> {
|
||||
let ip_collector = data.global_ctx.get_ip_collector();
|
||||
let net_ns = data.net_ns.clone();
|
||||
|
||||
connector.lock().await.set_ip_version(ip_version);
|
||||
|
||||
@@ -309,18 +308,11 @@ impl ManualConnectorManager {
|
||||
data.global_ctx.issue_event(GlobalCtxEvent::Connecting(
|
||||
connector.lock().await.remote_url().clone(),
|
||||
));
|
||||
|
||||
let _g = net_ns.guard();
|
||||
tracing::info!("reconnect try connect... conn: {:?}", connector);
|
||||
let tunnel = connector.lock().await.connect().await?;
|
||||
tracing::info!("reconnect get tunnel succ: {:?}", tunnel);
|
||||
assert_eq!(
|
||||
dead_url,
|
||||
tunnel.info().unwrap().remote_addr.unwrap().to_string(),
|
||||
"info: {:?}",
|
||||
tunnel.info()
|
||||
);
|
||||
let (peer_id, conn_id) = data.peer_manager.add_client_tunnel(tunnel).await?;
|
||||
let (peer_id, conn_id) = data
|
||||
.peer_manager
|
||||
.try_direct_connect(connector.lock().await.as_mut())
|
||||
.await?;
|
||||
tracing::info!("reconnect succ: {} {} {}", peer_id, conn_id, dead_url);
|
||||
Ok(ReconnResult {
|
||||
dead_url,
|
||||
|
||||
@@ -388,7 +388,7 @@ impl UdpHolePunchListener {
|
||||
tracing::warn!(?conn, "udp hole punching listener got peer connection");
|
||||
let peer_mgr = peer_mgr.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = peer_mgr.add_tunnel_as_server(conn).await {
|
||||
if let Err(e) = peer_mgr.add_tunnel_as_server(conn, false).await {
|
||||
tracing::error!(
|
||||
?e,
|
||||
"failed to add tunnel as server in hole punch listener"
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
use std::{
|
||||
ffi::OsString, fmt::Write, net::SocketAddr, path::PathBuf, sync::Mutex, time::Duration, vec,
|
||||
ffi::OsString,
|
||||
fmt::Write,
|
||||
net::{IpAddr, SocketAddr},
|
||||
path::PathBuf,
|
||||
sync::Mutex,
|
||||
time::Duration,
|
||||
vec,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Ok};
|
||||
use anyhow::Context;
|
||||
use clap::{command, Args, Parser, Subcommand};
|
||||
use humansize::format_size;
|
||||
use service_manager::*;
|
||||
@@ -311,7 +317,11 @@ impl CommandHandler {
|
||||
ipv4: route.ipv4_addr.map(|ip| ip.to_string()).unwrap_or_default(),
|
||||
hostname: route.hostname.clone(),
|
||||
cost: cost_to_str(route.cost),
|
||||
lat_ms: float_to_str(p.get_latency_ms().unwrap_or(0.0), 3),
|
||||
lat_ms: if route.cost == 1 {
|
||||
float_to_str(p.get_latency_ms().unwrap_or(0.0), 3)
|
||||
} else {
|
||||
route.path_latency_latency_first().to_string()
|
||||
},
|
||||
loss_rate: float_to_str(p.get_loss_rate().unwrap_or(0.0), 3),
|
||||
rx_bytes: format_size(p.get_rx_bytes().unwrap_or(0), humansize::DECIMAL),
|
||||
tx_bytes: format_size(p.get_tx_bytes().unwrap_or(0), humansize::DECIMAL),
|
||||
@@ -1036,6 +1046,7 @@ async fn main() -> Result<(), Error> {
|
||||
match sub_cmd.sub_command {
|
||||
Some(NodeSubCommand::Info) | None => {
|
||||
let stun_info = node_info.stun_info.clone().unwrap_or_default();
|
||||
let ip_list = node_info.ip_list.clone().unwrap_or_default();
|
||||
|
||||
let mut builder = tabled::builder::Builder::default();
|
||||
builder.push_record(vec!["Virtual IP", node_info.ipv4_addr.as_str()]);
|
||||
@@ -1045,11 +1056,32 @@ async fn main() -> Result<(), Error> {
|
||||
node_info.proxy_cidrs.join(", ").as_str(),
|
||||
]);
|
||||
builder.push_record(vec!["Peer ID", node_info.peer_id.to_string().as_str()]);
|
||||
builder.push_record(vec!["Public IP", stun_info.public_ip.join(", ").as_str()]);
|
||||
stun_info.public_ip.iter().for_each(|ip| {
|
||||
let Ok(ip) = ip.parse::<IpAddr>() else {
|
||||
return;
|
||||
};
|
||||
if ip.is_ipv4() {
|
||||
builder.push_record(vec!["Public IPv4", ip.to_string().as_str()]);
|
||||
} else {
|
||||
builder.push_record(vec!["Public IPv6", ip.to_string().as_str()]);
|
||||
}
|
||||
});
|
||||
builder.push_record(vec![
|
||||
"UDP Stun Type",
|
||||
format!("{:?}", stun_info.udp_nat_type()).as_str(),
|
||||
]);
|
||||
ip_list.interface_ipv4s.iter().for_each(|ip| {
|
||||
builder.push_record(vec![
|
||||
"Interface IPv4",
|
||||
format!("{}", ip.to_string()).as_str(),
|
||||
]);
|
||||
});
|
||||
ip_list.interface_ipv6s.iter().for_each(|ip| {
|
||||
builder.push_record(vec![
|
||||
"Interface IPv6",
|
||||
format!("{}", ip.to_string()).as_str(),
|
||||
]);
|
||||
});
|
||||
for (idx, l) in node_info.listeners.iter().enumerate() {
|
||||
if l.starts_with("ring") {
|
||||
continue;
|
||||
|
||||
@@ -308,12 +308,6 @@ struct Cli {
|
||||
)]
|
||||
socks5: Option<u16>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.ipv6_listener").to_string()
|
||||
)]
|
||||
ipv6_listener: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.compression").to_string(),
|
||||
@@ -576,11 +570,6 @@ impl TryFrom<&Cli> for TomlConfigLoader {
|
||||
f.disable_p2p = cli.disable_p2p;
|
||||
f.disable_udp_hole_punching = cli.disable_udp_hole_punching;
|
||||
f.relay_all_peer_rpc = cli.relay_all_peer_rpc;
|
||||
if let Some(ipv6_listener) = cli.ipv6_listener.as_ref() {
|
||||
f.ipv6_listener = ipv6_listener
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse ipv6 listener: {}", ipv6_listener))?
|
||||
}
|
||||
f.multi_thread = cli.multi_thread;
|
||||
f.data_compress_algo = match cli.compression.as_str() {
|
||||
"none" => CompressionAlgoPb::None,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use anyhow::Context;
|
||||
use async_trait::async_trait;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
@@ -49,6 +50,10 @@ pub fn get_listener_by_url(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_url_host_ipv6(l: &url::Url) -> bool {
|
||||
l.host_str().map_or(false, |h| h.contains(':'))
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait TunnelHandlerForListener {
|
||||
async fn handle_tunnel(&self, tunnel: Box<dyn Tunnel>) -> Result<(), Error>;
|
||||
@@ -58,7 +63,7 @@ pub trait TunnelHandlerForListener {
|
||||
impl TunnelHandlerForListener for PeerManager {
|
||||
#[tracing::instrument]
|
||||
async fn handle_tunnel(&self, tunnel: Box<dyn Tunnel>) -> Result<(), Error> {
|
||||
self.add_tunnel_as_server(tunnel).await
|
||||
self.add_tunnel_as_server(tunnel, true).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,22 +118,26 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
|
||||
continue;
|
||||
};
|
||||
let ctx = self.global_ctx.clone();
|
||||
self.add_listener(move || get_listener_by_url(&l, ctx.clone()).unwrap(), true)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if self.global_ctx.config.get_flags().enable_ipv6 {
|
||||
let ipv6_listener = self.global_ctx.config.get_flags().ipv6_listener.clone();
|
||||
let _ = self
|
||||
.add_listener(
|
||||
move || {
|
||||
Box::new(UdpTunnelListener::new(
|
||||
ipv6_listener.clone().parse().unwrap(),
|
||||
))
|
||||
},
|
||||
let listener = l.clone();
|
||||
self.add_listener(
|
||||
move || get_listener_by_url(&listener, ctx.clone()).unwrap(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if self.global_ctx.config.get_flags().enable_ipv6 && !is_url_host_ipv6(&l) {
|
||||
let mut ipv6_listener = l.clone();
|
||||
ipv6_listener
|
||||
.set_host(Some("[::]".to_string().as_str()))
|
||||
.with_context(|| format!("failed to set ipv6 host for listener: {}", l))?;
|
||||
let ctx = self.global_ctx.clone();
|
||||
self.add_listener(
|
||||
move || get_listener_by_url(&ipv6_listener, ctx.clone()).unwrap(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -161,11 +170,11 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
|
||||
global_ctx.issue_event(GlobalCtxEvent::ListenerAdded(l.local_url()));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(?e, ?l, "listener listen error");
|
||||
global_ctx.issue_event(GlobalCtxEvent::ListenerAddFailed(
|
||||
l.local_url(),
|
||||
format!("error: {:?}, retry listen later...", e),
|
||||
));
|
||||
tracing::error!(?e, ?l, "listener listen error");
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
continue;
|
||||
}
|
||||
@@ -217,6 +226,15 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
|
||||
|
||||
pub async fn run(&mut self) -> Result<(), Error> {
|
||||
for listener in &self.listeners {
|
||||
if listener.must_succ {
|
||||
// try listen once
|
||||
let mut l = (listener.creator_fn)();
|
||||
let _g = self.net_ns.guard();
|
||||
l.listen()
|
||||
.await
|
||||
.with_context(|| format!("failed to listen on {}", l.local_url()))?;
|
||||
}
|
||||
|
||||
self.tasks.spawn(Self::run_listener(
|
||||
listener.creator_fn.clone(),
|
||||
self.peer_manager.clone(),
|
||||
|
||||
@@ -695,7 +695,8 @@ mod tests {
|
||||
|
||||
let (a_ring, b_ring) = crate::tunnel::ring::create_ring_tunnel_pair();
|
||||
let b_mgr_copy = pm_center.clone();
|
||||
let s_ret = tokio::spawn(async move { b_mgr_copy.add_tunnel_as_server(b_ring).await });
|
||||
let s_ret =
|
||||
tokio::spawn(async move { b_mgr_copy.add_tunnel_as_server(b_ring, true).await });
|
||||
|
||||
pma_net1.add_client_tunnel(a_ring).await.unwrap();
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use super::{
|
||||
peer_conn::{PeerConn, PeerConnId},
|
||||
PacketRecvChan,
|
||||
};
|
||||
use crate::proto::cli::PeerConnInfo;
|
||||
use crate::{common::scoped_task::ScopedTask, proto::cli::PeerConnInfo};
|
||||
use crate::{
|
||||
common::{
|
||||
error::Error,
|
||||
@@ -36,7 +36,8 @@ pub struct Peer {
|
||||
|
||||
shutdown_notifier: Arc<tokio::sync::Notify>,
|
||||
|
||||
default_conn_id: AtomicCell<PeerConnId>,
|
||||
default_conn_id: Arc<AtomicCell<PeerConnId>>,
|
||||
default_conn_id_clear_task: ScopedTask<()>,
|
||||
}
|
||||
|
||||
impl Peer {
|
||||
@@ -88,6 +89,19 @@ impl Peer {
|
||||
)),
|
||||
);
|
||||
|
||||
let default_conn_id = Arc::new(AtomicCell::new(PeerConnId::default()));
|
||||
|
||||
let conns_copy = conns.clone();
|
||||
let default_conn_id_copy = default_conn_id.clone();
|
||||
let default_conn_id_clear_task = ScopedTask::from(tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
if conns_copy.len() > 1 {
|
||||
default_conn_id_copy.store(PeerConnId::default());
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
Peer {
|
||||
peer_node_id,
|
||||
conns: conns.clone(),
|
||||
@@ -98,7 +112,8 @@ impl Peer {
|
||||
close_event_listener,
|
||||
|
||||
shutdown_notifier,
|
||||
default_conn_id: AtomicCell::new(PeerConnId::default()),
|
||||
default_conn_id,
|
||||
default_conn_id_clear_task,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,14 +132,19 @@ impl Peer {
|
||||
return Some(conn.clone());
|
||||
}
|
||||
|
||||
let conn = self.conns.iter().next();
|
||||
if conn.is_none() {
|
||||
return None;
|
||||
// find a conn with the smallest latency
|
||||
let mut min_latency = std::u64::MAX;
|
||||
for conn in self.conns.iter() {
|
||||
let latency = conn.value().get_stats().latency_us;
|
||||
if latency < min_latency {
|
||||
min_latency = latency;
|
||||
self.default_conn_id.store(conn.get_conn_id());
|
||||
}
|
||||
}
|
||||
|
||||
let conn = conn.unwrap().clone();
|
||||
self.default_conn_id.store(conn.get_conn_id());
|
||||
Some(conn)
|
||||
self.conns
|
||||
.get(&self.default_conn_id.load())
|
||||
.map(|conn| conn.clone())
|
||||
}
|
||||
|
||||
pub async fn send_msg(&self, msg: ZCPacket) -> Result<(), Error> {
|
||||
@@ -158,6 +178,10 @@ impl Peer {
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn get_default_conn_id(&self) -> PeerConnId {
|
||||
self.default_conn_id.load()
|
||||
}
|
||||
}
|
||||
|
||||
// pritn on drop
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::{
|
||||
use anyhow::Context;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use dashmap::DashMap;
|
||||
use dashmap::{DashMap, DashSet};
|
||||
|
||||
use tokio::{
|
||||
sync::{
|
||||
@@ -23,7 +23,7 @@ use crate::{
|
||||
compressor::{Compressor as _, DefaultCompressor},
|
||||
constants::EASYTIER_VERSION,
|
||||
error::Error,
|
||||
global_ctx::{ArcGlobalCtx, NetworkIdentity},
|
||||
global_ctx::{ArcGlobalCtx, GlobalCtxEvent, NetworkIdentity},
|
||||
stun::StunInfoCollectorTrait,
|
||||
PeerId,
|
||||
},
|
||||
@@ -141,6 +141,9 @@ pub struct PeerManager {
|
||||
data_compress_algo: CompressorAlgo,
|
||||
|
||||
exit_nodes: Vec<Ipv4Addr>,
|
||||
|
||||
// conns that are directly connected (which are not hole punched)
|
||||
directly_connected_conn_map: Arc<DashMap<PeerId, DashSet<uuid::Uuid>>>,
|
||||
}
|
||||
|
||||
impl Debug for PeerManager {
|
||||
@@ -267,6 +270,8 @@ impl PeerManager {
|
||||
data_compress_algo,
|
||||
|
||||
exit_nodes,
|
||||
|
||||
directly_connected_conn_map: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,8 +330,48 @@ impl PeerManager {
|
||||
Ok((peer_id, conn_id))
|
||||
}
|
||||
|
||||
fn add_directly_connected_conn(&self, peer_id: PeerId, conn_id: uuid::Uuid) {
|
||||
let _ = self
|
||||
.directly_connected_conn_map
|
||||
.entry(peer_id)
|
||||
.or_insert_with(DashSet::new)
|
||||
.insert(conn_id);
|
||||
}
|
||||
|
||||
pub fn has_directly_connected_conn(&self, peer_id: PeerId) -> bool {
|
||||
self.directly_connected_conn_map
|
||||
.get(&peer_id)
|
||||
.map_or(false, |x| !x.is_empty())
|
||||
}
|
||||
|
||||
async fn start_peer_conn_close_event_handler(&self) {
|
||||
let dmap = self.directly_connected_conn_map.clone();
|
||||
let mut event_recv = self.global_ctx.subscribe();
|
||||
self.tasks.lock().await.spawn(async move {
|
||||
while let Ok(event) = event_recv.recv().await {
|
||||
match event {
|
||||
GlobalCtxEvent::PeerConnRemoved(info) => {
|
||||
if let Some(set) = dmap.get_mut(&info.peer_id) {
|
||||
let conn_id = info.conn_id.parse().unwrap();
|
||||
let old = set.remove(&conn_id);
|
||||
tracing::info!(
|
||||
?old,
|
||||
?info,
|
||||
"try remove conn id from directly connected map"
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn try_connect<C>(&self, mut connector: C) -> Result<(PeerId, PeerConnId), Error>
|
||||
pub async fn try_direct_connect<C>(
|
||||
&self,
|
||||
mut connector: C,
|
||||
) -> Result<(PeerId, PeerConnId), Error>
|
||||
where
|
||||
C: TunnelConnector + Debug,
|
||||
{
|
||||
@@ -334,18 +379,28 @@ impl PeerManager {
|
||||
let t = ns
|
||||
.run_async(|| async move { connector.connect().await })
|
||||
.await?;
|
||||
self.add_client_tunnel(t).await
|
||||
let (peer_id, conn_id) = self.add_client_tunnel(t).await?;
|
||||
self.add_directly_connected_conn(peer_id, conn_id);
|
||||
Ok((peer_id, conn_id))
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn add_tunnel_as_server(&self, tunnel: Box<dyn Tunnel>) -> Result<(), Error> {
|
||||
pub async fn add_tunnel_as_server(
|
||||
&self,
|
||||
tunnel: Box<dyn Tunnel>,
|
||||
is_directly_connected: bool,
|
||||
) -> Result<(), Error> {
|
||||
tracing::info!("add tunnel as server start");
|
||||
let mut peer = PeerConn::new(self.my_peer_id, self.global_ctx.clone(), tunnel);
|
||||
peer.do_handshake_as_server().await?;
|
||||
if peer.get_network_identity().network_name
|
||||
== self.global_ctx.get_network_identity().network_name
|
||||
{
|
||||
let (peer_id, conn_id) = (peer.get_peer_id(), peer.get_conn_id());
|
||||
self.add_new_peer_conn(peer).await?;
|
||||
if is_directly_connected {
|
||||
self.add_directly_connected_conn(peer_id, conn_id);
|
||||
}
|
||||
} else {
|
||||
self.foreign_network_manager.add_peer_conn(peer).await?;
|
||||
}
|
||||
@@ -857,9 +912,11 @@ impl PeerManager {
|
||||
|
||||
async fn run_clean_peer_without_conn_routine(&self) {
|
||||
let peer_map = self.peers.clone();
|
||||
let dmap = self.directly_connected_conn_map.clone();
|
||||
self.tasks.lock().await.spawn(async move {
|
||||
loop {
|
||||
peer_map.clean_peer_without_conn().await;
|
||||
dmap.retain(|p, v| peer_map.has_peer(*p) && !v.is_empty());
|
||||
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
|
||||
}
|
||||
});
|
||||
@@ -876,6 +933,8 @@ impl PeerManager {
|
||||
}
|
||||
|
||||
pub async fn run(&self) -> Result<(), Error> {
|
||||
self.start_peer_conn_close_event_handler().await;
|
||||
|
||||
match &self.route_algo_inst {
|
||||
RouteAlgoInst::Ospf(route) => self.add_route(route.clone()).await,
|
||||
RouteAlgoInst::None => {}
|
||||
@@ -924,7 +983,7 @@ impl PeerManager {
|
||||
self.foreign_network_client.clone()
|
||||
}
|
||||
|
||||
pub fn get_my_info(&self) -> cli::NodeInfo {
|
||||
pub async fn get_my_info(&self) -> cli::NodeInfo {
|
||||
cli::NodeInfo {
|
||||
peer_id: self.my_peer_id,
|
||||
ipv4_addr: self
|
||||
@@ -950,6 +1009,7 @@ impl PeerManager {
|
||||
config: self.global_ctx.config.dump(),
|
||||
version: EASYTIER_VERSION.to_string(),
|
||||
feature_flag: Some(self.global_ctx.get_feature_flags()),
|
||||
ip_list: Some(self.global_ctx.get_ip_collector().collect_ip_addrs().await),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -958,6 +1018,13 @@ impl PeerManager {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_directly_connections(&self, peer_id: PeerId) -> DashSet<uuid::Uuid> {
|
||||
self.directly_connected_conn_map
|
||||
.get(&peer_id)
|
||||
.map(|x| x.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1026,7 +1093,7 @@ mod tests {
|
||||
|
||||
tokio::spawn(async move {
|
||||
client.set_bind_addrs(vec![]);
|
||||
client_mgr.try_connect(client).await.unwrap();
|
||||
client_mgr.try_direct_connect(client).await.unwrap();
|
||||
});
|
||||
|
||||
server_mgr
|
||||
|
||||
@@ -212,6 +212,11 @@ impl PeerMap {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_peer_default_conn_id(&self, peer_id: PeerId) -> Option<PeerConnId> {
|
||||
self.get_peer_by_id(peer_id)
|
||||
.map(|p| p.get_default_conn_id())
|
||||
}
|
||||
|
||||
pub async fn close_peer_conn(
|
||||
&self,
|
||||
peer_id: PeerId,
|
||||
|
||||
@@ -32,12 +32,23 @@ impl PeerManagerRpcService {
|
||||
.await
|
||||
.iter(),
|
||||
);
|
||||
let peer_map = self.peer_manager.get_peer_map();
|
||||
let mut peer_infos = Vec::new();
|
||||
for peer in peers {
|
||||
let mut peer_info = PeerInfo::default();
|
||||
peer_info.peer_id = peer;
|
||||
peer_info.default_conn_id = peer_map
|
||||
.get_peer_default_conn_id(peer)
|
||||
.await
|
||||
.map(Into::into);
|
||||
peer_info.directly_connected_conns = self
|
||||
.peer_manager
|
||||
.get_directly_connections(peer)
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
|
||||
if let Some(conns) = self.peer_manager.get_peer_map().list_peer_conns(peer).await {
|
||||
if let Some(conns) = peer_map.list_peer_conns(peer).await {
|
||||
peer_info.conns = conns;
|
||||
} else if let Some(conns) = self
|
||||
.peer_manager
|
||||
@@ -121,7 +132,7 @@ impl PeerManageRpc for PeerManagerRpcService {
|
||||
_request: ShowNodeInfoRequest, // Accept request of type HelloRequest
|
||||
) -> Result<ShowNodeInfoResponse, rpc_types::error::Error> {
|
||||
Ok(ShowNodeInfoResponse {
|
||||
node_info: Some(self.peer_manager.get_my_info()),
|
||||
node_info: Some(self.peer_manager.get_my_info().await),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ pub async fn connect_peer_manager(client: Arc<PeerManager>, server: Arc<PeerMana
|
||||
});
|
||||
let b_mgr_copy = server.clone();
|
||||
tokio::spawn(async move {
|
||||
b_mgr_copy.add_tunnel_as_server(b_ring).await.unwrap();
|
||||
b_mgr_copy.add_tunnel_as_server(b_ring, true).await.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "common.proto";
|
||||
import "peer_rpc.proto";
|
||||
|
||||
package cli;
|
||||
|
||||
@@ -34,6 +35,8 @@ message PeerConnInfo {
|
||||
message PeerInfo {
|
||||
uint32 peer_id = 1;
|
||||
repeated PeerConnInfo conns = 2;
|
||||
common.UUID default_conn_id = 3;
|
||||
repeated common.UUID directly_connected_conns = 4;
|
||||
}
|
||||
|
||||
message ListPeerRequest {}
|
||||
@@ -79,6 +82,7 @@ message NodeInfo {
|
||||
string config = 8;
|
||||
string version = 9;
|
||||
common.PeerFeatureFlag feature_flag = 10;
|
||||
peer_rpc.GetIpListResponse ip_list = 11;
|
||||
}
|
||||
|
||||
message ShowNodeInfoRequest {}
|
||||
|
||||
@@ -4,10 +4,14 @@ impl PeerRoutePair {
|
||||
pub fn get_latency_ms(&self) -> Option<f64> {
|
||||
let mut ret = u64::MAX;
|
||||
let p = self.peer.as_ref()?;
|
||||
let default_conn_id = p.default_conn_id.map(|id| id.to_string());
|
||||
for conn in p.conns.iter() {
|
||||
let Some(stats) = &conn.stats else {
|
||||
continue;
|
||||
};
|
||||
if default_conn_id == Some(conn.conn_id.to_string()) {
|
||||
return Some(f64::from(stats.latency_us as u32) / 1000.0);
|
||||
}
|
||||
ret = ret.min(stats.latency_us);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ message FlagsInConfig {
|
||||
bool disable_p2p = 11;
|
||||
bool relay_all_peer_rpc = 12;
|
||||
bool disable_udp_hole_punching = 13;
|
||||
string ipv6_listener = 14;
|
||||
// string ipv6_listener = 14; [deprecated = true]; use -l udp://[::]:12345 instead
|
||||
bool multi_thread = 15;
|
||||
CompressionAlgoPb data_compress_algo = 16;
|
||||
bool bind_device = 17;
|
||||
|
||||
@@ -360,7 +360,13 @@ pub(crate) fn setup_sokcet2_ext(
|
||||
|
||||
socket2_socket.set_nonblocking(true)?;
|
||||
socket2_socket.set_reuse_address(true)?;
|
||||
socket2_socket.bind(&socket2::SockAddr::from(*bind_addr))?;
|
||||
if let Err(e) = socket2_socket.bind(&socket2::SockAddr::from(*bind_addr)) {
|
||||
if bind_addr.is_ipv4() {
|
||||
return Err(e.into());
|
||||
} else {
|
||||
tracing::warn!(?e, "bind failed, do not return error for ipv6");
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(all(unix, not(target_os = "solaris"), not(target_os = "illumos")))]
|
||||
// socket2_socket.set_reuse_port(true)?;
|
||||
|
||||
@@ -126,7 +126,7 @@ pub trait TunnelListener: Send {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[auto_impl::auto_impl(Box)]
|
||||
#[auto_impl::auto_impl(Box, &mut)]
|
||||
pub trait TunnelConnector: Send {
|
||||
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError>;
|
||||
fn remote_url(&self) -> url::Url;
|
||||
|
||||
@@ -150,9 +150,9 @@ impl TcpTunnelConnector {
|
||||
&mut self,
|
||||
addr: SocketAddr,
|
||||
) -> Result<Box<dyn Tunnel>, super::TunnelError> {
|
||||
tracing::info!(addr = ?self.addr, "connect tcp start");
|
||||
tracing::info!(url = ?self.addr, ?addr, "connect tcp start, bind addrs: {:?}", self.bind_addrs);
|
||||
let stream = TcpStream::connect(addr).await?;
|
||||
tracing::info!(addr = ?self.addr, "connect tcp succ");
|
||||
tracing::info!(url = ?self.addr, ?addr, "connect tcp succ");
|
||||
return get_tunnel_with_tcp_stream(stream, self.addr.clone().into());
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ impl super::TunnelConnector for TcpTunnelConnector {
|
||||
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, super::TunnelError> {
|
||||
let addr =
|
||||
check_scheme_and_get_socket_addr_ext::<SocketAddr>(&self.addr, "tcp", self.ip_version)?;
|
||||
if self.bind_addrs.is_empty() || addr.is_ipv6() {
|
||||
if self.bind_addrs.is_empty() {
|
||||
self.connect_with_default_bind(addr).await
|
||||
} else {
|
||||
self.connect_with_custom_bind(addr).await
|
||||
|
||||
@@ -141,12 +141,27 @@ async fn respond_stun_packet(
|
||||
.encode_into_bytes(resp_msg.clone())
|
||||
.map_err(|e| anyhow::anyhow!("stun encode error: {:?}", e))?;
|
||||
|
||||
socket
|
||||
.send_to(&rsp_buf, addr.clone())
|
||||
.await
|
||||
.with_context(|| "send stun response error")?;
|
||||
let change_req = req_msg
|
||||
.get_attribute::<ChangeRequest>()
|
||||
.map(|r| r.ip() || r.port())
|
||||
.unwrap_or(false);
|
||||
|
||||
tracing::debug!(?addr, ?req_msg, "udp respond stun packet done");
|
||||
if !change_req {
|
||||
socket
|
||||
.send_to(&rsp_buf, addr.clone())
|
||||
.await
|
||||
.with_context(|| "send stun response error")?;
|
||||
} else {
|
||||
// send from a new udp socket
|
||||
let socket = if addr.is_ipv4() {
|
||||
UdpSocket::bind("0.0.0.0:0").await?
|
||||
} else {
|
||||
UdpSocket::bind("[::]:0").await?
|
||||
};
|
||||
socket.send_to(&rsp_buf, addr.clone()).await?;
|
||||
}
|
||||
|
||||
tracing::debug!(?addr, ?req_msg, ?change_req, "udp respond stun packet done");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+55
-10
@@ -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,8 @@ pub struct KVNodeInfo {
|
||||
pub hostname: String,
|
||||
pub ipv4: String,
|
||||
pub latency_ms: f64,
|
||||
pub nat: String, // NAT类型
|
||||
pub loss_rate: f32,
|
||||
pub connections: Vec<KVNodeConnectionStats>,
|
||||
pub version: String,
|
||||
pub cost: i32,
|
||||
@@ -328,7 +330,41 @@ 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 {
|
||||
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
|
||||
},
|
||||
loss_rate: if let Some(peer) = &pair.peer {
|
||||
let mut total_loss_rate = 0.0;
|
||||
for conn in &peer.conns {
|
||||
total_loss_rate += conn.loss_rate;
|
||||
}
|
||||
total_loss_rate
|
||||
} else {
|
||||
0.0 // 如果没有连接信息,默认为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,
|
||||
@@ -537,20 +573,29 @@ pub fn create_server(
|
||||
specified_ip: String,
|
||||
room_name: String,
|
||||
room_password: String,
|
||||
severurl: String,
|
||||
severurl: Vec<String>,
|
||||
) {
|
||||
RT.spawn(async move {
|
||||
// 创建一个示例配置
|
||||
let cfg = create_config();
|
||||
cfg.set_hostname(Option::from(username));
|
||||
cfg.set_dhcp(enable_dhcp);
|
||||
let peer_config = PeerConfig {
|
||||
uri: ("tcp://".to_string() + &severurl).parse().unwrap(),
|
||||
};
|
||||
let peer_config2 = PeerConfig {
|
||||
uri: ("udp://".to_string() + &severurl).parse().unwrap(),
|
||||
};
|
||||
cfg.set_peers(vec![peer_config, peer_config2]);
|
||||
let mut flags = cfg.get_flags();
|
||||
// flags.dev_name = "astral".to_string();
|
||||
cfg.set_flags(flags);
|
||||
// 创建TCP和UDP连接配置列表
|
||||
let mut peer_configs = Vec::new();
|
||||
// 为每个服务器URL创建TCP和UDP配置
|
||||
for url in severurl {
|
||||
peer_configs.push(PeerConfig {
|
||||
uri: format!("tcp://{}", url).parse().unwrap(),
|
||||
});
|
||||
peer_configs.push(PeerConfig {
|
||||
uri: format!("udp://{}", url).parse().unwrap(),
|
||||
});
|
||||
}
|
||||
|
||||
cfg.set_peers(peer_configs);
|
||||
if enable_dhcp == false {
|
||||
// 使用完整路径引用 cidr 模块的 Ipv4Inet
|
||||
// 解析IP地址和子网掩码
|
||||
|
||||
@@ -107,7 +107,7 @@ fn wire__crate__api__simple__create_server_impl(
|
||||
let api_specified_ip = <String>::sse_decode(&mut deserializer);
|
||||
let api_room_name = <String>::sse_decode(&mut deserializer);
|
||||
let api_room_password = <String>::sse_decode(&mut deserializer);
|
||||
let api_severurl = <String>::sse_decode(&mut deserializer);
|
||||
let api_severurl = <Vec<String>>::sse_decode(&mut deserializer);
|
||||
deserializer.end();
|
||||
move |context| {
|
||||
transform_result_sse::<_, ()>((move || {
|
||||
@@ -496,6 +496,13 @@ impl SseDecode for bool {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for f32 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
deserializer.cursor.read_f32::<NativeEndian>().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for f64 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
@@ -546,6 +553,8 @@ 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_lossRate = <f32>::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 +563,8 @@ impl SseDecode for crate::api::simple::KVNodeInfo {
|
||||
hostname: var_hostname,
|
||||
ipv4: var_ipv4,
|
||||
latency_ms: var_latencyMs,
|
||||
nat: var_nat,
|
||||
loss_rate: var_lossRate,
|
||||
connections: var_connections,
|
||||
version: var_version,
|
||||
cost: var_cost,
|
||||
@@ -597,6 +608,18 @@ impl SseDecode for Vec<Route> {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for Vec<String> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut len_ = <i32>::sse_decode(deserializer);
|
||||
let mut ans_ = vec![];
|
||||
for idx_ in 0..len_ {
|
||||
ans_.push(<String>::sse_decode(deserializer));
|
||||
}
|
||||
return ans_;
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for Vec<crate::api::simple::KVNodeConnectionStats> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
@@ -819,6 +842,8 @@ 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.loss_rate.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(),
|
||||
@@ -925,6 +950,13 @@ impl SseEncode for bool {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for f32 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
serializer.cursor.write_f32::<NativeEndian>(self).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for f64 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
@@ -964,6 +996,8 @@ 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);
|
||||
<f32>::sse_encode(self.loss_rate, serializer);
|
||||
<Vec<crate::api::simple::KVNodeConnectionStats>>::sse_encode(self.connections, serializer);
|
||||
<String>::sse_encode(self.version, serializer);
|
||||
<i32>::sse_encode(self.cost, serializer);
|
||||
@@ -1000,6 +1034,16 @@ impl SseEncode for Vec<Route> {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for Vec<String> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<i32>::sse_encode(self.len() as _, serializer);
|
||||
for item in self {
|
||||
<String>::sse_encode(item, serializer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for Vec<crate::api::simple::KVNodeConnectionStats> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
@@ -1087,56 +1131,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 _);
|
||||
|
||||
@@ -5,56 +5,56 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051
|
||||
url: "https://pub.dev"
|
||||
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "64.0.0"
|
||||
version: "67.0.0"
|
||||
adaptive_number:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: adaptive_number
|
||||
sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893"
|
||||
url: "https://pub.dev"
|
||||
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.2.0"
|
||||
version: "6.4.1"
|
||||
args:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: args
|
||||
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
url: "https://pub.dev"
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
version: "2.13.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||
url: "https://pub.dev"
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
convert:
|
||||
@@ -62,23 +62,23 @@ packages:
|
||||
description:
|
||||
name: convert
|
||||
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
coverage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097"
|
||||
url: "https://pub.dev"
|
||||
sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.6.3"
|
||||
version: "1.11.1"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
ed25519_edwards:
|
||||
@@ -86,7 +86,7 @@ packages:
|
||||
description:
|
||||
name: ed25519_edwards
|
||||
sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
file:
|
||||
@@ -94,47 +94,47 @@ packages:
|
||||
description:
|
||||
name: file
|
||||
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
|
||||
url: "https://pub.dev"
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612"
|
||||
url: "https://pub.dev"
|
||||
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "4.0.0"
|
||||
github:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: github
|
||||
sha256: "9966bc13bf612342e916b0a343e95e5f046c88f602a14476440e9b75d2295411"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.17.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
|
||||
url: "https://pub.dev"
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.3"
|
||||
hex:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hex
|
||||
sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
http:
|
||||
@@ -142,55 +142,55 @@ packages:
|
||||
description:
|
||||
name: http
|
||||
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
|
||||
url: "https://pub.dev"
|
||||
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
version: "3.2.2"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
|
||||
url: "https://pub.dev"
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "1.0.5"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||
url: "https://pub.dev"
|
||||
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.6.7"
|
||||
version: "0.7.2"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
|
||||
url: "https://pub.dev"
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.8.1"
|
||||
version: "4.9.0"
|
||||
lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: lints
|
||||
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
logging:
|
||||
@@ -198,55 +198,55 @@ packages:
|
||||
description:
|
||||
name: logging
|
||||
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
|
||||
url: "https://pub.dev"
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.12.16"
|
||||
version: "0.12.17"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
|
||||
url: "https://pub.dev"
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
version: "1.16.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e
|
||||
url: "https://pub.dev"
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "2.0.0"
|
||||
node_preamble:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_preamble
|
||||
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
|
||||
url: "https://pub.dev"
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.2.0"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
petitparser:
|
||||
@@ -254,7 +254,7 @@ packages:
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.4.0"
|
||||
pool:
|
||||
@@ -262,23 +262,23 @@ packages:
|
||||
description:
|
||||
name: pool
|
||||
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
|
||||
url: "https://pub.dev"
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.2.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
shelf_packages_handler:
|
||||
@@ -286,168 +286,184 @@ packages:
|
||||
description:
|
||||
name: shelf_packages_handler
|
||||
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
shelf_static:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_static
|
||||
sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e
|
||||
url: "https://pub.dev"
|
||||
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.1.3"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
|
||||
url: "https://pub.dev"
|
||||
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "3.0.0"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_map_stack_trace
|
||||
sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae"
|
||||
url: "https://pub.dev"
|
||||
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
source_maps:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703"
|
||||
url: "https://pub.dev"
|
||||
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.10.12"
|
||||
version: "0.10.13"
|
||||
source_span:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
url: "https://pub.dev"
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
url: "https://pub.dev"
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.4"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
url: "https://pub.dev"
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.4.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||
url: "https://pub.dev"
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.2.2"
|
||||
test:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
sha256: "9b0dd8e36af4a5b1569029949d50a52cb2a2a2fdaa20cebb96e6603b9ae241f9"
|
||||
url: "https://pub.dev"
|
||||
sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.24.6"
|
||||
version: "1.25.15"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
||||
url: "https://pub.dev"
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.6.1"
|
||||
version: "0.7.4"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "4bef837e56375537055fdbbbf6dd458b1859881f4c7e6da936158f77d61ab265"
|
||||
url: "https://pub.dev"
|
||||
sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.5.6"
|
||||
version: "0.6.8"
|
||||
toml:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: toml
|
||||
sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.14.0"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
|
||||
url: "https://pub.dev"
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
version: "1.4.0"
|
||||
version:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: version
|
||||
sha256: "2307e23a45b43f96469eeab946208ed63293e8afca9c28cd8b5241ff31c55f55"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d"
|
||||
url: "https://pub.dev"
|
||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "11.9.0"
|
||||
version: "15.0.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
|
||||
url: "https://pub.dev"
|
||||
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.1.6"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
|
||||
url: "https://pub.dev"
|
||||
sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "3.0.2"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webkit_inspection_protocol
|
||||
sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d"
|
||||
url: "https://pub.dev"
|
||||
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.2.1"
|
||||
yaml:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: yaml
|
||||
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.0.0 <4.0.0"
|
||||
dart: ">=3.7.0-0 <4.0.0"
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
# Project-level configuration.
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(fltier LANGUAGES CXX)
|
||||
project(astral LANGUAGES CXX)
|
||||
|
||||
# The name of the executable created for the application. Change this to change
|
||||
# the on-disk name of your application.
|
||||
set(BINARY_NAME "fltier")
|
||||
set(BINARY_NAME "astral")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
@@ -75,6 +75,7 @@ set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
|
||||
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
|
||||
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <tray_manager/tray_manager_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
#include <windows_notification/windows_notification_plugin_c_api.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
||||
@@ -23,4 +24,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
WindowManagerPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
|
||||
WindowsNotificationPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("WindowsNotificationPluginCApi"));
|
||||
}
|
||||
|
||||
@@ -8,9 +8,11 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
tray_manager
|
||||
url_launcher_windows
|
||||
window_manager
|
||||
windows_notification
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
flutter_local_notifications_windows
|
||||
rust_lib_fltier
|
||||
)
|
||||
|
||||
|
||||
@@ -26,13 +26,21 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTT
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
|
||||
|
||||
SET_TARGET_PROPERTIES(${BINARY_NAME} PROPERTIES LINK_FLAGS "/MANIFESTUAC:\"level='requireAdministrator' uiAccess='false'\" /SUBSYSTEM:WINDOWS")
|
||||
# Disable Windows macros that collide with C++ standard library functions.
|
||||
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,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
@@ -11,4 +12,14 @@
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel
|
||||
level="requireAdministrator"
|
||||
uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
|
||||
Reference in New Issue
Block a user