mirror of
https://github.com/EasyTier/astral.git
synced 2025-05-19 10:30:24 +00:00
切换官方服务器
更换为官方服务器,提升稳定性。 移除模拟延迟 改为直接检测连接,优化体验。 修复延迟计算 从 peer 获取最小延迟(μs→ms),无效时用路由延迟。 修复管理员权限 解决管理员操作无权限问题。 配置存储位置调整 将配置信息改为运行目录,方便管理。 查看 NAT 类型 添加功能检测并显示 NAT 类型。 修复页面状态丢失 解决缩放导致导航栏移动时页面状态丢失问题。 优化延迟检测 始终检测延迟,移除暂停/开始功能,简化逻辑。 最小化通知 应用最小化时提供通知,提醒用户状态。 增加客户端搜索功能 添加客户端搜索功能,支持快速查找内容或设备。
This commit is contained in:
+16
-4
@@ -6,6 +6,7 @@ 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 {
|
||||
@@ -38,6 +39,17 @@ class _MyAppState extends State<MyApp> {
|
||||
|
||||
// 初始化系统托盘
|
||||
initSystemTray();
|
||||
|
||||
// 添加版本检查(在下一帧执行确保context可用)
|
||||
Future.microtask(() {
|
||||
final updateChecker = UpdateChecker(
|
||||
owner: 'ldoubil',
|
||||
repo: 'astral',
|
||||
);
|
||||
if (mounted) {
|
||||
updateChecker.scheckForUpdates(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化系统托盘
|
||||
@@ -88,10 +100,10 @@ class _MyAppState extends State<MyApp> {
|
||||
// 更改主题色的方法
|
||||
void changeSeedColor(Color color) {
|
||||
// 使用 Future.microtask 延迟状态更新,避免在当前帧中触发重建
|
||||
setState(() {
|
||||
_seedColor = color;
|
||||
AppConfig().setSeedColor(color);
|
||||
});
|
||||
setState(() {
|
||||
_seedColor = color;
|
||||
AppConfig().setSeedColor(color);
|
||||
});
|
||||
}
|
||||
|
||||
// 更改底部导航栏选中索引的方法
|
||||
|
||||
@@ -116,7 +116,7 @@ class _InfoPageState extends State<InfoPage>
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
AppInfoUtil.getFullVersion(),
|
||||
AppInfoUtil.getVersion(),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
|
||||
+82
-57
@@ -120,62 +120,89 @@ class _HomePageState extends State<HomePage> {
|
||||
roomName: roomName,
|
||||
roomPassword: roomPassword,
|
||||
severurl: Serverip);
|
||||
// 模拟连接过程,2秒后连接成功
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
|
||||
// 添加连接超时计数器
|
||||
int connectionTimeoutCounter = 0;
|
||||
|
||||
// 不再使用固定延迟模拟连接成功,而是通过定时检查IP来确定连接状态
|
||||
timer = Timer.periodic(const Duration(seconds: 1), (timer) async {
|
||||
// 检查组件是否仍然挂载
|
||||
if (!mounted) return;
|
||||
|
||||
if (isRunning) {
|
||||
// 确保用户没有在连接过程中取消
|
||||
setState(() {
|
||||
_connectionState = ConnectionState.connected;
|
||||
// 连接成功后开始计时
|
||||
|
||||
timer = Timer.periodic(const Duration(seconds: 1), (timer) async {
|
||||
// 检查组件是否仍然挂载
|
||||
if (!mounted) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 再次检查组件是否仍然挂载
|
||||
if (!mounted) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
runningTime += const Duration(seconds: 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
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);
|
||||
// 检查IP不为0.0.0.0时认为连接成功
|
||||
if (ipStr != "0.0.0.0") {
|
||||
if (publicIP != ipStr) {
|
||||
km.virtualIP = ipStr;
|
||||
}
|
||||
|
||||
// 如果当前状态还是连接中,则更新为已连接
|
||||
if (_connectionState == ConnectionState.connecting) {
|
||||
setState(() {
|
||||
_connectionState = ConnectionState.connected;
|
||||
});
|
||||
}
|
||||
|
||||
// 重置超时计数器
|
||||
connectionTimeoutCounter = 0;
|
||||
} else if (_connectionState == ConnectionState.connecting) {
|
||||
// 只在连接状态下增加超时计数
|
||||
connectionTimeoutCounter++;
|
||||
|
||||
// 如果连续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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 再次检查组件是否仍然挂载
|
||||
if (!mounted) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
runningTime += const Duration(seconds: 1);
|
||||
});
|
||||
});
|
||||
// 移除原来的Future.delayed模拟连接成功的代码
|
||||
} else {
|
||||
// 停止时重置状态
|
||||
_connectionState = ConnectionState.notStarted;
|
||||
@@ -217,7 +244,7 @@ class _HomePageState extends State<HomePage> {
|
||||
|
||||
// 再次检查挂载状态,确保在setState前组件仍然挂载
|
||||
if (!mounted) return;
|
||||
|
||||
|
||||
// 计算速度 (字节/秒 转换为 MB/秒)
|
||||
setState(() {
|
||||
_uploadBytes = totalUploadBytes;
|
||||
@@ -881,7 +908,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,
|
||||
@@ -940,5 +967,3 @@ Widget _buildVersionItem(
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+249
-204
@@ -18,6 +18,7 @@ class PlayerInfo {
|
||||
final int receivedPackets; // 接收包数量
|
||||
final double packetLossRate; // 丢包率(%)
|
||||
final String etVersion; // ET版本
|
||||
final String natType; // 添加NAT类型
|
||||
|
||||
PlayerInfo({
|
||||
required this.name,
|
||||
@@ -30,6 +31,7 @@ class PlayerInfo {
|
||||
required this.receivedPackets,
|
||||
required this.packetLossRate,
|
||||
required this.etVersion,
|
||||
required this.natType, // 添加NAT类型参数
|
||||
});
|
||||
}
|
||||
|
||||
@@ -44,13 +46,43 @@ class RoomPage extends StatefulWidget {
|
||||
|
||||
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
|
||||
@@ -60,7 +92,22 @@ class _RoomPageState extends State<RoomPage> {
|
||||
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) {
|
||||
@@ -190,6 +237,9 @@ class _RoomPageState extends State<RoomPage> {
|
||||
packetLossRate = double.parse(packetLossRate.toStringAsFixed(1));
|
||||
}
|
||||
|
||||
// 获取NAT类型
|
||||
String natType = _mapNatType(node.nat);
|
||||
|
||||
// 创建PlayerInfo对象
|
||||
nodePlayerInfos.add(
|
||||
PlayerInfo(
|
||||
@@ -203,6 +253,7 @@ class _RoomPageState extends State<RoomPage> {
|
||||
receivedPackets: receivedPackets,
|
||||
packetLossRate: packetLossRate,
|
||||
etVersion: node.version, // 获取版本信息
|
||||
natType: natType, // 添加NAT类型
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -211,6 +262,7 @@ class _RoomPageState extends State<RoomPage> {
|
||||
|
||||
setState(() {
|
||||
players = nodePlayerInfos;
|
||||
_filterPlayers(); // 更新过滤后的玩家列表
|
||||
isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -223,176 +275,6 @@ class _RoomPageState extends State<RoomPage> {
|
||||
}
|
||||
}
|
||||
|
||||
// 构建玩家信息卡片
|
||||
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) {
|
||||
// 根据延迟值确定颜色
|
||||
@@ -492,6 +374,15 @@ class _RoomPageState extends State<RoomPage> {
|
||||
colorScheme,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// NAT类型
|
||||
_buildInfoRow(
|
||||
_getNatTypeIcon(player.natType),
|
||||
'NAT类型',
|
||||
player.natType,
|
||||
colorScheme,
|
||||
valueColor: _getNatTypeColor(player.natType),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 丢包率信息
|
||||
_buildInfoRow(
|
||||
@@ -632,6 +523,16 @@ class _RoomPageState extends State<RoomPage> {
|
||||
player.etVersion,
|
||||
colorScheme,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// NAT类型
|
||||
_buildInfoRow(
|
||||
_getNatTypeIcon(player.natType),
|
||||
'NAT类型',
|
||||
player.natType,
|
||||
colorScheme,
|
||||
valueColor: _getNatTypeColor(player.natType),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -799,38 +700,6 @@ class _RoomPageState extends State<RoomPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// 构建对齐的网络数据项
|
||||
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) {
|
||||
@@ -907,4 +776,180 @@ class _RoomPageState extends State<RoomPage> {
|
||||
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),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
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';
|
||||
@@ -11,6 +13,11 @@ class SettingsPage extends StatefulWidget {
|
||||
State<SettingsPage> createState() => _SettingsPageState();
|
||||
}
|
||||
|
||||
final updateChecker = UpdateChecker(
|
||||
owner: 'ldoubil',
|
||||
repo: 'astral',
|
||||
);
|
||||
|
||||
class _SettingsPageState extends State<SettingsPage> {
|
||||
bool _notificationsEnabled = true;
|
||||
double _fontSize = 16.0;
|
||||
@@ -221,6 +228,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// updateChecker.checkForUpdates(context);
|
||||
serverIP = Provider.of<KM>(context).virtualIP;
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
@@ -347,18 +355,16 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
const ListTile(
|
||||
ListTile(
|
||||
leading: Icon(Icons.info),
|
||||
title: Text('应用版本'),
|
||||
subtitle: Text('灰度版本'),
|
||||
subtitle: Text(AppInfoUtil.getVersion()),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.update),
|
||||
title: const Text('检查更新'),
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('灰度版本不支持更新')),
|
||||
);
|
||||
updateChecker.checkForUpdates(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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,7 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import '../config/app_config.dart';
|
||||
import 'package:tray_manager/tray_manager.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});
|
||||
|
||||
@@ -26,17 +40,18 @@ class WindowControls extends StatelessWidget {
|
||||
tooltip: '最大化/还原',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () async {
|
||||
if (AppConfig().closeToTray) {
|
||||
await windowManager.hide(); // 隐藏主窗口
|
||||
await trayManager.setToolTip('FLN2N 正在后台运行'); // 设置托盘提示
|
||||
} else {
|
||||
windowManager.close();
|
||||
}
|
||||
},
|
||||
tooltip: '关闭',
|
||||
),
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () async {
|
||||
if (AppConfig().closeToTray) {
|
||||
await windowManager.hide(); // 隐藏主窗口
|
||||
// 替换托盘提示为系统通知
|
||||
_winNotifyPlugin.showNotificationPluginTemplate(message);
|
||||
} else {
|
||||
windowManager.close();
|
||||
}
|
||||
},
|
||||
tooltip: '关闭',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
[ ] 更换为官方服务器
|
||||
[ ] 添加服务器多选
|
||||
[ ] 移除模拟延迟(对其实不用等待2秒的那个是模拟的我给忘了)->改为检测是否成功连接
|
||||
[ ] 修复延迟计算:从peer连接获取最小延迟(μs->ms),无效则用路由延迟
|
||||
[x] 更换为官方服务器
|
||||
[x] 移除模拟延迟(对其实不用等待2秒的那个是模拟的我给忘了)->改为检测是否成功连接
|
||||
[x] 修复延迟计算:从peer连接获取最小延迟(μs->ms),无效则用路由延迟
|
||||
[x] 管理员问题没权限
|
||||
[ ] 配置信息改为运行目录
|
||||
[ ] 看nat类型
|
||||
[x] 配置信息改为运行目录
|
||||
[x] 看nat类型
|
||||
[x] 缩放尺寸让导航栏变为底部会导致页面状态丢失
|
||||
[x] 服务器始终检测延迟,去除暂停和开始反正也不怎么影响性能多此一举还增加复杂度😜
|
||||
[x] 增加自动更新检测 和自动更新
|
||||
[x] 最小化提供通知
|
||||
[x] 可以直接搜索玩家
|
||||
[ ] 增加自动更新检测 和自动更新
|
||||
[ ] 那就打包两个版本 一个便携版(配置文件随软件) 一个安装版(配置文件随软件)
|
||||
Reference in New Issue
Block a user