Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d52dc475d | ||
|
|
567b3518e8 | ||
|
|
45b6d939da | ||
|
|
769dab91f0 | ||
|
|
9192ee2cc3 |
@@ -4,7 +4,7 @@
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668"
|
||||
revision: "c23637390482d4cf9598c3ce3f2be31aa7332daf"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
@@ -13,11 +13,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
|
||||
|
||||
|
||||
@@ -1,16 +1,43 @@
|
||||
# fltier
|
||||
# AstralET 游戏联机工具
|
||||
|
||||
A new Flutter project.
|
||||
[](https://github.com/ldoubil/astral)
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
## 预览
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||
## 使用教程
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
|
||||
1. **下载并安装**
|
||||
前往 [GitHub 项目页面](https://github.com/ldoubil/astral) 下载最新版本的 AstralET。
|
||||
|
||||
2. **启动应用**
|
||||
安装完成后,启动 AstralET。
|
||||
|
||||
3. **开始联机**
|
||||
- 修改你喜欢的名字和房间密码,然后点击开始即可(如果无法获取到IP尝试管理员启动或者看看防火墙-后期更新会解决这个问题)。
|
||||
|
||||
4. **没有第四步了**
|
||||
- 剩下的功能我相信你一看就懂.
|
||||
|
||||
|
||||
## 介绍
|
||||
|
||||
AstralET 是一款基于 **Flutter** 和 **easytier** 开发的轻量级游戏联机工具,旨在为玩家提供简单、高效的联机体验。
|
||||
|
||||
## 特性
|
||||
|
||||
- **内置 easytier**:将 easytier 直接编译到 AstralET 中,无需额外安装,也不会保留任何后台进程。
|
||||
- **即开即用**:联机时启动应用,结束后关闭即可,操作简单便捷。
|
||||
- **活跃维护**:作者积极更新,随时修复问题并优化功能(上班摸鱼成果)。
|
||||
|
||||
## 联系我们 & 功能建议
|
||||
|
||||
- **QQ 群**: [点击加入 QQ 群](https://qm.qq.com/q/r4VsExDDt6)
|
||||
- **GitHub Issues**: [提交问题或建议](https://github.com/ldoubil/astral/issues)
|
||||
|
||||
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
// 更改底部导航栏选中索引的方法
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
@@ -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);
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import '../config/app_config.dart';
|
||||
import 'package:windows_notification/notification_message.dart';
|
||||
import 'package:windows_notification/windows_notification.dart';
|
||||
|
||||
// Create an instance of Windows Notification with your application name
|
||||
// application id must be null in packaged mode
|
||||
final _winNotifyPlugin = WindowsNotification(applicationId: 'Astral');
|
||||
// create new NotificationMessage instance with id, title, body, and images
|
||||
NotificationMessage message = NotificationMessage.fromPluginTemplate(
|
||||
"astral_minimized",
|
||||
"Astral 已最小化到托盘",
|
||||
"应用程序正在后台运行,点击托盘图标可以恢复窗口",
|
||||
// largeImage: "assets/images/icon.ico",
|
||||
// image: file_path
|
||||
);
|
||||
|
||||
class WindowControls extends StatelessWidget {
|
||||
const WindowControls({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
onPressed: () => windowManager.minimize(),
|
||||
tooltip: '最小化',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.crop_square),
|
||||
onPressed: () async {
|
||||
if (await windowManager.isMaximized()) {
|
||||
windowManager.unmaximize();
|
||||
} else {
|
||||
windowManager.maximize();
|
||||
}
|
||||
},
|
||||
tooltip: '最大化/还原',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () async {
|
||||
if (AppConfig().closeToTray) {
|
||||
await windowManager.hide(); // 隐藏主窗口
|
||||
// 替换托盘提示为系统通知
|
||||
_winNotifyPlugin.showNotificationPluginTemplate(message);
|
||||
} else {
|
||||
windowManager.close();
|
||||
}
|
||||
},
|
||||
tooltip: '关闭',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import '../config/app_config.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
class WindowControls extends StatelessWidget {
|
||||
const WindowControls({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
onPressed: () => windowManager.minimize(),
|
||||
tooltip: '最小化',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.crop_square),
|
||||
onPressed: () async {
|
||||
if (await windowManager.isMaximized()) {
|
||||
windowManager.unmaximize();
|
||||
} else {
|
||||
windowManager.maximize();
|
||||
}
|
||||
},
|
||||
tooltip: '最大化/还原',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () async {
|
||||
if (AppConfig().closeToTray) {
|
||||
await windowManager.hide(); // 隐藏主窗口
|
||||
await trayManager.setToolTip('FLN2N 正在后台运行'); // 设置托盘提示
|
||||
} else {
|
||||
windowManager.close();
|
||||
}
|
||||
},
|
||||
tooltip: '关闭',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
[x] 更换为官方服务器
|
||||
[x] 移除模拟延迟(对其实不用等待2秒的那个是模拟的我给忘了)->改为检测是否成功连接
|
||||
[x] 修复延迟计算:从peer连接获取最小延迟(μs->ms),无效则用路由延迟
|
||||
[x] 管理员问题没权限
|
||||
[x] 配置信息改为运行目录
|
||||
[x] 看nat类型
|
||||
[x] 缩放尺寸让导航栏变为底部会导致页面状态丢失
|
||||
[x] 服务器始终检测延迟,去除暂停和开始反正也不怎么影响性能多此一举还增加复杂度😜
|
||||
[x] 最小化提供通知
|
||||
[x] 可以直接搜索玩家
|
||||
[ ] 增加自动更新检测 和自动更新
|
||||
[ ] 那就打包两个版本 一个便携版(配置文件随软件) 一个安装版(配置文件随软件)
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 _);
|
||||
|
||||
@@ -5,56 +5,56 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051
|
||||
url: "https://pub.dev"
|
||||
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "64.0.0"
|
||||
version: "67.0.0"
|
||||
adaptive_number:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: adaptive_number
|
||||
sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893"
|
||||
url: "https://pub.dev"
|
||||
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.2.0"
|
||||
version: "6.4.1"
|
||||
args:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: args
|
||||
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
url: "https://pub.dev"
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
version: "2.13.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||
url: "https://pub.dev"
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
convert:
|
||||
@@ -62,23 +62,23 @@ packages:
|
||||
description:
|
||||
name: convert
|
||||
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
coverage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097"
|
||||
url: "https://pub.dev"
|
||||
sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.6.3"
|
||||
version: "1.11.1"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
ed25519_edwards:
|
||||
@@ -86,7 +86,7 @@ packages:
|
||||
description:
|
||||
name: ed25519_edwards
|
||||
sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
file:
|
||||
@@ -94,47 +94,47 @@ packages:
|
||||
description:
|
||||
name: file
|
||||
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
|
||||
url: "https://pub.dev"
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612"
|
||||
url: "https://pub.dev"
|
||||
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "4.0.0"
|
||||
github:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: github
|
||||
sha256: "9966bc13bf612342e916b0a343e95e5f046c88f602a14476440e9b75d2295411"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.17.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
|
||||
url: "https://pub.dev"
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.3"
|
||||
hex:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hex
|
||||
sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
http:
|
||||
@@ -142,55 +142,55 @@ packages:
|
||||
description:
|
||||
name: http
|
||||
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
|
||||
url: "https://pub.dev"
|
||||
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
version: "3.2.2"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
|
||||
url: "https://pub.dev"
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "1.0.5"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||
url: "https://pub.dev"
|
||||
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.6.7"
|
||||
version: "0.7.2"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
|
||||
url: "https://pub.dev"
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.8.1"
|
||||
version: "4.9.0"
|
||||
lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: lints
|
||||
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
logging:
|
||||
@@ -198,55 +198,55 @@ packages:
|
||||
description:
|
||||
name: logging
|
||||
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
|
||||
url: "https://pub.dev"
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.12.16"
|
||||
version: "0.12.17"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
|
||||
url: "https://pub.dev"
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
version: "1.16.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e
|
||||
url: "https://pub.dev"
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "2.0.0"
|
||||
node_preamble:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_preamble
|
||||
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
|
||||
url: "https://pub.dev"
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.2.0"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
petitparser:
|
||||
@@ -254,7 +254,7 @@ packages:
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.4.0"
|
||||
pool:
|
||||
@@ -262,23 +262,23 @@ packages:
|
||||
description:
|
||||
name: pool
|
||||
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
|
||||
url: "https://pub.dev"
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.2.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
shelf_packages_handler:
|
||||
@@ -286,168 +286,184 @@ packages:
|
||||
description:
|
||||
name: shelf_packages_handler
|
||||
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
shelf_static:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_static
|
||||
sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e
|
||||
url: "https://pub.dev"
|
||||
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.1.3"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
|
||||
url: "https://pub.dev"
|
||||
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "3.0.0"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_map_stack_trace
|
||||
sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae"
|
||||
url: "https://pub.dev"
|
||||
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
source_maps:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703"
|
||||
url: "https://pub.dev"
|
||||
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.10.12"
|
||||
version: "0.10.13"
|
||||
source_span:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
url: "https://pub.dev"
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
url: "https://pub.dev"
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.4"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
url: "https://pub.dev"
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.4.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||
url: "https://pub.dev"
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.2.2"
|
||||
test:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
sha256: "9b0dd8e36af4a5b1569029949d50a52cb2a2a2fdaa20cebb96e6603b9ae241f9"
|
||||
url: "https://pub.dev"
|
||||
sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.24.6"
|
||||
version: "1.25.15"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
||||
url: "https://pub.dev"
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.6.1"
|
||||
version: "0.7.4"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "4bef837e56375537055fdbbbf6dd458b1859881f4c7e6da936158f77d61ab265"
|
||||
url: "https://pub.dev"
|
||||
sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.5.6"
|
||||
version: "0.6.8"
|
||||
toml:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: toml
|
||||
sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.14.0"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
|
||||
url: "https://pub.dev"
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
version: "1.4.0"
|
||||
version:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: version
|
||||
sha256: "2307e23a45b43f96469eeab946208ed63293e8afca9c28cd8b5241ff31c55f55"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d"
|
||||
url: "https://pub.dev"
|
||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "11.9.0"
|
||||
version: "15.0.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
|
||||
url: "https://pub.dev"
|
||||
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.1.6"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
|
||||
url: "https://pub.dev"
|
||||
sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "3.0.2"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webkit_inspection_protocol
|
||||
sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d"
|
||||
url: "https://pub.dev"
|
||||
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.2.1"
|
||||
yaml:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: yaml
|
||||
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.0.0 <4.0.0"
|
||||
dart: ">=3.7.0-0 <4.0.0"
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// This is a basic Flutter widget test.
|
||||
//
|
||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:astral/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
expect(find.text('1'), findsNothing);
|
||||
|
||||
// Tap the '+' icon and trigger a frame.
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pump();
|
||||
|
||||
// Verify that our counter has incremented.
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
After Width: | Height: | Size: 917 B |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||