mirror of
https://github.com/EasyTier/astral.git
synced 2025-05-19 10:30:24 +00:00
first commit
This commit is contained in:
+156
@@ -0,0 +1,156 @@
|
||||
// 导入必要的Flutter包和自定义模块
|
||||
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 'config/app_config.dart';
|
||||
|
||||
// 定义应用程序的主要StatefulWidget
|
||||
class MyApp extends StatefulWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
// 应用程序的状态管理类
|
||||
class _MyAppState extends State<MyApp> {
|
||||
// 主题相关的状态变量
|
||||
late ThemeMode _themeMode;
|
||||
late bool useMaterial3;
|
||||
late Color _seedColor;
|
||||
int _currentIndex = 0;
|
||||
|
||||
// 系统托盘相关变量
|
||||
final SystemTray _systemTray = SystemTray();
|
||||
final AppWindow _appWindow = AppWindow();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 从配置中加载设置
|
||||
final config = AppConfig();
|
||||
_themeMode = config.themeMode;
|
||||
useMaterial3 = true;
|
||||
_seedColor = config.seedColor;
|
||||
|
||||
// 初始化系统托盘
|
||||
initSystemTray();
|
||||
}
|
||||
|
||||
// 初始化系统托盘
|
||||
Future<void> initSystemTray() async {
|
||||
// 设置托盘图标
|
||||
String path = 'assets/icon.ico';
|
||||
|
||||
// 初始化托盘
|
||||
await _systemTray.initSystemTray(
|
||||
title: "ASTRAL",
|
||||
iconPath: path,
|
||||
);
|
||||
|
||||
// 设置托盘菜单
|
||||
final Menu menu = Menu();
|
||||
await menu.buildFrom([
|
||||
MenuItemLabel(label: '打开应用', onClicked: (menuItem) => _appWindow.show()),
|
||||
MenuItemLabel(label: '退出', onClicked: (menuItem) => exit(0)),
|
||||
]);
|
||||
|
||||
// 设置托盘菜单
|
||||
await _systemTray.setContextMenu(menu);
|
||||
|
||||
// 设置托盘点击事件
|
||||
_systemTray.registerSystemTrayEventHandler((eventName) {
|
||||
if (eventName == kSystemTrayEventClick) {
|
||||
_appWindow.show();
|
||||
} else if (eventName == kSystemTrayEventRightClick) {
|
||||
_systemTray.popUpContextMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 切换主题模式的方法
|
||||
void toggleThemeMode() {
|
||||
setState(() {
|
||||
if (_themeMode == ThemeMode.light) {
|
||||
_themeMode = ThemeMode.dark;
|
||||
} else if (_themeMode == ThemeMode.dark) {
|
||||
_themeMode = ThemeMode.system;
|
||||
} else {
|
||||
_themeMode = ThemeMode.light;
|
||||
}
|
||||
AppConfig().setThemeMode(_themeMode);
|
||||
});
|
||||
}
|
||||
|
||||
// 更改主题色的方法
|
||||
void changeSeedColor(Color color) {
|
||||
// 使用 Future.microtask 延迟状态更新,避免在当前帧中触发重建
|
||||
setState(() {
|
||||
_seedColor = color;
|
||||
AppConfig().setSeedColor(color);
|
||||
});
|
||||
}
|
||||
|
||||
// 更改底部导航栏选中索引的方法
|
||||
void changeIndex(int index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 返回应用程序的根Widget
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false, // 隐藏调试标签
|
||||
localizationsDelegates: const [
|
||||
// 添加国际化支持
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
// Insert this line
|
||||
supportedLocales: const [Locale("zh", "CN"), Locale("en", "US")],
|
||||
theme: ThemeConfig.getLightTheme(
|
||||
useMaterial3: useMaterial3,
|
||||
seedColor: _seedColor,
|
||||
).copyWith(
|
||||
textTheme: Typography.material2021().black.apply(
|
||||
fontFamily: 'MiSans',
|
||||
),
|
||||
primaryTextTheme: Typography.material2021().black.apply(
|
||||
fontFamily: 'MiSans',
|
||||
)),
|
||||
darkTheme: ThemeConfig.getDarkTheme(
|
||||
useMaterial3: useMaterial3,
|
||||
seedColor: _seedColor,
|
||||
).copyWith(
|
||||
textTheme: Typography.material2021().white.apply(
|
||||
fontFamily: 'MiSans',
|
||||
),
|
||||
primaryTextTheme: Typography.material2021().white.apply(
|
||||
fontFamily: 'MiSans',
|
||||
),
|
||||
),
|
||||
themeMode: _themeMode, // 设置当前主题模式
|
||||
home: MainScreen(
|
||||
// 设置主屏幕
|
||||
// 切换主题模式的回调函数
|
||||
toggleThemeMode: toggleThemeMode,
|
||||
// 更改主题色的回调函数
|
||||
changeSeedColor: changeSeedColor,
|
||||
// 当前主题模式状态
|
||||
currentThemeMode: _themeMode,
|
||||
// 当前选中的底部导航栏索引
|
||||
currentIndex: _currentIndex,
|
||||
// 更改底部导航栏索引的回调函数
|
||||
changeIndex: changeIndex,
|
||||
// 当前主题色
|
||||
seedColor: _seedColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'dart:io'; // 添加 dart:io 导入以使用 Platform 类
|
||||
|
||||
class AppConfig {
|
||||
static final AppConfig _instance = AppConfig._internal();
|
||||
static late SharedPreferences _prefs;
|
||||
|
||||
factory AppConfig() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
AppConfig._internal();
|
||||
|
||||
// 初始化配置
|
||||
static Future<void> init() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
// 主题设置
|
||||
static const String _keyThemeMode = 'themeMode';
|
||||
ThemeMode get themeMode {
|
||||
final String? value = _prefs.getString(_keyThemeMode);
|
||||
return ThemeMode.values.firstWhere(
|
||||
(mode) => mode.toString() == value,
|
||||
orElse: () => ThemeMode.system,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setThemeMode(ThemeMode mode) async {
|
||||
await _prefs.setString(_keyThemeMode, mode.toString());
|
||||
}
|
||||
|
||||
// 主题色设置
|
||||
static const String _keySeedColor = 'seedColor';
|
||||
Color get seedColor {
|
||||
final int? value = _prefs.getInt(_keySeedColor);
|
||||
return value != null ? Color(value) : Colors.blue;
|
||||
}
|
||||
|
||||
Future<void> setSeedColor(Color color) async {
|
||||
await _prefs.setInt(_keySeedColor, color.value);
|
||||
}
|
||||
|
||||
// 服务器列表设置
|
||||
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'];
|
||||
}
|
||||
|
||||
Future<void> setServerList(List<String> servers) async {
|
||||
await _prefs.setStringList(_keyServerList, servers);
|
||||
}
|
||||
|
||||
// 当前选中的服务器设置
|
||||
static const String _keyCurrentServer = 'currentServer';
|
||||
String get currentServer {
|
||||
return _prefs.getString(_keyCurrentServer) ?? 'public.easytier.net:11010';
|
||||
}
|
||||
|
||||
Future<void> setCurrentServer(String server) async {
|
||||
await _prefs.setString(_keyCurrentServer, server);
|
||||
}
|
||||
|
||||
// 房间名设置
|
||||
static const String _keyRoomName = 'roomName';
|
||||
String get roomName {
|
||||
return _prefs.getString(_keyRoomName) ?? 'kevin';
|
||||
}
|
||||
|
||||
Future<void> setRoomName(String name) async {
|
||||
await _prefs.setString(_keyRoomName, name);
|
||||
}
|
||||
|
||||
// 房间密码设置
|
||||
static const String _keyRoomPassword = 'roomPassword';
|
||||
String get roomPassword {
|
||||
return _prefs.getString(_keyRoomPassword) ?? 'kevin';
|
||||
}
|
||||
|
||||
Future<void> setRoomPassword(String password) async {
|
||||
await _prefs.setString(_keyRoomPassword, password);
|
||||
}
|
||||
|
||||
// 用户名设置
|
||||
static const String _keyUsername = 'username';
|
||||
String get username {
|
||||
return _prefs.getString(_keyUsername) ?? Platform.localHostname;
|
||||
}
|
||||
|
||||
Future<void> setUsername(String name) async {
|
||||
await _prefs.setString(_keyUsername, name);
|
||||
}
|
||||
|
||||
// 虚拟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);
|
||||
}
|
||||
|
||||
Future<void> setVirtualIP(String ip) async {
|
||||
await _prefs.setString(_keyVirtualIP, ip);
|
||||
}
|
||||
|
||||
// 动态获取IP设置
|
||||
static const String _keyDynamicIP = 'dynamicIP';
|
||||
bool get dynamicIP {
|
||||
return _prefs.getBool(_keyDynamicIP) ?? true;
|
||||
}
|
||||
|
||||
Future<void> setDynamicIP(bool enabled) async {
|
||||
await _prefs.setBool(_keyDynamicIP, enabled);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// 应用主题配置
|
||||
class ThemeConfig {
|
||||
// 默认使用Material 3
|
||||
static const bool defaultUseMaterial3 = true;
|
||||
|
||||
// 默认种子颜色
|
||||
static const Color defaultSeedColor = Colors.blue;
|
||||
|
||||
// 默认主题模式
|
||||
static const ThemeMode defaultThemeMode = ThemeMode.system;
|
||||
|
||||
// 获取亮色主题
|
||||
static ThemeData getLightTheme({
|
||||
required bool useMaterial3,
|
||||
required Color seedColor,
|
||||
}) {
|
||||
return ThemeData(
|
||||
useMaterial3: useMaterial3,
|
||||
colorSchemeSeed: seedColor,
|
||||
brightness: Brightness.light,
|
||||
);
|
||||
}
|
||||
|
||||
// 获取暗色主题
|
||||
static ThemeData getDarkTheme({
|
||||
required bool useMaterial3,
|
||||
required Color seedColor,
|
||||
}) {
|
||||
return ThemeData(
|
||||
useMaterial3: useMaterial3,
|
||||
colorSchemeSeed: seedColor,
|
||||
brightness: Brightness.dark,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../screens/首页.dart';
|
||||
import '../screens/设置.dart';
|
||||
import '../screens/关于.dart';
|
||||
import '../screens/房间.dart';
|
||||
|
||||
class NavItem {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final IconData selectedIcon;
|
||||
final Widget Function() pageBuilder; // 修改为函数,延迟创建页面
|
||||
|
||||
const NavItem({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.selectedIcon,
|
||||
required this.pageBuilder,
|
||||
});
|
||||
}
|
||||
|
||||
class NavigationConfig {
|
||||
// 保存已创建的页面实例
|
||||
static final Map<int, Widget> _pageInstances = {};
|
||||
|
||||
static List<NavItem> getNavItems({
|
||||
required Function toggleThemeMode,
|
||||
required Function(Color) changeSeedColor,
|
||||
required ThemeMode currentThemeMode,
|
||||
}) {
|
||||
return [
|
||||
NavItem(
|
||||
label: '首页',
|
||||
icon: Icons.home_outlined,
|
||||
selectedIcon: Icons.home,
|
||||
pageBuilder: () => _getOrCreatePage(
|
||||
0,
|
||||
() => HomePage(
|
||||
toggleThemeMode: toggleThemeMode,
|
||||
changeSeedColor: changeSeedColor,
|
||||
currentThemeMode: currentThemeMode,
|
||||
)),
|
||||
),
|
||||
NavItem(
|
||||
label: '房间',
|
||||
icon: Icons.room_outlined,
|
||||
selectedIcon: Icons.room,
|
||||
pageBuilder: () => _getOrCreatePage(1, () => const RoomPage()),
|
||||
),
|
||||
NavItem(
|
||||
label: '设置',
|
||||
icon: Icons.settings_outlined,
|
||||
selectedIcon: Icons.settings,
|
||||
pageBuilder: () => _getOrCreatePage(2, () => const SettingsPage()),
|
||||
),
|
||||
NavItem(
|
||||
label: '关于',
|
||||
icon: Icons.info_outlined,
|
||||
selectedIcon: Icons.info,
|
||||
pageBuilder: () => _getOrCreatePage(3, () => const InfoPage()),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
// 获取或创建页面实例
|
||||
static Widget _getOrCreatePage(int index, Widget Function() creator) {
|
||||
if (!_pageInstances.containsKey(index)) {
|
||||
_pageInstances[index] = creator();
|
||||
}
|
||||
return _pageInstances[index]!;
|
||||
}
|
||||
|
||||
// 清除缓存的页面实例(在需要重新创建页面时调用,如主题变更)
|
||||
static void clearPageInstances() {
|
||||
_pageInstances.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
Future<void> setupWindow() async {
|
||||
WindowOptions windowOptions = const WindowOptions(
|
||||
size: Size(850, 520),
|
||||
minimumSize: Size(300, 300),
|
||||
center: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
skipTaskbar: false,
|
||||
titleBarStyle: TitleBarStyle.hidden, // 隐藏标题栏
|
||||
);
|
||||
|
||||
await windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ASTRAL/src/rust/frb_generated.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'app.dart';
|
||||
import 'config/窗口配置.dart';
|
||||
import 'config/app_config.dart';
|
||||
import 'utils/状态.dart';
|
||||
import 'package:provider/provider.dart'; // 添加这一行
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:ASTRAL/utils/app_info.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
// 初始化应用信息
|
||||
await AppInfoUtil.init();
|
||||
await windowManager.ensureInitialized();
|
||||
// 设置窗口属性
|
||||
await setupWindow();
|
||||
await AppConfig.init();
|
||||
// 初始化应用信息
|
||||
await RustLib.init();
|
||||
runApp(
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => KM(),
|
||||
child: const MyApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import '../widgets/窗口控制按钮.dart';
|
||||
import '../widgets/主题选择器.dart';
|
||||
import '../utils/主题工具.dart';
|
||||
import '../config/导航配置.dart';
|
||||
|
||||
class MainScreen extends StatefulWidget {
|
||||
final Function toggleThemeMode;
|
||||
final Function(Color) changeSeedColor;
|
||||
final ThemeMode currentThemeMode;
|
||||
final int currentIndex;
|
||||
final Function(int) changeIndex;
|
||||
final Color seedColor;
|
||||
|
||||
const MainScreen({
|
||||
super.key,
|
||||
required this.toggleThemeMode,
|
||||
required this.changeSeedColor,
|
||||
required this.currentThemeMode,
|
||||
required this.currentIndex,
|
||||
required this.changeIndex,
|
||||
required this.seedColor,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MainScreen> createState() => _MainScreenState();
|
||||
}
|
||||
|
||||
class _MainScreenState extends State<MainScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
// 缓存所有页面
|
||||
late List<Widget> _pages;
|
||||
late List<NavItem> _navItems;
|
||||
late AnimationController _titleAnimationController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initPages();
|
||||
|
||||
// 初始化标题动画控制器
|
||||
_titleAnimationController = AnimationController(
|
||||
duration: const Duration(seconds: 3),
|
||||
vsync: this,
|
||||
)..repeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_titleAnimationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(MainScreen oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
// 当主题模式或种子颜色变化时,重新初始化页面
|
||||
if (oldWidget.currentThemeMode != widget.currentThemeMode ||
|
||||
oldWidget.seedColor != widget.seedColor) {
|
||||
NavigationConfig.clearPageInstances();
|
||||
_initPages();
|
||||
}
|
||||
}
|
||||
|
||||
void _initPages() {
|
||||
_navItems = NavigationConfig.getNavItems(
|
||||
toggleThemeMode: widget.toggleThemeMode,
|
||||
changeSeedColor: widget.changeSeedColor,
|
||||
currentThemeMode: widget.currentThemeMode,
|
||||
);
|
||||
|
||||
// 预先创建所有页面
|
||||
_pages = _navItems.map((item) => item.pageBuilder()).toList();
|
||||
}
|
||||
|
||||
// 将侧边栏构建方法整合到MainScreen类中
|
||||
Widget buildSidebar(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: List.generate(
|
||||
_navItems.length,
|
||||
(index) => ListTile(
|
||||
leading: Icon(_navItems[index].icon),
|
||||
title: Text(_navItems[index].label),
|
||||
selected: widget.currentIndex == index,
|
||||
onTap: () => widget.changeIndex(index),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 获取屏幕宽度,用于判断使用哪种导航方式
|
||||
final double screenWidth = MediaQuery.of(context).size.width;
|
||||
// 设置一个阈值,当宽度大于此值时使用侧边栏
|
||||
final bool useSidebar = screenWidth > 600;
|
||||
|
||||
// 创建一个共享的 IndexedStack 实例
|
||||
final indexedStack = IndexedStack(
|
||||
index: widget.currentIndex,
|
||||
children: _pages,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: ShaderMask(
|
||||
shaderCallback: (bounds) => LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.primary,
|
||||
Theme.of(context).colorScheme.tertiary,
|
||||
],
|
||||
).createShader(bounds),
|
||||
child: const Text(
|
||||
'ASTRAL',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
elevation: 2,
|
||||
scrolledUnderElevation: 0, // 修改为0,防止滚动时出现阴影变化
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primaryContainer, // 使用更深沉的主题颜色
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimaryContainer, // 对应的前景色
|
||||
// 设置滚动时的背景色保持一致
|
||||
flexibleSpace: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onPanStart: (details) {
|
||||
windowManager.startDragging();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primaryContainer, // 与backgroundColor保持一致
|
||||
),
|
||||
),
|
||||
),
|
||||
titleSpacing: NavigationToolbar.kMiddleSpacing,
|
||||
toolbarHeight: 40, // 减小AppBar的高度
|
||||
iconTheme: const IconThemeData(size: 18), // 设置所有图标的大小
|
||||
actionsIconTheme: const IconThemeData(size: 18), // 设置操作区图标的大小
|
||||
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
widget.currentThemeMode == ThemeMode.light
|
||||
? Icons.wb_sunny
|
||||
: widget.currentThemeMode == ThemeMode.dark
|
||||
? Icons.nightlight_round
|
||||
: Icons.auto_mode,
|
||||
),
|
||||
onPressed: () => widget.toggleThemeMode(),
|
||||
tooltip: getThemeModeText(widget.currentThemeMode),
|
||||
padding: const EdgeInsets.all(8), // 减小按钮内边距
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.color_lens),
|
||||
onPressed: () => showThemeColorPicker(
|
||||
context, widget.seedColor, widget.changeSeedColor),
|
||||
tooltip: '选择主题颜色',
|
||||
padding: const EdgeInsets.all(8), // 减小按钮内边距
|
||||
),
|
||||
const WindowControls(),
|
||||
],
|
||||
),
|
||||
// 根据屏幕宽度决定使用侧边栏还是底部导航栏
|
||||
body: useSidebar
|
||||
? Row(
|
||||
children: [
|
||||
// 固定显示的侧边栏
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: buildSidebar(context),
|
||||
),
|
||||
const VerticalDivider(width: 1, thickness: 1),
|
||||
// 使用共享的 IndexedStack
|
||||
Expanded(
|
||||
child: indexedStack,
|
||||
),
|
||||
],
|
||||
)
|
||||
: indexedStack,
|
||||
// 只在窄屏幕时显示底部导航栏
|
||||
bottomNavigationBar: useSidebar
|
||||
? null
|
||||
: NavigationBar(
|
||||
selectedIndex: widget.currentIndex,
|
||||
onDestinationSelected: (index) => widget.changeIndex(index),
|
||||
destinations: List.generate(
|
||||
_navItems.length,
|
||||
(index) => NavigationDestination(
|
||||
icon: Icon(_navItems[index].icon),
|
||||
selectedIcon: Icon(_navItems[index].selectedIcon),
|
||||
label: _navItems[index].label,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
// ignore_for_file: file_names
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'package:ASTRAL/utils/app_info.dart';
|
||||
|
||||
class InfoPage extends StatefulWidget {
|
||||
const InfoPage({super.key});
|
||||
|
||||
@override
|
||||
State<InfoPage> createState() => _InfoPageState();
|
||||
}
|
||||
|
||||
class _InfoPageState extends State<InfoPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 10),
|
||||
vsync: this,
|
||||
)..repeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 旋转的图标效果
|
||||
AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (_, child) {
|
||||
return Transform.rotate(
|
||||
angle: _controller.value * 2 * math.pi,
|
||||
child: Container(
|
||||
height: 110,
|
||||
width: 110,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: SweepGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.primary,
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
Theme.of(context).colorScheme.tertiary,
|
||||
Theme.of(context).colorScheme.primary,
|
||||
],
|
||||
stops: const [0.0, 0.3, 0.6, 1.0],
|
||||
transform:
|
||||
GradientRotation(_controller.value * 2 * math.pi),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.5),
|
||||
blurRadius: 15,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.games,
|
||||
size: 50,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// 应用名称添加渐变效果
|
||||
ShaderMask(
|
||||
shaderCallback: (bounds) => LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.primary,
|
||||
Theme.of(context).colorScheme.tertiary,
|
||||
],
|
||||
).createShader(bounds),
|
||||
child: Text(
|
||||
'ASTRAL',
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 2.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 版本号添加动画效果
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0, end: 1),
|
||||
duration: const Duration(milliseconds: 800),
|
||||
builder: (context, value, child) {
|
||||
return Opacity(
|
||||
opacity: value,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, 20 * (1 - value)),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
AppInfoUtil.getFullVersion(),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// 卡片添加动画和阴影效果
|
||||
_buildAnimatedCard(
|
||||
context,
|
||||
'特别鸣谢',
|
||||
'特别感谢EasyTier作者所做的工作和帮助,为本项目提供了重要的技术支持。如果您有功能需求或遇到bug,欢迎加入我们的QQ群获取帮助和了解最新动态。',
|
||||
Icons.favorite,
|
||||
Colors.red,
|
||||
delay: 200,
|
||||
),
|
||||
// 合并后的卡片
|
||||
|
||||
const SizedBox(height: 30),
|
||||
// 按钮添加动画效果
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0, end: 1),
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
curve: Curves.elasticOut,
|
||||
builder: (context, value, child) {
|
||||
return Transform.scale(
|
||||
scale: value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.group_add),
|
||||
label: const Text('加入QQ群'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
elevation: 5,
|
||||
),
|
||||
onPressed: () async {
|
||||
final url = 'https://qm.qq.com/q/ErscyNPTzO';
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
await launchUrl(Uri.parse(url));
|
||||
} else {
|
||||
// 无法打开链接时显示提示
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('无法打开QQ群链接')),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// 添加版权信息
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0, end: 1),
|
||||
duration: const Duration(milliseconds: 1200),
|
||||
builder: (context, value, child) {
|
||||
return Opacity(
|
||||
opacity: value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'© ${DateTime.now().year} ASTRAL Team',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAnimatedCard(BuildContext context, String title, String content,
|
||||
IconData icon, Color iconColor,
|
||||
{int delay = 0}) {
|
||||
// 根据当前主题调整颜色
|
||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||
final cardColor = Theme.of(context).cardColor;
|
||||
final textColor = Theme.of(context).textTheme.bodyMedium?.color;
|
||||
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0, end: 1),
|
||||
duration: Duration(milliseconds: 800 + delay),
|
||||
curve: Curves.easeOutBack,
|
||||
builder: (context, value, child) {
|
||||
// 确保 opacity 值在有效范围内 (0.0 到 1.0)
|
||||
final safeOpacity = value.clamp(0.0, 1.0);
|
||||
return Opacity(
|
||||
opacity: safeOpacity,
|
||||
child: Transform.translate(
|
||||
offset: Offset(100 * (1 - value), 0),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Card(
|
||||
elevation: 8,
|
||||
shadowColor: iconColor.withOpacity(isDarkMode ? 0.3 : 0.4),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
cardColor,
|
||||
isDarkMode
|
||||
? cardColor.withOpacity(0.9).withBlue(cardColor.blue + 5)
|
||||
: iconColor.withOpacity(0.05),
|
||||
cardColor,
|
||||
],
|
||||
stops: const [0.0, 0.5, 1.0],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: iconColor.withOpacity(isDarkMode ? 0.05 : 0.1),
|
||||
blurRadius: 10,
|
||||
spreadRadius: -5,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 左侧图标 - 适配深色模式
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: iconColor.withOpacity(isDarkMode ? 0.2 : 0.15),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: iconColor.withOpacity(isDarkMode ? 0.15 : 0.2),
|
||||
blurRadius: isDarkMode ? 6 : 8,
|
||||
spreadRadius: isDarkMode ? 0 : 1,
|
||||
),
|
||||
],
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
iconColor.withOpacity(isDarkMode ? 0.8 : 0.7),
|
||||
iconColor.withOpacity(isDarkMode ? 0.2 : 0.1),
|
||||
],
|
||||
stops: const [0.0, 1.0],
|
||||
radius: 0.8,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// 右侧内容 - 适配深色模式
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
color:
|
||||
iconColor.withOpacity(isDarkMode ? 0.9 : 0.8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Divider(
|
||||
color: iconColor.withOpacity(isDarkMode ? 0.4 : 0.3),
|
||||
thickness: 1.5,
|
||||
endIndent: 60,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
content,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
height: 1.5,
|
||||
letterSpacing: 0.5,
|
||||
color: textColor
|
||||
?.withOpacity(isDarkMode ? 0.9 : 1.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,704 @@
|
||||
// 导入必要的包
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../config/app_config.dart';
|
||||
import '../utils/ping_util.dart';
|
||||
import 'package:ASTRAL/utils/状态.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class SettingsPage extends StatefulWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsPage> createState() => _SettingsPageState();
|
||||
}
|
||||
|
||||
class _SettingsPageState extends State<SettingsPage> {
|
||||
bool _notificationsEnabled = true;
|
||||
double _fontSize = 16.0;
|
||||
late List<String> _serverList;
|
||||
late String _currentServer;
|
||||
final _appConfig = AppConfig();
|
||||
bool _closeToTray = false; // 添加关闭进入托盘变量
|
||||
|
||||
String serverIP = "";
|
||||
// 添加 ping 相关状态
|
||||
Map<String, int?> pingResults = {};
|
||||
Map<String, bool> isPinging = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_serverList = _appConfig.serverList;
|
||||
_currentServer = _appConfig.currentServer;
|
||||
serverIP = _appConfig.currentServer;
|
||||
_closeToTray = _appConfig.closeToTray; // 初始化托盘设置
|
||||
|
||||
// 初始化 ping 状态
|
||||
for (var server in _serverList) {
|
||||
pingResults[server] = null;
|
||||
isPinging[server] = false;
|
||||
}
|
||||
|
||||
// 开始 ping 当前服务器,并设置为持续 ping
|
||||
_startPingServer(_currentServer, forceContinuous: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// 停止所有 ping
|
||||
for (var server in _serverList) {
|
||||
_stopPingServer(server);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// 修改开始 ping 方法,添加强制持续 ping 参数
|
||||
void _startPingServer(String server, {bool forceContinuous = false}) {
|
||||
if (isPinging[server] == true) return;
|
||||
|
||||
isPinging[server] = true;
|
||||
if (forceContinuous) {
|
||||
isPinging[server] = true; // 设置为持续 ping 状态
|
||||
}
|
||||
_pingServer(server);
|
||||
}
|
||||
|
||||
// 修改停止 ping 方法
|
||||
void _stopPingServer(String server) {
|
||||
// 如果是当前服务器,不允许停止
|
||||
if (server == _currentServer) return;
|
||||
isPinging[server] = false;
|
||||
}
|
||||
|
||||
// 执行 ping 操作
|
||||
Future<void> _pingServer(String server) async {
|
||||
if (isPinging[server] != true) return;
|
||||
|
||||
final pingResult = await PingUtil.ping(server);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
pingResults[server] = pingResult;
|
||||
});
|
||||
|
||||
// 1秒后再次 ping
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
_pingServer(server);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 添加服务器对话框
|
||||
Future<void> _showAddServerDialog() async {
|
||||
final controller = TextEditingController();
|
||||
final result = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('添加服务器'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '服务器地址',
|
||||
hintText: 'example.com:port',
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, controller.text),
|
||||
child: const Text('添加'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null && result.isNotEmpty) {
|
||||
setState(() {
|
||||
_serverList.add(result);
|
||||
_appConfig.setServerList(_serverList);
|
||||
|
||||
// 初始化新服务器的 ping 状态
|
||||
pingResults[result] = null;
|
||||
isPinging[result] = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑服务器对话框
|
||||
Future<void> _showEditServerDialog(int index) async {
|
||||
final controller = TextEditingController(text: _serverList[index]);
|
||||
final result = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('编辑服务器'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '服务器地址',
|
||||
hintText: 'example.com:port',
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, controller.text),
|
||||
child: const Text('保存'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null && result.isNotEmpty) {
|
||||
setState(() {
|
||||
_serverList[index] = result;
|
||||
_appConfig.setServerList(_serverList);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 删除服务器
|
||||
Future<void> _deleteServer(int index) async {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('确认删除'),
|
||||
content: const Text('确定要删除这个服务器吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('删除'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirm == true) {
|
||||
final server = _serverList[index];
|
||||
|
||||
// 停止 ping
|
||||
_stopPingServer(server);
|
||||
|
||||
setState(() {
|
||||
_serverList.removeAt(index);
|
||||
_appConfig.setServerList(_serverList);
|
||||
|
||||
// 移除 ping 状态
|
||||
pingResults.remove(server);
|
||||
isPinging.remove(server);
|
||||
|
||||
if (_currentServer == server && _serverList.isNotEmpty) {
|
||||
_currentServer = _serverList[0];
|
||||
_appConfig.setCurrentServer(_currentServer);
|
||||
_startPingServer(_currentServer);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 添加构建 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return const Text('点击测试', style: TextStyle(color: Colors.grey));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
serverIP = Provider.of<KM>(context).virtualIP;
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: [
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.dns),
|
||||
title: Row(
|
||||
children: [
|
||||
const Text('当前服务器'),
|
||||
const SizedBox(width: 8),
|
||||
_buildPingWidget(_currentServer),
|
||||
],
|
||||
),
|
||||
subtitle: Text(_currentServer),
|
||||
),
|
||||
ExpansionTile(
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
children: [
|
||||
// 在服务器列表前添加当前服务器的 ping 状态显示
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: _serverList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final server = _serverList[index];
|
||||
final pingResult = pingResults[server];
|
||||
|
||||
// 构建延迟显示组件
|
||||
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('点击测试',
|
||||
style: TextStyle(color: Colors.grey));
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.computer),
|
||||
title: Row(
|
||||
children: [
|
||||
Text('服务器 ${index + 1}'),
|
||||
const SizedBox(width: 8),
|
||||
pingWidget,
|
||||
],
|
||||
),
|
||||
subtitle: Text(server),
|
||||
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);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () => _showEditServerDialog(index),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () => _deleteServer(index),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_currentServer = server;
|
||||
Provider.of<KM>(context, listen: false).serverIP =
|
||||
server;
|
||||
// 开始 ping 新选择的服务器
|
||||
_startPingServer(server);
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('已切换到服务器: $server')),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add),
|
||||
title: const Text('添加新服务器'),
|
||||
onTap: _showAddServerDialog,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 添加应用设置卡片
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
const ListTile(
|
||||
leading: Icon(Icons.settings),
|
||||
title: Text('应用设置'),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('关闭时最小化到托盘'),
|
||||
subtitle: const Text('关闭窗口时应用将继续在后台运行'),
|
||||
value: _closeToTray,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_closeToTray = value;
|
||||
_appConfig.setCloseToTray(value);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
const ListTile(
|
||||
leading: Icon(Icons.info),
|
||||
title: Text('应用版本'),
|
||||
subtitle: Text('灰度版本'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.update),
|
||||
title: const Text('检查更新'),
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('灰度版本不支持更新')),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,922 @@
|
||||
// 导入必要的包
|
||||
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:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'dart:async';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../widgets/卡片.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import '../utils/runin.dart';
|
||||
|
||||
enum ConnectionState { notStarted, connecting, connected }
|
||||
|
||||
Runin parseRunin(String jsonString) {
|
||||
final Map<String, dynamic> jsonMap = json.decode(jsonString);
|
||||
return Runin.fromJson(jsonMap);
|
||||
}
|
||||
|
||||
// 辅助方法:将整数形式的IP转换为字符串
|
||||
String _intToIpv4String(int addr) {
|
||||
return [
|
||||
(addr >> 24) & 0xFF,
|
||||
(addr >> 16) & 0xFF,
|
||||
(addr >> 8) & 0xFF,
|
||||
addr & 0xFF,
|
||||
].join('.');
|
||||
}
|
||||
|
||||
/// 首页组件
|
||||
/// 用于显示应用的主页面,包含主题切换和问候功能
|
||||
class HomePage extends StatefulWidget {
|
||||
// 主题模式切换回调函数
|
||||
final Function toggleThemeMode;
|
||||
// 主题色更改回调函数
|
||||
final Function(Color) changeSeedColor;
|
||||
// 当前主题模式
|
||||
final ThemeMode currentThemeMode;
|
||||
|
||||
/// 构造函数
|
||||
const HomePage({
|
||||
super.key,
|
||||
required this.toggleThemeMode,
|
||||
required this.changeSeedColor,
|
||||
required this.currentThemeMode,
|
||||
});
|
||||
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
// 定义状态枚举
|
||||
|
||||
// 当前连接状态
|
||||
ConnectionState _connectionState = ConnectionState.notStarted;
|
||||
|
||||
bool isRunning = false; // 只用于控制启动/暂停状态
|
||||
Duration runningTime = Duration.zero;
|
||||
Timer? timer;
|
||||
|
||||
// 模拟数据
|
||||
double uploadSpeed = 0; // MB/s
|
||||
double downloadSpeed = 0; // MB/s
|
||||
String publicIP = "";
|
||||
// 房间名
|
||||
String roomName = "";
|
||||
// 房间密码
|
||||
String roomPassword = "";
|
||||
// 用户名
|
||||
String username = "";
|
||||
|
||||
bool _isAutoIP = true; // 只用于控制IP自动/手动模式
|
||||
|
||||
String Serverip = "";
|
||||
int _uploadBytes = 0;
|
||||
int _downloadBytes = 0;
|
||||
int _lastUploadBytes = 0;
|
||||
int _lastDownloadBytes = 0;
|
||||
// 定义卡片列表
|
||||
late final List<Widget Function(ColorScheme)> _cardBuilders;
|
||||
|
||||
// 添加 TextEditingController
|
||||
late final TextEditingController _roomNameController;
|
||||
late final TextEditingController _roomPasswordController;
|
||||
late final TextEditingController _usernameController;
|
||||
late final TextEditingController _virtualIPController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 初始化 TextEditingController
|
||||
_roomNameController = TextEditingController(text: roomName);
|
||||
_roomPasswordController = TextEditingController(text: roomPassword);
|
||||
_usernameController = TextEditingController(text: username);
|
||||
_virtualIPController = TextEditingController(text: publicIP);
|
||||
|
||||
// 修改卡片构建器列表,添加版本信息卡片
|
||||
_cardBuilders = [
|
||||
_buildNetworkStatusCard, // 网络状态卡片
|
||||
_buildUserInfoCard, // 用户信息卡片
|
||||
_buildRoomInfoCard, // 房间信息卡片
|
||||
_buildVersionInfoCard, // 新增版本信息卡片
|
||||
];
|
||||
}
|
||||
|
||||
void toggleRunning() {
|
||||
final km = Provider.of<KM>(context, listen: false);
|
||||
setState(() {
|
||||
isRunning = !isRunning;
|
||||
if (isRunning) {
|
||||
// 切换到连接中状态
|
||||
_connectionState = ConnectionState.connecting;
|
||||
createServer(
|
||||
username: username,
|
||||
enableDhcp: _isAutoIP,
|
||||
specifiedIp: publicIP,
|
||||
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;
|
||||
|
||||
// 更新网络流量数据
|
||||
_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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 停止时重置状态
|
||||
_connectionState = ConnectionState.notStarted;
|
||||
closeAllServer();
|
||||
timer?.cancel();
|
||||
runningTime = Duration.zero;
|
||||
// 重置网络统计数据
|
||||
_uploadBytes = 0;
|
||||
_downloadBytes = 0;
|
||||
_lastUploadBytes = 0;
|
||||
_lastDownloadBytes = 0;
|
||||
uploadSpeed = 0;
|
||||
downloadSpeed = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 添加网络统计数据更新方法
|
||||
void _updateNetworkStats(dynamic nodes) {
|
||||
if (nodes == null || nodes.isEmpty) return;
|
||||
|
||||
int totalUploadBytes = 0;
|
||||
int totalDownloadBytes = 0;
|
||||
String myIP = Provider.of<KM>(context, listen: false).virtualIP;
|
||||
|
||||
// 查找本机节点
|
||||
for (var node in nodes) {
|
||||
if (node.ipv4 == myIP) {
|
||||
// 找到本机节点,计算上传下载总量
|
||||
if (node.connections != null && node.connections.isNotEmpty) {
|
||||
for (var conn in node.connections) {
|
||||
totalUploadBytes += (conn.txBytes as BigInt).toInt();
|
||||
totalDownloadBytes += (conn.rxBytes as BigInt).toInt();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算速度 (字节/秒 转换为 MB/秒)
|
||||
setState(() {
|
||||
_uploadBytes = totalUploadBytes;
|
||||
_downloadBytes = totalDownloadBytes;
|
||||
|
||||
// 计算速度差值
|
||||
if (_lastUploadBytes > 0) {
|
||||
uploadSpeed =
|
||||
(_uploadBytes - _lastUploadBytes) / (1024 * 1024); // 转换为MB/s
|
||||
uploadSpeed = double.parse(uploadSpeed.toStringAsFixed(2)); // 保留两位小数
|
||||
}
|
||||
|
||||
if (_lastDownloadBytes > 0) {
|
||||
downloadSpeed =
|
||||
(_downloadBytes - _lastDownloadBytes) / (1024 * 1024); // 转换为MB/s
|
||||
downloadSpeed =
|
||||
double.parse(downloadSpeed.toStringAsFixed(2)); // 保留两位小数
|
||||
}
|
||||
|
||||
// 更新上次的值
|
||||
_lastUploadBytes = _uploadBytes;
|
||||
_lastDownloadBytes = _downloadBytes;
|
||||
});
|
||||
}
|
||||
|
||||
// 根据屏幕宽度计算列数
|
||||
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();
|
||||
final km = Provider.of<KM>(context);
|
||||
_roomNameController.value = TextEditingValue(
|
||||
text: km.roomName,
|
||||
selection: _roomNameController.selection,
|
||||
);
|
||||
_roomPasswordController.value = TextEditingValue(
|
||||
text: km.roomPassword,
|
||||
selection: _roomPasswordController.selection,
|
||||
);
|
||||
_usernameController.value = TextEditingValue(
|
||||
text: km.username,
|
||||
selection: _usernameController.selection,
|
||||
);
|
||||
_virtualIPController.value = TextEditingValue(
|
||||
text: km.virtualIP,
|
||||
selection: _virtualIPController.selection,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
publicIP = Provider.of<KM>(context).virtualIP;
|
||||
_isAutoIP = Provider.of<KM>(context).dynamicIP; // 更新自动IP状态
|
||||
//我的房间
|
||||
roomName = Provider.of<KM>(context).roomName;
|
||||
//我的密码
|
||||
roomPassword = Provider.of<KM>(context).roomPassword;
|
||||
//我的用户名
|
||||
username = Provider.of<KM>(context).username;
|
||||
Serverip = Provider.of<KM>(context).serverIP;
|
||||
// 使用 SliverPadding 包裹 SliverList
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOutCubic,
|
||||
width: _connectionState != ConnectionState.notStarted ? 180 : 100,
|
||||
height: 60,
|
||||
child: FloatingActionButton.extended(
|
||||
onPressed: _connectionState == ConnectionState.connecting
|
||||
? null
|
||||
: toggleRunning,
|
||||
extendedPadding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
splashColor: _connectionState != ConnectionState.notStarted
|
||||
? colorScheme.onTertiary.withOpacity(0.2)
|
||||
: colorScheme.onPrimary.withOpacity(0.2),
|
||||
highlightElevation: 6,
|
||||
elevation: 2,
|
||||
icon: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
switchInCurve: Curves.easeInOut,
|
||||
switchOutCurve: Curves.easeInOut,
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: ScaleTransition(
|
||||
scale: animation,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: _getButtonIcon(_connectionState),
|
||||
),
|
||||
label: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 0),
|
||||
switchInCurve: Curves.easeOutQuad,
|
||||
switchOutCurve: Curves.easeInQuad,
|
||||
child: _getButtonLabel(_connectionState),
|
||||
),
|
||||
backgroundColor: _getButtonColor(_connectionState, colorScheme),
|
||||
foregroundColor:
|
||||
_getButtonForegroundColor(_connectionState, colorScheme),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 根据连接状态获取按钮图标
|
||||
Widget _getButtonIcon(ConnectionState state) {
|
||||
switch (state) {
|
||||
case ConnectionState.notStarted:
|
||||
return const Icon(
|
||||
Icons.play_arrow,
|
||||
key: ValueKey('play'),
|
||||
size: 34,
|
||||
);
|
||||
case ConnectionState.connecting:
|
||||
return SizedBox(
|
||||
key: const ValueKey('connecting'),
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.5,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.onTertiary),
|
||||
),
|
||||
);
|
||||
case ConnectionState.connected:
|
||||
return const Icon(
|
||||
Icons.pause,
|
||||
key: ValueKey('pause'),
|
||||
size: 34,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 根据连接状态获取按钮文本
|
||||
Widget _getButtonLabel(ConnectionState state) {
|
||||
switch (state) {
|
||||
case ConnectionState.notStarted:
|
||||
return const Text(
|
||||
'启动',
|
||||
key: ValueKey('start_text'),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 18,
|
||||
),
|
||||
);
|
||||
case ConnectionState.connecting:
|
||||
return const Text(
|
||||
'连接中...',
|
||||
key: ValueKey('connecting_text'),
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
);
|
||||
case ConnectionState.connected:
|
||||
return Text(
|
||||
_formatDuration(runningTime),
|
||||
key: const ValueKey('running_text'),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 根据连接状态获取按钮背景色
|
||||
Color _getButtonColor(ConnectionState state, ColorScheme colorScheme) {
|
||||
switch (state) {
|
||||
case ConnectionState.notStarted:
|
||||
return colorScheme.primary;
|
||||
case ConnectionState.connecting:
|
||||
return colorScheme.tertiary.withOpacity(0.7);
|
||||
case ConnectionState.connected:
|
||||
return colorScheme.tertiary;
|
||||
}
|
||||
}
|
||||
|
||||
// 根据连接状态获取按钮前景色
|
||||
Color _getButtonForegroundColor(
|
||||
ConnectionState state, ColorScheme colorScheme) {
|
||||
switch (state) {
|
||||
case ConnectionState.notStarted:
|
||||
return colorScheme.onPrimary;
|
||||
case ConnectionState.connecting:
|
||||
case ConnectionState.connected:
|
||||
return colorScheme.onTertiary;
|
||||
}
|
||||
}
|
||||
|
||||
// 修改卡片构建方法,移除多余的内边距
|
||||
Widget _buildDashboardCard(ColorScheme colorScheme) {
|
||||
return FloatingCard(
|
||||
colorScheme: colorScheme,
|
||||
maxWidth: 600, // 设置最大宽度
|
||||
height: 200,
|
||||
child: SizedBox(
|
||||
child: PieChart(
|
||||
PieChartData(
|
||||
sections: [
|
||||
PieChartSectionData(
|
||||
value: uploadSpeed,
|
||||
title: '上传',
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
PieChartSectionData(
|
||||
value: downloadSpeed,
|
||||
title: '下载',
|
||||
color: colorScheme.secondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// 修改流量统计卡片,移除多余的内边距
|
||||
Widget _buildTrafficCard(ColorScheme colorScheme) {
|
||||
return FloatingCard(
|
||||
colorScheme: colorScheme,
|
||||
maxWidth: 600,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 修改标题为图标+文字组合
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.data_usage, color: colorScheme.primary, size: 22),
|
||||
const SizedBox(width: 8),
|
||||
const Text('流量统计',
|
||||
style:
|
||||
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildTrafficInfo('上传速度', '$uploadSpeed MB/s', Icons.upload,
|
||||
colorScheme.primary),
|
||||
_buildTrafficInfo('下载速度', '$downloadSpeed MB/s', Icons.download,
|
||||
colorScheme.secondary),
|
||||
],
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
// 修改IP地址卡片,移除多余的内边距
|
||||
Widget _buildIPCard(ColorScheme colorScheme) {
|
||||
return FloatingCard(
|
||||
colorScheme: colorScheme,
|
||||
maxWidth: 600,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 修改标题为图标+文字组合
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.wifi, color: colorScheme.primary, size: 22),
|
||||
const SizedBox(width: 8),
|
||||
const Text('网络信息',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildIPInfo('虚拟 IP', publicIP, Icons.public, colorScheme),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTrafficInfo(
|
||||
String label, String value, IconData icon, Color color) {
|
||||
return Column(
|
||||
children: [
|
||||
Icon(icon, size: 24, color: color),
|
||||
const SizedBox(height: 8),
|
||||
Text(label),
|
||||
Text(value,
|
||||
style: TextStyle(fontWeight: FontWeight.bold, color: color)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIPInfo(
|
||||
String label, String value, IconData icon, ColorScheme colorScheme) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 20, color: colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text('$label: ', style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text(value, style: TextStyle(color: colorScheme.secondary)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 新增合并后的网络状态卡片(合并了流量统计和IP信息)
|
||||
Widget _buildNetworkStatusCard(ColorScheme colorScheme) {
|
||||
return FloatingCard(
|
||||
colorScheme: colorScheme,
|
||||
maxWidth: 600,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题栏带有连接状态指示器
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.network_check, color: colorScheme.primary, size: 22),
|
||||
const SizedBox(width: 8),
|
||||
const Text('网络状态',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const Spacer(),
|
||||
// 添加状态指示器
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(_connectionState, colorScheme),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
_getStatusText(_connectionState),
|
||||
style: TextStyle(
|
||||
color: colorScheme.onPrimary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// IP信息部分
|
||||
_buildIPInfo('虚拟 IP', publicIP, Icons.public, colorScheme),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 流量统计部分
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
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),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildTrafficInfo('上传速度', '$uploadSpeed MB/s', Icons.upload,
|
||||
colorScheme.primary),
|
||||
_buildTrafficInfo('下载速度', '$downloadSpeed MB/s', Icons.download,
|
||||
colorScheme.secondary),
|
||||
],
|
||||
),
|
||||
|
||||
// 添加运行时间显示
|
||||
if (_connectionState == ConnectionState.connected) ...[
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.timer, size: 16, color: colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'运行时间: ${_formatDuration(runningTime)}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
String _getStatusText(ConnectionState state) {
|
||||
switch (state) {
|
||||
case ConnectionState.notStarted:
|
||||
return '未连接';
|
||||
case ConnectionState.connecting:
|
||||
return '连接中';
|
||||
case ConnectionState.connected:
|
||||
return '已连接';
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
Color _getStatusColor(ConnectionState state, ColorScheme colorScheme) {
|
||||
switch (state) {
|
||||
case ConnectionState.notStarted:
|
||||
return Colors.grey;
|
||||
case ConnectionState.connecting:
|
||||
return Colors.orange;
|
||||
case ConnectionState.connected:
|
||||
return Colors.green;
|
||||
}
|
||||
}
|
||||
|
||||
// 优化用户信息卡片
|
||||
Widget _buildUserInfoCard(ColorScheme colorScheme) {
|
||||
final km = Provider.of<KM>(context, listen: false);
|
||||
final isValidIP = _isAutoIP || _isValidIPv4(km.virtualIP);
|
||||
|
||||
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),
|
||||
const Text('用户信息',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const Spacer(),
|
||||
// 添加编辑状态指示器
|
||||
if (_connectionState == ConnectionState.connected)
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Text(
|
||||
'已锁定',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 用户名输入框
|
||||
TextField(
|
||||
controller: _usernameController,
|
||||
enabled: _connectionState != ConnectionState.connected,
|
||||
onChanged: (value) {
|
||||
km.username = value;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: '用户名',
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.person, color: colorScheme.primary),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// IP设置部分
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _virtualIPController,
|
||||
enabled: !_isAutoIP &&
|
||||
_connectionState != ConnectionState.connected,
|
||||
onChanged: (value) {
|
||||
if (!_isAutoIP) {
|
||||
setState(() {});
|
||||
km.virtualIP = value;
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: '虚拟网IP',
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.lan, color: colorScheme.primary),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
errorText: !isValidIP && !_isAutoIP ? '请输入有效的IPv4地址' : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
children: [
|
||||
Switch(
|
||||
value: _isAutoIP,
|
||||
onChanged: _connectionState != ConnectionState.connected
|
||||
? (value) {
|
||||
setState(() {
|
||||
_isAutoIP = value;
|
||||
});
|
||||
km.dynamicIP = value;
|
||||
}
|
||||
: null,
|
||||
),
|
||||
Text(
|
||||
_isAutoIP ? "自动" : "手动",
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_isAutoIP)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'系统将自动分配虚拟网IP',
|
||||
style: TextStyle(
|
||||
color: colorScheme.secondary,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 优化房间信息卡片
|
||||
Widget _buildRoomInfoCard(ColorScheme colorScheme) {
|
||||
final km = Provider.of<KM>(context, listen: false);
|
||||
return FloatingCard(
|
||||
colorScheme: colorScheme,
|
||||
maxWidth: 600,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题栏
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.meeting_room, color: colorScheme.primary, size: 22),
|
||||
const SizedBox(width: 8),
|
||||
const Text('房间信息',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const Spacer(),
|
||||
// 添加编辑状态指示器
|
||||
if (_connectionState == ConnectionState.connected)
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Text(
|
||||
'已锁定',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 房间名称输入框
|
||||
TextField(
|
||||
controller: _roomNameController,
|
||||
enabled: _connectionState != ConnectionState.connected,
|
||||
onChanged: (value) {
|
||||
km.roomName = value;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: '房间名称',
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.meeting_room, color: colorScheme.primary),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 房间密码输入框
|
||||
TextField(
|
||||
controller: _roomPasswordController,
|
||||
enabled: _connectionState != ConnectionState.connected,
|
||||
onChanged: (value) {
|
||||
km.roomPassword = value;
|
||||
},
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: '房间密码',
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.lock, color: colorScheme.primary),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
// helperText: '留空表示无密码',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 添加IPv4地址验证方法
|
||||
bool _isValidIPv4(String ip) {
|
||||
if (ip.isEmpty) return false;
|
||||
|
||||
// 使用正则表达式验证IPv4地址格式
|
||||
final ipv4Pattern = RegExp(
|
||||
r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$');
|
||||
|
||||
return ipv4Pattern.hasMatch(ip);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加版本信息卡片
|
||||
Widget _buildVersionInfoCard(ColorScheme colorScheme) {
|
||||
// 这里可以从配置或API获取实际版本号
|
||||
final String appVersion = AppInfoUtil.getFullVersion();
|
||||
|
||||
return FloatingCard(
|
||||
colorScheme: colorScheme,
|
||||
maxWidth: 600,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题栏
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: colorScheme.primary, size: 22),
|
||||
const SizedBox(width: 8),
|
||||
const Text('版本信息',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 版本信息列表
|
||||
_buildVersionItem('软件版本', appVersion, Icons.apps, colorScheme),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 使用FutureBuilder处理异步获取的版本信息
|
||||
FutureBuilder<String>(
|
||||
future: easytierVersion(),
|
||||
builder: (context, snapshot) {
|
||||
String version = snapshot.hasData ? snapshot.data! : "加载中...";
|
||||
return _buildVersionItem(
|
||||
'ET内核版本', version, Icons.memory, colorScheme);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 版本信息项构建方法
|
||||
Widget _buildVersionItem(
|
||||
String label, String version, IconData icon, ColorScheme colorScheme) {
|
||||
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),
|
||||
Text(
|
||||
version,
|
||||
style: TextStyle(
|
||||
color: colorScheme.secondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
// This file is automatically generated, so please do not edit it.
|
||||
// @generated by `flutter_rust_bridge`@ 2.9.0.
|
||||
|
||||
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
|
||||
|
||||
import '../frb_generated.dart';
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||
|
||||
// These functions are ignored because they are not marked as `pub`: `create_and_store_network_instance`, `create_config`
|
||||
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `RT`
|
||||
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `deref`, `initialize`
|
||||
|
||||
Future<(List<PeerInfo>, List<Route>)> getPeersAndRoutes() =>
|
||||
RustLib.instance.api.crateApiSimpleGetPeersAndRoutes();
|
||||
|
||||
Future<List<PeerRoutePair>> getPeerRoutePairs() =>
|
||||
RustLib.instance.api.crateApiSimpleGetPeerRoutePairs();
|
||||
|
||||
Future<MyNodeInfo> getNodeInfo() =>
|
||||
RustLib.instance.api.crateApiSimpleGetNodeInfo();
|
||||
|
||||
Future<String> easytierVersion() =>
|
||||
RustLib.instance.api.crateApiSimpleEasytierVersion();
|
||||
|
||||
Future<bool> isEasytierRunning() =>
|
||||
RustLib.instance.api.crateApiSimpleIsEasytierRunning();
|
||||
|
||||
Future<KVNetworkStatus> getNetworkStatus() =>
|
||||
RustLib.instance.api.crateApiSimpleGetNetworkStatus();
|
||||
|
||||
Future<String> getRunningInfo() =>
|
||||
RustLib.instance.api.crateApiSimpleGetRunningInfo();
|
||||
|
||||
Future<void> createServer(
|
||||
{required String username,
|
||||
required bool enableDhcp,
|
||||
required String specifiedIp,
|
||||
required String roomName,
|
||||
required String roomPassword,
|
||||
required String severurl}) =>
|
||||
RustLib.instance.api.crateApiSimpleCreateServer(
|
||||
username: username,
|
||||
enableDhcp: enableDhcp,
|
||||
specifiedIp: specifiedIp,
|
||||
roomName: roomName,
|
||||
roomPassword: roomPassword,
|
||||
severurl: severurl);
|
||||
|
||||
Future<void> closeAllServer() =>
|
||||
RustLib.instance.api.crateApiSimpleCloseAllServer();
|
||||
|
||||
Future<void> initApp() => RustLib.instance.api.crateApiSimpleInitApp();
|
||||
|
||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<MyNodeInfo>>
|
||||
abstract class MyNodeInfo implements RustOpaqueInterface {}
|
||||
|
||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PeerInfo>>
|
||||
abstract class PeerInfo implements RustOpaqueInterface {}
|
||||
|
||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PeerRoutePair>>
|
||||
abstract class PeerRoutePair implements RustOpaqueInterface {}
|
||||
|
||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<Route>>
|
||||
abstract class Route implements RustOpaqueInterface {}
|
||||
|
||||
class KVNetworkStatus {
|
||||
final BigInt totalNodes;
|
||||
final List<KVNodeInfo> nodes;
|
||||
|
||||
const KVNetworkStatus({
|
||||
required this.totalNodes,
|
||||
required this.nodes,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode => totalNodes.hashCode ^ nodes.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is KVNetworkStatus &&
|
||||
runtimeType == other.runtimeType &&
|
||||
totalNodes == other.totalNodes &&
|
||||
nodes == other.nodes;
|
||||
}
|
||||
|
||||
class KVNodeConnectionStats {
|
||||
final String connType;
|
||||
final BigInt rxBytes;
|
||||
final BigInt txBytes;
|
||||
final BigInt rxPackets;
|
||||
final BigInt txPackets;
|
||||
|
||||
const KVNodeConnectionStats({
|
||||
required this.connType,
|
||||
required this.rxBytes,
|
||||
required this.txBytes,
|
||||
required this.rxPackets,
|
||||
required this.txPackets,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
connType.hashCode ^
|
||||
rxBytes.hashCode ^
|
||||
txBytes.hashCode ^
|
||||
rxPackets.hashCode ^
|
||||
txPackets.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is KVNodeConnectionStats &&
|
||||
runtimeType == other.runtimeType &&
|
||||
connType == other.connType &&
|
||||
rxBytes == other.rxBytes &&
|
||||
txBytes == other.txBytes &&
|
||||
rxPackets == other.rxPackets &&
|
||||
txPackets == other.txPackets;
|
||||
}
|
||||
|
||||
class KVNodeInfo {
|
||||
final String hostname;
|
||||
final String ipv4;
|
||||
final double latencyMs;
|
||||
final List<KVNodeConnectionStats> connections;
|
||||
final String version;
|
||||
final int cost;
|
||||
|
||||
const KVNodeInfo({
|
||||
required this.hostname,
|
||||
required this.ipv4,
|
||||
required this.latencyMs,
|
||||
required this.connections,
|
||||
required this.version,
|
||||
required this.cost,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
hostname.hashCode ^
|
||||
ipv4.hashCode ^
|
||||
latencyMs.hashCode ^
|
||||
connections.hashCode ^
|
||||
version.hashCode ^
|
||||
cost.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is KVNodeInfo &&
|
||||
runtimeType == other.runtimeType &&
|
||||
hostname == other.hostname &&
|
||||
ipv4 == other.ipv4 &&
|
||||
latencyMs == other.latencyMs &&
|
||||
connections == other.connections &&
|
||||
version == other.version &&
|
||||
cost == other.cost;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,494 @@
|
||||
// This file is automatically generated, so please do not edit it.
|
||||
// @generated by `flutter_rust_bridge`@ 2.9.0.
|
||||
|
||||
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
|
||||
|
||||
import 'api/simple.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi' as ffi;
|
||||
import 'frb_generated.dart';
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
|
||||
|
||||
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
RustLibApiImplPlatform({
|
||||
required super.handler,
|
||||
required super.wire,
|
||||
required super.generalizedFrbRustBinding,
|
||||
required super.portManager,
|
||||
});
|
||||
|
||||
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_MyNodeInfoPtr =>
|
||||
wire._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfoPtr;
|
||||
|
||||
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_PeerInfoPtr => wire
|
||||
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfoPtr;
|
||||
|
||||
CrossPlatformFinalizerArg
|
||||
get rust_arc_decrement_strong_count_PeerRoutePairPtr => wire
|
||||
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePairPtr;
|
||||
|
||||
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RoutePtr => wire
|
||||
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoutePtr;
|
||||
|
||||
@protected
|
||||
MyNodeInfo
|
||||
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
PeerInfo
|
||||
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
PeerRoutePair
|
||||
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
Route
|
||||
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
MyNodeInfo
|
||||
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
PeerInfo
|
||||
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
PeerRoutePair
|
||||
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
Route
|
||||
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
String dco_decode_String(dynamic raw);
|
||||
|
||||
@protected
|
||||
bool dco_decode_bool(dynamic raw);
|
||||
|
||||
@protected
|
||||
double dco_decode_f_64(dynamic raw);
|
||||
|
||||
@protected
|
||||
int dco_decode_i_32(dynamic raw);
|
||||
|
||||
@protected
|
||||
KVNetworkStatus dco_decode_kv_network_status(dynamic raw);
|
||||
|
||||
@protected
|
||||
KVNodeConnectionStats dco_decode_kv_node_connection_stats(dynamic raw);
|
||||
|
||||
@protected
|
||||
KVNodeInfo dco_decode_kv_node_info(dynamic raw);
|
||||
|
||||
@protected
|
||||
List<PeerInfo>
|
||||
dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
List<PeerRoutePair>
|
||||
dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
List<Route>
|
||||
dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
List<KVNodeConnectionStats> dco_decode_list_kv_node_connection_stats(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
List<KVNodeInfo> dco_decode_list_kv_node_info(dynamic raw);
|
||||
|
||||
@protected
|
||||
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
|
||||
|
||||
@protected
|
||||
(
|
||||
List<PeerInfo>,
|
||||
List<Route>
|
||||
) dco_decode_record_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_peer_info_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_route(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
BigInt dco_decode_u_64(dynamic raw);
|
||||
|
||||
@protected
|
||||
int dco_decode_u_8(dynamic raw);
|
||||
|
||||
@protected
|
||||
void dco_decode_unit(dynamic raw);
|
||||
|
||||
@protected
|
||||
BigInt dco_decode_usize(dynamic raw);
|
||||
|
||||
@protected
|
||||
MyNodeInfo
|
||||
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
PeerInfo
|
||||
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
PeerRoutePair
|
||||
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
Route
|
||||
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
MyNodeInfo
|
||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
PeerInfo
|
||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
PeerRoutePair
|
||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
Route
|
||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
String sse_decode_String(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
bool sse_decode_bool(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
double sse_decode_f_64(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
int sse_decode_i_32(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
KVNetworkStatus sse_decode_kv_network_status(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
KVNodeConnectionStats sse_decode_kv_node_connection_stats(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
KVNodeInfo sse_decode_kv_node_info(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<PeerInfo>
|
||||
sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<PeerRoutePair>
|
||||
sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<Route>
|
||||
sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<KVNodeConnectionStats> sse_decode_list_kv_node_connection_stats(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<KVNodeInfo> sse_decode_list_kv_node_info(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
(
|
||||
List<PeerInfo>,
|
||||
List<Route>
|
||||
) sse_decode_record_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_peer_info_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_route(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
BigInt sse_decode_u_64(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
int sse_decode_u_8(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
void sse_decode_unit(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
BigInt sse_decode_usize(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
MyNodeInfo self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
PeerInfo self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
PeerRoutePair self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
Route self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
MyNodeInfo self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
PeerInfo self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
PeerRoutePair self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
Route self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_String(String self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_bool(bool self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_f_64(double self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_i_32(int self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_kv_network_status(
|
||||
KVNetworkStatus self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_kv_node_connection_stats(
|
||||
KVNodeConnectionStats self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_kv_node_info(KVNodeInfo self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
List<PeerInfo> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
List<PeerRoutePair> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
List<Route> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_kv_node_connection_stats(
|
||||
List<KVNodeConnectionStats> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_kv_node_info(
|
||||
List<KVNodeInfo> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_prim_u_8_strict(
|
||||
Uint8List self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_record_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_peer_info_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_route(
|
||||
(List<PeerInfo>, List<Route>) self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_u_64(BigInt self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_u_8(int self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_unit(void self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_usize(BigInt self, SseSerializer serializer);
|
||||
}
|
||||
|
||||
// Section: wire_class
|
||||
|
||||
class RustLibWire implements BaseWire {
|
||||
factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) =>
|
||||
RustLibWire(lib.ffiDynamicLibrary);
|
||||
|
||||
/// Holds the symbol lookup function.
|
||||
final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
|
||||
_lookup;
|
||||
|
||||
/// The symbols are looked up in [dynamicLibrary].
|
||||
RustLibWire(ffi.DynamicLibrary dynamicLibrary)
|
||||
: _lookup = dynamicLibrary.lookup;
|
||||
|
||||
void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
ffi.Pointer<ffi.Void> ptr,
|
||||
) {
|
||||
return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
ptr,
|
||||
);
|
||||
}
|
||||
|
||||
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');
|
||||
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>)>();
|
||||
|
||||
void
|
||||
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
ffi.Pointer<ffi.Void> ptr,
|
||||
) {
|
||||
return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
ptr,
|
||||
);
|
||||
}
|
||||
|
||||
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');
|
||||
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>)>();
|
||||
|
||||
void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
ffi.Pointer<ffi.Void> ptr,
|
||||
) {
|
||||
return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
ptr,
|
||||
);
|
||||
}
|
||||
|
||||
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');
|
||||
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>)>();
|
||||
|
||||
void
|
||||
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
ffi.Pointer<ffi.Void> ptr,
|
||||
) {
|
||||
return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
ptr,
|
||||
);
|
||||
}
|
||||
|
||||
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');
|
||||
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>)>();
|
||||
|
||||
void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
ffi.Pointer<ffi.Void> ptr,
|
||||
) {
|
||||
return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
ptr,
|
||||
);
|
||||
}
|
||||
|
||||
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');
|
||||
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>)>();
|
||||
|
||||
void
|
||||
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
ffi.Pointer<ffi.Void> ptr,
|
||||
) {
|
||||
return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
ptr,
|
||||
);
|
||||
}
|
||||
|
||||
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');
|
||||
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>)>();
|
||||
|
||||
void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
ffi.Pointer<ffi.Void> ptr,
|
||||
) {
|
||||
return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
ptr,
|
||||
);
|
||||
}
|
||||
|
||||
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');
|
||||
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>)>();
|
||||
|
||||
void
|
||||
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
ffi.Pointer<ffi.Void> ptr,
|
||||
) {
|
||||
return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
ptr,
|
||||
);
|
||||
}
|
||||
|
||||
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');
|
||||
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>)>();
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
// This file is automatically generated, so please do not edit it.
|
||||
// @generated by `flutter_rust_bridge`@ 2.9.0.
|
||||
|
||||
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
|
||||
|
||||
// Static analysis wrongly picks the IO variant, thus ignore this
|
||||
// ignore_for_file: argument_type_not_assignable
|
||||
|
||||
import 'api/simple.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'frb_generated.dart';
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
|
||||
|
||||
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
RustLibApiImplPlatform({
|
||||
required super.handler,
|
||||
required super.wire,
|
||||
required super.generalizedFrbRustBinding,
|
||||
required super.portManager,
|
||||
});
|
||||
|
||||
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_MyNodeInfoPtr =>
|
||||
wire.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo;
|
||||
|
||||
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_PeerInfoPtr => wire
|
||||
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo;
|
||||
|
||||
CrossPlatformFinalizerArg
|
||||
get rust_arc_decrement_strong_count_PeerRoutePairPtr => wire
|
||||
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair;
|
||||
|
||||
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RoutePtr => wire
|
||||
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute;
|
||||
|
||||
@protected
|
||||
MyNodeInfo
|
||||
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
PeerInfo
|
||||
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
PeerRoutePair
|
||||
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
Route
|
||||
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
MyNodeInfo
|
||||
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
PeerInfo
|
||||
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
PeerRoutePair
|
||||
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
Route
|
||||
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
String dco_decode_String(dynamic raw);
|
||||
|
||||
@protected
|
||||
bool dco_decode_bool(dynamic raw);
|
||||
|
||||
@protected
|
||||
double dco_decode_f_64(dynamic raw);
|
||||
|
||||
@protected
|
||||
int dco_decode_i_32(dynamic raw);
|
||||
|
||||
@protected
|
||||
KVNetworkStatus dco_decode_kv_network_status(dynamic raw);
|
||||
|
||||
@protected
|
||||
KVNodeConnectionStats dco_decode_kv_node_connection_stats(dynamic raw);
|
||||
|
||||
@protected
|
||||
KVNodeInfo dco_decode_kv_node_info(dynamic raw);
|
||||
|
||||
@protected
|
||||
List<PeerInfo>
|
||||
dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
List<PeerRoutePair>
|
||||
dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
List<Route>
|
||||
dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
List<KVNodeConnectionStats> dco_decode_list_kv_node_connection_stats(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
List<KVNodeInfo> dco_decode_list_kv_node_info(dynamic raw);
|
||||
|
||||
@protected
|
||||
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
|
||||
|
||||
@protected
|
||||
(
|
||||
List<PeerInfo>,
|
||||
List<Route>
|
||||
) dco_decode_record_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_peer_info_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_route(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
BigInt dco_decode_u_64(dynamic raw);
|
||||
|
||||
@protected
|
||||
int dco_decode_u_8(dynamic raw);
|
||||
|
||||
@protected
|
||||
void dco_decode_unit(dynamic raw);
|
||||
|
||||
@protected
|
||||
BigInt dco_decode_usize(dynamic raw);
|
||||
|
||||
@protected
|
||||
MyNodeInfo
|
||||
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
PeerInfo
|
||||
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
PeerRoutePair
|
||||
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
Route
|
||||
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
MyNodeInfo
|
||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
PeerInfo
|
||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
PeerRoutePair
|
||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
Route
|
||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
String sse_decode_String(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
bool sse_decode_bool(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
double sse_decode_f_64(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
int sse_decode_i_32(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
KVNetworkStatus sse_decode_kv_network_status(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
KVNodeConnectionStats sse_decode_kv_node_connection_stats(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
KVNodeInfo sse_decode_kv_node_info(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<PeerInfo>
|
||||
sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<PeerRoutePair>
|
||||
sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<Route>
|
||||
sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<KVNodeConnectionStats> sse_decode_list_kv_node_connection_stats(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<KVNodeInfo> sse_decode_list_kv_node_info(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
(
|
||||
List<PeerInfo>,
|
||||
List<Route>
|
||||
) sse_decode_record_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_peer_info_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_route(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
BigInt sse_decode_u_64(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
int sse_decode_u_8(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
void sse_decode_unit(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
BigInt sse_decode_usize(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
MyNodeInfo self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
PeerInfo self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
PeerRoutePair self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
Route self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
MyNodeInfo self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
PeerInfo self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
PeerRoutePair self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
Route self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_String(String self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_bool(bool self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_f_64(double self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_i_32(int self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_kv_network_status(
|
||||
KVNetworkStatus self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_kv_node_connection_stats(
|
||||
KVNodeConnectionStats self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_kv_node_info(KVNodeInfo self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
List<PeerInfo> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
List<PeerRoutePair> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
List<Route> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_kv_node_connection_stats(
|
||||
List<KVNodeConnectionStats> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_kv_node_info(
|
||||
List<KVNodeInfo> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_prim_u_8_strict(
|
||||
Uint8List self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_record_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_peer_info_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_route(
|
||||
(List<PeerInfo>, List<Route>) self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_u_64(BigInt self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_u_8(int self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_unit(void self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_usize(BigInt self, SseSerializer serializer);
|
||||
}
|
||||
|
||||
// Section: wire_class
|
||||
|
||||
class RustLibWire implements BaseWire {
|
||||
RustLibWire.fromExternalLibrary(ExternalLibrary lib);
|
||||
|
||||
void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
int ptr) =>
|
||||
wasmModule
|
||||
.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
ptr);
|
||||
|
||||
void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
int ptr) =>
|
||||
wasmModule
|
||||
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
ptr);
|
||||
|
||||
void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
int ptr) =>
|
||||
wasmModule
|
||||
.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
ptr);
|
||||
|
||||
void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
int ptr) =>
|
||||
wasmModule
|
||||
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
ptr);
|
||||
|
||||
void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
int ptr) =>
|
||||
wasmModule
|
||||
.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
ptr);
|
||||
|
||||
void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
int ptr) =>
|
||||
wasmModule
|
||||
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
ptr);
|
||||
|
||||
void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
int ptr) =>
|
||||
wasmModule
|
||||
.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
ptr);
|
||||
|
||||
void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
int ptr) =>
|
||||
wasmModule
|
||||
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
ptr);
|
||||
}
|
||||
|
||||
@JS('wasm_bindgen')
|
||||
external RustLibWasmModule get wasmModule;
|
||||
|
||||
@JS()
|
||||
@anonymous
|
||||
extension type RustLibWasmModule._(JSObject _) implements JSObject {
|
||||
external void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
int ptr);
|
||||
|
||||
external void
|
||||
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
int ptr);
|
||||
|
||||
external void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
int ptr);
|
||||
|
||||
external void
|
||||
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
int ptr);
|
||||
|
||||
external void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
int ptr);
|
||||
|
||||
external void
|
||||
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
int ptr);
|
||||
|
||||
external void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
int ptr);
|
||||
|
||||
external void
|
||||
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
int ptr);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
class AppInfoUtil {
|
||||
static PackageInfo? _packageInfo;
|
||||
|
||||
/// 初始化应用信息
|
||||
static Future<void> init() async {
|
||||
_packageInfo = await PackageInfo.fromPlatform();
|
||||
}
|
||||
|
||||
/// 获取应用版本号 (例如: 1.0.0)
|
||||
static String getVersion() {
|
||||
return _packageInfo?.version ?? '';
|
||||
}
|
||||
|
||||
/// 获取应用构建号 (例如: 1)
|
||||
static String getBuildNumber() {
|
||||
return _packageInfo?.buildNumber ?? '';
|
||||
}
|
||||
|
||||
/// 获取完整版本号 (例如: 1.0.0+1)
|
||||
static String getFullVersion() {
|
||||
final version = getVersion();
|
||||
final buildNumber = getBuildNumber();
|
||||
return '$version+$buildNumber';
|
||||
}
|
||||
|
||||
/// 获取应用名称
|
||||
static String getAppName() {
|
||||
return _packageInfo?.appName ?? '';
|
||||
}
|
||||
|
||||
/// 获取包名
|
||||
static String getPackageName() {
|
||||
return _packageInfo?.packageName ?? '';
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
|
||||
class PingUtil {
|
||||
static Future<int?> ping(String host) async {
|
||||
try {
|
||||
// 从 host:port 格式中提取主机名
|
||||
final hostname = host.split(':')[0];
|
||||
final post = host.split(':')[1];
|
||||
|
||||
// 使用 Socket 连接来测量实际网络延迟
|
||||
final startTime = DateTime.now();
|
||||
final socket = await Socket.connect(hostname, int.parse(post),
|
||||
timeout: const Duration(seconds: 2));
|
||||
final endTime = DateTime.now();
|
||||
|
||||
// 关闭连接
|
||||
await socket.close();
|
||||
|
||||
return endTime.difference(startTime).inMilliseconds;
|
||||
} on SocketException {
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'runin.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class Ipv4Addr {
|
||||
final int addr;
|
||||
|
||||
Ipv4Addr({required this.addr});
|
||||
|
||||
factory Ipv4Addr.fromJson(Map<String, dynamic> json) =>
|
||||
_$Ipv4AddrFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$Ipv4AddrToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Ipv4Inet {
|
||||
@JsonKey(name: 'address')
|
||||
final Ipv4Addr? address;
|
||||
@JsonKey(name: 'network_length')
|
||||
final int networkLength;
|
||||
|
||||
Ipv4Inet({this.address, required this.networkLength});
|
||||
|
||||
factory Ipv4Inet.fromJson(Map<String, dynamic> json) =>
|
||||
_$Ipv4InetFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$Ipv4InetToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class StunInfo {
|
||||
@JsonKey(name: 'udp_nat_type')
|
||||
final int udpNatType;
|
||||
@JsonKey(name: 'tcp_nat_type')
|
||||
final int tcpNatType;
|
||||
@JsonKey(name: 'last_update_time')
|
||||
final int lastUpdateTime;
|
||||
@JsonKey(name: 'public_ip')
|
||||
final List<String> publicIp;
|
||||
@JsonKey(name: 'min_port')
|
||||
final int minPort;
|
||||
@JsonKey(name: 'max_port')
|
||||
final int maxPort;
|
||||
|
||||
StunInfo({
|
||||
required this.udpNatType,
|
||||
required this.tcpNatType,
|
||||
required this.lastUpdateTime,
|
||||
required this.publicIp,
|
||||
required this.minPort,
|
||||
required this.maxPort,
|
||||
});
|
||||
|
||||
factory StunInfo.fromJson(Map<String, dynamic> json) =>
|
||||
_$StunInfoFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$StunInfoToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class PeerFeatureFlag {
|
||||
@JsonKey(name: 'is_public_server')
|
||||
final bool isPublicServer;
|
||||
@JsonKey(name: 'avoid_relay_data')
|
||||
final bool avoidRelayData;
|
||||
@JsonKey(name: 'kcp_input')
|
||||
final bool kcpInput;
|
||||
@JsonKey(name: 'no_relay_kcp')
|
||||
final bool noRelayKcp;
|
||||
|
||||
PeerFeatureFlag({
|
||||
required this.isPublicServer,
|
||||
required this.avoidRelayData,
|
||||
required this.kcpInput,
|
||||
required this.noRelayKcp,
|
||||
});
|
||||
|
||||
factory PeerFeatureFlag.fromJson(Map<String, dynamic> json) =>
|
||||
_$PeerFeatureFlagFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$PeerFeatureFlagToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Url {
|
||||
final String url;
|
||||
|
||||
Url({required this.url});
|
||||
|
||||
factory Url.fromJson(Map<String, dynamic> json) => _$UrlFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$UrlToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Route {
|
||||
@JsonKey(name: 'peer_id')
|
||||
final int? peerId;
|
||||
@JsonKey(name: 'ipv4_addr')
|
||||
final Ipv4Inet? ipv4Addr;
|
||||
@JsonKey(name: 'next_hop_peer_id')
|
||||
final int? nextHopPeerId;
|
||||
final int? cost;
|
||||
@JsonKey(name: 'path_latency')
|
||||
final int? pathLatency;
|
||||
@JsonKey(name: 'proxy_cidrs')
|
||||
final List<String>? proxyCidrs;
|
||||
final String? hostname;
|
||||
@JsonKey(name: 'stun_info')
|
||||
final StunInfo? stunInfo;
|
||||
@JsonKey(name: 'inst_id')
|
||||
final String? instId;
|
||||
final String? version;
|
||||
@JsonKey(name: 'feature_flag')
|
||||
final PeerFeatureFlag? featureFlag;
|
||||
@JsonKey(name: 'next_hop_peer_id_latency_first')
|
||||
final int? nextHopPeerIdLatencyFirst;
|
||||
@JsonKey(name: 'cost_latency_first')
|
||||
final int? costLatencyFirst;
|
||||
@JsonKey(name: 'path_latency_latency_first')
|
||||
final int? pathLatencyLatencyFirst;
|
||||
|
||||
Route({
|
||||
this.peerId,
|
||||
this.ipv4Addr,
|
||||
this.nextHopPeerId,
|
||||
this.cost,
|
||||
this.pathLatency,
|
||||
this.proxyCidrs,
|
||||
this.hostname,
|
||||
this.stunInfo,
|
||||
this.instId,
|
||||
this.version,
|
||||
this.featureFlag,
|
||||
this.nextHopPeerIdLatencyFirst,
|
||||
this.costLatencyFirst,
|
||||
this.pathLatencyLatencyFirst,
|
||||
});
|
||||
|
||||
factory Route.fromJson(Map<String, dynamic> json) => _$RouteFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$RouteToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class TunnelInfo {
|
||||
@JsonKey(name: 'tunnel_type')
|
||||
final String tunnelType;
|
||||
@JsonKey(name: 'local_addr')
|
||||
final Url? localAddr;
|
||||
@JsonKey(name: 'remote_addr')
|
||||
final Url? remoteAddr;
|
||||
|
||||
TunnelInfo({
|
||||
required this.tunnelType,
|
||||
this.localAddr,
|
||||
this.remoteAddr,
|
||||
});
|
||||
|
||||
factory TunnelInfo.fromJson(Map<String, dynamic> json) =>
|
||||
_$TunnelInfoFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$TunnelInfoToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class PeerConnStats {
|
||||
@JsonKey(name: 'rx_bytes')
|
||||
final int rxBytes;
|
||||
@JsonKey(name: 'tx_bytes')
|
||||
final int txBytes;
|
||||
@JsonKey(name: 'rx_packets')
|
||||
final int rxPackets;
|
||||
@JsonKey(name: 'tx_packets')
|
||||
final int txPackets;
|
||||
@JsonKey(name: 'latency_us')
|
||||
final int latencyUs;
|
||||
|
||||
PeerConnStats({
|
||||
required this.rxBytes,
|
||||
required this.txBytes,
|
||||
required this.rxPackets,
|
||||
required this.txPackets,
|
||||
required this.latencyUs,
|
||||
});
|
||||
|
||||
factory PeerConnStats.fromJson(Map<String, dynamic> json) =>
|
||||
_$PeerConnStatsFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$PeerConnStatsToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class PeerConnInfo {
|
||||
@JsonKey(name: 'conn_id')
|
||||
final String connId;
|
||||
@JsonKey(name: 'my_peer_id')
|
||||
final int myPeerId;
|
||||
@JsonKey(name: 'peer_id')
|
||||
final int peerId;
|
||||
final List<String> features;
|
||||
final TunnelInfo? tunnel;
|
||||
final PeerConnStats? stats;
|
||||
@JsonKey(name: 'loss_rate')
|
||||
final double lossRate;
|
||||
@JsonKey(name: 'is_client')
|
||||
final bool isClient;
|
||||
@JsonKey(name: 'network_name')
|
||||
final String networkName;
|
||||
|
||||
PeerConnInfo({
|
||||
required this.connId,
|
||||
required this.myPeerId,
|
||||
required this.peerId,
|
||||
required this.features,
|
||||
this.tunnel,
|
||||
this.stats,
|
||||
required this.lossRate,
|
||||
required this.isClient,
|
||||
required this.networkName,
|
||||
});
|
||||
|
||||
factory PeerConnInfo.fromJson(Map<String, dynamic> json) =>
|
||||
_$PeerConnInfoFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$PeerConnInfoToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class PeerInfo {
|
||||
@JsonKey(name: 'peer_id')
|
||||
final int peerId;
|
||||
final List<PeerConnInfo> conns;
|
||||
|
||||
PeerInfo({
|
||||
required this.peerId,
|
||||
required this.conns,
|
||||
});
|
||||
|
||||
factory PeerInfo.fromJson(Map<String, dynamic> json) =>
|
||||
_$PeerInfoFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$PeerInfoToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class PeerRoutePair {
|
||||
final Route? route;
|
||||
final PeerInfo? peer;
|
||||
|
||||
PeerRoutePair({
|
||||
this.route,
|
||||
this.peer,
|
||||
});
|
||||
|
||||
factory PeerRoutePair.fromJson(Map<String, dynamic> json) =>
|
||||
_$PeerRoutePairFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$PeerRoutePairToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Ipv6Addr {
|
||||
@JsonKey(name: 'part1')
|
||||
final int part1;
|
||||
@JsonKey(name: 'part2')
|
||||
final int part2;
|
||||
@JsonKey(name: 'part3')
|
||||
final int part3;
|
||||
@JsonKey(name: 'part4')
|
||||
final int part4;
|
||||
|
||||
Ipv6Addr({
|
||||
required this.part1,
|
||||
required this.part2,
|
||||
required this.part3,
|
||||
required this.part4,
|
||||
});
|
||||
|
||||
factory Ipv6Addr.fromJson(Map<String, dynamic> json) =>
|
||||
_$Ipv6AddrFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$Ipv6AddrToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class GetIpListResponse {
|
||||
@JsonKey(name: 'public_ipv4')
|
||||
final Ipv4Addr? publicIpv4;
|
||||
@JsonKey(name: 'interface_ipv4s')
|
||||
final List<Ipv4Addr> interfaceIpv4s;
|
||||
@JsonKey(name: 'public_ipv6')
|
||||
final Ipv6Addr? publicIpv6;
|
||||
@JsonKey(name: 'interface_ipv6s')
|
||||
final List<Ipv6Addr> interfaceIpv6s;
|
||||
final List<Url> listeners;
|
||||
|
||||
GetIpListResponse({
|
||||
this.publicIpv4,
|
||||
required this.interfaceIpv4s,
|
||||
this.publicIpv6,
|
||||
required this.interfaceIpv6s,
|
||||
required this.listeners,
|
||||
});
|
||||
|
||||
factory GetIpListResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$GetIpListResponseFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$GetIpListResponseToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class MyNodeInfo {
|
||||
@JsonKey(name: 'virtual_ipv4')
|
||||
final Ipv4Inet? virtualIpv4;
|
||||
final String hostname;
|
||||
final String version;
|
||||
final GetIpListResponse? ips;
|
||||
@JsonKey(name: 'stun_info')
|
||||
final StunInfo? stunInfo;
|
||||
final List<Url> listeners;
|
||||
@JsonKey(name: 'vpn_portal_cfg')
|
||||
final String? vpnPortalCfg;
|
||||
|
||||
MyNodeInfo({
|
||||
this.virtualIpv4,
|
||||
required this.hostname,
|
||||
required this.version,
|
||||
this.ips,
|
||||
this.stunInfo,
|
||||
required this.listeners,
|
||||
this.vpnPortalCfg,
|
||||
});
|
||||
|
||||
factory MyNodeInfo.fromJson(Map<String, dynamic> json) =>
|
||||
_$MyNodeInfoFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$MyNodeInfoToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Runin {
|
||||
@JsonKey(name: 'dev_name')
|
||||
final String? devName;
|
||||
@JsonKey(name: 'my_node_info')
|
||||
final MyNodeInfo? myNodeInfo;
|
||||
final List<String> events;
|
||||
final List<Route> routes;
|
||||
final List<PeerInfo>? peers;
|
||||
@JsonKey(name: 'peer_route_pairs')
|
||||
final List<PeerRoutePair>? peerRoutePairs;
|
||||
final bool running;
|
||||
@JsonKey(name: 'error_msg')
|
||||
final String? errorMsg;
|
||||
|
||||
Runin({
|
||||
this.devName,
|
||||
this.myNodeInfo,
|
||||
required this.events,
|
||||
required this.routes,
|
||||
this.peers,
|
||||
this.peerRoutePairs,
|
||||
required this.running,
|
||||
this.errorMsg,
|
||||
});
|
||||
|
||||
factory Runin.fromJson(Map<String, dynamic> json) => _$RuninFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$RuninToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'runin.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Ipv4Addr _$Ipv4AddrFromJson(Map<String, dynamic> json) => Ipv4Addr(
|
||||
addr: (json['addr'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$Ipv4AddrToJson(Ipv4Addr instance) => <String, dynamic>{
|
||||
'addr': instance.addr,
|
||||
};
|
||||
|
||||
Ipv4Inet _$Ipv4InetFromJson(Map<String, dynamic> json) => Ipv4Inet(
|
||||
address: json['address'] == null
|
||||
? null
|
||||
: Ipv4Addr.fromJson(json['address'] as Map<String, dynamic>),
|
||||
networkLength: (json['network_length'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$Ipv4InetToJson(Ipv4Inet instance) => <String, dynamic>{
|
||||
'address': instance.address,
|
||||
'network_length': instance.networkLength,
|
||||
};
|
||||
|
||||
StunInfo _$StunInfoFromJson(Map<String, dynamic> json) => StunInfo(
|
||||
udpNatType: (json['udp_nat_type'] as num).toInt(),
|
||||
tcpNatType: (json['tcp_nat_type'] as num).toInt(),
|
||||
lastUpdateTime: (json['last_update_time'] as num).toInt(),
|
||||
publicIp:
|
||||
(json['public_ip'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
minPort: (json['min_port'] as num).toInt(),
|
||||
maxPort: (json['max_port'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$StunInfoToJson(StunInfo instance) => <String, dynamic>{
|
||||
'udp_nat_type': instance.udpNatType,
|
||||
'tcp_nat_type': instance.tcpNatType,
|
||||
'last_update_time': instance.lastUpdateTime,
|
||||
'public_ip': instance.publicIp,
|
||||
'min_port': instance.minPort,
|
||||
'max_port': instance.maxPort,
|
||||
};
|
||||
|
||||
PeerFeatureFlag _$PeerFeatureFlagFromJson(Map<String, dynamic> json) =>
|
||||
PeerFeatureFlag(
|
||||
isPublicServer: json['is_public_server'] as bool,
|
||||
avoidRelayData: json['avoid_relay_data'] as bool,
|
||||
kcpInput: json['kcp_input'] as bool,
|
||||
noRelayKcp: json['no_relay_kcp'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PeerFeatureFlagToJson(PeerFeatureFlag instance) =>
|
||||
<String, dynamic>{
|
||||
'is_public_server': instance.isPublicServer,
|
||||
'avoid_relay_data': instance.avoidRelayData,
|
||||
'kcp_input': instance.kcpInput,
|
||||
'no_relay_kcp': instance.noRelayKcp,
|
||||
};
|
||||
|
||||
Url _$UrlFromJson(Map<String, dynamic> json) => Url(
|
||||
url: json['url'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UrlToJson(Url instance) => <String, dynamic>{
|
||||
'url': instance.url,
|
||||
};
|
||||
|
||||
Route _$RouteFromJson(Map<String, dynamic> json) => Route(
|
||||
peerId: (json['peer_id'] as num?)?.toInt(),
|
||||
ipv4Addr: json['ipv4_addr'] == null
|
||||
? null
|
||||
: Ipv4Inet.fromJson(json['ipv4_addr'] as Map<String, dynamic>),
|
||||
nextHopPeerId: (json['next_hop_peer_id'] as num?)?.toInt(),
|
||||
cost: (json['cost'] as num?)?.toInt(),
|
||||
pathLatency: (json['path_latency'] as num?)?.toInt(),
|
||||
proxyCidrs: (json['proxy_cidrs'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
hostname: json['hostname'] as String?,
|
||||
stunInfo: json['stun_info'] == null
|
||||
? null
|
||||
: StunInfo.fromJson(json['stun_info'] as Map<String, dynamic>),
|
||||
instId: json['inst_id'] as String?,
|
||||
version: json['version'] as String?,
|
||||
featureFlag: json['feature_flag'] == null
|
||||
? null
|
||||
: PeerFeatureFlag.fromJson(
|
||||
json['feature_flag'] as Map<String, dynamic>),
|
||||
nextHopPeerIdLatencyFirst:
|
||||
(json['next_hop_peer_id_latency_first'] as num?)?.toInt(),
|
||||
costLatencyFirst: (json['cost_latency_first'] as num?)?.toInt(),
|
||||
pathLatencyLatencyFirst:
|
||||
(json['path_latency_latency_first'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RouteToJson(Route instance) => <String, dynamic>{
|
||||
'peer_id': instance.peerId,
|
||||
'ipv4_addr': instance.ipv4Addr,
|
||||
'next_hop_peer_id': instance.nextHopPeerId,
|
||||
'cost': instance.cost,
|
||||
'path_latency': instance.pathLatency,
|
||||
'proxy_cidrs': instance.proxyCidrs,
|
||||
'hostname': instance.hostname,
|
||||
'stun_info': instance.stunInfo,
|
||||
'inst_id': instance.instId,
|
||||
'version': instance.version,
|
||||
'feature_flag': instance.featureFlag,
|
||||
'next_hop_peer_id_latency_first': instance.nextHopPeerIdLatencyFirst,
|
||||
'cost_latency_first': instance.costLatencyFirst,
|
||||
'path_latency_latency_first': instance.pathLatencyLatencyFirst,
|
||||
};
|
||||
|
||||
TunnelInfo _$TunnelInfoFromJson(Map<String, dynamic> json) => TunnelInfo(
|
||||
tunnelType: json['tunnel_type'] as String,
|
||||
localAddr: json['local_addr'] == null
|
||||
? null
|
||||
: Url.fromJson(json['local_addr'] as Map<String, dynamic>),
|
||||
remoteAddr: json['remote_addr'] == null
|
||||
? null
|
||||
: Url.fromJson(json['remote_addr'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TunnelInfoToJson(TunnelInfo instance) =>
|
||||
<String, dynamic>{
|
||||
'tunnel_type': instance.tunnelType,
|
||||
'local_addr': instance.localAddr,
|
||||
'remote_addr': instance.remoteAddr,
|
||||
};
|
||||
|
||||
PeerConnStats _$PeerConnStatsFromJson(Map<String, dynamic> json) =>
|
||||
PeerConnStats(
|
||||
rxBytes: (json['rx_bytes'] as num).toInt(),
|
||||
txBytes: (json['tx_bytes'] as num).toInt(),
|
||||
rxPackets: (json['rx_packets'] as num).toInt(),
|
||||
txPackets: (json['tx_packets'] as num).toInt(),
|
||||
latencyUs: (json['latency_us'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PeerConnStatsToJson(PeerConnStats instance) =>
|
||||
<String, dynamic>{
|
||||
'rx_bytes': instance.rxBytes,
|
||||
'tx_bytes': instance.txBytes,
|
||||
'rx_packets': instance.rxPackets,
|
||||
'tx_packets': instance.txPackets,
|
||||
'latency_us': instance.latencyUs,
|
||||
};
|
||||
|
||||
PeerConnInfo _$PeerConnInfoFromJson(Map<String, dynamic> json) => PeerConnInfo(
|
||||
connId: json['conn_id'] as String,
|
||||
myPeerId: (json['my_peer_id'] as num).toInt(),
|
||||
peerId: (json['peer_id'] as num).toInt(),
|
||||
features:
|
||||
(json['features'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
tunnel: json['tunnel'] == null
|
||||
? null
|
||||
: TunnelInfo.fromJson(json['tunnel'] as Map<String, dynamic>),
|
||||
stats: json['stats'] == null
|
||||
? null
|
||||
: PeerConnStats.fromJson(json['stats'] as Map<String, dynamic>),
|
||||
lossRate: (json['loss_rate'] as num).toDouble(),
|
||||
isClient: json['is_client'] as bool,
|
||||
networkName: json['network_name'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PeerConnInfoToJson(PeerConnInfo instance) =>
|
||||
<String, dynamic>{
|
||||
'conn_id': instance.connId,
|
||||
'my_peer_id': instance.myPeerId,
|
||||
'peer_id': instance.peerId,
|
||||
'features': instance.features,
|
||||
'tunnel': instance.tunnel,
|
||||
'stats': instance.stats,
|
||||
'loss_rate': instance.lossRate,
|
||||
'is_client': instance.isClient,
|
||||
'network_name': instance.networkName,
|
||||
};
|
||||
|
||||
PeerInfo _$PeerInfoFromJson(Map<String, dynamic> json) => PeerInfo(
|
||||
peerId: (json['peer_id'] as num).toInt(),
|
||||
conns: (json['conns'] as List<dynamic>)
|
||||
.map((e) => PeerConnInfo.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PeerInfoToJson(PeerInfo instance) => <String, dynamic>{
|
||||
'peer_id': instance.peerId,
|
||||
'conns': instance.conns,
|
||||
};
|
||||
|
||||
PeerRoutePair _$PeerRoutePairFromJson(Map<String, dynamic> json) =>
|
||||
PeerRoutePair(
|
||||
route: json['route'] == null
|
||||
? null
|
||||
: Route.fromJson(json['route'] as Map<String, dynamic>),
|
||||
peer: json['peer'] == null
|
||||
? null
|
||||
: PeerInfo.fromJson(json['peer'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PeerRoutePairToJson(PeerRoutePair instance) =>
|
||||
<String, dynamic>{
|
||||
'route': instance.route,
|
||||
'peer': instance.peer,
|
||||
};
|
||||
|
||||
Ipv6Addr _$Ipv6AddrFromJson(Map<String, dynamic> json) => Ipv6Addr(
|
||||
part1: (json['part1'] as num).toInt(),
|
||||
part2: (json['part2'] as num).toInt(),
|
||||
part3: (json['part3'] as num).toInt(),
|
||||
part4: (json['part4'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$Ipv6AddrToJson(Ipv6Addr instance) => <String, dynamic>{
|
||||
'part1': instance.part1,
|
||||
'part2': instance.part2,
|
||||
'part3': instance.part3,
|
||||
'part4': instance.part4,
|
||||
};
|
||||
|
||||
GetIpListResponse _$GetIpListResponseFromJson(Map<String, dynamic> json) =>
|
||||
GetIpListResponse(
|
||||
publicIpv4: json['public_ipv4'] == null
|
||||
? null
|
||||
: Ipv4Addr.fromJson(json['public_ipv4'] as Map<String, dynamic>),
|
||||
interfaceIpv4s: (json['interface_ipv4s'] as List<dynamic>)
|
||||
.map((e) => Ipv4Addr.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
publicIpv6: json['public_ipv6'] == null
|
||||
? null
|
||||
: Ipv6Addr.fromJson(json['public_ipv6'] as Map<String, dynamic>),
|
||||
interfaceIpv6s: (json['interface_ipv6s'] as List<dynamic>)
|
||||
.map((e) => Ipv6Addr.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
listeners: (json['listeners'] as List<dynamic>)
|
||||
.map((e) => Url.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$GetIpListResponseToJson(GetIpListResponse instance) =>
|
||||
<String, dynamic>{
|
||||
'public_ipv4': instance.publicIpv4,
|
||||
'interface_ipv4s': instance.interfaceIpv4s,
|
||||
'public_ipv6': instance.publicIpv6,
|
||||
'interface_ipv6s': instance.interfaceIpv6s,
|
||||
'listeners': instance.listeners,
|
||||
};
|
||||
|
||||
MyNodeInfo _$MyNodeInfoFromJson(Map<String, dynamic> json) => MyNodeInfo(
|
||||
virtualIpv4: json['virtual_ipv4'] == null
|
||||
? null
|
||||
: Ipv4Inet.fromJson(json['virtual_ipv4'] as Map<String, dynamic>),
|
||||
hostname: json['hostname'] as String,
|
||||
version: json['version'] as String,
|
||||
ips: json['ips'] == null
|
||||
? null
|
||||
: GetIpListResponse.fromJson(json['ips'] as Map<String, dynamic>),
|
||||
stunInfo: json['stun_info'] == null
|
||||
? null
|
||||
: StunInfo.fromJson(json['stun_info'] as Map<String, dynamic>),
|
||||
listeners: (json['listeners'] as List<dynamic>)
|
||||
.map((e) => Url.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
vpnPortalCfg: json['vpn_portal_cfg'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MyNodeInfoToJson(MyNodeInfo instance) =>
|
||||
<String, dynamic>{
|
||||
'virtual_ipv4': instance.virtualIpv4,
|
||||
'hostname': instance.hostname,
|
||||
'version': instance.version,
|
||||
'ips': instance.ips,
|
||||
'stun_info': instance.stunInfo,
|
||||
'listeners': instance.listeners,
|
||||
'vpn_portal_cfg': instance.vpnPortalCfg,
|
||||
};
|
||||
|
||||
Runin _$RuninFromJson(Map<String, dynamic> json) => Runin(
|
||||
devName: json['dev_name'] as String?,
|
||||
myNodeInfo: json['my_node_info'] == null
|
||||
? null
|
||||
: MyNodeInfo.fromJson(json['my_node_info'] as Map<String, dynamic>),
|
||||
events:
|
||||
(json['events'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
routes: (json['routes'] as List<dynamic>)
|
||||
.map((e) => Route.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
peers: (json['peers'] as List<dynamic>?)
|
||||
?.map((e) => PeerInfo.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
peerRoutePairs: (json['peer_route_pairs'] as List<dynamic>?)
|
||||
?.map((e) => PeerRoutePair.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
running: json['running'] as bool,
|
||||
errorMsg: json['error_msg'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RuninToJson(Runin instance) => <String, dynamic>{
|
||||
'dev_name': instance.devName,
|
||||
'my_node_info': instance.myNodeInfo,
|
||||
'events': instance.events,
|
||||
'routes': instance.routes,
|
||||
'peers': instance.peers,
|
||||
'peer_route_pairs': instance.peerRoutePairs,
|
||||
'running': instance.running,
|
||||
'error_msg': instance.errorMsg,
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
String getThemeModeText(ThemeMode mode) {
|
||||
return switch (mode) {
|
||||
ThemeMode.light => '浅色模式',
|
||||
ThemeMode.dark => '深色模式',
|
||||
ThemeMode.system => '跟随系统',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'package:ASTRAL/src/rust/api/simple.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../config/app_config.dart';
|
||||
|
||||
class KM extends ChangeNotifier {
|
||||
final _config = AppConfig();
|
||||
int _count = 0;
|
||||
int get count => _count;
|
||||
|
||||
void increment() {
|
||||
_count++;
|
||||
notifyListeners(); // 通知监听器重建UI
|
||||
}
|
||||
|
||||
// 房间名
|
||||
String get roomName => _config.roomName;
|
||||
set roomName(String value) {
|
||||
_config.setRoomName(value);
|
||||
notifyListeners(); // 通知监听器重建UI
|
||||
}
|
||||
|
||||
//房间密码设置
|
||||
String get roomPassword => _config.roomPassword;
|
||||
set roomPassword(String value) {
|
||||
_config.setRoomPassword(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
//用户名设置
|
||||
String get username => _config.username;
|
||||
set username(String value) {
|
||||
_config.setUsername(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
//虚拟IP设置
|
||||
String get virtualIP => _config.virtualIP;
|
||||
set virtualIP(String value) {
|
||||
_config.setVirtualIP(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
//动态获取IP设置
|
||||
bool get dynamicIP => _config.dynamicIP;
|
||||
set dynamicIP(bool value) {
|
||||
_config.setDynamicIP(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
//当前服务器IP
|
||||
String get serverIP => _config.currentServer;
|
||||
set serverIP(String value) {
|
||||
_config.setCurrentServer(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 节点列表
|
||||
List<KVNodeInfo> _nodes = [];
|
||||
List<KVNodeInfo> get nodes => _nodes;
|
||||
set nodes(List<KVNodeInfo> value) {
|
||||
_nodes = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||
|
||||
void showThemeColorPicker(
|
||||
BuildContext context, Color seedColor, Function(Color) changeSeedColor) {
|
||||
// 创建一个临时颜色变量,避免直接修改状态
|
||||
Color pickerColor = seedColor;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('选择主题颜色'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 使用更简单的BlockPicker替代复杂的ColorPicker
|
||||
BlockPicker(
|
||||
pickerColor: pickerColor,
|
||||
onColorChanged: (color) {
|
||||
pickerColor = color;
|
||||
},
|
||||
availableColors: const [
|
||||
Colors.red,
|
||||
Colors.pink,
|
||||
Colors.purple,
|
||||
Colors.deepPurple,
|
||||
Colors.indigo,
|
||||
Colors.blue,
|
||||
Colors.lightBlue,
|
||||
Colors.cyan,
|
||||
Colors.teal,
|
||||
Colors.green,
|
||||
Colors.lightGreen,
|
||||
Colors.lime,
|
||||
Colors.yellow,
|
||||
Colors.amber,
|
||||
Colors.orange,
|
||||
Colors.deepOrange,
|
||||
Colors.brown,
|
||||
Colors.grey,
|
||||
Colors.blueGrey,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// 添加一个自定义颜色按钮
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.color_lens),
|
||||
label: const Text('自定义颜色'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_showAdvancedColorPicker(
|
||||
context, pickerColor, changeSeedColor);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: const Text('取消'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('确定'),
|
||||
onPressed: () {
|
||||
// 使用 Future.microtask 延迟颜色更改,避免在当前帧中触发重建
|
||||
Navigator.of(context).pop();
|
||||
changeSeedColor(pickerColor);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showAdvancedColorPicker(
|
||||
BuildContext context, Color initialColor, Function(Color) changeSeedColor) {
|
||||
Color pickerColor = initialColor;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('自定义颜色'),
|
||||
content: SingleChildScrollView(
|
||||
child: ColorPicker(
|
||||
pickerColor: pickerColor,
|
||||
onColorChanged: (color) {
|
||||
pickerColor = color;
|
||||
},
|
||||
pickerAreaHeightPercent: 0.8,
|
||||
enableAlpha: false,
|
||||
displayThumbColor: true,
|
||||
paletteType: PaletteType.hsvWithHue,
|
||||
pickerAreaBorderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
labelTypes: const [ColorLabelType.rgb, ColorLabelType.hex],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: const Text('取消'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('确定'),
|
||||
onPressed: () {
|
||||
// 使用 Future.microtask 延迟颜色更改,避免在当前帧中触发重建
|
||||
Navigator.of(context).pop();
|
||||
Future.microtask(() {
|
||||
changeSeedColor(pickerColor);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
class FloatingCard extends StatefulWidget {
|
||||
final ColorScheme colorScheme;
|
||||
final Widget child;
|
||||
final double elevation;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final Duration duration;
|
||||
final double hoverElevation;
|
||||
final double? maxWidth; // 添加最大宽度参数
|
||||
final double? height; // 添加最大宽度参数
|
||||
final bool enable3DEffect; // 是否启用3D效果
|
||||
final double maxRotationDegree; // 最大旋转角度
|
||||
final bool enableTranslateEffect; // 是否启用偏移效果
|
||||
final double maxTranslateDistance; // 最大偏移距离
|
||||
final double zTranslation; // Z轴偏移距离
|
||||
final bool riseOnHover; // 控制悬浮时是升起还是降下
|
||||
|
||||
const FloatingCard({
|
||||
super.key,
|
||||
required this.colorScheme,
|
||||
required this.child,
|
||||
this.elevation = 4,
|
||||
this.padding = const EdgeInsets.all(16.0),
|
||||
this.duration = const Duration(milliseconds: 200),
|
||||
this.hoverElevation = 8,
|
||||
this.maxWidth,
|
||||
this.height,
|
||||
this.enable3DEffect = false, // 默认启用3D效果
|
||||
this.maxRotationDegree = 10, // 默认最大旋转角度为10度
|
||||
this.enableTranslateEffect = false, // 默认启用偏移效果
|
||||
this.maxTranslateDistance = 0.1, // 默认最大偏移距离为5
|
||||
this.zTranslation = 10, // 默认Z轴偏移距离为20
|
||||
this.riseOnHover = true, // 默认悬浮时升起
|
||||
});
|
||||
|
||||
@override
|
||||
State<FloatingCard> createState() => _FloatingCardState();
|
||||
}
|
||||
|
||||
class _FloatingCardState extends State<FloatingCard> {
|
||||
bool isHovered = false;
|
||||
Offset mousePosition = Offset.zero;
|
||||
final GlobalKey _cardKey = GlobalKey();
|
||||
|
||||
// 获取卡片的尺寸和位置
|
||||
Rect? _getCardRect() {
|
||||
final RenderBox? renderBox =
|
||||
_cardKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (renderBox == null) return null;
|
||||
final position = renderBox.localToGlobal(Offset.zero);
|
||||
return Rect.fromLTWH(
|
||||
position.dx, position.dy, renderBox.size.width, renderBox.size.height);
|
||||
}
|
||||
|
||||
// 计算旋转角度
|
||||
(double, double) _calculateRotation() {
|
||||
final rect = _getCardRect();
|
||||
if (rect == null) return (0, 0);
|
||||
|
||||
// 计算鼠标相对于卡片中心的位置
|
||||
final centerX = rect.width / 2;
|
||||
final centerY = rect.height / 2;
|
||||
final deltaX = (mousePosition.dx - centerX) / centerX;
|
||||
final deltaY = (mousePosition.dy - centerY) / centerY;
|
||||
|
||||
// 计算旋转角度,鼠标在右侧时向左倾斜(Y轴正向旋转),鼠标在下方时向上倾斜(X轴负向旋转)
|
||||
final rotateY = deltaX * widget.maxRotationDegree;
|
||||
final rotateX = -deltaY * widget.maxRotationDegree;
|
||||
|
||||
return (rotateX, rotateY);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 根据是否启用3D效果计算旋转角度
|
||||
final (rotateX, rotateY) =
|
||||
isHovered && widget.enable3DEffect ? _calculateRotation() : (0.0, 0.0);
|
||||
|
||||
// 根据是否启用偏移效果计算偏移距离
|
||||
final translateX = isHovered && widget.enableTranslateEffect
|
||||
? rotateY * widget.maxTranslateDistance
|
||||
: 0.0;
|
||||
final translateY = isHovered && widget.enableTranslateEffect
|
||||
? rotateX * widget.maxTranslateDistance
|
||||
: 0.0;
|
||||
|
||||
// 根据riseOnHover决定Z轴偏移方向
|
||||
final zDirection = widget.riseOnHover ? 1.0 : -1.0;
|
||||
final translateZ = isHovered && widget.enableTranslateEffect
|
||||
? widget.zTranslation * zDirection
|
||||
: 0.0;
|
||||
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => isHovered = true),
|
||||
onExit: (_) => setState(() => isHovered = false),
|
||||
onHover: (event) {
|
||||
if (widget.enable3DEffect || widget.enableTranslateEffect) {
|
||||
setState(() {
|
||||
mousePosition = event.localPosition;
|
||||
});
|
||||
}
|
||||
},
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: widget.maxWidth ?? double.infinity,
|
||||
),
|
||||
child: AnimatedContainer(
|
||||
key: _cardKey,
|
||||
duration: widget.duration,
|
||||
transform: Matrix4.identity()
|
||||
..setEntry(3, 2, 0.001) // 透视效果
|
||||
// 仅当启用3D效果时应用旋转
|
||||
..rotateX(widget.enable3DEffect ? rotateX * math.pi / 180 : 0)
|
||||
..rotateY(widget.enable3DEffect ? rotateY * math.pi / 180 : 0)
|
||||
// 仅当启用偏移效果时应用偏移
|
||||
..translate(translateX, translateY, translateZ),
|
||||
transformAlignment: Alignment.center,
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: isHovered ? widget.hoverElevation : widget.elevation,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
splashColor: widget.colorScheme.primary.withOpacity(0.1),
|
||||
hoverColor: widget.colorScheme.primary.withOpacity(0.05),
|
||||
onTap: () {
|
||||
// 可以添加点击事件处理
|
||||
},
|
||||
child: Padding(
|
||||
padding: widget.padding,
|
||||
child: SizedBox(
|
||||
height: widget.height,
|
||||
child: widget.child,
|
||||
)),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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: '关闭',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user