4 Commits
Author SHA1 Message Date
会做饭的二哈 c72617aaf0 修复内存 2025-03-17 18:18:49 +08:00
会做饭的二哈 b1e48999c3 修复内存泄漏 2025-03-17 18:15:21 +08:00
会做饭的二哈 2f6195ecd9 1 2025-03-16 23:54:30 +08:00
会做饭的二哈 4fb5387f5b 修复延迟检测问题 更新逻辑不正确 若干体验上问题 2025-03-16 23:32:59 +08:00
30 changed files with 664 additions and 868 deletions
-3
View File
@@ -15,9 +15,6 @@ migration:
- platform: root
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: web
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
# User provided section
+6
View File
@@ -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",
-12
View File
@@ -6,7 +6,6 @@ import 'dart:io';
import 'screens/Home.dart';
import 'config/themeconfiguration.dart';
import 'config/app_config.dart';
import 'package:astral/utils/up.dart'; // 添加这行导入
// 定义应用程序的主要StatefulWidget
class MyApp extends StatefulWidget {
@@ -39,17 +38,6 @@ class _MyAppState extends State<MyApp> {
// 初始化系统托盘
initSystemTray();
// 添加版本检查(在下一帧执行确保context可用)
Future.microtask(() {
final updateChecker = UpdateChecker(
owner: 'ldoubil',
repo: 'astral',
);
if (mounted) {
updateChecker.scheckForUpdates(context);
}
});
}
// 初始化系统托盘
+11
View File
@@ -44,6 +44,7 @@ class AppConfig {
},
'system': {
'closeToTray': true,
'enablePing': true, // 添加全局ping开关默认配置
},
},
);
@@ -158,4 +159,14 @@ class AppConfig {
_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();
}
}
+6 -6
View File
@@ -40,12 +40,12 @@ class NavigationConfig {
currentThemeMode: currentThemeMode,
)),
),
NavItem(
label: '房间',
icon: Icons.room_outlined,
selectedIcon: Icons.room,
pageBuilder: () => _getOrCreatePage(1, () => const RoomPage()),
),
// NavItem(
// label: '房间',
// icon: Icons.room_outlined,
// selectedIcon: Icons.room,
// pageBuilder: () => _getOrCreatePage(1, () => const RoomPage()),
// ),
NavItem(
label: '设置',
icon: Icons.settings_outlined,
+14 -1
View File
@@ -1,3 +1,4 @@
import 'package:astral/utils/up.dart';
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
import '../widgets/window_control_buttons.dart';
@@ -46,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
@@ -229,7 +241,8 @@ class PageKeepAlive extends StatefulWidget {
State<PageKeepAlive> createState() => _PageKeepAliveState();
}
class _PageKeepAliveState extends State<PageKeepAlive> with AutomaticKeepAliveClientMixin {
class _PageKeepAliveState extends State<PageKeepAlive>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
+118 -277
View File
@@ -2,35 +2,11 @@
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 {
class InfoPage extends StatelessWidget {
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(
@@ -40,156 +16,95 @@ class _InfoPageState extends State<InfoPage>
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,
),
),
// 静态图标
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),
// 应用名称添加渐变效果
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,
),
),
// 应用名称
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),
// 版本号添加动画效果
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.getVersion(),
style: Theme.of(context).textTheme.bodyLarge,
),
// 版本号
Text(
AppInfoUtil.getVersion(),
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 20),
// 卡片添加动画和阴影效果
_buildAnimatedCard(
// 静态卡片
_buildCard(
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,
// 静态按钮
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),
),
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群链接')),
);
}
}
},
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),
),
),
// 版权信息
Text(
'© ${DateTime.now().year} ASTRAL Team',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.6),
),
),
],
),
@@ -198,129 +113,55 @@ class _InfoPageState extends State<InfoPage>
);
}
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),
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,
),
],
),
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),
),
),
],
),
),
],
),
),
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,
),
],
),
),
],
),
),
);
+146 -140
View File
@@ -52,8 +52,7 @@ class HomePage extends StatefulWidget {
}
class _HomePageState extends State<HomePage> {
// 定义状态枚举
int connectionTimeoutCounter = 0;
// 当前连接状态
ConnectionState _connectionState = ConnectionState.notStarted;
@@ -87,25 +86,104 @@ 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,
_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(() {
@@ -121,9 +199,6 @@ class _HomePageState extends State<HomePage> {
roomPassword: roomPassword,
severurl: Serverip);
// 添加连接超时计数器
int connectionTimeoutCounter = 0;
// 不再使用固定延迟模拟连接成功,而是通过定时检查IP来确定连接状态
timer = Timer.periodic(const Duration(seconds: 1), (timer) async {
// 检查组件是否仍然挂载
@@ -146,49 +221,40 @@ class _HomePageState extends State<HomePage> {
final int? version = runin.myNodeInfo?.virtualIpv4?.address?.addr;
if (version != null) {
String ipStr = _intToIpv4String(version);
// 检查IP不为0.0.0.0时认为连接成功
if (ipStr != "0.0.0.0") {
if (publicIP != ipStr) {
km.virtualIP = ipStr;
}
if (publicIP != ipStr) {
km.virtualIP = ipStr;
}
// 如果当前状态还是连接中,则更新为已连接
if (_connectionState == ConnectionState.connecting) {
setState(() {
_connectionState = ConnectionState.connected;
});
}
// 重置超时计数器
// 如果当前状态还是连接中,则更新为已连接
if (_connectionState == ConnectionState.connecting) {
setState(() {
_connectionState = ConnectionState.connected;
});
}
connectionTimeoutCounter = 0;
} else if (_connectionState == ConnectionState.connecting) {
connectionTimeoutCounter++;
if (connectionTimeoutCounter >= 10) {
connectionTimeoutCounter = 0;
} else if (_connectionState == ConnectionState.connecting) {
// 只在连接状态下增加超时计数
connectionTimeoutCounter++;
timer.cancel();
setState(() {
isRunning = false;
_connectionState = ConnectionState.notStarted;
runningTime = Duration.zero;
});
// 如果连续10秒都是0.0.0.0,判断为连接失败
if (connectionTimeoutCounter >= 10) {
// 停止连接并显示失败消息
timer.cancel();
setState(() {
isRunning = false;
_connectionState = ConnectionState.notStarted;
runningTime = Duration.zero;
});
// 关闭服务器连接
closeAllServer();
// 显示连接失败提示
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('连接失败,请检查网络或房间信息后重试'),
duration: Duration(seconds: 3),
),
);
}
return;
closeAllServer();
// 清空玩家列表数据
Provider.of<KM>(context, listen: false).nodes = [];
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('连接失败,未能获取节点信息'),
duration: Duration(seconds: 3),
),
);
}
return;
}
}
@@ -207,6 +273,8 @@ class _HomePageState extends State<HomePage> {
// 停止时重置状态
_connectionState = ConnectionState.notStarted;
closeAllServer();
// 清空玩家列表数据
Provider.of<KM>(context, listen: false).nodes = [];
timer?.cancel();
runningTime = Duration.zero;
// 重置网络统计数据
@@ -216,6 +284,7 @@ class _HomePageState extends State<HomePage> {
_lastDownloadBytes = 0;
uploadSpeed = 0;
downloadSpeed = 0;
//connectionTimeoutCounter
}
});
}
@@ -286,6 +355,7 @@ class _HomePageState extends State<HomePage> {
text: km.username,
selection: _usernameController.selection,
);
// 添加虚拟IP控制器的值同步
_virtualIPController.value = TextEditingValue(
text: km.virtualIP,
selection: _virtualIPController.selection,
@@ -478,88 +548,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(
@@ -743,9 +731,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: '用户名',
@@ -762,12 +753,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(
@@ -790,6 +786,10 @@ class _HomePageState extends State<HomePage> {
_isAutoIP = value;
});
km.dynamicIP = value;
// 切换模式时同步最新值
if (!value) {
km.virtualIP = _virtualIPController.text;
}
}
: null,
),
@@ -859,9 +859,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: '房间名称',
@@ -875,9 +878,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(
+1 -7
View File
@@ -230,13 +230,7 @@ class _RoomPageState extends State<RoomPage> {
}
// 计算丢包率 (简单估算)
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));
}
double packetLossRate = node.lossRate;
// 获取NAT类型
String natType = _mapNatType(node.nat);
+59 -64
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'package:astral/utils/app_info.dart';
import 'package:astral/utils/up.dart';
import 'package:flutter/material.dart';
@@ -19,17 +20,15 @@ final updateChecker = UpdateChecker(
);
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; // 添加关闭进入托盘变量
bool _pingEnabled = true; // 添加全局ping开关
String serverIP = "";
// 添加 ping 相关状态
Map<String, int?> pingResults = {};
Map<String, bool> isPinging = {};
@override
void initState() {
@@ -38,59 +37,74 @@ class _SettingsPageState extends State<SettingsPage> {
_currentServer = _appConfig.currentServer;
serverIP = _appConfig.currentServer;
_closeToTray = _appConfig.closeToTray; // 初始化托盘设置
// 初始化全局ping开关
_pingEnabled = _appConfig.enablePing;
// 初始化 ping 状态
for (var server in _serverList) {
pingResults[server] = null;
isPinging[server] = true; // 默认所有服务器都开启ping
}
// 开始 ping 所有服务器
for (var server in _serverList) {
_pingServer(server);
}
_startPingAllServers();
}
// 添加一个计时器变量来控制 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);
}
}
});
}
@override
void dispose() {
// 停止所有 ping
for (var server in _serverList) {
isPinging[server] = false;
}
// 取消计时器
_pingTimer?.cancel();
_pingTimer = null;
super.dispose();
}
// 简化 ping 方法,移除强制持续 ping 参数
void _startPingServer(String server) {
if (isPinging[server] == true) return;
isPinging[server] = true;
_pingServer(server);
}
// 修改停止 ping 方法 - 实际上不再需要,但保留以防将来需要
void _stopPingServer(String server) {
isPinging[server] = false;
}
// 执行 ping 操作
Future<void> _pingServer(String server) async {
if (isPinging[server] != true) return;
// 执行单次 ping 操作
Future<void> _pingServerOnce(String server) async {
final pingResult = await PingUtil.ping(server);
if (mounted) {
setState(() {
pingResults[server] = pingResult;
});
// 1秒后再次 ping
Future.delayed(const Duration(seconds: 1), () {
_pingServer(server);
});
}
}
// 切换全局ping状态
void _togglePingStatus(bool value) {
setState(() {
_pingEnabled = value;
_appConfig.setEnablePing(_pingEnabled);
if (!_pingEnabled) {
// 如果关闭ping,清空所有结果
for (var server in _serverList) {
pingResults[server] = null;
}
}
});
}
// 添加服务器对话框
Future<void> _showAddServerDialog() async {
final controller = TextEditingController();
@@ -123,10 +137,8 @@ class _SettingsPageState extends State<SettingsPage> {
_serverList.add(result);
_appConfig.setServerList(_serverList);
// 初始化新服务器的 ping 状态并立即开始 ping
// 初始化新服务器的 ping 状态
pingResults[result] = null;
isPinging[result] = true;
_pingServer(result);
});
}
}
@@ -189,21 +201,16 @@ class _SettingsPageState extends State<SettingsPage> {
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);
}
});
}
@@ -212,7 +219,9 @@ class _SettingsPageState extends State<SettingsPage> {
// 添加构建 ping 显示组件的方法
Widget _buildPingWidget(String server) {
final pingResult = pingResults[server];
if (pingResult == null) {
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(
@@ -250,9 +259,6 @@ class _SettingsPageState extends State<SettingsPage> {
ExpansionTile(
leading: const Icon(Icons.list),
title: const Text('服务器列表'),
onExpansionChanged: (expanded) {
// 不再需要处理展开折叠时的 ping 状态,所有服务器都持续 ping
},
children: [
// 在服务器列表前添加当前服务器的 ping 状态显示
ListView.builder(
@@ -261,22 +267,6 @@ class _SettingsPageState extends State<SettingsPage> {
itemCount: _serverList.length,
itemBuilder: (context, index) {
final server = _serverList[index];
final pingResult = pingResults[server];
// 构建延迟显示组件
Widget pingWidget;
if (pingResult == null) {
pingWidget = const Text('测试中...',
style: TextStyle(color: Colors.grey));
} else {
pingWidget = Text('${pingResult}ms',
style: TextStyle(
color: pingResult < 100
? Colors.green
: (pingResult < 300
? Colors.orange
: Colors.red)));
}
return ListTile(
leading: const Icon(Icons.computer),
@@ -284,14 +274,13 @@ class _SettingsPageState extends State<SettingsPage> {
children: [
Text('服务器 ${index + 1}'),
const SizedBox(width: 8),
pingWidget,
_buildPingWidget(server),
],
),
subtitle: Text(server),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// 移除 ping 按钮,只保留编辑和删除按钮
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _showEditServerDialog(index),
@@ -307,7 +296,7 @@ class _SettingsPageState extends State<SettingsPage> {
_currentServer = server;
Provider.of<KM>(context, listen: false).serverIP =
server;
// 不再需要特别开始 ping 新选择的服务器,因为所有服务器都在 ping
_appConfig.setCurrentServer(_currentServer);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('已切换到服务器: $server')),
@@ -347,6 +336,12 @@ class _SettingsPageState extends State<SettingsPage> {
});
},
),
SwitchListTile(
title: const Text('启用服务器Ping测试'),
subtitle: const Text('定期测试所有服务器的网络延迟'),
value: _pingEnabled,
onChanged: _togglePingStatus,
),
],
),
),
+4
View File
@@ -124,6 +124,7 @@ class KVNodeInfo {
final String ipv4;
final double latencyMs;
final String nat;
final double lossRate;
final List<KVNodeConnectionStats> connections;
final String version;
final int cost;
@@ -133,6 +134,7 @@ class KVNodeInfo {
required this.ipv4,
required this.latencyMs,
required this.nat,
required this.lossRate,
required this.connections,
required this.version,
required this.cost,
@@ -144,6 +146,7 @@ class KVNodeInfo {
ipv4.hashCode ^
latencyMs.hashCode ^
nat.hashCode ^
lossRate.hashCode ^
connections.hashCode ^
version.hashCode ^
cost.hashCode;
@@ -157,6 +160,7 @@ class KVNodeInfo {
ipv4 == other.ipv4 &&
latencyMs == other.latencyMs &&
nat == other.nat &&
lossRate == other.lossRate &&
connections == other.connections &&
version == other.version &&
cost == other.cost;
+27 -5
View File
@@ -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,16 +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 != 7)
throw Exception('unexpected arr length: expect 7 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]),
nat: dco_decode_String(arr[3]),
connections: dco_decode_list_kv_node_connection_stats(arr[4]),
version: dco_decode_String(arr[5]),
cost: dco_decode_i_32(arr[6]),
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]),
);
}
@@ -753,6 +760,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
@@ -797,6 +810,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
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);
@@ -806,6 +820,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
ipv4: var_ipv4,
latencyMs: var_latencyMs,
nat: var_nat,
lossRate: var_lossRate,
connections: var_connections,
version: var_version,
cost: var_cost);
@@ -1016,6 +1031,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
@@ -1054,6 +1075,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
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);
+9
View File
@@ -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);
@@ -182,6 +185,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);
@@ -288,6 +294,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);
+9
View File
@@ -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);
@@ -184,6 +187,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);
@@ -290,6 +296,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);
+8 -5
View File
@@ -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;
}
}
}
+2 -78
View File
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'dart:math' as math;
class FloatingCard extends StatefulWidget {
final ColorScheme colorScheme;
@@ -8,14 +7,8 @@ class FloatingCard extends StatefulWidget {
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; // 控制悬浮时是升起还是降下
final double? maxWidth;
final double? height;
const FloatingCard({
super.key,
@@ -27,12 +20,6 @@ class FloatingCard extends StatefulWidget {
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
@@ -41,67 +28,12 @@ class FloatingCard extends StatefulWidget {
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(
@@ -109,15 +41,7 @@ class _FloatingCardState extends State<FloatingCard> {
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(
+188 -175
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -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.1
version: 1.0.3
environment:
sdk: ^3.5.4
@@ -42,7 +42,6 @@ dependencies:
flutter_rust_bridge: 2.9.0
flutter_colorpicker: ^1.1.0
window_manager: ^0.4.3
fl_chart: ^0.70.2
flutter_staggered_grid_view: ^0.7.0
json_annotation: ^4.8.1
tray_manager: ^0.3.2
@@ -52,6 +51,7 @@ dependencies:
yaml: ^3.1.2
flutter_local_notifications: ^19.0.0
windows_notification: ^1.3.0
fl_chart: ^0.70.2
dev_dependencies:
flutter_test:
+24 -8
View File
@@ -292,6 +292,7 @@ pub struct KVNodeInfo {
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,
@@ -330,7 +331,6 @@ pub fn get_network_status() -> KVNetworkStatus {
hostname: route.hostname.clone(),
ipv4,
latency_ms: if let Some(peer) = &pair.peer {
// 类似cli.rs中的get_latency_ms方法
let mut min_latency = u64::MAX;
for conn in &peer.conns {
if let Some(stats) = &conn.stats {
@@ -348,6 +348,15 @@ pub fn get_network_status() -> KVNetworkStatus {
// 如果没有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| {
@@ -571,13 +580,20 @@ pub fn create_server(
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 peer_configs = vec![
PeerConfig {
uri: format!("tcp://{}", severurl).parse().unwrap(),
},
PeerConfig {
uri: format!("udp://{}", severurl).parse().unwrap(),
}
];
cfg.set_peers(peer_configs);
if enable_dhcp == false {
// 使用完整路径引用 cidr 模块的 Ipv4Inet
// 解析IP地址和子网掩码
+18
View File
@@ -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 {
@@ -547,6 +554,7 @@ impl SseDecode for crate::api::simple::KVNodeInfo {
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);
@@ -556,6 +564,7 @@ impl SseDecode for crate::api::simple::KVNodeInfo {
ipv4: var_ipv4,
latency_ms: var_latencyMs,
nat: var_nat,
loss_rate: var_lossRate,
connections: var_connections,
version: var_version,
cost: var_cost,
@@ -822,6 +831,7 @@ impl flutter_rust_bridge::IntoDart for crate::api::simple::KVNodeInfo {
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(),
@@ -928,6 +938,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) {
@@ -968,6 +985,7 @@ impl SseEncode for crate::api::simple::KVNodeInfo {
<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);
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

-38
View File
@@ -1,38 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="astral">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>astral</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>
-35
View File
@@ -1,35 +0,0 @@
{
"name": "astral",
"short_name": "astral",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/Icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icons/Icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
-4
View File
@@ -75,10 +75,6 @@ set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
# 添加管理员权限要求(修正路径格式)
target_link_options(${BINARY_NAME} PRIVATE
"LINKER:/MANIFESTUAC:level='requireAdministrator' uiAccess='false'"
$<$<C_COMPILER_ID:MSVC>:"LINKER:/MANIFESTINPUT:${CMAKE_CURRENT_SOURCE_DIR}/runner/runner.exe.manifest">)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
+1 -1
View File
@@ -26,7 +26,7 @@ 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")
+11 -7
View File
@@ -1,12 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
@@ -18,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>