first commit

This commit is contained in:
会做饭的二哈
2025-03-15 23:25:22 +08:00
commit cffd27dcbe
249 changed files with 63999 additions and 0 deletions
+156
View File
@@ -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,
),
);
}
}
+125
View File
@@ -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);
}
}
+37
View File
@@ -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,
);
}
}
+76
View File
@@ -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();
}
}
+18
View File
@@ -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();
});
}
+28
View File
@@ -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(),
),
);
}
+209
View File
@@ -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,
),
),
),
);
}
}
+328
View File
@@ -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),
),
),
],
),
),
],
),
),
),
),
);
}
}
+704
View File
@@ -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;
}
}
}
+415
View File
@@ -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('灰度版本不支持更新')),
);
},
),
],
),
),
],
);
}
}
+922
View File
@@ -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,
),
),
],
);
}
+159
View File
@@ -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
+494
View File
@@ -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>)>();
}
+446
View File
@@ -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);
}
+37
View File
@@ -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
+27
View File
@@ -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;
}
}
}
+357
View File
@@ -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);
}
+310
View File
@@ -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,
};
+9
View File
@@ -0,0 +1,9 @@
import 'package:flutter/material.dart';
String getThemeModeText(ThemeMode mode) {
return switch (mode) {
ThemeMode.light => '浅色模式',
ThemeMode.dark => '深色模式',
ThemeMode.system => '跟随系统',
};
}
+64
View File
@@ -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();
}
}
+125
View File
@@ -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);
});
},
),
],
);
},
);
}
+146
View File
@@ -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,
)),
)),
),
),
),
);
}
}
+43
View File
@@ -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: '关闭',
),
],
);
}
}