​切换官方服务器

更换为官方服务器,提升稳定性。

​移除模拟延迟
改为直接检测连接,优化体验。

​修复延迟计算
从 peer 获取最小延迟(μs→ms),无效时用路由延迟。

​修复管理员权限
解决管理员操作无权限问题。

​配置存储位置调整
将配置信息改为运行目录,方便管理。

​查看 NAT 类型
添加功能检测并显示 NAT 类型。

​修复页面状态丢失
解决缩放导致导航栏移动时页面状态丢失问题。

​优化延迟检测
始终检测延迟,移除暂停/开始功能,简化逻辑。

​最小化通知
应用最小化时提供通知,提醒用户状态。

​增加客户端搜索功能
添加客户端搜索功能,支持快速查找内容或设备。
This commit is contained in:
会做饭的二哈
2025-03-16 21:15:38 +08:00
parent 45b6d939da
commit 567b3518e8
14 changed files with 903 additions and 413 deletions
+16 -4
View File
@@ -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);
});
}
// 更改底部导航栏选中索引的方法
+1 -1
View File
@@ -116,7 +116,7 @@ class _InfoPageState extends State<InfoPage>
);
},
child: Text(
AppInfoUtil.getFullVersion(),
AppInfoUtil.getVersion(),
style: Theme.of(context).textTheme.bodyLarge,
),
),
+82 -57
View File
@@ -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
View File
@@ -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),
);
},
);
},
);
}
}
+11 -5
View File
@@ -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);
},
),
],
+291
View File
@@ -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');
}
}
}
+27 -12
View File
@@ -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: '关闭',
),
],
);
}
+8 -7
View File
@@ -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] 可以直接搜索玩家
[ ] 增加自动更新检测 和自动更新
[ ] 那就打包两个版本 一个便携版(配置文件随软件) 一个安装版(配置文件随软件)