5 Commits
Author SHA1 Message Date
会做饭的二哈 7d52dc475d 1.0.1版本 2025-03-16 21:18:03 +08:00
会做饭的二哈 567b3518e8 ​切换官方服务器
更换为官方服务器,提升稳定性。

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

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

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

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

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

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

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

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

​增加客户端搜索功能
添加客户端搜索功能,支持快速查找内容或设备。
2025-03-16 21:15:38 +08:00
会做饭的二哈 45b6d939da 22 2025-03-16 17:53:06 +08:00
会做饭的二哈 769dab91f0 222 2025-03-16 17:52:56 +08:00
会做饭的二哈 9192ee2cc3 1 2025-03-16 09:12:01 +08:00
50 changed files with 2268 additions and 1168 deletions
+6 -6
View File
@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668"
revision: "c23637390482d4cf9598c3ce3f2be31aa7332daf"
channel: "stable"
project_type: app
@@ -13,11 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: windows
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: web
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
# User provided section
+37 -10
View File
@@ -1,16 +1,43 @@
# fltier
# AstralET 游戏联机工具
A new Flutter project.
[![GitHub](https://img.shields.io/badge/GitHub-AstralET-blue)](https://github.com/ldoubil/astral)
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
## 预览
![manage-step1](/assets/AstralET1.png)
![manage-step1](/assets/AstralET2.png)
![manage-step1](/assets/AstralET3.png)
![manage-step1](/assets/AstralET4.png)
![manage-step1](/assets/AstralET5.png)
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
## 使用教程
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
1. **下载并安装**
前往 [GitHub 项目页面](https://github.com/ldoubil/astral) 下载最新版本的 AstralET。
2. **启动应用**
安装完成后,启动 AstralET。
3. **开始联机**
- ​修改你喜欢的名字和房间密码,然后点击开始即可(如果无法获取到IP尝试管理员启动或者看看防火墙-后期更新会解决这个问题)。
4. **没有第四步了**
- 剩下的功能我相信你一看就懂.
## 介绍
AstralET 是一款基于 **Flutter** 和 **easytier** 开发的轻量级游戏联机工具,旨在为玩家提供简单、高效的联机体验。
## 特性
- **内置 easytier**:将 easytier 直接编译到 AstralET 中,无需额外安装,也不会保留任何后台进程。
- ​**即开即用**:联机时启动应用,结束后关闭即可,操作简单便捷。
- ​**活跃维护**:作者积极更新,随时修复问题并优化功能(上班摸鱼成果)。
## 联系我们 & 功能建议
- **QQ 群** [点击加入 QQ 群](https://qm.qq.com/q/r4VsExDDt6)
- **GitHub Issues** [提交问题或建议](https://github.com/ldoubil/astral/issues)
Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

+18 -6
View File
@@ -3,9 +3,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:system_tray/system_tray.dart';
import 'dart:io';
import 'screens/主屏幕.dart';
import 'config/主题配置.dart';
import 'screens/Home.dart';
import 'config/themeconfiguration.dart';
import 'config/app_config.dart';
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);
});
}
// 更改底部导航栏选中索引的方法
+79 -43
View File
@@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:io'; // 添加 dart:io 导入以使用 Platform 类
import 'dart:io';
import 'package:path/path.dart' as path;
import 'cof.dart';
class AppConfig {
static final AppConfig _instance = AppConfig._internal();
static late SharedPreferences _prefs;
static late ConfigManager _configManager;
static late String _configDirectory;
factory AppConfig() {
return _instance;
@@ -14,112 +16,146 @@ class AppConfig {
// 初始化配置
static Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
// 获取可执行文件所在目录而不是当前工作目录
_configDirectory = File(Platform.resolvedExecutable).parent.path;
final configPath = path.join(_configDirectory, 'config.yaml');
_configManager = ConfigManager(
filePath: configPath,
defaultConfig: {
'theme': {
'mode': 'system',
'seedColor': Colors.blue.value,
},
'server': {
'list': ['public.easytier.cn:11010'],
'current': 'public.easytier.cn:11010',
},
'room': {
'name': 'kevin',
'password': 'kevin',
},
'user': {
'name': Platform.localHostname,
},
'network': {
'virtualIP': '',
'dynamicIP': true,
},
'system': {
'closeToTray': true,
},
},
);
await _configManager.load();
}
// 主题设置
static const String _keyThemeMode = 'themeMode';
ThemeMode get themeMode {
final String? value = _prefs.getString(_keyThemeMode);
final String? value = _configManager.get<String>('theme.mode');
return ThemeMode.values.firstWhere(
(mode) => mode.toString() == value,
(mode) => mode.toString() == 'ThemeMode.$value',
orElse: () => ThemeMode.system,
);
}
Future<void> setThemeMode(ThemeMode mode) async {
await _prefs.setString(_keyThemeMode, mode.toString());
final modeString = mode.toString().split('.').last.toLowerCase();
_configManager.set('theme.mode', modeString);
await _configManager.save();
}
// 主题色设置
static const String _keySeedColor = 'seedColor';
Color get seedColor {
final int? value = _prefs.getInt(_keySeedColor);
final int? value = _configManager.get<int>('theme.seedColor');
return value != null ? Color(value) : Colors.blue;
}
Future<void> setSeedColor(Color color) async {
await _prefs.setInt(_keySeedColor, color.value);
_configManager.set('theme.seedColor', color.value);
await _configManager.save();
}
// 服务器列表设置
static const String _keyServerList = 'serverList';
List<String> get serverList {
final List<String>? value = _prefs.getStringList(_keyServerList);
return value?.isNotEmpty == true ? value! : ['public.easytier.net:11010'];
final List? value = _configManager.get<List>('server.list');
return value?.cast<String>() ?? ['public.easytier.cn:11010'];
}
Future<void> setServerList(List<String> servers) async {
await _prefs.setStringList(_keyServerList, servers);
_configManager.set('server.list', servers);
await _configManager.save();
}
// 当前选中的服务器设置
static const String _keyCurrentServer = 'currentServer';
String get currentServer {
return _prefs.getString(_keyCurrentServer) ?? 'public.easytier.net:11010';
return _configManager.get<String>('server.current') ??
'public.easytier.cn:11010';
}
Future<void> setCurrentServer(String server) async {
await _prefs.setString(_keyCurrentServer, server);
_configManager.set('server.current', server);
await _configManager.save();
}
// 房间名设置
static const String _keyRoomName = 'roomName';
String get roomName {
return _prefs.getString(_keyRoomName) ?? 'kevin';
return _configManager.get<String>('room.name') ?? 'kevin';
}
Future<void> setRoomName(String name) async {
await _prefs.setString(_keyRoomName, name);
_configManager.set('room.name', name);
await _configManager.save();
}
// 房间密码设置
static const String _keyRoomPassword = 'roomPassword';
String get roomPassword {
return _prefs.getString(_keyRoomPassword) ?? 'kevin';
return _configManager.get<String>('room.password') ?? 'kevin';
}
Future<void> setRoomPassword(String password) async {
await _prefs.setString(_keyRoomPassword, password);
_configManager.set('room.password', password);
await _configManager.save();
}
// 用户名设置
static const String _keyUsername = 'username';
String get username {
return _prefs.getString(_keyUsername) ?? Platform.localHostname;
return _configManager.get<String>('user.name') ?? Platform.localHostname;
}
Future<void> setUsername(String name) async {
await _prefs.setString(_keyUsername, name);
_configManager.set('user.name', name);
await _configManager.save();
}
// 虚拟IP设置
static const String _keyVirtualIP = 'virtualIP';
String get virtualIP {
return _prefs.getString(_keyVirtualIP) ?? '';
}
// 关闭按钮进入托盘
static const String _keyCloseToTray = 'closeToTray';
bool get closeToTray {
return _prefs.getBool(_keyCloseToTray) ?? true;
}
Future<void> setCloseToTray(bool enabled) async {
await _prefs.setBool(_keyCloseToTray, enabled);
return _configManager.get<String>('network.virtualIP') ?? '';
}
Future<void> setVirtualIP(String ip) async {
await _prefs.setString(_keyVirtualIP, ip);
_configManager.set('network.virtualIP', ip);
await _configManager.save();
}
// 关闭按钮进入托盘
bool get closeToTray {
return _configManager.get<bool>('system.closeToTray') ?? true;
}
Future<void> setCloseToTray(bool enabled) async {
_configManager.set('system.closeToTray', enabled);
await _configManager.save();
}
// 动态获取IP设置
static const String _keyDynamicIP = 'dynamicIP';
bool get dynamicIP {
return _prefs.getBool(_keyDynamicIP) ?? true;
return _configManager.get<bool>('network.dynamicIP') ?? true;
}
Future<void> setDynamicIP(bool enabled) async {
await _prefs.setBool(_keyDynamicIP, enabled);
_configManager.set('network.dynamicIP', enabled);
await _configManager.save();
}
}
+159
View File
@@ -0,0 +1,159 @@
import 'dart:io';
import 'package:yaml/yaml.dart';
class ConfigManager {
final String filePath;
final Map<String, dynamic> defaultConfig;
late Map<String, dynamic> _config;
ConfigManager({
required this.filePath,
required this.defaultConfig,
}) : _config = Map.from(defaultConfig);
/// 加载配置文件(如果不存在则创建默认配置)
Future<void> load() async {
final file = File(filePath);
if (!await file.exists()) {
await _createDefaultConfig();
return;
}
try {
final content = await file.readAsString();
final yamlMap = loadYaml(content);
_config = _mergeConfigs(_convertYaml(yamlMap));
} catch (e) {
throw Exception('配置文件解析失败: $e');
}
}
/// 保存当前配置到文件
Future<void> save() async {
final yamlString = _generateYaml();
await File(filePath).writeAsString(yamlString);
}
/// 获取配置值(支持点分隔符访问嵌套字段)
T? get<T>(String keyPath) {
final keys = keyPath.split('.');
dynamic value = _config;
for (final key in keys) {
if (value is Map && value.containsKey(key)) {
value = value[key];
} else {
return null;
}
}
return value is T ? value : null;
}
/// 设置配置值(支持点分隔符访问嵌套字段)
void set<T>(String keyPath, T value) {
final keys = keyPath.split('.');
dynamic current = _config;
for (int i = 0; i < keys.length - 1; i++) {
final key = keys[i];
current = current.putIfAbsent(key, () => <String, dynamic>{});
}
current[keys.last] = value;
}
/// 合并用户配置与默认配置
Map<String, dynamic> _mergeConfigs(Map<String, dynamic> userConfig) {
return _deepMerge(defaultConfig, userConfig);
}
/// 深度合并两个Map
Map<String, dynamic> _deepMerge(
Map<String, dynamic> base, Map<String, dynamic> override) {
final result = Map<String, dynamic>.from(base);
override.forEach((key, value) {
if (value is Map<String, dynamic> &&
result[key] is Map<String, dynamic>) {
result[key] = _deepMerge(result[key], value);
} else {
result[key] = value;
}
});
return result;
}
/// 转换YAML结构为Dart Map
dynamic _convertYaml(dynamic yaml) {
if (yaml is YamlMap) {
return yaml.map((k, v) => MapEntry(k.toString(), _convertYaml(v)));
}
if (yaml is YamlList) {
return yaml.map((e) => _convertYaml(e)).toList();
}
return yaml;
}
/// 生成YAML字符串
String _generateYaml() {
final buffer = StringBuffer();
_writeMap(_config, buffer, 0);
return buffer.toString();
}
/// 递归写入Map结构
void _writeMap(Map<String, dynamic> map, StringBuffer buffer, int indent) {
final prefix = ' ' * indent;
map.forEach((key, value) {
buffer.write('$prefix$key: ');
if (value is Map<String, dynamic>) {
buffer.writeln();
_writeMap(value, buffer, indent + 1);
} else if (value is List) {
_writeList(value, buffer, indent + 1);
} else {
buffer.writeln(_formatValue(value));
}
});
}
/// 处理列表值
void _writeList(List list, StringBuffer buffer, int indent) {
final prefix = ' ' * indent;
if (list.isEmpty) {
buffer.writeln('[]');
return;
}
buffer.writeln();
for (final item in list) {
buffer.write('$prefix- ');
if (item is Map) {
_writeMap(item.cast<String, dynamic>(), buffer, indent + 1);
} else if (item is List) {
_writeList(item, buffer, indent + 1);
} else {
buffer.writeln(_formatValue(item));
}
}
}
/// 格式值处理
String _formatValue(dynamic value) {
if (value is String) return '"$value"';
if (value is bool) return value.toString();
return value.toString();
}
/// 创建默认配置文件
Future<void> _createDefaultConfig() async {
_config = Map.from(defaultConfig);
await save();
}
}
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import '../screens/首页.dart';
import '../screens/设置.dart';
import '../screens/关于.dart';
import '../screens/房间.dart';
import '../screens/front.dart';
import '../screens/settings.dart';
import '../screens/about.dart';
import '../screens/kvroom.dart';
class NavItem {
final String label;
+4 -5
View File
@@ -1,13 +1,12 @@
import 'package:flutter/material.dart';
import 'package:ASTRAL/src/rust/frb_generated.dart';
import 'package:astral/src/rust/frb_generated.dart';
import 'package:window_manager/window_manager.dart';
import 'app.dart';
import 'config/窗口配置.dart';
import 'config/windowconfiguration.dart';
import 'config/app_config.dart';
import 'utils/状态.dart';
import 'utils/kv_state.dart';
import 'package:provider/provider.dart'; // 添加这一行
import 'package:tray_manager/tray_manager.dart';
import 'package:ASTRAL/utils/app_info.dart';
import 'package:astral/utils/app_info.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
import '../widgets/窗口控制按钮.dart';
import '../widgets/主题选择器.dart';
import '../utils/主题工具.dart';
import '../config/导航配置.dart';
import '../widgets/window_control_buttons.dart';
import '../widgets/theme_selector.dart';
import '../utils/theme_tools.dart';
import '../config/navigationconfiguration.dart';
class MainScreen extends StatefulWidget {
final Function toggleThemeMode;
@@ -33,6 +33,8 @@ class _MainScreenState extends State<MainScreen>
late List<Widget> _pages;
late List<NavItem> _navItems;
late AnimationController _titleAnimationController;
// 添加一个key用于IndexedStack
final GlobalKey _indexedStackKey = GlobalKey();
@override
void initState() {
@@ -70,8 +72,16 @@ class _MainScreenState extends State<MainScreen>
currentThemeMode: widget.currentThemeMode,
);
// 预先创建所有页面
_pages = _navItems.map((item) => item.pageBuilder()).toList();
// 预先创建所有页面,并包装在AutomaticKeepAlive中
_pages = _navItems.map((item) {
final page = item.pageBuilder();
// 如果页面已经实现了AutomaticKeepAliveClientMixin,则不需要包装
if (page is AutomaticKeepAliveClientMixin) {
return page;
}
// 否则包装在PageKeepAlive中
return PageKeepAlive(child: page);
}).toList();
}
// 将侧边栏构建方法整合到MainScreen类中
@@ -100,8 +110,9 @@ class _MainScreenState extends State<MainScreen>
// 设置一个阈值,当宽度大于此值时使用侧边栏
final bool useSidebar = screenWidth > 600;
// 创建一个共享的 IndexedStack 实例
// 创建一个共享的 IndexedStack 实例,使用相同的key
final indexedStack = IndexedStack(
key: _indexedStackKey,
index: widget.currentIndex,
children: _pages,
);
@@ -207,3 +218,24 @@ class _MainScreenState extends State<MainScreen>
);
}
}
// 添加一个包装类来保持页面状态
class PageKeepAlive extends StatefulWidget {
final Widget child;
const PageKeepAlive({Key? key, required this.child}) : super(key: key);
@override
State<PageKeepAlive> createState() => _PageKeepAliveState();
}
class _PageKeepAliveState extends State<PageKeepAlive> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
}
@@ -3,7 +3,7 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'dart:math' as math;
import 'package:ASTRAL/utils/app_info.dart';
import 'package:astral/utils/app_info.dart';
class InfoPage extends StatefulWidget {
const InfoPage({super.key});
@@ -116,7 +116,7 @@ class _InfoPageState extends State<InfoPage>
);
},
child: Text(
AppInfoUtil.getFullVersion(),
AppInfoUtil.getVersion(),
style: Theme.of(context).textTheme.bodyLarge,
),
),
@@ -1,14 +1,14 @@
// 导入必要的包
import 'dart:convert';
import 'package:ASTRAL/src/rust/api/simple.dart';
import 'package:ASTRAL/utils/%E7%8A%B6%E6%80%81.dart';
import 'package:ASTRAL/utils/app_info.dart';
import 'package:astral/src/rust/api/simple.dart';
import 'package:astral/utils/kv_state.dart';
import 'package:astral/utils/app_info.dart';
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'dart:async';
import 'package:provider/provider.dart';
import '../widgets/卡片.dart';
import '../widgets/card.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import '../utils/runin.dart';
@@ -120,50 +120,89 @@ class _HomePageState extends State<HomePage> {
roomName: roomName,
roomPassword: roomPassword,
severurl: Serverip);
// 模拟连接过程,2秒后连接成功
Future.delayed(const Duration(seconds: 2), () {
if (isRunning) {
// 确保用户没有在连接过程中取消
setState(() {
_connectionState = ConnectionState.connected;
// 连接成功后开始计时
timer = Timer.periodic(const Duration(seconds: 1), (timer) async {
final info = await getRunningInfo();
// 打印运行信息的详细内容
// print("运行信息详情:${info}");
Runin runin = parseRunin(info);
// 获取网络状态
// 获取网络状态
final networkStatus = await getNetworkStatus();
km.nodes = networkStatus.nodes;
// 添加连接超时计数器
int connectionTimeoutCounter = 0;
// 更新网络流量数据
_updateNetworkStats(networkStatus.nodes);
// print('设备名称: ${runin.devName}');
// print(
// '设备ID: ${_intToIpv4String(runin.myNodeInfo?.virtualIpv4?.address?.addr ?? 0)}');
final int? version =
runin.myNodeInfo?.virtualIpv4?.address?.addr;
if (version != null) {
String ipStr = _intToIpv4String(version);
// 检查IP不为0.0.0.0且与当前IP不同时才更新
if (ipStr != "0.0.0.0" && publicIP != ipStr) {
km.virtualIP = ipStr;
}
}
// print("- 用户名: ${info?.myNodeInfo?.hostname}");
// print("- 虚拟IPv4: ${info?.myNodeInfo?.virtualIpv4?.address}");
// print("- version: ${info?.myNodeInfo?.version}");
// print("- 本地IP: ${info.myNodeInfo.}");
setState(() {
runningTime += const Duration(seconds: 1);
});
});
});
// 不再使用固定延迟模拟连接成功,而是通过定时检查IP来确定连接状态
timer = Timer.periodic(const Duration(seconds: 1), (timer) async {
// 检查组件是否仍然挂载
if (!mounted) {
timer.cancel();
return;
}
final info = await getRunningInfo();
Runin runin = parseRunin(info);
// 获取网络状态
final networkStatus = await getNetworkStatus();
final km = Provider.of<KM>(context, listen: false);
km.nodes = networkStatus.nodes;
// 更新网络流量数据
_updateNetworkStats(networkStatus.nodes);
final int? version = runin.myNodeInfo?.virtualIpv4?.address?.addr;
if (version != null) {
String ipStr = _intToIpv4String(version);
// 检查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;
@@ -203,6 +242,9 @@ class _HomePageState extends State<HomePage> {
}
}
// 再次检查挂载状态,确保在setState前组件仍然挂载
if (!mounted) return;
// 计算速度 (字节/秒 转换为 MB/秒)
setState(() {
_uploadBytes = totalUploadBytes;
@@ -228,26 +270,6 @@ class _HomePageState extends State<HomePage> {
});
}
// 根据屏幕宽度计算列数
int _getColumnCount(BuildContext context) {
final width = MediaQuery.of(context).size.width;
if (width < 600) {
return 1; // 手机屏幕显示1列
} else if (width < 900) {
return 2; // 平板或小屏幕显示2列
} else {
return 3; // 大屏幕显示3列
}
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
String hours = twoDigits(duration.inHours);
String minutes = twoDigits(duration.inMinutes.remainder(60));
String seconds = twoDigits(duration.inSeconds.remainder(60));
return '$hours:$minutes:$seconds';
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
@@ -282,31 +304,37 @@ class _HomePageState extends State<HomePage> {
//我的用户名
username = Provider.of<KM>(context).username;
Serverip = Provider.of<KM>(context).serverIP;
// 使用 SliverPadding 包裹 SliverList
// 使用 LayoutBuilder 来处理布局变化,同时保留状态
return Scaffold(
body: CustomScrollView(
// 添加这个属性来控制滚动行为
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
// 替换原有的 SliverList 为 SliverPadding + SliverGrid
SliverPadding(
padding: const EdgeInsets.all(16.0),
sliver: SliverMasonryGrid.count(
crossAxisCount: _getColumnCount(context), // 根据屏幕宽度动态设置列数
mainAxisSpacing: 16, // 主轴间距
crossAxisSpacing: 16, // 交叉轴间距
childCount: _cardBuilders.length,
itemBuilder: (context, index) {
// 直接从列表中获取构建函数并调用
return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 100),
child: _cardBuilders[index](colorScheme),
);
},
body: LayoutBuilder(builder: (context, constraints) {
// 根据约束计算列数
final columnCount = _getColumnCount(constraints.maxWidth);
return CustomScrollView(
// 添加这个属性来控制滚动行为
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
// 替换原有的 SliverList 为 SliverPadding + SliverGrid
SliverPadding(
padding: const EdgeInsets.all(16.0),
sliver: SliverMasonryGrid.count(
crossAxisCount: columnCount, // 使用计算出的列数
mainAxisSpacing: 16, // 主轴间距
crossAxisSpacing: 16, // 交叉轴间距
childCount: _cardBuilders.length,
itemBuilder: (context, index) {
// 直接从列表中获取构建函数并调用
return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 100),
child: _cardBuilders[index](colorScheme),
);
},
),
),
),
],
),
],
);
}),
floatingActionButton: AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutCubic,
@@ -351,6 +379,25 @@ class _HomePageState extends State<HomePage> {
);
}
// 修改为接受宽度参数,而不是使用 MediaQuery
int _getColumnCount(double width) {
if (width < 600) {
return 1; // 手机屏幕显示1列
} else if (width < 900) {
return 2; // 平板或小屏幕显示2列
} else {
return 3; // 大屏幕显示3列
}
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
String hours = twoDigits(duration.inHours);
String minutes = twoDigits(duration.inMinutes.remainder(60));
String seconds = twoDigits(duration.inSeconds.remainder(60));
return '$hours:$minutes:$seconds';
}
// 根据连接状态获取按钮图标
Widget _getButtonIcon(ConnectionState state) {
switch (state) {
@@ -861,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,
+955
View File
@@ -0,0 +1,955 @@
// 导入必要的包
import 'package:flutter/services.dart'; // 添加这一行导入剪贴板服务
import 'package:astral/utils/kv_state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../widgets/card.dart';
/// 玩家信息模型类
class PlayerInfo {
final String name;
final String ip;
final int latency; // 延迟(ms)
final String connectionType; // 连接类型:直链、中转、本机
final int uploadSpeed; // 上传速度(KB/s)
final int downloadSpeed; // 下载速度(KB/s)
final int sentPackets; // 发送包数量
final int receivedPackets; // 接收包数量
final double packetLossRate; // 丢包率(%)
final String etVersion; // ET版本
final String natType; // 添加NAT类型
PlayerInfo({
required this.name,
required this.ip,
required this.latency,
required this.connectionType,
required this.uploadSpeed,
required this.downloadSpeed,
required this.sentPackets,
required this.receivedPackets,
required this.packetLossRate,
required this.etVersion,
required this.natType, // 添加NAT类型参数
});
}
/// 房间页面组件
/// 用于显示所有玩家的信息
class RoomPage extends StatefulWidget {
const RoomPage({super.key});
@override
State<RoomPage> createState() => _RoomPageState();
}
class _RoomPageState extends State<RoomPage> {
List<PlayerInfo> players = [];
List<PlayerInfo> filteredPlayers = []; // 添加过滤后的玩家列表
bool isLoading = true;
String searchQuery = ''; // 添加搜索查询字符串
TextEditingController searchController = TextEditingController(); // 添加搜索控制器
@override
void initState() {
super.initState();
isLoading = true;
searchController.addListener(_onSearchChanged); // 添加搜索监听器
}
@override
void dispose() {
searchController.removeListener(_onSearchChanged); // 移除搜索监听器
searchController.dispose(); // 释放控制器资源
super.dispose();
}
// 搜索变化处理函数
void _onSearchChanged() {
setState(() {
searchQuery = searchController.text;
_filterPlayers(); // 过滤玩家列表
});
}
// 过滤玩家列表
void _filterPlayers() {
if (searchQuery.isEmpty) {
filteredPlayers = List.from(players); // 如果搜索为空,显示所有玩家
} else {
filteredPlayers = players
.where((player) =>
player.name.toLowerCase().contains(searchQuery.toLowerCase()))
.toList(); // 根据名称过滤玩家
}
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('房间成员'),
actions: [
// 添加搜索按钮
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
showSearch(
context: context,
delegate: _PlayerSearchDelegate(
players: players,
colorScheme: colorScheme,
buildPlayerListItem: _buildPlayerListItem,
),
);
},
),
],
),
body: Consumer<KM>(
builder: (context, km, child) {
// 当 KM 更新时,这个 builder 会被重新调用
// 异步处理数据
_processNodeData(km);
if (isLoading) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: colorScheme.primary,
),
const SizedBox(height: 16),
Text(
'加载玩家信息...',
style: TextStyle(
color: colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
],
),
);
} else if (players.isEmpty) {
// 添加空数据状态显示
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.people_outline,
size: 64,
color: colorScheme.primary.withOpacity(0.6),
),
const SizedBox(height: 16),
Text(
'暂无房间成员',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.primary,
),
),
const SizedBox(height: 8),
Text(
'当前没有其他玩家连接到房间',
style: TextStyle(
color: colorScheme.onSurface.withOpacity(0.7),
),
),
],
),
);
} else {
return CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverPadding(
padding: const EdgeInsets.all(16.0),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child:
_buildPlayerListItem(players[index], colorScheme),
);
},
childCount: players.length,
),
),
),
],
);
}
},
),
);
}
int _getColumnCount(BuildContext context) {
final width = MediaQuery.of(context).size.width;
if (width < 600) {
return 1; // 手机屏幕显示1列
} else if (width < 900) {
return 2; // 平板或小屏幕显示2列
} else {
return 3; // 大屏幕显示3列
}
}
// 处理节点数据 - 合并了原来的两个相似方法
Future<void> _processNodeData(KM km) async {
try {
final nodes = await km.nodes; // 获取最新的节点信息
// 将节点数据转换为PlayerInfo对象
List<PlayerInfo> nodePlayerInfos = [];
for (var node in nodes) {
// 计算上传下载速度和包数量总和
int uploadSpeed = 0;
int downloadSpeed = 0;
int sentPackets = 0;
int receivedPackets = 0;
String connectionType =
_mapConnectionType(node.cost, node.ipv4, km.virtualIP);
// 如果有连接信息,计算网络统计数据
if (node.connections.isNotEmpty) {
for (var conn in node.connections) {
uploadSpeed += conn.txBytes.toInt() ~/ 1024; // 转换为KB
downloadSpeed += conn.rxBytes.toInt() ~/ 1024; // 转换为KB
sentPackets += conn.txPackets.toInt();
receivedPackets += conn.rxPackets.toInt();
}
}
// 计算丢包率 (简单估算)
double packetLossRate = 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));
}
// 获取NAT类型
String natType = _mapNatType(node.nat);
// 创建PlayerInfo对象
nodePlayerInfos.add(
PlayerInfo(
name: node.hostname,
ip: node.ipv4, // 临时IP,实际应从节点信息中获取
latency: (node.latencyMs).toInt(), // 转换为毫秒
connectionType: connectionType,
uploadSpeed: uploadSpeed,
downloadSpeed: downloadSpeed,
sentPackets: sentPackets,
receivedPackets: receivedPackets,
packetLossRate: packetLossRate,
etVersion: node.version, // 获取版本信息
natType: natType, // 添加NAT类型
),
);
}
if (!mounted) return; // 检查组件是否仍然挂载
setState(() {
players = nodePlayerInfos;
_filterPlayers(); // 更新过滤后的玩家列表
isLoading = false;
});
} catch (e) {
print("加载节点数据失败: $e");
if (mounted) {
setState(() {
isLoading = false;
});
}
}
}
// 构建列表项视图
Widget _buildPlayerListItem(PlayerInfo player, ColorScheme colorScheme) {
// 根据延迟值确定颜色
Color latencyColor = _getLatencyColor(player.latency);
// 根据连接类型选择图标
IconData connectionIcon = _getConnectionIcon(player.connectionType);
// 检测是否为小屏幕设备
final isSmallScreen = MediaQuery.of(context).size.width < 600;
return FloatingCard(
colorScheme: colorScheme,
maxWidth: double.infinity,
child: isSmallScreen
? _buildMobilePlayerListItem(
player, colorScheme, latencyColor, connectionIcon)
: _buildDesktopPlayerListItem(
player, colorScheme, latencyColor, connectionIcon),
);
}
// 为移动设备优化的列表项布局
Widget _buildMobilePlayerListItem(PlayerInfo player, ColorScheme colorScheme,
Color latencyColor, IconData connectionIcon) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 玩家名称和连接类型
Row(
children: [
Icon(Icons.person, color: colorScheme.primary, size: 22),
const SizedBox(width: 8),
Expanded(
child: Text(
player.name,
style:
const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color:
_getConnectionTypeColor(player.connectionType, colorScheme),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
connectionIcon,
size: 14,
color: Colors.white,
),
const SizedBox(width: 4),
Text(
player.connectionType,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
const SizedBox(height: 12),
// IP地址
_buildInfoRow(
Icons.lan,
'IP地址',
player.ip,
colorScheme,
showCopyButton: true,
),
const SizedBox(height: 8),
// 延迟信息
_buildInfoRow(
Icons.speed,
'延迟',
'${player.latency} ms',
colorScheme,
valueColor: latencyColor,
),
const SizedBox(height: 8),
// ET版本
_buildInfoRow(
Icons.memory,
'ET版本',
player.etVersion,
colorScheme,
),
const SizedBox(height: 8),
// NAT类型
_buildInfoRow(
_getNatTypeIcon(player.natType),
'NAT类型',
player.natType,
colorScheme,
valueColor: _getNatTypeColor(player.natType),
),
const SizedBox(height: 8),
// 丢包率信息
_buildInfoRow(
Icons.error_outline,
'丢包率',
'${player.packetLossRate}%',
colorScheme,
valueColor: _getPacketLossColor(player.packetLossRate),
),
// 网络数据部分
const Divider(height: 16),
// 网络数据信息 - 移动设备上使用紧凑布局
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
children: [
Row(
children: [
Expanded(
child: _buildNetworkDataItem(
'上传',
'${player.uploadSpeed} KB/s',
Icons.upload,
colorScheme.primary,
),
),
Expanded(
child: _buildNetworkDataItem(
'下载',
'${player.downloadSpeed} KB/s',
Icons.download,
colorScheme.secondary,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildNetworkDataItem(
'发送包',
'${player.sentPackets}',
Icons.send,
colorScheme.primary,
),
),
Expanded(
child: _buildNetworkDataItem(
'接收包',
'${player.receivedPackets}',
Icons.call_received,
colorScheme.secondary,
),
),
],
),
],
),
),
],
);
}
// 为桌面设备优化的列表项布局
Widget _buildDesktopPlayerListItem(PlayerInfo player, ColorScheme colorScheme,
Color latencyColor, IconData connectionIcon) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 左侧玩家基本信息
Expanded(
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 玩家名称和连接类型
Row(
children: [
Icon(Icons.person, color: colorScheme.primary, size: 22),
const SizedBox(width: 8),
Expanded(
child: Text(
player.name,
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getConnectionTypeColor(
player.connectionType, colorScheme),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
connectionIcon,
size: 14,
color: Colors.white,
),
const SizedBox(width: 4),
Text(
player.connectionType,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
const SizedBox(height: 12),
// IP地址
_buildInfoRow(
Icons.lan,
'IP地址',
player.ip,
colorScheme,
showCopyButton: true,
),
const SizedBox(height: 8),
// ET版本
_buildInfoRow(
Icons.memory,
'ET版本',
player.etVersion,
colorScheme,
),
const SizedBox(height: 8),
// NAT类型
_buildInfoRow(
_getNatTypeIcon(player.natType),
'NAT类型',
player.natType,
colorScheme,
valueColor: _getNatTypeColor(player.natType),
),
],
),
),
// 中间网络状态信息
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 延迟信息
_buildInfoRow(
Icons.speed,
'延迟',
'${player.latency} ms',
colorScheme,
valueColor: latencyColor,
),
const SizedBox(height: 8),
// 丢包率信息
_buildInfoRow(
Icons.error_outline,
'丢包率',
'${player.packetLossRate}%',
colorScheme,
valueColor: _getPacketLossColor(player.packetLossRate),
),
const SizedBox(height: 8),
// 上传下载速度
Row(
children: [
Expanded(
child: _buildNetworkDataItem(
'上传',
'${player.uploadSpeed} KB/s',
Icons.upload,
colorScheme.primary,
),
),
Expanded(
child: _buildNetworkDataItem(
'下载',
'${player.downloadSpeed} KB/s',
Icons.download,
colorScheme.secondary,
),
),
],
),
],
),
),
// 右侧包数据信息
Expanded(
flex: 1,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildNetworkDataItem(
'发送包',
'${player.sentPackets}',
Icons.send,
colorScheme.primary,
),
const SizedBox(height: 8),
_buildNetworkDataItem(
'接收包',
'${player.receivedPackets}',
Icons.call_received,
colorScheme.secondary,
),
],
),
),
],
);
}
// 更紧凑的网络数据项
Widget _buildNetworkDataItem(
String label,
String value,
IconData icon,
Color color,
) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 18, color: color),
const SizedBox(width: 4),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(fontSize: 12),
),
Text(
value,
style: TextStyle(
fontWeight: FontWeight.bold,
color: color,
fontSize: 13,
),
),
],
),
],
);
}
// 构建信息行
Widget _buildInfoRow(
IconData icon,
String label,
String value,
ColorScheme colorScheme, {
Color? valueColor,
bool showCopyButton = false,
}) {
return Row(
children: [
Icon(icon, size: 20, color: colorScheme.primary),
const SizedBox(width: 12),
Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 8),
// 添加复制按钮到标签和值之间
if (showCopyButton)
IconButton(
icon: const Icon(Icons.copy, size: 18),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
tooltip: '复制$label',
onPressed: () {
// 复制到剪贴板
Clipboard.setData(ClipboardData(text: value));
// 显示提示
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已复制: $value'),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
),
);
},
),
const SizedBox(width: 8),
Expanded(
child: Text(
value,
style: TextStyle(
color: valueColor ?? colorScheme.secondary,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
),
],
);
}
// 根据延迟值获取颜色
Color _getLatencyColor(int latency) {
if (latency < 50) {
return Colors.green;
} else if (latency < 100) {
return Colors.orange;
} else {
return Colors.red;
}
}
// 根据丢包率获取颜色
Color _getPacketLossColor(double lossRate) {
if (lossRate < 1.0) {
return Colors.green;
} else if (lossRate < 5.0) {
return Colors.orange;
} else {
return Colors.red;
}
}
// 如果传入数值=1就是p2p 否则是relay 最后判断是不是等于本机IP如果等于就是direct 本机ip传入
String _mapConnectionType(int connType, String ip, String thisip) {
// 新增服务器IP判断
if (ip == "0.0.0.0") {
return '服务器';
}
// 如果是本机IP,返回direct
if (ip == thisip) {
return '本机';
}
// 根据连接成本判断连接类型
if (connType == 1) {
return '直链';
} else if (connType >= 2) {
return '中转';
}
return '未知';
}
// 根据连接类型获取图标
IconData _getConnectionIcon(String connectionType) {
// 将连接类型转为小写并进行匹配
String lowerType = connectionType.toLowerCase();
// 新增服务器图标
if (lowerType.contains('server') || lowerType.contains('服务器')) {
return Icons.dns;
} else if (lowerType.contains('p2p') || lowerType.contains('直链')) {
return Icons.link;
} else if (lowerType.contains('relay') || lowerType.contains('中转')) {
return Icons.swap_horiz;
} else if (lowerType.contains('direct') || lowerType.contains('本机')) {
return Icons.computer;
} else {
return Icons.device_unknown;
}
}
// 根据连接类型获取颜色
Color _getConnectionTypeColor(
String connectionType, ColorScheme colorScheme) {
// 将连接类型转为小写并进行匹配
String lowerType = connectionType.toLowerCase();
if (lowerType.contains('server') || lowerType.contains('服务器')) {
return Colors.deepPurple;
} else if (lowerType.contains('p2p') || lowerType.contains('直链')) {
return Colors.green;
} else if (lowerType.contains('relay') || lowerType.contains('中转')) {
return Colors.orange;
} else if (lowerType.contains('direct') || lowerType.contains('本机')) {
return colorScheme.primary;
} else {
return Colors.grey;
}
}
// 将NAT类型转换为中文
String _mapNatType(String natType) {
switch (natType) {
case 'Unknown':
return '未知';
case 'OpenInternet':
return '开放网络';
case 'NoPat':
return '无PAT';
case 'FullCone':
return '全锥形';
case 'Restricted':
return '受限锥形';
case 'PortRestricted':
return '端口受限锥形';
case 'Symmetric':
return '对称型';
case 'SymUdpFirewall':
return '对称UDP防火墙';
case 'SymmetricEasyInc':
return '对称递增型';
case 'SymmetricEasyDec':
return '对称递减型';
default:
return '未知';
}
}
// 根据NAT类型获取图标
IconData _getNatTypeIcon(String natType) {
if (natType.contains('开放') || natType.contains('全锥形')) {
return Icons.public;
} else if (natType.contains('受限')) {
return Icons.shield;
} else if (natType.contains('端口受限')) {
return Icons.security;
} else if (natType.contains('对称')) {
return Icons.sync_alt;
} else if (natType.contains('防火墙')) {
return Icons.fireplace;
} else if (natType.contains('递增')) {
return Icons.trending_up;
} else if (natType.contains('递减')) {
return Icons.trending_down;
} else if (natType.contains('无PAT')) {
return Icons.router;
} else {
return Icons.help_outline;
}
}
// 根据NAT类型获取颜色
Color _getNatTypeColor(String natType) {
if (natType.contains('开放') ||
natType.contains('全锥形') ||
natType.contains('无PAT')) {
return Colors.green;
} else if (natType.contains('受限') || natType.contains('端口受限')) {
return Colors.orange;
} else if (natType.contains('对称') || natType.contains('防火墙')) {
return Colors.red;
} else {
return Colors.grey;
}
}
}
// 玩家搜索委托类
class _PlayerSearchDelegate extends SearchDelegate<String> {
final List<PlayerInfo> players;
final ColorScheme colorScheme;
final Function(PlayerInfo, ColorScheme) buildPlayerListItem;
_PlayerSearchDelegate({
required this.players,
required this.colorScheme,
required this.buildPlayerListItem,
});
@override
List<Widget> buildActions(BuildContext context) {
return [
IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
// 如果搜索框为空,直接返回
if (query.isEmpty) {
close(context, '');
} else {
// 否则清空搜索内容
query = '';
showSuggestions(context);
}
},
),
];
}
@override
Widget buildLeading(BuildContext context) {
return IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
close(context, '');
},
);
}
@override
Widget buildResults(BuildContext context) {
return _buildSearchResults(context);
}
@override
Widget buildSuggestions(BuildContext context) {
return _buildSearchResults(context);
}
// 修改方法签名,添加 BuildContext 参数
Widget _buildSearchResults(BuildContext context) {
final filteredPlayers = query.isEmpty
? players
: players
.where((player) =>
player.name.toLowerCase().contains(query.toLowerCase()))
.toList();
if (filteredPlayers.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: colorScheme.primary.withOpacity(0.6),
),
const SizedBox(height: 16),
Text(
'未找到匹配的玩家',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.primary,
),
),
const SizedBox(height: 8),
Text(
'尝试使用其他搜索关键词',
style: TextStyle(
color: colorScheme.onSurface.withOpacity(0.7),
),
),
],
),
);
}
// 添加对屏幕宽度的检测
return LayoutBuilder(
builder: (context, constraints) {
// 使用约束条件获取当前宽度
return ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: filteredPlayers.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: buildPlayerListItem(filteredPlayers[index], colorScheme),
);
},
);
},
);
}
}
@@ -1,7 +1,9 @@
import 'package:astral/utils/app_info.dart';
import 'package:astral/utils/up.dart';
import 'package:flutter/material.dart';
import '../config/app_config.dart';
import '../utils/ping_util.dart';
import 'package:ASTRAL/utils/状态.dart';
import 'package:astral/utils/kv_state.dart';
import 'package:provider/provider.dart';
class SettingsPage extends StatefulWidget {
@@ -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;
@@ -35,37 +42,34 @@ class _SettingsPageState extends State<SettingsPage> {
// 初始化 ping 状态
for (var server in _serverList) {
pingResults[server] = null;
isPinging[server] = false;
isPinging[server] = true; // 默认所有服务器都开启ping
}
// 开始 ping 当前服务器,并设置为持续 ping
_startPingServer(_currentServer, forceContinuous: true);
// 开始 ping 所有服务器
for (var server in _serverList) {
_pingServer(server);
}
}
@override
void dispose() {
// 停止所有 ping
for (var server in _serverList) {
_stopPingServer(server);
isPinging[server] = false;
}
super.dispose();
}
// 修改开始 ping 方法,添加强制持续 ping 参数
void _startPingServer(String server, {bool forceContinuous = false}) {
// 简化 ping 方法,移除强制持续 ping 参数
void _startPingServer(String server) {
if (isPinging[server] == true) return;
isPinging[server] = true;
if (forceContinuous) {
isPinging[server] = true; // 设置为持续 ping 状态
}
_pingServer(server);
}
// 修改停止 ping 方法
// 修改停止 ping 方法 - 实际上不再需要,但保留以防将来需要
void _stopPingServer(String server) {
// 如果是当前服务器,不允许停止
if (server == _currentServer) return;
isPinging[server] = false;
}
@@ -119,9 +123,10 @@ class _SettingsPageState extends State<SettingsPage> {
_serverList.add(result);
_appConfig.setServerList(_serverList);
// 初始化新服务器的 ping 状态
// 初始化新服务器的 ping 状态并立即开始 ping
pingResults[result] = null;
isPinging[result] = false;
isPinging[result] = true;
_pingServer(result);
});
}
}
@@ -207,25 +212,23 @@ class _SettingsPageState extends State<SettingsPage> {
// 添加构建 ping 显示组件的方法
Widget _buildPingWidget(String server) {
final pingResult = pingResults[server];
if (server == _currentServer || isPinging[server] == true) {
if (pingResult == null) {
return const Text('测试中...', style: TextStyle(color: Colors.grey));
} else {
return Text(
'${pingResult}ms',
style: TextStyle(
color: pingResult < 100
? Colors.green
: (pingResult < 300 ? Colors.orange : Colors.red),
),
);
}
if (pingResult == null) {
return const Text('测试中...', style: TextStyle(color: Colors.grey));
} else {
return Text(
'${pingResult}ms',
style: TextStyle(
color: pingResult < 100
? Colors.green
: (pingResult < 300 ? Colors.orange : Colors.red),
),
);
}
return const Text('点击测试', style: TextStyle(color: Colors.grey));
}
@override
Widget build(BuildContext context) {
// updateChecker.checkForUpdates(context);
serverIP = Provider.of<KM>(context).virtualIP;
return ListView(
padding: const EdgeInsets.all(16.0),
@@ -248,22 +251,7 @@ class _SettingsPageState extends State<SettingsPage> {
leading: const Icon(Icons.list),
title: const Text('服务器列表'),
onExpansionChanged: (expanded) {
// 展开/折叠时处理其他服务器的 ping 状态
setState(() {
for (var server in _serverList) {
if (server != _currentServer) {
if (expanded) {
// 如果之前是手动开启的,则恢复 ping
if (isPinging[server] == true) {
_startPingServer(server);
}
} else {
// 折叠时暂停所有非当前服务器的 ping
_stopPingServer(server);
}
}
}
});
// 不再需要处理展开折叠时的 ping 状态,所有服务器都持续 ping
},
children: [
// 在服务器列表前添加当前服务器的 ping 状态显示
@@ -277,22 +265,17 @@ class _SettingsPageState extends State<SettingsPage> {
// 构建延迟显示组件
Widget pingWidget;
if (isPinging[server] == true) {
if (pingResult == null) {
pingWidget = const Text('测试中...',
style: TextStyle(color: Colors.grey));
} else {
pingWidget = Text('${pingResult}ms',
style: TextStyle(
color: pingResult < 100
? Colors.green
: (pingResult < 300
? Colors.orange
: Colors.red)));
}
} else {
pingWidget = const Text('点击测试',
if (pingResult == null) {
pingWidget = const Text('测试中...',
style: TextStyle(color: Colors.grey));
} else {
pingWidget = Text('${pingResult}ms',
style: TextStyle(
color: pingResult < 100
? Colors.green
: (pingResult < 300
? Colors.orange
: Colors.red)));
}
return ListTile(
@@ -308,26 +291,7 @@ class _SettingsPageState extends State<SettingsPage> {
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// 添加 ping 按钮
IconButton(
icon: Icon(
isPinging[server] == true
? Icons.pause
: Icons.play_arrow,
color: isPinging[server] == true
? Colors.blue
: null,
),
onPressed: () {
setState(() {
if (isPinging[server] == true) {
_stopPingServer(server);
} else {
_startPingServer(server);
}
});
},
),
// 移除 ping 按钮,只保留编辑和删除按钮
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _showEditServerDialog(index),
@@ -343,8 +307,7 @@ class _SettingsPageState extends State<SettingsPage> {
_currentServer = server;
Provider.of<KM>(context, listen: false).serverIP =
server;
// 开始 ping 新选择的服务器
_startPingServer(server);
// 不再需要特别开始 ping 新选择的服务器,因为所有服务器都在 ping
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('已切换到服务器: $server')),
@@ -364,7 +327,7 @@ class _SettingsPageState extends State<SettingsPage> {
),
),
const SizedBox(height: 16),
// 添加应用设置卡片
Card(
child: Column(
@@ -387,23 +350,21 @@ class _SettingsPageState extends State<SettingsPage> {
],
),
),
const SizedBox(height: 16),
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);
},
),
],
-704
View File
@@ -1,704 +0,0 @@
// 导入必要的包
import 'dart:convert';
import 'package:flutter/services.dart'; // 添加这一行导入剪贴板服务
import 'package:ASTRAL/src/rust/api/simple.dart';
import 'package:ASTRAL/utils/%E7%8A%B6%E6%80%81.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../widgets/卡片.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
/// 玩家信息模型类
class PlayerInfo {
final String name;
final String ip;
final int latency; // 延迟(ms)
final String connectionType; // 连接类型:直链、中转、本机
final int uploadSpeed; // 上传速度(KB/s)
final int downloadSpeed; // 下载速度(KB/s)
final int sentPackets; // 发送包数量
final int receivedPackets; // 接收包数量
final double packetLossRate; // 丢包率(%)
final String etVersion; // ET版本
PlayerInfo({
required this.name,
required this.ip,
required this.latency,
required this.connectionType,
required this.uploadSpeed,
required this.downloadSpeed,
required this.sentPackets,
required this.receivedPackets,
required this.packetLossRate,
required this.etVersion,
});
}
/// 房间页面组件
/// 用于显示所有玩家的信息
class RoomPage extends StatefulWidget {
const RoomPage({super.key});
@override
State<RoomPage> createState() => _RoomPageState();
}
class _RoomPageState extends State<RoomPage> {
List<PlayerInfo> players = [];
bool isLoading = true;
// 添加布局类型状态变量
bool isGridLayout = true; // 默认使用网格布局
@override
void initState() {
super.initState();
isLoading = true;
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('房间成员'),
// 添加布局切换按钮
actions: [
IconButton(
icon: Icon(isGridLayout ? Icons.view_list : Icons.grid_view),
tooltip: isGridLayout ? '切换到列表视图' : '切换到网格视图',
onPressed: () {
setState(() {
isGridLayout = !isGridLayout;
});
},
),
],
),
body: Consumer<KM>(
builder: (context, km, child) {
// 当 KM 更新时,这个 builder 会被重新调用
// 异步处理数据
_processNodeData(km);
if (isLoading) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: colorScheme.primary,
),
const SizedBox(height: 16),
Text(
'加载玩家信息...',
style: TextStyle(
color: colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
],
),
);
} else if (players.isEmpty) {
// 添加空数据状态显示
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.people_outline,
size: 64,
color: colorScheme.primary.withOpacity(0.6),
),
const SizedBox(height: 16),
Text(
'暂无房间成员',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.primary,
),
),
const SizedBox(height: 8),
Text(
'当前没有其他玩家连接到房间',
style: TextStyle(
color: colorScheme.onSurface.withOpacity(0.7),
),
),
],
),
);
} else {
return CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverPadding(
padding: const EdgeInsets.all(16.0),
sliver: isGridLayout
? SliverMasonryGrid.count(
crossAxisCount: _getColumnCount(context),
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childCount: players.length,
itemBuilder: (context, index) {
return _buildPlayerCard(
players[index], colorScheme);
},
)
: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: _buildPlayerListItem(
players[index], colorScheme),
);
},
childCount: players.length,
),
),
),
],
);
}
},
),
);
}
int _getColumnCount(BuildContext context) {
final width = MediaQuery.of(context).size.width;
if (width < 600) {
return 1; // 手机屏幕显示1列
} else if (width < 900) {
return 2; // 平板或小屏幕显示2列
} else {
return 3; // 大屏幕显示3列
}
}
// 处理节点数据 - 合并了原来的两个相似方法
Future<void> _processNodeData(KM km) async {
try {
final nodes = await km.nodes; // 获取最新的节点信息
// 将节点数据转换为PlayerInfo对象
List<PlayerInfo> nodePlayerInfos = [];
for (var node in nodes) {
// 计算上传下载速度和包数量总和
int uploadSpeed = 0;
int downloadSpeed = 0;
int sentPackets = 0;
int receivedPackets = 0;
String connectionType =
_mapConnectionType(node.cost, node.ipv4, km.virtualIP);
// 如果有连接信息,计算网络统计数据
if (node.connections.isNotEmpty) {
for (var conn in node.connections) {
uploadSpeed += (conn.txBytes as BigInt).toInt() ~/ 1024; // 转换为KB
downloadSpeed += (conn.rxBytes as BigInt).toInt() ~/ 1024; // 转换为KB
sentPackets += conn.txPackets.toInt();
receivedPackets += conn.rxPackets.toInt();
}
}
// 计算丢包率 (简单估算)
double packetLossRate = 0.0;
if (sentPackets > 0 && receivedPackets > 0) {
packetLossRate = (1.0 - (receivedPackets / sentPackets)).abs() * 100;
if (packetLossRate > 100) packetLossRate = 100.0;
packetLossRate = double.parse(packetLossRate.toStringAsFixed(1));
}
// 创建PlayerInfo对象
nodePlayerInfos.add(
PlayerInfo(
name: node.hostname,
ip: node.ipv4, // 临时IP,实际应从节点信息中获取
latency: (node.latencyMs * 1000).toInt(), // 转换为毫秒
connectionType: connectionType,
uploadSpeed: uploadSpeed,
downloadSpeed: downloadSpeed,
sentPackets: sentPackets,
receivedPackets: receivedPackets,
packetLossRate: packetLossRate,
etVersion: node.version, // 获取版本信息
),
);
}
if (!mounted) return; // 检查组件是否仍然挂载
setState(() {
players = nodePlayerInfos;
isLoading = false;
});
} catch (e) {
print("加载节点数据失败: $e");
if (mounted) {
setState(() {
isLoading = false;
});
}
}
}
// 构建玩家信息卡片
Widget _buildPlayerCard(PlayerInfo player, ColorScheme colorScheme) {
// 根据延迟值确定颜色
Color latencyColor = _getLatencyColor(player.latency);
// 根据连接类型选择图标
IconData connectionIcon = _getConnectionIcon(player.connectionType);
return FloatingCard(
colorScheme: colorScheme,
maxWidth: 600,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 玩家名称和连接类型
Row(
children: [
Icon(Icons.person, color: colorScheme.primary, size: 22),
const SizedBox(width: 8),
Expanded(
child: Text(
player.name,
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getConnectionTypeColor(
player.connectionType, colorScheme),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
connectionIcon,
size: 14,
color: Colors.white,
),
const SizedBox(width: 4),
Text(
player.connectionType,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
const SizedBox(height: 16),
// IP地址
_buildInfoRow(
Icons.lan,
'IP地址',
player.ip,
colorScheme,
showCopyButton: true,
),
const SizedBox(height: 12),
// 延迟信息
_buildInfoRow(
Icons.speed,
'延迟',
'${player.latency} ms',
colorScheme,
valueColor: latencyColor,
),
const SizedBox(height: 12),
// ET版本
_buildInfoRow(
Icons.memory,
'ET版本',
player.etVersion,
colorScheme,
),
const Divider(height: 24),
// 网络数据部分标题
Row(
children: [
Icon(Icons.data_usage, color: colorScheme.primary, size: 18),
const SizedBox(width: 8),
Text(
'网络数据',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: colorScheme.primary,
),
),
],
),
const SizedBox(height: 12),
// 网络数据信息 - 优化对齐方式
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(
children: [
Row(
children: [
Expanded(
child: _buildNetworkDataItemAligned(
'上传',
'${player.uploadSpeed} KB/s',
Icons.upload,
colorScheme.primary,
),
),
Expanded(
child: _buildNetworkDataItemAligned(
'下载',
'${player.downloadSpeed} KB/s',
Icons.download,
colorScheme.secondary,
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildNetworkDataItemAligned(
'发送包',
'${player.sentPackets}',
Icons.send,
colorScheme.primary,
),
),
Expanded(
child: _buildNetworkDataItemAligned(
'接收包',
'${player.receivedPackets}',
Icons.call_received,
colorScheme.secondary,
),
),
],
),
],
),
),
const Divider(height: 24),
// 丢包率信息
_buildInfoRow(
Icons.error_outline,
'丢包率',
'${player.packetLossRate}%',
colorScheme,
valueColor: _getPacketLossColor(player.packetLossRate),
),
],
),
);
}
// 构建列表项视图
Widget _buildPlayerListItem(PlayerInfo player, ColorScheme colorScheme) {
// 根据延迟值确定颜色
Color latencyColor = _getLatencyColor(player.latency);
// 根据连接类型选择图标
IconData connectionIcon = _getConnectionIcon(player.connectionType);
return FloatingCard(
colorScheme: colorScheme,
maxWidth: double.infinity,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 左侧玩家信息
Expanded(
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 玩家名称和连接类型
Row(
children: [
Icon(Icons.person, color: colorScheme.primary, size: 22),
const SizedBox(width: 8),
Expanded(
child: Text(
player.name,
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 8),
// IP地址
_buildInfoRow(
Icons.lan,
'IP地址',
player.ip,
colorScheme,
showCopyButton: true,
),
const SizedBox(height: 8),
// ET版本
_buildInfoRow(
Icons.memory,
'ET版本',
player.etVersion,
colorScheme,
),
],
),
),
// 右侧网络状态信息
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 连接类型标签
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getConnectionTypeColor(
player.connectionType, colorScheme),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
connectionIcon,
size: 14,
color: Colors.white,
),
const SizedBox(width: 4),
Text(
player.connectionType,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
const SizedBox(height: 8),
// 延迟信息
_buildInfoRow(
Icons.speed,
'延迟',
'${player.latency} ms',
colorScheme,
valueColor: latencyColor,
),
const SizedBox(height: 8),
// 丢包率信息
_buildInfoRow(
Icons.error_outline,
'丢包率',
'${player.packetLossRate}%',
colorScheme,
valueColor: _getPacketLossColor(player.packetLossRate),
),
],
),
),
],
),
);
}
// 构建信息行
// 构建信息行
Widget _buildInfoRow(
IconData icon,
String label,
String value,
ColorScheme colorScheme, {
Color? valueColor,
bool showCopyButton = false,
}) {
return Row(
children: [
Icon(icon, size: 20, color: colorScheme.primary),
const SizedBox(width: 12),
Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 8),
// 添加复制按钮到标签和值之间
if (showCopyButton)
IconButton(
icon: const Icon(Icons.copy, size: 18),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
tooltip: '复制$label',
onPressed: () {
// 复制到剪贴板
Clipboard.setData(ClipboardData(text: value));
// 显示提示
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已复制: $value'),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
),
);
},
),
const SizedBox(width: 8),
Expanded(
child: Text(
value,
style: TextStyle(
color: valueColor ?? colorScheme.secondary,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
),
],
);
}
// 构建对齐的网络数据项
Widget _buildNetworkDataItemAligned(
String label,
String value,
IconData icon,
Color color,
) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(icon, size: 22, color: color),
const SizedBox(height: 6),
Text(
label,
style: const TextStyle(fontSize: 13),
textAlign: TextAlign.center,
),
const SizedBox(height: 2),
Text(
value,
style: TextStyle(
fontWeight: FontWeight.bold,
color: color,
fontSize: 14,
),
textAlign: TextAlign.center,
),
],
);
}
// 根据延迟值获取颜色
Color _getLatencyColor(int latency) {
if (latency < 50) {
return Colors.green;
} else if (latency < 100) {
return Colors.orange;
} else {
return Colors.red;
}
}
// 根据丢包率获取颜色
Color _getPacketLossColor(double lossRate) {
if (lossRate < 1.0) {
return Colors.green;
} else if (lossRate < 5.0) {
return Colors.orange;
} else {
return Colors.red;
}
}
// 如果传入数值=1就是p2p 否则是relay 最后判断是不是等于本机IP如果等于就是direct 本机ip传入
String _mapConnectionType(int connType, String ip, String thisip) {
// 新增服务器IP判断
if (ip == "0.0.0.0") {
return '服务器';
}
// 如果是本机IP,返回direct
if (ip == thisip) {
return '本机';
}
// 根据连接成本判断连接类型
if (connType == 1) {
return '直链';
} else if (connType >= 2) {
return '中转';
}
return '未知';
}
// 根据连接类型获取图标
IconData _getConnectionIcon(String connectionType) {
// 将连接类型转为小写并进行匹配
String lowerType = connectionType.toLowerCase();
// 新增服务器图标
if (lowerType.contains('server') || lowerType.contains('服务器')) {
return Icons.dns;
} else if (lowerType.contains('p2p') || lowerType.contains('直链')) {
return Icons.link;
} else if (lowerType.contains('relay') || lowerType.contains('中转')) {
return Icons.swap_horiz;
} else if (lowerType.contains('direct') || lowerType.contains('本机')) {
return Icons.computer;
} else {
return Icons.device_unknown;
}
}
// 根据连接类型获取颜色
Color _getConnectionTypeColor(
String connectionType, ColorScheme colorScheme) {
// 将连接类型转为小写并进行匹配
String lowerType = connectionType.toLowerCase();
if (lowerType.contains('server') || lowerType.contains('服务器')) {
return Colors.deepPurple;
} else if (lowerType.contains('p2p') || lowerType.contains('直链')) {
return Colors.green;
} else if (lowerType.contains('relay') || lowerType.contains('中转')) {
return Colors.orange;
} else if (lowerType.contains('direct') || lowerType.contains('本机')) {
return colorScheme.primary;
} else {
return Colors.grey;
}
}
}
+4
View File
@@ -123,6 +123,7 @@ class KVNodeInfo {
final String hostname;
final String ipv4;
final double latencyMs;
final String nat;
final List<KVNodeConnectionStats> connections;
final String version;
final int cost;
@@ -131,6 +132,7 @@ class KVNodeInfo {
required this.hostname,
required this.ipv4,
required this.latencyMs,
required this.nat,
required this.connections,
required this.version,
required this.cost,
@@ -141,6 +143,7 @@ class KVNodeInfo {
hostname.hashCode ^
ipv4.hashCode ^
latencyMs.hashCode ^
nat.hashCode ^
connections.hashCode ^
version.hashCode ^
cost.hashCode;
@@ -153,6 +156,7 @@ class KVNodeInfo {
hostname == other.hostname &&
ipv4 == other.ipv4 &&
latencyMs == other.latencyMs &&
nat == other.nat &&
connections == other.connections &&
version == other.version &&
cost == other.cost;
+9 -5
View File
@@ -558,15 +558,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
KVNodeInfo dco_decode_kv_node_info(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>;
if (arr.length != 6)
throw Exception('unexpected arr length: expect 6 but see ${arr.length}');
if (arr.length != 7)
throw Exception('unexpected arr length: expect 7 but see ${arr.length}');
return KVNodeInfo(
hostname: dco_decode_String(arr[0]),
ipv4: dco_decode_String(arr[1]),
latencyMs: dco_decode_f_64(arr[2]),
connections: dco_decode_list_kv_node_connection_stats(arr[3]),
version: dco_decode_String(arr[4]),
cost: dco_decode_i_32(arr[5]),
nat: dco_decode_String(arr[3]),
connections: dco_decode_list_kv_node_connection_stats(arr[4]),
version: dco_decode_String(arr[5]),
cost: dco_decode_i_32(arr[6]),
);
}
@@ -795,6 +796,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
var var_hostname = sse_decode_String(deserializer);
var var_ipv4 = sse_decode_String(deserializer);
var var_latencyMs = sse_decode_f_64(deserializer);
var var_nat = sse_decode_String(deserializer);
var var_connections =
sse_decode_list_kv_node_connection_stats(deserializer);
var var_version = sse_decode_String(deserializer);
@@ -803,6 +805,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
hostname: var_hostname,
ipv4: var_ipv4,
latencyMs: var_latencyMs,
nat: var_nat,
connections: var_connections,
version: var_version,
cost: var_cost);
@@ -1050,6 +1053,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_String(self.hostname, serializer);
sse_encode_String(self.ipv4, serializer);
sse_encode_f_64(self.latencyMs, serializer);
sse_encode_String(self.nat, serializer);
sse_encode_list_kv_node_connection_stats(self.connections, serializer);
sse_encode_String(self.version, serializer);
sse_encode_i_32(self.cost, serializer);
+8 -8
View File
@@ -375,7 +375,7 @@ class RustLibWire implements BaseWire {
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfoPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo');
'frbgen_astral_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo');
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo =
_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfoPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
@@ -391,7 +391,7 @@ class RustLibWire implements BaseWire {
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfoPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo');
'frbgen_astral_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo');
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo =
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfoPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
@@ -407,7 +407,7 @@ class RustLibWire implements BaseWire {
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfoPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo');
'frbgen_astral_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo');
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo =
_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfoPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
@@ -423,7 +423,7 @@ class RustLibWire implements BaseWire {
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfoPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo');
'frbgen_astral_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo');
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo =
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfoPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
@@ -439,7 +439,7 @@ class RustLibWire implements BaseWire {
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePairPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair');
'frbgen_astral_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair');
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair =
_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePairPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
@@ -455,7 +455,7 @@ class RustLibWire implements BaseWire {
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePairPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair');
'frbgen_astral_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair');
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair =
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePairPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
@@ -471,7 +471,7 @@ class RustLibWire implements BaseWire {
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoutePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute');
'frbgen_astral_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute');
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute =
_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoutePtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
@@ -487,7 +487,7 @@ class RustLibWire implements BaseWire {
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoutePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute');
'frbgen_astral_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute');
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute =
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoutePtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
@@ -1,4 +1,4 @@
import 'package:ASTRAL/src/rust/api/simple.dart';
import 'package:astral/src/rust/api/simple.dart';
import 'package:flutter/foundation.dart';
import '../config/app_config.dart';
+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');
}
}
}
+58
View File
@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
import '../config/app_config.dart';
import 'package:windows_notification/notification_message.dart';
import 'package:windows_notification/windows_notification.dart';
// Create an instance of Windows Notification with your application name
// application id must be null in packaged mode
final _winNotifyPlugin = WindowsNotification(applicationId: 'Astral');
// create new NotificationMessage instance with id, title, body, and images
NotificationMessage message = NotificationMessage.fromPluginTemplate(
"astral_minimized",
"Astral 已最小化到托盘",
"应用程序正在后台运行,点击托盘图标可以恢复窗口",
// largeImage: "assets/images/icon.ico",
// image: file_path
);
class WindowControls extends StatelessWidget {
const WindowControls({super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: () => windowManager.minimize(),
tooltip: '最小化',
),
IconButton(
icon: const Icon(Icons.crop_square),
onPressed: () async {
if (await windowManager.isMaximized()) {
windowManager.unmaximize();
} else {
windowManager.maximize();
}
},
tooltip: '最大化/还原',
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () async {
if (AppConfig().closeToTray) {
await windowManager.hide(); // 隐藏主窗口
// 替换托盘提示为系统通知
_winNotifyPlugin.showNotificationPluginTemplate(message);
} else {
windowManager.close();
}
},
tooltip: '关闭',
),
],
);
}
}
-43
View File
@@ -1,43 +0,0 @@
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
import '../config/app_config.dart';
import 'package:tray_manager/tray_manager.dart';
class WindowControls extends StatelessWidget {
const WindowControls({super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: () => windowManager.minimize(),
tooltip: '最小化',
),
IconButton(
icon: const Icon(Icons.crop_square),
onPressed: () async {
if (await windowManager.isMaximized()) {
windowManager.unmaximize();
} else {
windowManager.maximize();
}
},
tooltip: '最大化/还原',
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () async {
if (AppConfig().closeToTray) {
await windowManager.hide(); // 隐藏主窗口
await trayManager.setToolTip('FLN2N 正在后台运行'); // 设置托盘提示
} else {
windowManager.close();
}
},
tooltip: '关闭',
),
],
);
}
}
+12
View File
@@ -0,0 +1,12 @@
[x] 更换为官方服务器
[x] 移除模拟延迟(对其实不用等待2秒的那个是模拟的我给忘了)->改为检测是否成功连接
[x] 修复延迟计算:从peer连接获取最小延迟(μs->ms),无效则用路由延迟
[x] 管理员问题没权限
[x] 配置信息改为运行目录
[x] 看nat类型
[x] 缩放尺寸让导航栏变为底部会导致页面状态丢失
[x] 服务器始终检测延迟,去除暂停和开始反正也不怎么影响性能多此一举还增加复杂度😜
[x] 最小化提供通知
[x] 可以直接搜索玩家
[ ] 增加自动更新检测 和自动更新
[ ] 那就打包两个版本 一个便携版(配置文件随软件) 一个安装版(配置文件随软件)
+79 -7
View File
@@ -185,6 +185,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.1"
dbus:
dependency: transitive
description:
name: dbus
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.11"
equatable:
dependency: transitive
description:
@@ -229,10 +237,10 @@ packages:
dependency: "direct main"
description:
name: fl_chart
sha256: c1e26c7e48496be85104c16c040950b0436674cdf0737f3f6e95511b2529b592
sha256: "5276944c6ffc975ae796569a826c38a62d2abcf264e26b88fa6f482e107f4237"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.63.0"
version: "0.70.2"
flutter:
dependency: "direct main"
description: flutter
@@ -255,10 +263,42 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.0.0"
version: "5.0.0"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: d59eeafd6df92174b1d5f68fc9d66634c97ce2e7cfe2293476236547bb19bbbd
url: "https://pub.flutter-io.cn"
source: hosted
version: "19.0.0"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.0.0"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "2569b973fc9d1f63a37410a9f7c1c552081226c597190cb359ef5d5762d1631c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "9.0.0"
flutter_local_notifications_windows:
dependency: transitive
description:
name: flutter_local_notifications_windows
sha256: f8fc0652a601f83419d623c85723a3e82ad81f92b33eaa9bcc21ea1b94773e6e
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
@@ -416,10 +456,10 @@ packages:
dependency: transitive
description:
name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.0.0"
version: "5.1.1"
logging:
dependency: transitive
description:
@@ -532,6 +572,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.0"
platform:
dependency: transitive
description:
@@ -808,6 +856,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.4"
timezone:
dependency: transitive
description:
name: timezone
sha256: ffc9d5f4d1193534ef051f9254063fa53d588609418c84299956c3db9383587d
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.10.0"
timing:
dependency: transitive
description:
@@ -976,6 +1032,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.4.3"
windows_notification:
dependency: "direct main"
description:
name: windows_notification
sha256: be3e650874615f315402c9b9f3656e29af156709c4b5cc272cb4ca0ab7ba94a8
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0"
xdg_directories:
dependency: transitive
description:
@@ -984,8 +1048,16 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
yaml:
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.5.0"
yaml:
dependency: "direct main"
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
+8 -5
View File
@@ -1,4 +1,4 @@
name: ASTRAL
name: astral
description: "ASTRAL"
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
version: 1.0.1
environment:
sdk: ^3.5.4
@@ -42,13 +42,16 @@ dependencies:
flutter_rust_bridge: 2.9.0
flutter_colorpicker: ^1.1.0
window_manager: ^0.4.3
fl_chart: ^0.63.0
fl_chart: ^0.70.2
flutter_staggered_grid_view: ^0.7.0
json_annotation: ^4.8.1
tray_manager: ^0.3.2
system_tray: ^2.0.3
url_launcher: ^6.3.1
package_info_plus: ^8.3.0 # 添加这一行
yaml: ^3.1.2
flutter_local_notifications: ^19.0.0
windows_notification: ^1.3.0
dev_dependencies:
flutter_test:
@@ -60,7 +63,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^4.0.0
flutter_lints: ^5.0.0
integration_test:
sdk: flutter
@@ -70,7 +73,7 @@ dev_dependencies:
# The following section is specific to Flutter packages.
flutter:
assets:
- assets/dlls/
# - assets/dlls/
- assets/icon.ico
fonts:
+29 -2
View File
@@ -280,7 +280,7 @@ pub fn is_easytier_running() -> bool {
// 定义节点连接统计信息结构体
pub struct KVNodeConnectionStats {
pub conn_type: String,
pub conn_type: String, // 连接类型
pub rx_bytes: u64,
pub tx_bytes: u64,
pub rx_packets: u64,
@@ -291,6 +291,7 @@ pub struct KVNodeInfo {
pub hostname: String,
pub ipv4: String,
pub latency_ms: f64,
pub nat: String, // NAT类型
pub connections: Vec<KVNodeConnectionStats>,
pub version: String,
pub cost: i32,
@@ -328,7 +329,33 @@ pub fn get_network_status() -> KVNetworkStatus {
let mut node_info = KVNodeInfo {
hostname: route.hostname.clone(),
ipv4,
latency_ms: f64::from(route.path_latency.max(0)) / 1000.0,
latency_ms: if let Some(peer) = &pair.peer {
// 类似cli.rs中的get_latency_ms方法
let mut min_latency = u64::MAX;
for conn in &peer.conns {
if let Some(stats) = &conn.stats {
min_latency = min_latency.min(stats.latency_us);
}
}
if min_latency == u64::MAX {
// 如果没有找到有效的连接延迟,则使用路由路径延迟
f64::from(route.path_latency.max(0)) / 1000.0
} else {
// 将微秒转换为毫秒
f64::from(min_latency as u32) / 1000.0
}
} else {
// 如果没有peer信息,则使用路由路径延迟
f64::from(route.path_latency.max(0)) / 1000.0
},
nat: route.stun_info.as_ref().map_or_else(
|| "Unknown".to_string(),
|stun| {
// 使用NatType枚举替代直接匹配数字
let nat_type = NatType::try_from(stun.udp_nat_type).unwrap_or(NatType::Unknown);
format!("{:?}", nat_type)
}
),
connections: Vec::new(),
version: route.version.clone(),
cost,
+12 -8
View File
@@ -546,6 +546,7 @@ impl SseDecode for crate::api::simple::KVNodeInfo {
let mut var_hostname = <String>::sse_decode(deserializer);
let mut var_ipv4 = <String>::sse_decode(deserializer);
let mut var_latencyMs = <f64>::sse_decode(deserializer);
let mut var_nat = <String>::sse_decode(deserializer);
let mut var_connections =
<Vec<crate::api::simple::KVNodeConnectionStats>>::sse_decode(deserializer);
let mut var_version = <String>::sse_decode(deserializer);
@@ -554,6 +555,7 @@ impl SseDecode for crate::api::simple::KVNodeInfo {
hostname: var_hostname,
ipv4: var_ipv4,
latency_ms: var_latencyMs,
nat: var_nat,
connections: var_connections,
version: var_version,
cost: var_cost,
@@ -819,6 +821,7 @@ impl flutter_rust_bridge::IntoDart for crate::api::simple::KVNodeInfo {
self.hostname.into_into_dart().into_dart(),
self.ipv4.into_into_dart().into_dart(),
self.latency_ms.into_into_dart().into_dart(),
self.nat.into_into_dart().into_dart(),
self.connections.into_into_dart().into_dart(),
self.version.into_into_dart().into_dart(),
self.cost.into_into_dart().into_dart(),
@@ -964,6 +967,7 @@ impl SseEncode for crate::api::simple::KVNodeInfo {
<String>::sse_encode(self.hostname, serializer);
<String>::sse_encode(self.ipv4, serializer);
<f64>::sse_encode(self.latency_ms, serializer);
<String>::sse_encode(self.nat, serializer);
<Vec<crate::api::simple::KVNodeConnectionStats>>::sse_encode(self.connections, serializer);
<String>::sse_encode(self.version, serializer);
<i32>::sse_encode(self.cost, serializer);
@@ -1087,56 +1091,56 @@ mod io {
flutter_rust_bridge::frb_generated_boilerplate_io!();
#[unsafe(no_mangle)]
pub extern "C" fn frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
pub extern "C" fn frbgen_astral_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
ptr: *const std::ffi::c_void,
) {
MoiArc::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<MyNodeInfo>>::increment_strong_count(ptr as _);
}
#[unsafe(no_mangle)]
pub extern "C" fn frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
pub extern "C" fn frbgen_astral_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
ptr: *const std::ffi::c_void,
) {
MoiArc::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<MyNodeInfo>>::decrement_strong_count(ptr as _);
}
#[unsafe(no_mangle)]
pub extern "C" fn frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
pub extern "C" fn frbgen_astral_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
ptr: *const std::ffi::c_void,
) {
MoiArc::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PeerInfo>>::increment_strong_count(ptr as _);
}
#[unsafe(no_mangle)]
pub extern "C" fn frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
pub extern "C" fn frbgen_astral_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
ptr: *const std::ffi::c_void,
) {
MoiArc::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PeerInfo>>::decrement_strong_count(ptr as _);
}
#[unsafe(no_mangle)]
pub extern "C" fn frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
pub extern "C" fn frbgen_astral_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
ptr: *const std::ffi::c_void,
) {
MoiArc::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PeerRoutePair>>::increment_strong_count(ptr as _);
}
#[unsafe(no_mangle)]
pub extern "C" fn frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
pub extern "C" fn frbgen_astral_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
ptr: *const std::ffi::c_void,
) {
MoiArc::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PeerRoutePair>>::decrement_strong_count(ptr as _);
}
#[unsafe(no_mangle)]
pub extern "C" fn frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
pub extern "C" fn frbgen_astral_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
ptr: *const std::ffi::c_void,
) {
MoiArc::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<Route>>::increment_strong_count(ptr as _);
}
#[unsafe(no_mangle)]
pub extern "C" fn frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
pub extern "C" fn frbgen_astral_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
ptr: *const std::ffi::c_void,
) {
MoiArc::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<Route>>::decrement_strong_count(ptr as _);
+139 -123
View File
@@ -5,56 +5,56 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051
url: "https://pub.dev"
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
url: "https://pub.flutter-io.cn"
source: hosted
version: "64.0.0"
version: "67.0.0"
adaptive_number:
dependency: transitive
description:
name: adaptive_number
sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893"
url: "https://pub.dev"
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.2.0"
version: "6.4.1"
args:
dependency: "direct main"
description:
name: args
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.2"
async:
dependency: transitive
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev"
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.11.0"
version: "2.13.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.dev"
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
version: "2.1.2"
collection:
dependency: "direct main"
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.18.0"
convert:
@@ -62,23 +62,23 @@ packages:
description:
name: convert
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.1"
coverage:
dependency: transitive
description:
name: coverage
sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097"
url: "https://pub.dev"
sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.6.3"
version: "1.11.1"
crypto:
dependency: "direct main"
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.3"
ed25519_edwards:
@@ -86,7 +86,7 @@ packages:
description:
name: ed25519_edwards
sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.1"
file:
@@ -94,47 +94,47 @@ packages:
description:
name: file
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.4"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
url: "https://pub.dev"
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
version: "1.1.1"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612"
url: "https://pub.dev"
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.2.0"
version: "4.0.0"
github:
dependency: "direct main"
description:
name: github
sha256: "9966bc13bf612342e916b0a343e95e5f046c88f602a14476440e9b75d2295411"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "9.17.0"
glob:
dependency: transitive
description:
name: glob
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
url: "https://pub.dev"
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
version: "2.1.3"
hex:
dependency: "direct main"
description:
name: hex
sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.0"
http:
@@ -142,55 +142,55 @@ packages:
description:
name: http
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
url: "https://pub.dev"
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.2.1"
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.0.2"
io:
dependency: transitive
description:
name: io
sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
url: "https://pub.dev"
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.4"
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.6.7"
version: "0.7.2"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
url: "https://pub.dev"
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.8.1"
version: "4.9.0"
lints:
dependency: "direct dev"
description:
name: lints
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
logging:
@@ -198,55 +198,55 @@ packages:
description:
name: logging
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
url: "https://pub.dev"
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.16"
version: "0.12.17"
meta:
dependency: transitive
description:
name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
url: "https://pub.dev"
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.1"
version: "1.16.0"
mime:
dependency: transitive
description:
name: mime
sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e
url: "https://pub.dev"
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.4"
version: "2.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
name: package_config
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
url: "https://pub.dev"
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0"
version: "2.2.0"
path:
dependency: "direct main"
description:
name: path
sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.8.0"
petitparser:
@@ -254,7 +254,7 @@ packages:
description:
name: petitparser
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.4.0"
pool:
@@ -262,23 +262,23 @@ packages:
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.5.1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
url: "https://pub.dev"
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.4"
version: "2.2.0"
shelf:
dependency: transitive
description:
name: shelf
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.1"
shelf_packages_handler:
@@ -286,168 +286,184 @@ packages:
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e
url: "https://pub.dev"
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.2"
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
url: "https://pub.dev"
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.4"
version: "3.0.0"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae"
url: "https://pub.dev"
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703"
url: "https://pub.dev"
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.10.12"
version: "0.10.13"
source_span:
dependency: "direct main"
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.10.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev"
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.11.1"
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://pub.dev"
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.0"
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.1"
version: "1.2.2"
test:
dependency: "direct dev"
description:
name: test
sha256: "9b0dd8e36af4a5b1569029949d50a52cb2a2a2fdaa20cebb96e6603b9ae241f9"
url: "https://pub.dev"
sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.24.6"
version: "1.25.15"
test_api:
dependency: transitive
description:
name: test_api
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
url: "https://pub.dev"
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.6.1"
version: "0.7.4"
test_core:
dependency: transitive
description:
name: test_core
sha256: "4bef837e56375537055fdbbbf6dd458b1859881f4c7e6da936158f77d61ab265"
url: "https://pub.dev"
sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.5.6"
version: "0.6.8"
toml:
dependency: "direct main"
description:
name: toml
sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.14.0"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://pub.dev"
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.2"
version: "1.4.0"
version:
dependency: "direct main"
description:
name: version
sha256: "2307e23a45b43f96469eeab946208ed63293e8afca9c28cd8b5241ff31c55f55"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d"
url: "https://pub.dev"
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.flutter-io.cn"
source: hosted
version: "11.9.0"
version: "15.0.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
url: "https://pub.dev"
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
version: "1.1.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.1.6"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
url: "https://pub.dev"
sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.0"
version: "3.0.2"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d"
url: "https://pub.dev"
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.0"
version: "1.2.1"
yaml:
dependency: "direct main"
description:
name: yaml
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.0.0 <4.0.0"
dart: ">=3.7.0-0 <4.0.0"
+30
View File
@@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:astral/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+38
View File
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="astral">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>astral</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>
+35
View File
@@ -0,0 +1,35 @@
{
"name": "astral",
"short_name": "astral",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/Icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icons/Icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
+7 -2
View File
@@ -1,10 +1,10 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.14)
project(fltier LANGUAGES CXX)
project(astral LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "fltier")
set(BINARY_NAME "astral")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
@@ -75,6 +75,11 @@ set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
# 添加管理员权限要求(修正路径格式)
target_link_options(${BINARY_NAME} PRIVATE
"LINKER:/MANIFESTUAC:level='requireAdministrator' uiAccess='false'"
$<$<C_COMPILER_ID:MSVC>:"LINKER:/MANIFESTINPUT:${CMAKE_CURRENT_SOURCE_DIR}/runner/runner.exe.manifest">)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
@@ -11,6 +11,7 @@
#include <tray_manager/tray_manager_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h>
#include <windows_notification/windows_notification_plugin_c_api.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
@@ -23,4 +24,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
WindowsNotificationPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowsNotificationPluginCApi"));
}
+2
View File
@@ -8,9 +8,11 @@ list(APPEND FLUTTER_PLUGIN_LIST
tray_manager
url_launcher_windows
window_manager
windows_notification
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
flutter_local_notifications_windows
rust_lib_fltier
)
+8
View File
@@ -33,6 +33,14 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
# Add dependency libraries and include directories. Add any application-specific
# dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
# 添加资源复制指令到可执行目标所在目录
add_custom_command(TARGET ${BINARY_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_CURRENT_SOURCE_DIR}/../../assets/dlls"
"${CMAKE_INSTALL_PREFIX}/"
COMMENT "Copying DLL files to output directory"
VERBATIM
)
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+7
View File
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>