30 Commits
Author SHA1 Message Date
会做饭的二哈 c4c7d93ca8 11 2025-03-19 18:32:20 +08:00
会做饭的二哈 086c3a7818 refactor(widgets): 将 WindowControls 从 StatelessWidget 转换为 StatefulWidget
为了支持窗口最大化状态的管理,将 WindowControls 从 StatelessWidget 转换为 StatefulWidget。添加了窗口最大化状态监听和手动状态更新逻辑,以确保按钮图标和窗口状态同步。
2025-03-18 22:38:23 +08:00
会做饭的二哈 afcf43bcf7 Merge branch 'main' of https://github.com/ldoubil/astral 2025-03-18 21:50:49 +08:00
会做饭的二哈 f19853a49e chore: 更新版本号至1.0.6 2025-03-18 21:50:44 +08:00
会做饭的二哈andGitHub 92e9d0fc15 Update dart.yml 2025-03-18 21:49:56 +08:00
会做饭的二哈andGitHub 0d567ce686 Update dart.yml 2025-03-18 21:47:55 +08:00
会做饭的二哈 912dc263b0 refactor(settings): 将公共服务器列表从底部表单改为对话框
修改了公共服务器列表的显示方式,从底部表单改为对话框,简化了界面结构并提升了用户体验。同时,移除了不必要的约束和重复代码。
2025-03-18 21:40:05 +08:00
会做饭的二哈 38acea1bf5 feat: 添加窗口标题并优化Windows应用配置
- 在`windowconfiguration.dart`中添加窗口标题"Astral"并确保窗口管理器初始化
- 在`runner.exe.manifest`中添加UTF-8编码、长路径支持和SegmentHeap堆类型配置
- 在`Runner.rc`中更新应用描述、内部名称、原始文件名和产品名称为"Astral"
- 在`main.cpp`中实现单实例应用逻辑并更新窗口标题为"Astral"
2025-03-18 21:00:17 +08:00
会做饭的二哈 16d31c01fc fix: 将丢包率显示保留两位小数 2025-03-18 20:25:03 +08:00
会做饭的二哈andGitHub 48c1894cc4 Update dart.yml 2025-03-18 17:54:37 +08:00
会做饭的二哈andGitHub 5f66d65fe4 Update dart.yml 2025-03-18 17:53:08 +08:00
会做饭的二哈andGitHub ad875e0e8a Update dart.yml 2025-03-18 17:50:35 +08:00
会做饭的二哈andGitHub 1ff1067b98 Update dart.yml 2025-03-18 17:31:10 +08:00
会做饭的二哈andGitHub fca3d3b17f Update dart.yml 2025-03-18 17:27:41 +08:00
会做饭的二哈andGitHub 5646d550cb Update dart.yml 2025-03-18 17:27:30 +08:00
会做饭的二哈andGitHub fa8885bec3 Update dart.yml 2025-03-18 17:22:26 +08:00
会做饭的二哈andGitHub 27bd2b0c1b Update dart.yml 2025-03-18 17:17:54 +08:00
会做饭的二哈 0fef772a01 refactor(front.dart): 优化数据压缩算法的赋值逻辑
将 `dataCompressAlgo` 的赋值逻辑从简单的字符串映射改为数值映射,以提高代码的可读性和维护性。具体修改包括将 "Invalid" 映射为 0,"None" 映射为 1,"Zstd" 映射为 2,其他情况默认为 1。
2025-03-18 17:12:55 +08:00
会做饭的二哈andGitHub 87e6e6d70d Update dart.yml 2025-03-18 17:02:26 +08:00
会做饭的二哈andGitHub e6049383bb Update dart.yml 2025-03-18 16:40:16 +08:00
会做饭的二哈andGitHub d506555741 构建.yml 2025-03-18 16:37:12 +08:00
会做饭的二哈 01a4d1a81e 1. 将原有的Provider状态管理替换为Riverpod,提升状态管理效率
2. 重构AppConfig类,使用Hive进行持久化存储
3. 新增AdvancedConfig类,用于管理高级配置项
4. 优化服务器列表和节点状态的管理逻辑
5. 添加多个Provider来管理不同模块的状态
6. 更新依赖项,添加Riverpod及相关依赖
2025-03-18 16:28:59 +08:00
会做饭的二哈 ca49264ad6 自动验证配置文件 2025-03-17 23:19:31 +08:00
会做饭的二哈 99ed31f992 自动验证配置文件 2025-03-17 23:19:25 +08:00
会做饭的二哈 f6de2f3849 1.0.4 2025-03-17 22:56:35 +08:00
会做饭的二哈 3e8fae1c3e 1.0.4 2025-03-17 22:55:52 +08:00
会做饭的二哈 c72617aaf0 修复内存 2025-03-17 18:18:49 +08:00
会做饭的二哈 b1e48999c3 修复内存泄漏 2025-03-17 18:15:21 +08:00
会做饭的二哈 2f6195ecd9 1 2025-03-16 23:54:30 +08:00
会做饭的二哈 4fb5387f5b 修复延迟检测问题 更新逻辑不正确 若干体验上问题 2025-03-16 23:32:59 +08:00
86 changed files with 5509 additions and 6875 deletions
+69
View File
@@ -0,0 +1,69 @@
name: Dart and Flutter Build
on:
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g., v1.0.0)'
required: true
type: string
jobs:
build:
runs-on: windows-latest
# 调整 GITHUB_TOKEN 的权限
permissions:
contents: write # 允许写入仓库内容(上传文件)
steps:
- uses: actions/checkout@v4
# 安装 Flutter
- name: Install Flutter
run: |
echo "Cloning Flutter stable branch..."
git clone https://github.com/flutter/flutter.git -b stable
echo "Adding Flutter to PATH..."
echo "$env:GITHUB_WORKSPACE\flutter\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
echo "Flutter installation completed."
# 安装 Flutter 依赖
- name: Install Flutter dependencies
run: |
echo "Fetching Flutter dependencies..."
flutter pub get
echo "Flutter dependencies installed."
# 编译 Flutter Windows 应用
- name: Build Flutter Windows Release
run: |
echo "Building Flutter Windows Release..."
flutter build windows --release
echo "Flutter Windows Release build completed."
# 压缩 build\windows\x64\runner\Release 目录
- name: Compress Release Directory
run: |
echo "Compressing Release directory..."
$releaseDir = "$env:GITHUB_WORKSPACE\build\windows\x64\runner\Release"
$zipFile = "$env:GITHUB_WORKSPACE\release.zip"
Compress-Archive -Path $releaseDir -DestinationPath $zipFile
echo "Release directory compressed to $zipFile."
# 上传压缩包作为 Artifact
- name: Upload Release Artifact
uses: actions/upload-artifact@v4
with:
name: release
path: ${{ github.workspace }}/release.zip
# 创建 GitHub Release 并上传压缩包
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: Release ${{ inputs.version }}
draft: true
files: ${{ github.workspace }}/release.zip
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ inputs.version }}
+1 -1
View File
@@ -15,7 +15,7 @@ migration:
- platform: root
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: web
- platform: android
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
+6
View File
@@ -4,6 +4,12 @@
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Flutter",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "Flutter Run",
"request": "launch",
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
+56
View File
@@ -0,0 +1,56 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.astral"
compileSdk = flutter.compileSdkVersion
// ndkVersion = flutter.ndkVersion
ndkVersion = "27.2.12479018"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
// 启用 core library desugaring
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
// 添加 buildFeatures 配置
buildFeatures {
buildConfig = true
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.astral"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
// 添加 dependencies 块
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+45
View File
@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="astral"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package com.example.astral
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+21
View File
@@ -0,0 +1,21 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+3
View File
@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
+5
View File
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
+25
View File
@@ -0,0 +1,25 @@
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.0" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
}
include(":app")
+3 -15
View File
@@ -4,9 +4,8 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:system_tray/system_tray.dart';
import 'dart:io';
import 'screens/Home.dart';
import 'config/themeconfiguration.dart';
import 'config/themeconfiguration.dart' as theme_config;
import 'config/app_config.dart';
import 'package:astral/utils/up.dart'; // 添加这行导入
// 定义应用程序的主要StatefulWidget
class MyApp extends StatefulWidget {
@@ -39,17 +38,6 @@ class _MyAppState extends State<MyApp> {
// 初始化系统托盘
initSystemTray();
// 添加版本检查(在下一帧执行确保context可用)
Future.microtask(() {
final updateChecker = UpdateChecker(
owner: 'ldoubil',
repo: 'astral',
);
if (mounted) {
updateChecker.scheckForUpdates(context);
}
});
}
// 初始化系统托盘
@@ -126,7 +114,7 @@ class _MyAppState extends State<MyApp> {
],
// Insert this line
supportedLocales: const [Locale("zh", "CN"), Locale("en", "US")],
theme: ThemeConfig.getLightTheme(
theme: theme_config.ThemeConfig.getLightTheme(
useMaterial3: useMaterial3,
seedColor: _seedColor,
).copyWith(
@@ -136,7 +124,7 @@ class _MyAppState extends State<MyApp> {
primaryTextTheme: Typography.material2021().black.apply(
fontFamily: 'MiSans',
)),
darkTheme: ThemeConfig.getDarkTheme(
darkTheme: theme_config.ThemeConfig.getDarkTheme(
useMaterial3: useMaterial3,
seedColor: _seedColor,
).copyWith(
+828 -108
View File
@@ -1,161 +1,881 @@
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:path/path.dart' as path;
import 'cof.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
// 配置模型基类
abstract class ConfigModel {
Map<String, dynamic> toJson();
String get configKey; // 配置在存储中的键名
}
// 添加高级配置类
class AdvancedConfig implements ConfigModel {
final String defaultProtocol;
final String devName;
final bool enableEncryption;
final bool enableIpv6;
final int mtu;
final bool latencyFirst;
final bool enableExitNode;
final bool proxyForwardBySystem;
final bool noTun;
final bool useSmoltcp;
final String relayNetworkWhitelist;
final bool disableP2p;
final bool relayAllPeerRpc;
final bool disableUdpHolePunching;
final bool multiThread;
final String dataCompressAlgo;
final bool bindDevice;
final bool enableKcpProxy;
final bool disableKcpInput;
final bool disableRelayKcp;
@override
String get configKey => 'advanced';
AdvancedConfig({
this.defaultProtocol = "tcp",
this.devName = "",
this.enableEncryption = true,
this.enableIpv6 = true,
this.mtu = 1380,
this.latencyFirst = false,
this.enableExitNode = false,
this.proxyForwardBySystem = false,
this.noTun = false,
this.useSmoltcp = false,
this.relayNetworkWhitelist = "*",
this.disableP2p = false,
this.relayAllPeerRpc = false,
this.disableUdpHolePunching = false,
this.multiThread = true,
this.dataCompressAlgo = "None",
this.bindDevice = true,
this.enableKcpProxy = false,
this.disableKcpInput = false,
this.disableRelayKcp = true,
});
factory AdvancedConfig.fromJson(Map<String, dynamic> json) {
return AdvancedConfig(
defaultProtocol: json['defaultProtocol'] ?? "tcp",
devName: json['devName'] ?? "",
enableEncryption: json['enableEncryption'] ?? true,
enableIpv6: json['enableIpv6'] ?? true,
mtu: json['mtu'] ?? 1380,
latencyFirst: json['latencyFirst'] ?? false,
enableExitNode: json['enableExitNode'] ?? false,
proxyForwardBySystem: json['proxyForwardBySystem'] ?? false,
noTun: json['noTun'] ?? false,
useSmoltcp: json['useSmoltcp'] ?? false,
relayNetworkWhitelist: json['relayNetworkWhitelist'] ?? "*",
disableP2p: json['disableP2p'] ?? false,
relayAllPeerRpc: json['relayAllPeerRpc'] ?? false,
disableUdpHolePunching: json['disableUdpHolePunching'] ?? false,
multiThread: json['multiThread'] ?? true,
dataCompressAlgo: json['dataCompressAlgo'] ?? "None",
bindDevice: json['bindDevice'] ?? true,
enableKcpProxy: json['enableKcpProxy'] ?? false,
disableKcpInput: json['disableKcpInput'] ?? false,
disableRelayKcp: json['disableRelayKcp'] ?? true,
);
}
@override
Map<String, dynamic> toJson() => {
'defaultProtocol': defaultProtocol,
'devName': devName,
'enableEncryption': enableEncryption,
'enableIpv6': enableIpv6,
'mtu': mtu,
'latencyFirst': latencyFirst,
'enableExitNode': enableExitNode,
'proxyForwardBySystem': proxyForwardBySystem,
'noTun': noTun,
'useSmoltcp': useSmoltcp,
'relayNetworkWhitelist': relayNetworkWhitelist,
'disableP2p': disableP2p,
'relayAllPeerRpc': relayAllPeerRpc,
'disableUdpHolePunching': disableUdpHolePunching,
'multiThread': multiThread,
'dataCompressAlgo': dataCompressAlgo,
'bindDevice': bindDevice,
'enableKcpProxy': enableKcpProxy,
'disableKcpInput': disableKcpInput,
'disableRelayKcp': disableRelayKcp,
};
}
// 配置模型类 - 保持原有结构
class ThemeConfig implements ConfigModel {
final String mode;
final int seedColor;
@override
String get configKey => 'theme';
ThemeConfig({
this.mode = 'system',
this.seedColor = 0xFF2196F3, // Colors.blue.value
});
factory ThemeConfig.fromJson(Map<String, dynamic> json) {
return ThemeConfig(
mode: json['mode'] ?? 'system',
seedColor: json['seedColor'] ?? 0xFF2196F3,
);
}
@override
Map<String, dynamic> toJson() => {
'mode': mode,
'seedColor': seedColor,
};
}
class ServerConfig implements ConfigModel {
final String url;
final String name;
final bool selected;
final bool tcp;
final bool udp;
final bool ws;
final bool wss;
final bool quic;
@override
String get configKey => 'server';
ServerConfig({
required this.url,
required this.name,
this.selected = false,
this.tcp = true,
this.udp = true,
this.ws = false,
this.wss = false,
this.quic = false,
});
factory ServerConfig.fromJson(Map<String, dynamic> json) {
return ServerConfig(
url: json['url'] ?? '',
name: json['name'] ?? '',
selected: json['selected'] ?? false,
tcp: json['tcp'] ?? true,
udp: json['udp'] ?? true,
ws: json['ws'] ?? false,
wss: json['wss'] ?? false,
quic: json['quic'] ?? false,
);
}
Map<String, dynamic> toJson() => {
'url': url,
'name': name,
'selected': selected,
'tcp': tcp,
'udp': udp,
'ws': ws,
'wss': wss,
'quic': quic,
};
}
// 创建一个新的服务器列表配置类
class ServerListConfig implements ConfigModel {
final List<ServerConfig> servers;
@override
String get configKey => 'server';
ServerListConfig({
required this.servers,
});
factory ServerListConfig.fromJson(Map<String, dynamic> json) {
List<ServerConfig> serverList = [];
if (json['list'] is List) {
try {
serverList = (json['list'] as List).map((item) {
// 安全地将 Map<dynamic, dynamic> 转换为 Map<String, dynamic>
if (item is Map) {
Map<String, dynamic> serverMap = {};
item.forEach((key, value) {
if (key is String) {
serverMap[key] = value;
}
});
return ServerConfig.fromJson(serverMap);
}
// 如果不是 Map,返回默认服务器配置
return ServerConfig(
url: 'public.easytier.cn:11010',
name: '公共服务器',
selected: true,
tcp: true,
udp: true,
ws: false,
wss: false,
quic: false,
);
}).toList();
// 确保至少有一个服务器被选中
if (!serverList.any((server) => server.selected) &&
serverList.isNotEmpty) {
print('没有选中的服务器,将第一个服务器设为选中状态');
serverList[0] = ServerConfig(
url: serverList[0].url,
name: serverList[0].name,
selected: true,
tcp: serverList[0].tcp,
udp: serverList[0].udp,
ws: serverList[0].ws,
wss: serverList[0].wss,
quic: serverList[0].quic,
);
} else {
print('已有选中的服务器,保持原状');
}
} catch (e) {
print('解析服务器列表失败: $e');
// 解析失败时使用默认值
}
}
// 如果列表为空,添加默认服务器
if (serverList.isEmpty) {
print('服务器列表为空,添加默认服务器');
serverList = [
ServerConfig(
url: 'public.easytier.cn:11010',
name: '公共服务器',
selected: true,
tcp: true,
udp: true,
ws: false,
wss: false,
quic: false,
)
];
}
return ServerListConfig(servers: serverList);
}
@override
Map<String, dynamic> toJson() => {
'list': servers.map((server) => server.toJson()).toList(),
};
}
class RoomConfig implements ConfigModel {
final String name;
final String password;
@override
String get configKey => 'room';
RoomConfig({
this.name = 'kevin',
this.password = 'kevin',
});
factory RoomConfig.fromJson(Map<String, dynamic> json) {
return RoomConfig(
name: json['name'] ?? 'kevin',
password: json['password'] ?? 'kevin',
);
}
Map<String, dynamic> toJson() => {
'name': name,
'password': password,
};
}
class UserConfig implements ConfigModel {
final String name;
@override
String get configKey => 'user';
UserConfig({
required this.name,
});
factory UserConfig.fromJson(Map<String, dynamic> json) {
return UserConfig(
name: json['name'] ?? Platform.localHostname,
);
}
Map<String, dynamic> toJson() => {
'name': name,
};
}
class NetworkConfig implements ConfigModel {
final String virtualIP;
final bool dynamicIP;
@override
String get configKey => 'network';
NetworkConfig({
this.virtualIP = '',
this.dynamicIP = true,
});
factory NetworkConfig.fromJson(Map<String, dynamic> json) {
return NetworkConfig(
virtualIP: json['virtualIP'] ?? '',
dynamicIP: json['dynamicIP'] ?? true,
);
}
Map<String, dynamic> toJson() => {
'virtualIP': virtualIP,
'dynamicIP': dynamicIP,
};
}
class SystemConfig implements ConfigModel {
final bool closeToTray;
final bool enablePing;
@override
String get configKey => 'system';
SystemConfig({
this.closeToTray = true,
this.enablePing = true,
});
factory SystemConfig.fromJson(Map<String, dynamic> json) {
return SystemConfig(
closeToTray: json['closeToTray'] ?? true,
enablePing: json['enablePing'] ?? true,
);
}
Map<String, dynamic> toJson() => {
'closeToTray': closeToTray,
'enablePing': enablePing,
};
}
// 配置管理器
class AppConfig {
static final AppConfig _instance = AppConfig._internal();
static late ConfigManager _configManager;
static late Box _configBox;
static late String _configDirectory;
static bool _initialized = false;
// 配置模型映射表
final Map<Type, ConfigModel> _configModels = {};
// 配置类型与工厂函数映射
final Map<Type, Function> _configFactories = {};
factory AppConfig() {
return _instance;
}
AppConfig._internal();
// 初始化配置
static Future<void> init() async {
// 获取可执行文件所在目录而不是当前工作目录
_configDirectory = File(Platform.resolvedExecutable).parent.path;
final configPath = path.join(_configDirectory, 'config.yaml');
_configManager = ConfigManager(
filePath: configPath,
defaultConfig: {
'theme': {
'mode': 'system',
'seedColor': Colors.blue.value,
},
'server': {
'list': ['public.easytier.cn:11010'],
'current': 'public.easytier.cn:11010',
},
'room': {
'name': 'kevin',
'password': 'kevin',
},
'user': {
'name': Platform.localHostname,
},
'network': {
'virtualIP': '',
'dynamicIP': true,
},
'system': {
'closeToTray': true,
},
},
);
await _configManager.load();
// 在AppConfig类中添加静态方法setConfigDir
static void setConfigDir(String dirPath) {
_configDirectory = dirPath;
print('配置目录已设置为: $_configDirectory');
}
AppConfig._internal() {
// 注册所有配置类型
_registerConfig<ThemeConfig>(
(json) => ThemeConfig.fromJson(json),
ThemeConfig(),
);
_registerConfig<RoomConfig>(
(json) => RoomConfig.fromJson(json),
RoomConfig(),
);
_registerConfig<UserConfig>(
(json) => UserConfig.fromJson(json),
UserConfig(name: Platform.localHostname),
);
_registerConfig<NetworkConfig>(
(json) => NetworkConfig.fromJson(json),
NetworkConfig(),
);
_registerConfig<SystemConfig>(
(json) => SystemConfig.fromJson(json),
SystemConfig(),
);
// 注册服务器列表配置
_registerConfig<ServerListConfig>(
(json) => ServerListConfig.fromJson(json),
ServerListConfig(servers: [
ServerConfig(
url: 'public.easytier.cn:11010',
name: '公共服务器',
selected: true,
tcp: true,
udp: true,
ws: false,
wss: false,
quic: false,
)
]),
);
// 注册高级配置
_registerConfig<AdvancedConfig>(
(json) => AdvancedConfig.fromJson(json),
AdvancedConfig(),
);
}
// 注册配置类型
void _registerConfig<T extends ConfigModel>(
T Function(Map<String, dynamic>) fromJson,
T defaultValue,
) {
_configFactories[T] = fromJson;
_configModels[T] = defaultValue;
}
static Future<void> init() async {
if (_initialized) return;
try {
// 如果没有预先设置配置目录,则尝试获取可执行文件所在目录
if (!_configDirectory.isNotEmpty) {
_configDirectory = File(Platform.resolvedExecutable).parent.path;
}
print('配置目录: $_configDirectory');
// 初始化Hive
await Hive.initFlutter(_configDirectory);
print('Hive初始化完成');
// 打开配置Box
_configBox = await Hive.openBox('app_config');
print('配置Box打开成功,包含 ${_configBox.length} 个条目');
// 打印所有键值,用于调试
print('配置Box中的所有键: ${_configBox.keys.toList()}');
// 先创建实例并注册所有配置类型
// 由于AppConfig是单例模式,这里不需要显式创建实例
// 加载并验证配置
await _instance._loadAndValidateConfig();
print('配置加载和验证完成');
_initialized = true;
} catch (e) {
print('AppConfig初始化失败: $e');
// 尝试使用备用目录
try {
// 使用应用文档目录作为备用目录
final appDocDir = Directory(path.join(
Directory.current.path,
'config',
));
// 确保目录存在
if (!appDocDir.existsSync()) {
appDocDir.createSync(recursive: true);
}
_configDirectory = appDocDir.path;
print('尝试使用备用目录: $_configDirectory');
await Hive.initFlutter(_configDirectory);
_configBox = await Hive.openBox('app_config');
await _instance._loadAndValidateConfig();
_initialized = true;
print('使用备用目录初始化成功');
} catch (e2) {
print('备用初始化也失败: $e2');
rethrow;
}
}
}
// 加载并验证所有配置
Future<void> _loadAndValidateConfig() async {
print('开始加载配置...');
// 加载所有注册的配置
for (var entry in _configFactories.entries) {
final type = entry.key;
final fromJson = entry.value;
final defaultValue = _configModels[type]!;
final configKey = defaultValue.configKey;
print('加载配置: $configKey (${type.toString()})');
_configModels[type] = _loadConfig(
configKey,
fromJson as ConfigModel Function(Map<String, dynamic>),
defaultValue,
);
}
// 更新服务器列表缓存
_serverConfigs = (getModel<ServerListConfig>()).servers;
print('服务器列表缓存更新完成,共 ${_serverConfigs.length} 个服务器');
// 打印服务器列表,用于调试
for (var server in _serverConfigs) {
print(
'服务器: ${server.name} (${server.url}), 选中: ${server.selected}, 协议: TCP=${server.tcp}, UDP=${server.udp}, WS=${server.ws}, WSS=${server.wss}, QUIC=${server.quic}');
}
}
// 通用配置加载方法
T _loadConfig<T extends ConfigModel>(
String key,
T Function(Map<String, dynamic>) fromJson,
T defaultValue,
) {
final dynamic config = _configBox.get(key);
print('读取配置 $key: ${config != null ? '存在' : '不存在'}');
if (config != null) {
try {
print('配置内容: $config');
final result = fromJson(Map<String, dynamic>.from(config));
print('配置解析成功');
return result;
} catch (e) {
print('配置解析失败: $e');
// 如果解析失败,使用默认值
}
}
// 保存默认值
final defaultMap = defaultValue.toJson();
print('使用默认配置: $defaultMap');
_configBox.put(key, defaultMap);
return defaultValue;
}
// 获取配置
T getModel<T extends ConfigModel>() {
if (!_configModels.containsKey(T)) {
throw Exception('未注册的配置类型: $T');
}
return _configModels[T] as T;
}
// 更新配置
Future<void> updateModel<T extends ConfigModel>(T newConfig) async {
if (!_configModels.containsKey(T)) {
throw Exception('未注册的配置类型: $T');
}
_configModels[T] = newConfig;
await _configBox.put(newConfig.configKey, newConfig.toJson());
}
// 缓存的服务器配置列表
late List<ServerConfig> _serverConfigs;
// 保存服务器列表
Future<void> _saveServerList() async {
try {
// 打印调试信息
print('保存服务器列表: ${_serverConfigs.length} 个服务器');
// 确保配置模型已更新
await updateModel<ServerListConfig>(
ServerListConfig(servers: _serverConfigs));
// 强制刷新 Hive 存储
await _configBox.flush();
print('服务器列表保存完成');
} catch (e) {
print('保存服务器列表时出错: $e');
}
}
// 以下是为了保持原有API的getter和setter
// 主题设置
ThemeConfig get theme => getModel<ThemeConfig>();
ThemeMode get themeMode {
final String? value = _configManager.get<String>('theme.mode');
final String mode = theme.mode.toLowerCase();
return ThemeMode.values.firstWhere(
(mode) => mode.toString() == 'ThemeMode.$value',
(m) => m.toString().split('.').last.toLowerCase() == mode,
orElse: () => ThemeMode.system,
);
}
Future<void> setThemeMode(ThemeMode mode) async {
final modeString = mode.toString().split('.').last.toLowerCase();
_configManager.set('theme.mode', modeString);
await _configManager.save();
await updateModel<ThemeConfig>(ThemeConfig(
mode: modeString,
seedColor: theme.seedColor,
));
}
// 主题色设置
Color get seedColor {
final int? value = _configManager.get<int>('theme.seedColor');
return value != null ? Color(value) : Colors.blue;
}
Color get seedColor => Color(theme.seedColor);
Future<void> setSeedColor(Color color) async {
_configManager.set('theme.seedColor', color.value);
await _configManager.save();
await updateModel<ThemeConfig>(ThemeConfig(
mode: theme.mode,
seedColor: color.value,
));
}
// 服务器列表设置
List<String> get serverList {
final List? value = _configManager.get<List>('server.list');
return value?.cast<String>() ?? ['public.easytier.cn:11010'];
List<Map<String, dynamic>> get serverList {
return _serverConfigs.map((server) => server.toJson()).toList();
}
Future<void> setServerList(List<String> servers) async {
_configManager.set('server.list', servers);
await _configManager.save();
Future<void> setServerList(List<Map<String, dynamic>> servers) async {
try {
print('设置服务器列表: ${servers.length} 个服务器');
_serverConfigs = servers.map((server) {
// 确保所有必要的字段都存在
return ServerConfig(
url: server['url'] ?? '',
name: server['name'] ?? '',
selected: server['selected'] ?? false,
tcp: server['tcp'] ?? true,
udp: server['udp'] ?? true,
ws: server['ws'] ?? false,
wss: server['wss'] ?? false,
quic: server['quic'] ?? false,
);
}).toList();
await _saveServerList();
// 打印保存后的服务器列表,用于调试
print(
'服务器列表已更新: ${_serverConfigs.map((s) => '${s.name}(${s.url})').join(', ')}');
} catch (e) {
print('设置服务器列表时出错: $e');
}
}
// 当前选中的服务器设
String get currentServer {
return _configManager.get<String>('server.current') ??
'public.easytier.cn:11010';
}
Future<void> setCurrentServer(String server) async {
_configManager.set('server.current', server);
await _configManager.save();
}
// 房间名设置
String get roomName {
return _configManager.get<String>('room.name') ?? 'kevin';
}
// 房间配
RoomConfig get room => getModel<RoomConfig>();
String get roomName => room.name;
String get roomPassword => room.password;
Future<void> setRoomName(String name) async {
_configManager.set('room.name', name);
await _configManager.save();
}
// 房间密码设置
String get roomPassword {
return _configManager.get<String>('room.password') ?? 'kevin';
await updateModel<RoomConfig>(RoomConfig(
name: name,
password: room.password,
));
}
Future<void> setRoomPassword(String password) async {
_configManager.set('room.password', password);
await _configManager.save();
await updateModel<RoomConfig>(RoomConfig(
name: room.name,
password: password,
));
}
// 用户名设
String get username {
return _configManager.get<String>('user.name') ?? Platform.localHostname;
}
// 用户
UserConfig get user => getModel<UserConfig>();
String get username => user.name;
Future<void> setUsername(String name) async {
_configManager.set('user.name', name);
await _configManager.save();
await updateModel<UserConfig>(UserConfig(name: name));
}
// 虚拟IP设
String get virtualIP {
return _configManager.get<String>('network.virtualIP') ?? '';
}
// 网络配
NetworkConfig get network => getModel<NetworkConfig>();
String get virtualIP => network.virtualIP;
bool get dynamicIP => network.dynamicIP;
Future<void> setVirtualIP(String ip) async {
_configManager.set('network.virtualIP', ip);
await _configManager.save();
}
// 关闭按钮进入托盘
bool get closeToTray {
return _configManager.get<bool>('system.closeToTray') ?? true;
}
Future<void> setCloseToTray(bool enabled) async {
_configManager.set('system.closeToTray', enabled);
await _configManager.save();
}
// 动态获取IP设置
bool get dynamicIP {
return _configManager.get<bool>('network.dynamicIP') ?? true;
await updateModel<NetworkConfig>(NetworkConfig(
virtualIP: ip,
dynamicIP: network.dynamicIP,
));
}
Future<void> setDynamicIP(bool enabled) async {
_configManager.set('network.dynamicIP', enabled);
await _configManager.save();
await updateModel<NetworkConfig>(NetworkConfig(
virtualIP: network.virtualIP,
dynamicIP: enabled,
));
}
// 系统配置
SystemConfig get system => getModel<SystemConfig>();
bool get closeToTray => system.closeToTray;
bool get enablePing => system.enablePing;
Future<void> setCloseToTray(bool enabled) async {
await updateModel<SystemConfig>(SystemConfig(
closeToTray: enabled,
enablePing: system.enablePing,
));
}
Future<void> setEnablePing(bool enabled) async {
await updateModel<SystemConfig>(SystemConfig(
closeToTray: system.closeToTray,
enablePing: enabled,
));
}
// 通用配置获取方法
T? getConfig<T>(String key) {
return _configBox.get(key) as T?;
}
// 通用配置设置方法
Future<void> setConfig<T>(String key, T value) async {
await _configBox.put(key, value);
// 更新缓存的配置对象
await _loadAndValidateConfig();
}
// 高级配置
AdvancedConfig get advanced => getModel<AdvancedConfig>();
// 高级配置 getter
String get defaultProtocol => advanced.defaultProtocol;
String get devName => advanced.devName;
bool get enableEncryption => advanced.enableEncryption;
bool get enableIpv6 => advanced.enableIpv6;
int get mtu => advanced.mtu;
bool get latencyFirst => advanced.latencyFirst;
bool get enableExitNode => advanced.enableExitNode;
bool get proxyForwardBySystem => advanced.proxyForwardBySystem;
bool get noTun => advanced.noTun;
bool get useSmoltcp => advanced.useSmoltcp;
String get relayNetworkWhitelist => advanced.relayNetworkWhitelist;
bool get disableP2p => advanced.disableP2p;
bool get relayAllPeerRpc => advanced.relayAllPeerRpc;
bool get disableUdpHolePunching => advanced.disableUdpHolePunching;
bool get multiThread => advanced.multiThread;
String get dataCompressAlgo => advanced.dataCompressAlgo;
bool get bindDevice => advanced.bindDevice;
bool get enableKcpProxy => advanced.enableKcpProxy;
bool get disableKcpInput => advanced.disableKcpInput;
bool get disableRelayKcp => advanced.disableRelayKcp;
// 高级配置 setter 方法
Future<void> setDefaultProtocol(String value) async {
await updateModel<AdvancedConfig>(AdvancedConfig(
defaultProtocol: value,
devName: advanced.devName,
enableEncryption: advanced.enableEncryption,
enableIpv6: advanced.enableIpv6,
mtu: advanced.mtu,
latencyFirst: advanced.latencyFirst,
enableExitNode: advanced.enableExitNode,
proxyForwardBySystem: advanced.proxyForwardBySystem,
noTun: advanced.noTun,
useSmoltcp: advanced.useSmoltcp,
relayNetworkWhitelist: advanced.relayNetworkWhitelist,
disableP2p: advanced.disableP2p,
relayAllPeerRpc: advanced.relayAllPeerRpc,
disableUdpHolePunching: advanced.disableUdpHolePunching,
multiThread: advanced.multiThread,
dataCompressAlgo: advanced.dataCompressAlgo,
bindDevice: advanced.bindDevice,
enableKcpProxy: advanced.enableKcpProxy,
disableKcpInput: advanced.disableKcpInput,
disableRelayKcp: advanced.disableRelayKcp,
));
}
Future<void> setDevName(String value) async {
await updateModel<AdvancedConfig>(AdvancedConfig(
defaultProtocol: advanced.defaultProtocol,
devName: value,
enableEncryption: advanced.enableEncryption,
enableIpv6: advanced.enableIpv6,
mtu: advanced.mtu,
latencyFirst: advanced.latencyFirst,
enableExitNode: advanced.enableExitNode,
proxyForwardBySystem: advanced.proxyForwardBySystem,
noTun: advanced.noTun,
useSmoltcp: advanced.useSmoltcp,
relayNetworkWhitelist: advanced.relayNetworkWhitelist,
disableP2p: advanced.disableP2p,
relayAllPeerRpc: advanced.relayAllPeerRpc,
disableUdpHolePunching: advanced.disableUdpHolePunching,
multiThread: advanced.multiThread,
dataCompressAlgo: advanced.dataCompressAlgo,
bindDevice: advanced.bindDevice,
enableKcpProxy: advanced.enableKcpProxy,
disableKcpInput: advanced.disableKcpInput,
disableRelayKcp: advanced.disableRelayKcp,
));
}
// 更新高级配置的通用方法
Future<void> updateAdvancedConfig({
String? defaultProtocol,
String? devName,
bool? enableEncryption,
bool? enableIpv6,
int? mtu,
bool? latencyFirst,
bool? enableExitNode,
bool? proxyForwardBySystem,
bool? noTun,
bool? useSmoltcp,
String? relayNetworkWhitelist,
bool? disableP2p,
bool? relayAllPeerRpc,
bool? disableUdpHolePunching,
bool? multiThread,
String? dataCompressAlgo,
bool? bindDevice,
bool? enableKcpProxy,
bool? disableKcpInput,
bool? disableRelayKcp,
}) async {
await updateModel<AdvancedConfig>(AdvancedConfig(
defaultProtocol: defaultProtocol ?? advanced.defaultProtocol,
devName: devName ?? advanced.devName,
enableEncryption: enableEncryption ?? advanced.enableEncryption,
enableIpv6: enableIpv6 ?? advanced.enableIpv6,
mtu: mtu ?? advanced.mtu,
latencyFirst: latencyFirst ?? advanced.latencyFirst,
enableExitNode: enableExitNode ?? advanced.enableExitNode,
proxyForwardBySystem:
proxyForwardBySystem ?? advanced.proxyForwardBySystem,
noTun: noTun ?? advanced.noTun,
useSmoltcp: useSmoltcp ?? advanced.useSmoltcp,
relayNetworkWhitelist:
relayNetworkWhitelist ?? advanced.relayNetworkWhitelist,
disableP2p: disableP2p ?? advanced.disableP2p,
relayAllPeerRpc: relayAllPeerRpc ?? advanced.relayAllPeerRpc,
disableUdpHolePunching:
disableUdpHolePunching ?? advanced.disableUdpHolePunching,
multiThread: multiThread ?? advanced.multiThread,
dataCompressAlgo: dataCompressAlgo ?? advanced.dataCompressAlgo,
bindDevice: bindDevice ?? advanced.bindDevice,
enableKcpProxy: enableKcpProxy ?? advanced.enableKcpProxy,
disableKcpInput: disableKcpInput ?? advanced.disableKcpInput,
disableRelayKcp: disableRelayKcp ?? advanced.disableRelayKcp,
));
}
}
+39 -3
View File
@@ -25,7 +25,10 @@ class ConfigManager {
final yamlMap = loadYaml(content);
_config = _mergeConfigs(_convertYaml(yamlMap));
} catch (e) {
throw Exception('配置文件解析失败: $e');
// 提供更详细的错误信息并使用默认配置
print('配置文件解析失败: $e');
print('将使用默认配置继续运行。请检查配置文件格式是否正确。');
_config = Map.from(defaultConfig);
}
}
@@ -61,7 +64,15 @@ class ConfigManager {
current = current.putIfAbsent(key, () => <String, dynamic>{});
}
current[keys.last] = value;
// 类型转换确保值类型兼容性
if (value is Map<String, dynamic>) {
current[keys.last] = Map<String, Object>.from(value);
} else if (value is List<Map<String, dynamic>>) {
current[keys.last] =
value.map((e) => Map<String, Object>.from(e)).toList();
} else {
current[keys.last] = value;
}
}
/// 合并用户配置与默认配置
@@ -135,7 +146,13 @@ class ConfigManager {
for (final item in list) {
buffer.write('$prefix- ');
if (item is Map) {
_writeMap(item.cast<String, dynamic>(), buffer, indent + 1);
if (item.isEmpty) {
buffer.writeln('{}');
} else {
buffer.writeln();
// 注意这里使用了正确的缩进级别
_writeMapInList(item.cast<String, dynamic>(), buffer, indent + 1);
}
} else if (item is List) {
_writeList(item, buffer, indent + 1);
} else {
@@ -144,6 +161,25 @@ class ConfigManager {
}
}
/// 专门用于处理列表中的Map项,确保正确缩进
void _writeMapInList(
Map<String, dynamic> map, StringBuffer buffer, int indent) {
final prefix = ' ' * indent;
map.forEach((key, value) {
buffer.write('$prefix$key: ');
if (value is Map<String, dynamic>) {
buffer.writeln();
_writeMap(value, buffer, indent + 1);
} else if (value is List) {
_writeList(value, buffer, indent + 1);
} else {
buffer.writeln(_formatValue(value));
}
});
}
/// 格式值处理
String _formatValue(dynamic value) {
if (value is String) return '"$value"';
+2
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
Future<void> setupWindow() async {
await windowManager.ensureInitialized();
WindowOptions windowOptions = const WindowOptions(
size: Size(850, 520),
minimumSize: Size(300, 300),
@@ -9,6 +10,7 @@ Future<void> setupWindow() async {
backgroundColor: Colors.transparent,
skipTaskbar: false,
titleBarStyle: TitleBarStyle.hidden, // 隐藏标题栏
title: "Astral", // 添加窗口标题
);
await windowManager.waitUntilReadyToShow(windowOptions, () async {
+17 -6
View File
@@ -1,26 +1,37 @@
import 'package:flutter/material.dart';
import 'package:astral/src/rust/frb_generated.dart';
import 'package:window_manager/window_manager.dart';
import 'dart:io' show Platform, Directory;
import 'package:path_provider/path_provider.dart';
import 'app.dart';
import 'config/windowconfiguration.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'config/app_config.dart';
import 'utils/kv_state.dart';
import 'package:provider/provider.dart'; // 添加这一行
import 'package:astral/utils/app_info.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// 初始化应用信息
await AppInfoUtil.init();
await windowManager.ensureInitialized();
// 设置窗口属性
await setupWindow();
// 为Android平台设置配置目录
if (Platform.isAndroid) {
final appDir = await getApplicationDocumentsDirectory();
AppConfig.setConfigDir(appDir.path);
}
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
await windowManager.ensureInitialized();
// 设置窗口属性
await setupWindow();
}
await AppConfig.init();
// 初始化应用信息
await RustLib.init();
runApp(
ChangeNotifierProvider(
create: (context) => KM(),
ProviderScope(
child: const MyApp(),
),
);
+14 -1
View File
@@ -1,3 +1,4 @@
import 'package:astral/utils/up.dart';
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
import '../widgets/window_control_buttons.dart';
@@ -46,6 +47,17 @@ class _MainScreenState extends State<MainScreen>
duration: const Duration(seconds: 3),
vsync: this,
)..repeat();
// 添加异步更新检查(推荐方式)
WidgetsBinding.instance.addPostFrameCallback((_) {
final updateChecker = UpdateChecker(
owner: 'ldoubil',
repo: 'astral',
);
if (mounted) {
updateChecker.scheckForUpdates(context);
}
});
}
@override
@@ -229,7 +241,8 @@ class PageKeepAlive extends StatefulWidget {
State<PageKeepAlive> createState() => _PageKeepAliveState();
}
class _PageKeepAliveState extends State<PageKeepAlive> with AutomaticKeepAliveClientMixin {
class _PageKeepAliveState extends State<PageKeepAlive>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
+118 -277
View File
@@ -2,35 +2,11 @@
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 {
class InfoPage extends StatelessWidget {
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(
@@ -40,156 +16,95 @@ class _InfoPageState extends State<InfoPage>
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,
),
),
// 静态图标
Container(
height: 110,
width: 110,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.primary,
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,
),
),
// 应用名称
Text(
'ASTRAL',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 2.0,
color: Theme.of(context).colorScheme.primary,
),
),
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.getVersion(),
style: Theme.of(context).textTheme.bodyLarge,
),
// 版本号
Text(
AppInfoUtil.getVersion(),
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 20),
// 卡片添加动画和阴影效果
_buildAnimatedCard(
// 静态卡片
_buildCard(
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,
// 静态按钮
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),
),
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群链接')),
);
}
}
},
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),
),
),
// 版权信息
Text(
'© ${DateTime.now().year} ASTRAL Team',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.6),
),
),
],
),
@@ -198,129 +113,55 @@ class _InfoPageState extends State<InfoPage>
);
}
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),
Widget _buildCard(
BuildContext context,
String title,
String content,
IconData icon,
) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: Theme.of(context).colorScheme.primary,
size: 30,
),
],
),
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),
),
),
],
),
),
],
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 8),
Text(
content,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
],
),
),
);
+311 -202
View File
@@ -1,13 +1,17 @@
// 导入必要的包
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:astral/config/app_config.dart';
import 'package:astral/src/rust/api/simple.dart';
import 'package:astral/src/rust/frb_generated.dart';
import 'package:astral/utils/kv_state.dart';
import 'package:astral/utils/app_info.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'dart:async';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; // 替换 provider 导入
import '../widgets/card.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import '../utils/runin.dart';
@@ -31,7 +35,7 @@ String _intToIpv4String(int addr) {
/// 首页组件
/// 用于显示应用的主页面,包含主题切换和问候功能
class HomePage extends StatefulWidget {
class HomePage extends ConsumerStatefulWidget {
// 主题模式切换回调函数
final Function toggleThemeMode;
// 主题色更改回调函数
@@ -48,12 +52,11 @@ class HomePage extends StatefulWidget {
});
@override
State<HomePage> createState() => _HomePageState();
ConsumerState<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// 定义状态枚举
class _HomePageState extends ConsumerState<HomePage> {
int connectionTimeoutCounter = 0;
// 当前连接状态
ConnectionState _connectionState = ConnectionState.notStarted;
@@ -74,7 +77,8 @@ class _HomePageState extends State<HomePage> {
bool _isAutoIP = true; // 只用于控制IP自动/手动模式
String Serverip = "";
List<String> Serverip = [""];
List<ServerConfig> Serveripz = [];
int _uploadBytes = 0;
int _downloadBytes = 0;
int _lastUploadBytes = 0;
@@ -87,42 +91,185 @@ class _HomePageState extends State<HomePage> {
late final TextEditingController _roomPasswordController;
late final TextEditingController _usernameController;
late final TextEditingController _virtualIPController;
// 添加FocusNode来监听焦点变化
late final FocusNode _virtualIPFocusNode;
late final FocusNode _usernameControllerFocusNode;
late final FocusNode _roomNameControllerFocusNode;
late final FocusNode _roomPasswordControllerFocusNode;
@override
void initState() {
super.initState();
// 初始化 TextEditingController
// 初始化所有TextEditingController
_roomNameController = TextEditingController(text: roomName);
_roomPasswordController = TextEditingController(text: roomPassword);
_usernameController = TextEditingController(text: username);
_virtualIPController = TextEditingController(text: publicIP);
_virtualIPController = TextEditingController(text: publicIP); // 添加缺失的初始化
// 修改卡片构建器列表,添加版本信息卡片
// 初始化FocusNode并添加监听器
_virtualIPFocusNode = FocusNode();
_virtualIPFocusNode.addListener(_onVirtualIPFocusChange);
// 初始化用户名、房间名和密码的FocusNode
_usernameControllerFocusNode = FocusNode();
_usernameControllerFocusNode.addListener(_onUsernameFocusChange);
_roomNameControllerFocusNode = FocusNode();
_roomNameControllerFocusNode.addListener(_onRoomNameFocusChange);
_roomPasswordControllerFocusNode = FocusNode();
_roomPasswordControllerFocusNode.addListener(_onRoomPasswordFocusChange);
// 修改卡片构建器列表,添加服务器列表卡片
_cardBuilders = [
_buildNetworkStatusCard, // 网络状态卡片
_buildUserInfoCard, // 用户信息卡片
_buildRoomInfoCard, // 房间信息卡片
_buildVersionInfoCard, // 新增版本信息卡片
_buildNetworkStatusCard,
_buildUserInfoCard,
_buildRoomInfoCard,
_buildServerListCard,
_buildVersionInfoCard,
];
// 启动内存监控
}
// 添加焦点变化监听方法
void _onVirtualIPFocusChange() {
if (!_virtualIPFocusNode.hasFocus && !_isAutoIP) {
// 当失去焦点且不是自动IP模式时更新值
ref
.read(virtualIPProvider.notifier)
.setVirtualIP(_virtualIPController.text);
}
}
// 添加用户名焦点变化监听方法
void _onUsernameFocusChange() {
if (!_usernameControllerFocusNode.hasFocus) {
ref.read(usernameProvider.notifier).setUsername(_usernameController.text);
}
}
// 添加房间名焦点变化监听方法
void _onRoomNameFocusChange() {
if (!_roomNameControllerFocusNode.hasFocus) {
ref.read(roomNameProvider.notifier).setRoomName(_roomNameController.text);
}
}
// 添加房间密码焦点变化监听方法
void _onRoomPasswordFocusChange() {
if (!_roomPasswordControllerFocusNode.hasFocus) {
ref
.read(roomPasswordProvider.notifier)
.setRoomPassword(_roomPasswordController.text);
}
}
@override
void dispose() {
// 取消所有计时器
timer?.cancel();
timer = null;
// 释放所有控制器和焦点节点
_roomNameController.dispose();
_roomPasswordController.dispose();
_usernameController.dispose();
_virtualIPController.dispose();
_virtualIPFocusNode.removeListener(_onVirtualIPFocusChange);
_virtualIPFocusNode.dispose();
_usernameControllerFocusNode.removeListener(_onUsernameFocusChange);
_usernameControllerFocusNode.dispose();
_roomNameControllerFocusNode.removeListener(_onRoomNameFocusChange);
_roomNameControllerFocusNode.dispose();
_roomPasswordControllerFocusNode.removeListener(_onRoomPasswordFocusChange);
_roomPasswordControllerFocusNode.dispose();
super.dispose();
}
void toggleRunning() {
final km = Provider.of<KM>(context, listen: false);
setState(() {
isRunning = !isRunning;
if (isRunning) {
// 切换到连接中状态
_connectionState = ConnectionState.connecting;
//利用 Serveripz 重组
List<String> ssServerip = [];
print(ssServerip);
for (var item in Serveripz) {
if (item.tcp) {
ssServerip.add("tcp://" + item.url);
}
if (item.udp) {
ssServerip.add("udp://" + item.url);
}
if (item.ws) {
ssServerip.add("ws://" + item.url);
}
if (item.wss) {
ssServerip.add("wss://" + item.url);
}
if (item.quic) {
ssServerip.add("quic://" + item.url);
}
}
// 复制
createServer(
username: username,
enableDhcp: _isAutoIP,
specifiedIp: publicIP,
roomName: roomName,
roomPassword: roomPassword,
severurl: Serverip);
// 添加连接超时计数器
int connectionTimeoutCounter = 0;
severurl: ssServerip,
flag: FlagsC(
defaultProtocol:
ref.read(advancedConfigProvider)['defaultProtocol'] ??
"tcp",
devName: ref.read(advancedConfigProvider)['devName'] ?? "",
enableEncryption:
ref.read(advancedConfigProvider)['enableEncryption'] ??
true,
enableIpv6:
ref.read(advancedConfigProvider)['enableIpv6'] ?? true,
mtu: ref.read(advancedConfigProvider)['mtu'] ?? 1380,
multiThread:
ref.read(advancedConfigProvider)['multiThread'] ?? true,
latencyFirst:
ref.read(advancedConfigProvider)['latencyFirst'] ?? false,
enableExitNode:
ref.read(advancedConfigProvider)['enableExitNode'] ?? false,
noTun: ref.read(advancedConfigProvider)['noTun'] ?? false,
useSmoltcp:
ref.read(advancedConfigProvider)['useSmoltcp'] ?? false,
relayNetworkWhitelist:
ref.read(advancedConfigProvider)['relayNetworkWhitelist'] ??
"*",
disableP2P:
ref.read(advancedConfigProvider)['disableP2p'] ?? false,
relayAllPeerRpc:
ref.read(advancedConfigProvider)['relayAllPeerRpc'] ??
false,
disableUdpHolePunching:
ref.read(advancedConfigProvider)['disableUdpHolePunching'] ??
false,
dataCompressAlgo: ref.read(advancedConfigProvider)['dataCompressAlgo'] ==
"Invalid"
? 0
: ref.read(advancedConfigProvider)['dataCompressAlgo'] == "None"
? 1
: ref.read(advancedConfigProvider)['dataCompressAlgo'] == "Zstd"
? 2
: 1,
bindDevice: ref.read(advancedConfigProvider)['bindDevice'] ?? true,
enableKcpProxy: ref.read(advancedConfigProvider)['enableKcpProxy'] ?? false,
disableKcpInput: ref.read(advancedConfigProvider)['disableKcpInput'] ?? false,
disableRelayKcp: ref.read(advancedConfigProvider)['disableRelayKcp'] ?? true,
proxyForwardBySystem: ref.read(advancedConfigProvider)['proxyForwardBySystem'] ?? false));
// 不再使用固定延迟模拟连接成功,而是通过定时检查IP来确定连接状态
timer = Timer.periodic(const Duration(seconds: 1), (timer) async {
@@ -137,8 +284,7 @@ class _HomePageState extends State<HomePage> {
// 获取网络状态
final networkStatus = await getNetworkStatus();
final km = Provider.of<KM>(context, listen: false);
km.nodes = networkStatus.nodes;
ref.read(nodesProvider.notifier).setNodes(networkStatus.nodes);
// 更新网络流量数据
_updateNetworkStats(networkStatus.nodes);
@@ -146,49 +292,40 @@ class _HomePageState extends State<HomePage> {
final int? version = runin.myNodeInfo?.virtualIpv4?.address?.addr;
if (version != null) {
String ipStr = _intToIpv4String(version);
// 检查IP不为0.0.0.0时认为连接成功
if (ipStr != "0.0.0.0") {
if (publicIP != ipStr) {
km.virtualIP = ipStr;
}
if (publicIP != ipStr) {
ref.read(virtualIPProvider.notifier).setVirtualIP(ipStr);
}
// 如果当前状态还是连接中,则更新为已连接
if (_connectionState == ConnectionState.connecting) {
setState(() {
_connectionState = ConnectionState.connected;
});
}
// 重置超时计数器
// 如果当前状态还是连接中,则更新为已连接
if (_connectionState == ConnectionState.connecting) {
setState(() {
_connectionState = ConnectionState.connected;
});
}
connectionTimeoutCounter = 0;
} else if (_connectionState == ConnectionState.connecting) {
connectionTimeoutCounter++;
if (connectionTimeoutCounter >= 10) {
connectionTimeoutCounter = 0;
} else if (_connectionState == ConnectionState.connecting) {
// 只在连接状态下增加超时计数
connectionTimeoutCounter++;
timer.cancel();
setState(() {
isRunning = false;
_connectionState = ConnectionState.notStarted;
runningTime = Duration.zero;
});
// 如果连续10秒都是0.0.0.0,判断为连接失败
if (connectionTimeoutCounter >= 10) {
// 停止连接并显示失败消息
timer.cancel();
setState(() {
isRunning = false;
_connectionState = ConnectionState.notStarted;
runningTime = Duration.zero;
});
// 关闭服务器连接
closeAllServer();
// 显示连接失败提示
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('连接失败,请检查网络或房间信息后重试'),
duration: Duration(seconds: 3),
),
);
}
return;
closeAllServer();
// 清空玩家列表数据
ref.read(nodesProvider.notifier).setNodes([]);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('连接失败,未能获取节点信息'),
duration: Duration(seconds: 3),
),
);
}
return;
}
}
@@ -202,11 +339,12 @@ class _HomePageState extends State<HomePage> {
runningTime += const Duration(seconds: 1);
});
});
// 移除原来的Future.delayed模拟连接成功的代码
} else {
// 停止时重置状态
_connectionState = ConnectionState.notStarted;
closeAllServer();
// 清空玩家列表数据
ref.read(nodesProvider.notifier).setNodes([]);
timer?.cancel();
runningTime = Duration.zero;
// 重置网络统计数据
@@ -226,7 +364,7 @@ class _HomePageState extends State<HomePage> {
int totalUploadBytes = 0;
int totalDownloadBytes = 0;
String myIP = Provider.of<KM>(context, listen: false).virtualIP;
String myIP = ref.read(virtualIPProvider);
// 查找本机节点
for (var node in nodes) {
@@ -273,21 +411,22 @@ class _HomePageState extends State<HomePage> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
final km = Provider.of<KM>(context);
// 使用 Riverpod 读取数据
_roomNameController.value = TextEditingValue(
text: km.roomName,
text: ref.read(roomNameProvider),
selection: _roomNameController.selection,
);
_roomPasswordController.value = TextEditingValue(
text: km.roomPassword,
text: ref.read(roomPasswordProvider),
selection: _roomPasswordController.selection,
);
_usernameController.value = TextEditingValue(
text: km.username,
text: ref.read(usernameProvider),
selection: _usernameController.selection,
);
// 添加虚拟IP控制器的值同步
_virtualIPController.value = TextEditingValue(
text: km.virtualIP,
text: ref.read(virtualIPProvider),
selection: _virtualIPController.selection,
);
}
@@ -295,15 +434,17 @@ class _HomePageState extends State<HomePage> {
@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;
// 使用 Riverpod 读取数据
publicIP = ref.watch(virtualIPProvider);
_isAutoIP = ref.watch(dynamicIPProvider);
roomName = ref.watch(roomNameProvider);
roomPassword = ref.watch(roomPasswordProvider);
username = ref.watch(usernameProvider);
Serverip = ref.watch(serverIPProvider);
// 从 selectedServerProvider 获取服务器配置列表
final serverConfigs =
ref.watch(selectedServerProvider) as List<ServerConfig>;
Serveripz = serverConfigs;
// 使用 LayoutBuilder 来处理布局变化,同时保留状态
return Scaffold(
@@ -325,10 +466,7 @@ class _HomePageState extends State<HomePage> {
childCount: _cardBuilders.length,
itemBuilder: (context, index) {
// 直接从列表中获取构建函数并调用
return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 100),
child: _cardBuilders[index](colorScheme),
);
return _cardBuilders[index](colorScheme);
},
),
),
@@ -478,88 +616,6 @@ class _HomePageState extends State<HomePage> {
}
}
// 修改卡片构建方法,移除多余的内边距
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(
@@ -585,7 +641,64 @@ class _HomePageState extends State<HomePage> {
);
}
// 添加服务器列表卡片
Widget _buildServerListCard(ColorScheme colorScheme) {
return Consumer(
builder: (context, ref, child) {
// 直接使用 serverIP 列表
final serverUrls = ref.watch(serverIPProvider);
return FloatingCard(
colorScheme: colorScheme,
maxWidth: 600,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题栏
Row(
children: [
Icon(Icons.dns, color: colorScheme.primary, size: 22),
const SizedBox(width: 8),
const Text('当前服务器',
style:
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 16),
// 当前选中的服务器
Wrap(
spacing: 8,
runSpacing: 8,
children: serverUrls.isEmpty ||
(serverUrls.length == 1 && serverUrls[0].isEmpty)
? [
Chip(
avatar: Icon(Icons.info_outline,
size: 16, color: colorScheme.error),
label: const Text('未选择服务器'),
backgroundColor:
colorScheme.errorContainer.withOpacity(0.3),
)
]
: serverUrls
.map((url) => Chip(
avatar: Icon(Icons.dns,
size: 16, color: colorScheme.primary),
label: Text(url),
backgroundColor: colorScheme.surfaceVariant,
))
.toList(),
),
],
),
);
},
);
}
// 新增合并后的网络状态卡片(合并了流量统计和IP信息)
// 修改网络状态卡片,去除流量统计部分
Widget _buildNetworkStatusCard(ColorScheme colorScheme) {
return FloatingCard(
colorScheme: colorScheme,
@@ -623,33 +736,6 @@ class _HomePageState extends State<HomePage> {
// 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) ...[
@@ -701,8 +787,7 @@ class _HomePageState extends State<HomePage> {
// 优化用户信息卡片
Widget _buildUserInfoCard(ColorScheme colorScheme) {
final km = Provider.of<KM>(context, listen: false);
final isValidIP = _isAutoIP || _isValidIPv4(km.virtualIP);
final isValidIP = _isAutoIP || _isValidIPv4(ref.watch(virtualIPProvider));
return FloatingCard(
colorScheme: colorScheme,
@@ -743,9 +828,13 @@ class _HomePageState extends State<HomePage> {
// 用户名输入框
TextField(
controller: _usernameController,
focusNode: _usernameControllerFocusNode, // 添加焦点节点
enabled: _connectionState != ConnectionState.connected,
onChanged: (value) {
km.username = value;
onEditingComplete: () {
// 改为完成编辑时更新
ref
.read(usernameProvider.notifier)
.setUsername(_usernameController.text);
},
decoration: InputDecoration(
labelText: '用户名',
@@ -762,12 +851,18 @@ class _HomePageState extends State<HomePage> {
Expanded(
child: TextField(
controller: _virtualIPController,
focusNode: _virtualIPFocusNode, // 添加焦点节点
enabled: !_isAutoIP &&
_connectionState != ConnectionState.connected,
onChanged: (value) {
// 保留空回调以避免实时更新
},
onEditingComplete: () {
// 添加完成编辑回调
if (!_isAutoIP) {
setState(() {});
km.virtualIP = value;
ref
.read(virtualIPProvider.notifier)
.setVirtualIP(_virtualIPController.text);
}
},
decoration: InputDecoration(
@@ -789,7 +884,15 @@ class _HomePageState extends State<HomePage> {
setState(() {
_isAutoIP = value;
});
km.dynamicIP = value;
ref
.read(dynamicIPProvider.notifier)
.setDynamicIP(value);
// 切换模式时同步最新值
if (!value) {
ref
.read(virtualIPProvider.notifier)
.setVirtualIP(_virtualIPController.text);
}
}
: null,
),
@@ -819,7 +922,6 @@ class _HomePageState extends State<HomePage> {
// 优化房间信息卡片
Widget _buildRoomInfoCard(ColorScheme colorScheme) {
final km = Provider.of<KM>(context, listen: false);
return FloatingCard(
colorScheme: colorScheme,
maxWidth: 600,
@@ -859,9 +961,13 @@ class _HomePageState extends State<HomePage> {
// 房间名称输入框
TextField(
controller: _roomNameController,
focusNode: _roomNameControllerFocusNode, // 添加焦点节点
enabled: _connectionState != ConnectionState.connected,
onChanged: (value) {
km.roomName = value;
onEditingComplete: () {
// 改为完成编辑时更新
ref
.read(roomNameProvider.notifier)
.setRoomName(_roomNameController.text);
},
decoration: InputDecoration(
labelText: '房间名称',
@@ -875,9 +981,12 @@ class _HomePageState extends State<HomePage> {
// 房间密码输入框
TextField(
controller: _roomPasswordController,
focusNode: _roomPasswordControllerFocusNode, // 添加焦点节点
enabled: _connectionState != ConnectionState.connected,
onChanged: (value) {
km.roomPassword = value;
onEditingComplete: () {
ref
.read(roomPasswordProvider.notifier)
.setRoomPassword(_roomPasswordController.text);
},
obscureText: true,
decoration: InputDecoration(
+18 -21
View File
@@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; // 添加这一行导入剪贴板服务
import 'package:astral/utils/kv_state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; // 替换 provider 导入
import '../widgets/card.dart';
/// 玩家信息模型类
@@ -37,14 +37,17 @@ class PlayerInfo {
/// 房间页面组件
/// 用于显示所有玩家的信息
class RoomPage extends StatefulWidget {
class RoomPage extends ConsumerStatefulWidget {
// 修改为 ConsumerStatefulWidget
const RoomPage({super.key});
@override
State<RoomPage> createState() => _RoomPageState();
ConsumerState<RoomPage> createState() =>
_RoomPageState(); // 修改为 ConsumerState
}
class _RoomPageState extends State<RoomPage> {
class _RoomPageState extends ConsumerState<RoomPage> {
// 修改为 ConsumerState
List<PlayerInfo> players = [];
List<PlayerInfo> filteredPlayers = []; // 添加过滤后的玩家列表
bool isLoading = true;
@@ -88,6 +91,7 @@ class _RoomPageState extends State<RoomPage> {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
// 使用 Riverpod 监听节点数据
return Scaffold(
appBar: AppBar(
@@ -109,11 +113,10 @@ class _RoomPageState extends State<RoomPage> {
),
],
),
body: Consumer<KM>(
builder: (context, km, child) {
// 当 KM 更新时,这个 builder 会被重新调用
body: Builder(
builder: (context) {
// 异步处理数据
_processNodeData(km);
_processNodeData();
if (isLoading) {
return Center(
@@ -203,9 +206,9 @@ class _RoomPageState extends State<RoomPage> {
}
// 处理节点数据 - 合并了原来的两个相似方法
Future<void> _processNodeData(KM km) async {
Future<void> _processNodeData() async {
try {
final nodes = await km.nodes; // 获取最新的节点信息
final nodes = await ref.read(nodesProvider); // 获取最新的节点信息
// 将节点数据转换为PlayerInfo对象
List<PlayerInfo> nodePlayerInfos = [];
@@ -216,8 +219,8 @@ class _RoomPageState extends State<RoomPage> {
int downloadSpeed = 0;
int sentPackets = 0;
int receivedPackets = 0;
String connectionType =
_mapConnectionType(node.cost, node.ipv4, km.virtualIP);
String connectionType = _mapConnectionType(
node.cost, node.ipv4, ref.read(virtualIPProvider));
// 如果有连接信息,计算网络统计数据
if (node.connections.isNotEmpty) {
@@ -230,13 +233,7 @@ class _RoomPageState extends State<RoomPage> {
}
// 计算丢包率 (简单估算)
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));
}
double packetLossRate = node.lossRate;
// 获取NAT类型
String natType = _mapNatType(node.nat);
@@ -388,7 +385,7 @@ class _RoomPageState extends State<RoomPage> {
_buildInfoRow(
Icons.error_outline,
'丢包率',
'${player.packetLossRate}%',
'${player.packetLossRate.toStringAsFixed(2)}%', // 修改这里,保留2位小数
colorScheme,
valueColor: _getPacketLossColor(player.packetLossRate),
),
@@ -557,7 +554,7 @@ class _RoomPageState extends State<RoomPage> {
_buildInfoRow(
Icons.error_outline,
'丢包率',
'${player.packetLossRate}%',
'${player.packetLossRate.toStringAsFixed(2)}%', // 修改这里,保留2位小数
colorScheme,
valueColor: _getPacketLossColor(player.packetLossRate),
),
+1490 -238
View File
File diff suppressed because it is too large Load Diff
+105 -2
View File
@@ -37,14 +37,16 @@ Future<void> createServer(
required String specifiedIp,
required String roomName,
required String roomPassword,
required String severurl}) =>
required List<String> severurl,
required FlagsC flag}) =>
RustLib.instance.api.crateApiSimpleCreateServer(
username: username,
enableDhcp: enableDhcp,
specifiedIp: specifiedIp,
roomName: roomName,
roomPassword: roomPassword,
severurl: severurl);
severurl: severurl,
flag: flag);
Future<void> closeAllServer() =>
RustLib.instance.api.crateApiSimpleCloseAllServer();
@@ -63,6 +65,103 @@ abstract class PeerRoutePair implements RustOpaqueInterface {}
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<Route>>
abstract class Route implements RustOpaqueInterface {}
class FlagsC {
final String defaultProtocol;
final String devName;
final bool enableEncryption;
final bool enableIpv6;
final int mtu;
final bool latencyFirst;
final bool enableExitNode;
final bool noTun;
final bool useSmoltcp;
final String relayNetworkWhitelist;
final bool disableP2P;
final bool relayAllPeerRpc;
final bool disableUdpHolePunching;
/// string ipv6_listener = 14; \[deprecated = true\]; use -l udp://\[::\]:12345 instead
final bool multiThread;
final int dataCompressAlgo;
final bool bindDevice;
final bool enableKcpProxy;
final bool disableKcpInput;
final bool disableRelayKcp;
final bool proxyForwardBySystem;
const FlagsC({
required this.defaultProtocol,
required this.devName,
required this.enableEncryption,
required this.enableIpv6,
required this.mtu,
required this.latencyFirst,
required this.enableExitNode,
required this.noTun,
required this.useSmoltcp,
required this.relayNetworkWhitelist,
required this.disableP2P,
required this.relayAllPeerRpc,
required this.disableUdpHolePunching,
required this.multiThread,
required this.dataCompressAlgo,
required this.bindDevice,
required this.enableKcpProxy,
required this.disableKcpInput,
required this.disableRelayKcp,
required this.proxyForwardBySystem,
});
@override
int get hashCode =>
defaultProtocol.hashCode ^
devName.hashCode ^
enableEncryption.hashCode ^
enableIpv6.hashCode ^
mtu.hashCode ^
latencyFirst.hashCode ^
enableExitNode.hashCode ^
noTun.hashCode ^
useSmoltcp.hashCode ^
relayNetworkWhitelist.hashCode ^
disableP2P.hashCode ^
relayAllPeerRpc.hashCode ^
disableUdpHolePunching.hashCode ^
multiThread.hashCode ^
dataCompressAlgo.hashCode ^
bindDevice.hashCode ^
enableKcpProxy.hashCode ^
disableKcpInput.hashCode ^
disableRelayKcp.hashCode ^
proxyForwardBySystem.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is FlagsC &&
runtimeType == other.runtimeType &&
defaultProtocol == other.defaultProtocol &&
devName == other.devName &&
enableEncryption == other.enableEncryption &&
enableIpv6 == other.enableIpv6 &&
mtu == other.mtu &&
latencyFirst == other.latencyFirst &&
enableExitNode == other.enableExitNode &&
noTun == other.noTun &&
useSmoltcp == other.useSmoltcp &&
relayNetworkWhitelist == other.relayNetworkWhitelist &&
disableP2P == other.disableP2P &&
relayAllPeerRpc == other.relayAllPeerRpc &&
disableUdpHolePunching == other.disableUdpHolePunching &&
multiThread == other.multiThread &&
dataCompressAlgo == other.dataCompressAlgo &&
bindDevice == other.bindDevice &&
enableKcpProxy == other.enableKcpProxy &&
disableKcpInput == other.disableKcpInput &&
disableRelayKcp == other.disableRelayKcp &&
proxyForwardBySystem == other.proxyForwardBySystem;
}
class KVNetworkStatus {
final BigInt totalNodes;
final List<KVNodeInfo> nodes;
@@ -124,6 +223,7 @@ class KVNodeInfo {
final String ipv4;
final double latencyMs;
final String nat;
final double lossRate;
final List<KVNodeConnectionStats> connections;
final String version;
final int cost;
@@ -133,6 +233,7 @@ class KVNodeInfo {
required this.ipv4,
required this.latencyMs,
required this.nat,
required this.lossRate,
required this.connections,
required this.version,
required this.cost,
@@ -144,6 +245,7 @@ class KVNodeInfo {
ipv4.hashCode ^
latencyMs.hashCode ^
nat.hashCode ^
lossRate.hashCode ^
connections.hashCode ^
version.hashCode ^
cost.hashCode;
@@ -157,6 +259,7 @@ class KVNodeInfo {
ipv4 == other.ipv4 &&
latencyMs == other.latencyMs &&
nat == other.nat &&
lossRate == other.lossRate &&
connections == other.connections &&
version == other.version &&
cost == other.cost;
+201 -10
View File
@@ -85,7 +85,8 @@ abstract class RustLibApi extends BaseApi {
required String specifiedIp,
required String roomName,
required String roomPassword,
required String severurl});
required List<String> severurl,
required FlagsC flag});
Future<String> crateApiSimpleEasytierVersion();
@@ -174,7 +175,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
required String specifiedIp,
required String roomName,
required String roomPassword,
required String severurl}) {
required List<String> severurl,
required FlagsC flag}) {
return handler.executeNormal(NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
@@ -183,7 +185,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_String(specifiedIp, serializer);
sse_encode_String(roomName, serializer);
sse_encode_String(roomPassword, serializer);
sse_encode_String(severurl, serializer);
sse_encode_list_String(severurl, serializer);
sse_encode_box_autoadd_flags_c(flag, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer,
funcId: 2, port: port_);
},
@@ -198,7 +201,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
specifiedIp,
roomName,
roomPassword,
severurl
severurl,
flag
],
apiImpl: this,
));
@@ -212,7 +216,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
"specifiedIp",
"roomName",
"roomPassword",
"severurl"
"severurl",
"flag"
],
);
@@ -515,12 +520,54 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return raw as bool;
}
@protected
FlagsC dco_decode_box_autoadd_flags_c(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return dco_decode_flags_c(raw);
}
@protected
double dco_decode_f_32(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return raw as double;
}
@protected
double dco_decode_f_64(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return raw as double;
}
@protected
FlagsC dco_decode_flags_c(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>;
if (arr.length != 20)
throw Exception('unexpected arr length: expect 20 but see ${arr.length}');
return FlagsC(
defaultProtocol: dco_decode_String(arr[0]),
devName: dco_decode_String(arr[1]),
enableEncryption: dco_decode_bool(arr[2]),
enableIpv6: dco_decode_bool(arr[3]),
mtu: dco_decode_u_32(arr[4]),
latencyFirst: dco_decode_bool(arr[5]),
enableExitNode: dco_decode_bool(arr[6]),
noTun: dco_decode_bool(arr[7]),
useSmoltcp: dco_decode_bool(arr[8]),
relayNetworkWhitelist: dco_decode_String(arr[9]),
disableP2P: dco_decode_bool(arr[10]),
relayAllPeerRpc: dco_decode_bool(arr[11]),
disableUdpHolePunching: dco_decode_bool(arr[12]),
multiThread: dco_decode_bool(arr[13]),
dataCompressAlgo: dco_decode_i_32(arr[14]),
bindDevice: dco_decode_bool(arr[15]),
enableKcpProxy: dco_decode_bool(arr[16]),
disableKcpInput: dco_decode_bool(arr[17]),
disableRelayKcp: dco_decode_bool(arr[18]),
proxyForwardBySystem: dco_decode_bool(arr[19]),
);
}
@protected
int dco_decode_i_32(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -558,16 +605,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
KVNodeInfo dco_decode_kv_node_info(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>;
if (arr.length != 7)
throw Exception('unexpected arr length: expect 7 but see ${arr.length}');
if (arr.length != 8)
throw Exception('unexpected arr length: expect 8 but see ${arr.length}');
return KVNodeInfo(
hostname: dco_decode_String(arr[0]),
ipv4: dco_decode_String(arr[1]),
latencyMs: dco_decode_f_64(arr[2]),
nat: dco_decode_String(arr[3]),
connections: dco_decode_list_kv_node_connection_stats(arr[4]),
version: dco_decode_String(arr[5]),
cost: dco_decode_i_32(arr[6]),
lossRate: dco_decode_f_32(arr[4]),
connections: dco_decode_list_kv_node_connection_stats(arr[5]),
version: dco_decode_String(arr[6]),
cost: dco_decode_i_32(arr[7]),
);
}
@@ -604,6 +652,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
.toList();
}
@protected
List<String> dco_decode_list_String(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return (raw as List<dynamic>).map(dco_decode_String).toList();
}
@protected
List<KVNodeConnectionStats> dco_decode_list_kv_node_connection_stats(
dynamic raw) {
@@ -644,6 +698,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
);
}
@protected
int dco_decode_u_32(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return raw as int;
}
@protected
BigInt dco_decode_u_64(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -753,12 +813,70 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return deserializer.buffer.getUint8() != 0;
}
@protected
FlagsC sse_decode_box_autoadd_flags_c(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
return (sse_decode_flags_c(deserializer));
}
@protected
double sse_decode_f_32(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
return deserializer.buffer.getFloat32();
}
@protected
double sse_decode_f_64(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
return deserializer.buffer.getFloat64();
}
@protected
FlagsC sse_decode_flags_c(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var var_defaultProtocol = sse_decode_String(deserializer);
var var_devName = sse_decode_String(deserializer);
var var_enableEncryption = sse_decode_bool(deserializer);
var var_enableIpv6 = sse_decode_bool(deserializer);
var var_mtu = sse_decode_u_32(deserializer);
var var_latencyFirst = sse_decode_bool(deserializer);
var var_enableExitNode = sse_decode_bool(deserializer);
var var_noTun = sse_decode_bool(deserializer);
var var_useSmoltcp = sse_decode_bool(deserializer);
var var_relayNetworkWhitelist = sse_decode_String(deserializer);
var var_disableP2P = sse_decode_bool(deserializer);
var var_relayAllPeerRpc = sse_decode_bool(deserializer);
var var_disableUdpHolePunching = sse_decode_bool(deserializer);
var var_multiThread = sse_decode_bool(deserializer);
var var_dataCompressAlgo = sse_decode_i_32(deserializer);
var var_bindDevice = sse_decode_bool(deserializer);
var var_enableKcpProxy = sse_decode_bool(deserializer);
var var_disableKcpInput = sse_decode_bool(deserializer);
var var_disableRelayKcp = sse_decode_bool(deserializer);
var var_proxyForwardBySystem = sse_decode_bool(deserializer);
return FlagsC(
defaultProtocol: var_defaultProtocol,
devName: var_devName,
enableEncryption: var_enableEncryption,
enableIpv6: var_enableIpv6,
mtu: var_mtu,
latencyFirst: var_latencyFirst,
enableExitNode: var_enableExitNode,
noTun: var_noTun,
useSmoltcp: var_useSmoltcp,
relayNetworkWhitelist: var_relayNetworkWhitelist,
disableP2P: var_disableP2P,
relayAllPeerRpc: var_relayAllPeerRpc,
disableUdpHolePunching: var_disableUdpHolePunching,
multiThread: var_multiThread,
dataCompressAlgo: var_dataCompressAlgo,
bindDevice: var_bindDevice,
enableKcpProxy: var_enableKcpProxy,
disableKcpInput: var_disableKcpInput,
disableRelayKcp: var_disableRelayKcp,
proxyForwardBySystem: var_proxyForwardBySystem);
}
@protected
int sse_decode_i_32(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -797,6 +915,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
var var_ipv4 = sse_decode_String(deserializer);
var var_latencyMs = sse_decode_f_64(deserializer);
var var_nat = sse_decode_String(deserializer);
var var_lossRate = sse_decode_f_32(deserializer);
var var_connections =
sse_decode_list_kv_node_connection_stats(deserializer);
var var_version = sse_decode_String(deserializer);
@@ -806,6 +925,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
ipv4: var_ipv4,
latencyMs: var_latencyMs,
nat: var_nat,
lossRate: var_lossRate,
connections: var_connections,
version: var_version,
cost: var_cost);
@@ -859,6 +979,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return ans_;
}
@protected
List<String> sse_decode_list_String(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var len_ = sse_decode_i_32(deserializer);
var ans_ = <String>[];
for (var idx_ = 0; idx_ < len_; ++idx_) {
ans_.add(sse_decode_String(deserializer));
}
return ans_;
}
@protected
List<KVNodeConnectionStats> sse_decode_list_kv_node_connection_stats(
SseDeserializer deserializer) {
@@ -907,6 +1039,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return (var_field0, var_field1);
}
@protected
int sse_decode_u_32(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
return deserializer.buffer.getUint32();
}
@protected
BigInt sse_decode_u_64(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -1016,12 +1154,49 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
serializer.buffer.putUint8(self ? 1 : 0);
}
@protected
void sse_encode_box_autoadd_flags_c(FlagsC self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_flags_c(self, serializer);
}
@protected
void sse_encode_f_32(double self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
serializer.buffer.putFloat32(self);
}
@protected
void sse_encode_f_64(double self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
serializer.buffer.putFloat64(self);
}
@protected
void sse_encode_flags_c(FlagsC self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_String(self.defaultProtocol, serializer);
sse_encode_String(self.devName, serializer);
sse_encode_bool(self.enableEncryption, serializer);
sse_encode_bool(self.enableIpv6, serializer);
sse_encode_u_32(self.mtu, serializer);
sse_encode_bool(self.latencyFirst, serializer);
sse_encode_bool(self.enableExitNode, serializer);
sse_encode_bool(self.noTun, serializer);
sse_encode_bool(self.useSmoltcp, serializer);
sse_encode_String(self.relayNetworkWhitelist, serializer);
sse_encode_bool(self.disableP2P, serializer);
sse_encode_bool(self.relayAllPeerRpc, serializer);
sse_encode_bool(self.disableUdpHolePunching, serializer);
sse_encode_bool(self.multiThread, serializer);
sse_encode_i_32(self.dataCompressAlgo, serializer);
sse_encode_bool(self.bindDevice, serializer);
sse_encode_bool(self.enableKcpProxy, serializer);
sse_encode_bool(self.disableKcpInput, serializer);
sse_encode_bool(self.disableRelayKcp, serializer);
sse_encode_bool(self.proxyForwardBySystem, serializer);
}
@protected
void sse_encode_i_32(int self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -1054,6 +1229,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_String(self.ipv4, serializer);
sse_encode_f_64(self.latencyMs, serializer);
sse_encode_String(self.nat, serializer);
sse_encode_f_32(self.lossRate, serializer);
sse_encode_list_kv_node_connection_stats(self.connections, serializer);
sse_encode_String(self.version, serializer);
sse_encode_i_32(self.cost, serializer);
@@ -1095,6 +1271,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
}
}
@protected
void sse_encode_list_String(List<String> self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_i_32(self.length, serializer);
for (final item in self) {
sse_encode_String(item, serializer);
}
}
@protected
void sse_encode_list_kv_node_connection_stats(
List<KVNodeConnectionStats> self, SseSerializer serializer) {
@@ -1134,6 +1319,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
self.$2, serializer);
}
@protected
void sse_encode_u_32(int self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
serializer.buffer.putUint32(self);
}
@protected
void sse_encode_u_64(BigInt self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
+45
View File
@@ -77,9 +77,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
bool dco_decode_bool(dynamic raw);
@protected
FlagsC dco_decode_box_autoadd_flags_c(dynamic raw);
@protected
double dco_decode_f_32(dynamic raw);
@protected
double dco_decode_f_64(dynamic raw);
@protected
FlagsC dco_decode_flags_c(dynamic raw);
@protected
int dco_decode_i_32(dynamic raw);
@@ -107,6 +116,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
dynamic raw);
@protected
List<String> dco_decode_list_String(dynamic raw);
@protected
List<KVNodeConnectionStats> dco_decode_list_kv_node_connection_stats(
dynamic raw);
@@ -124,6 +136,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
) 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
int dco_decode_u_32(dynamic raw);
@protected
BigInt dco_decode_u_64(dynamic raw);
@@ -182,9 +197,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
bool sse_decode_bool(SseDeserializer deserializer);
@protected
FlagsC sse_decode_box_autoadd_flags_c(SseDeserializer deserializer);
@protected
double sse_decode_f_32(SseDeserializer deserializer);
@protected
double sse_decode_f_64(SseDeserializer deserializer);
@protected
FlagsC sse_decode_flags_c(SseDeserializer deserializer);
@protected
int sse_decode_i_32(SseDeserializer deserializer);
@@ -213,6 +237,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
SseDeserializer deserializer);
@protected
List<String> sse_decode_list_String(SseDeserializer deserializer);
@protected
List<KVNodeConnectionStats> sse_decode_list_kv_node_connection_stats(
SseDeserializer deserializer);
@@ -230,6 +257,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
) 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
int sse_decode_u_32(SseDeserializer deserializer);
@protected
BigInt sse_decode_u_64(SseDeserializer deserializer);
@@ -288,9 +318,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_bool(bool self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_flags_c(FlagsC self, SseSerializer serializer);
@protected
void sse_encode_f_32(double self, SseSerializer serializer);
@protected
void sse_encode_f_64(double self, SseSerializer serializer);
@protected
void sse_encode_flags_c(FlagsC self, SseSerializer serializer);
@protected
void sse_encode_i_32(int self, SseSerializer serializer);
@@ -320,6 +359,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
List<Route> self, SseSerializer serializer);
@protected
void sse_encode_list_String(List<String> self, SseSerializer serializer);
@protected
void sse_encode_list_kv_node_connection_stats(
List<KVNodeConnectionStats> self, SseSerializer serializer);
@@ -337,6 +379,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
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_32(int self, SseSerializer serializer);
@protected
void sse_encode_u_64(BigInt self, SseSerializer serializer);
+45
View File
@@ -79,9 +79,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
bool dco_decode_bool(dynamic raw);
@protected
FlagsC dco_decode_box_autoadd_flags_c(dynamic raw);
@protected
double dco_decode_f_32(dynamic raw);
@protected
double dco_decode_f_64(dynamic raw);
@protected
FlagsC dco_decode_flags_c(dynamic raw);
@protected
int dco_decode_i_32(dynamic raw);
@@ -109,6 +118,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
dynamic raw);
@protected
List<String> dco_decode_list_String(dynamic raw);
@protected
List<KVNodeConnectionStats> dco_decode_list_kv_node_connection_stats(
dynamic raw);
@@ -126,6 +138,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
) 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
int dco_decode_u_32(dynamic raw);
@protected
BigInt dco_decode_u_64(dynamic raw);
@@ -184,9 +199,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
bool sse_decode_bool(SseDeserializer deserializer);
@protected
FlagsC sse_decode_box_autoadd_flags_c(SseDeserializer deserializer);
@protected
double sse_decode_f_32(SseDeserializer deserializer);
@protected
double sse_decode_f_64(SseDeserializer deserializer);
@protected
FlagsC sse_decode_flags_c(SseDeserializer deserializer);
@protected
int sse_decode_i_32(SseDeserializer deserializer);
@@ -215,6 +239,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
SseDeserializer deserializer);
@protected
List<String> sse_decode_list_String(SseDeserializer deserializer);
@protected
List<KVNodeConnectionStats> sse_decode_list_kv_node_connection_stats(
SseDeserializer deserializer);
@@ -232,6 +259,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
) 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
int sse_decode_u_32(SseDeserializer deserializer);
@protected
BigInt sse_decode_u_64(SseDeserializer deserializer);
@@ -290,9 +320,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_bool(bool self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_flags_c(FlagsC self, SseSerializer serializer);
@protected
void sse_encode_f_32(double self, SseSerializer serializer);
@protected
void sse_encode_f_64(double self, SseSerializer serializer);
@protected
void sse_encode_flags_c(FlagsC self, SseSerializer serializer);
@protected
void sse_encode_i_32(int self, SseSerializer serializer);
@@ -322,6 +361,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
List<Route> self, SseSerializer serializer);
@protected
void sse_encode_list_String(List<String> self, SseSerializer serializer);
@protected
void sse_encode_list_kv_node_connection_stats(
List<KVNodeConnectionStats> self, SseSerializer serializer);
@@ -339,6 +381,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
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_32(int self, SseSerializer serializer);
@protected
void sse_encode_u_64(BigInt self, SseSerializer serializer);
+427 -55
View File
@@ -1,64 +1,436 @@
import 'package:astral/src/rust/api/simple.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../config/app_config.dart';
class KM extends ChangeNotifier {
final _config = AppConfig();
int _count = 0;
int get count => _count;
// 全局配置实例
final appConfigProvider = Provider<AppConfig>((ref) => AppConfig());
void increment() {
_count++;
notifyListeners(); // 通知监听器重建UI
}
// 计数器相关
class CountNotifier extends StateNotifier<int> {
CountNotifier() : super(0);
// 房间名
String get roomName => _config.roomName;
set roomName(String value) {
void increment() => state++;
}
final countProvider =
StateNotifierProvider<CountNotifier, int>((ref) => CountNotifier());
// 房间名相关
class RoomNameNotifier extends StateNotifier<String> {
final AppConfig _config;
RoomNameNotifier(this._config) : super(_config.roomName);
void setRoomName(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();
state = value;
}
}
final roomNameProvider = StateNotifierProvider<RoomNameNotifier, String>((ref) {
return RoomNameNotifier(ref.watch(appConfigProvider));
});
// 房间密码相关
class RoomPasswordNotifier extends StateNotifier<String> {
final AppConfig _config;
RoomPasswordNotifier(this._config) : super(_config.roomPassword);
void setRoomPassword(String value) {
_config.setRoomPassword(value);
state = value;
}
}
final roomPasswordProvider =
StateNotifierProvider<RoomPasswordNotifier, String>((ref) {
return RoomPasswordNotifier(ref.watch(appConfigProvider));
});
// 用户名相关
class UsernameNotifier extends StateNotifier<String> {
final AppConfig _config;
UsernameNotifier(this._config) : super(_config.username);
void setUsername(String value) {
_config.setUsername(value);
state = value;
}
}
final usernameProvider = StateNotifierProvider<UsernameNotifier, String>((ref) {
return UsernameNotifier(ref.watch(appConfigProvider));
});
// 虚拟IP相关
class VirtualIPNotifier extends StateNotifier<String> {
final AppConfig _config;
VirtualIPNotifier(this._config) : super(_config.virtualIP);
void setVirtualIP(String value) {
_config.setVirtualIP(value);
state = value;
}
}
final virtualIPProvider =
StateNotifierProvider<VirtualIPNotifier, String>((ref) {
return VirtualIPNotifier(ref.watch(appConfigProvider));
});
// 动态获取IP设置相关
class DynamicIPNotifier extends StateNotifier<bool> {
final AppConfig _config;
DynamicIPNotifier(this._config) : super(_config.dynamicIP);
void setDynamicIP(bool value) {
_config.setDynamicIP(value);
state = value;
}
}
final dynamicIPProvider = StateNotifierProvider<DynamicIPNotifier, bool>((ref) {
return DynamicIPNotifier(ref.watch(appConfigProvider));
});
// 服务器列表相关
class ServerListNotifier extends StateNotifier<List<Map<String, dynamic>>> {
final AppConfig _config;
ServerListNotifier(this._config) : super(_config.serverList);
void setServerList(List<Map<String, dynamic>> value) {
List<Map<String, dynamic>> convertedList = value.map((item) {
return Map<String, dynamic>.from(item);
}).toList();
_config.setServerList(convertedList);
state = convertedList;
}
void setServerSelected(String url, bool selected) {
final servers = [...state];
for (var i = 0; i < servers.length; i++) {
if (servers[i]['url'] == url) {
servers[i]['selected'] = selected;
}
}
setServerList(servers);
}
}
final serverListProvider =
StateNotifierProvider<ServerListNotifier, List<Map<String, dynamic>>>(
(ref) {
return ServerListNotifier(ref.watch(appConfigProvider));
});
// 选中的服务器IP
final serverIPProvider = Provider<List<String>>((ref) {
final serverList = ref.watch(serverListProvider);
try {
final selected =
serverList.where((server) => server['selected'] == true).toList();
if (selected.isEmpty && serverList.isNotEmpty) {
final firstServer = serverList.first;
if (firstServer['url'] is String) {
return [firstServer['url'] as String];
}
return [];
}
return selected
.where((server) => server['url'] is String)
.map((server) => server['url'] as String)
.toList();
} catch (e) {
debugPrint('获取服务器IP时出错: $e');
return [];
}
});
//返回选中的服务器
final selectedServerProvider = Provider<List<ServerConfig>>((ref) {
final serverList = ref.watch(serverListProvider);
final selected =
serverList.where((server) => server['selected'] == true).toList();
if (selected.isEmpty) {
return [];
}
// 将选中的服务器转换为ServerConfig对象列表
return selected
.map((server) => ServerConfig(
url: server['url'] ?? '',
name: server['name'] ?? '',
selected: true,
tcp: server['tcp'] ?? true,
udp: server['udp'] ?? true,
ws: server['ws'] ?? false,
wss: server['wss'] ?? false,
quic: server['quic'] ?? false,
))
.toList();
});
// 节点列表相关
class NodesNotifier extends StateNotifier<List<KVNodeInfo>> {
NodesNotifier() : super([]);
void setNodes(List<KVNodeInfo> value) {
state = value;
}
}
final nodesProvider = StateNotifierProvider<NodesNotifier, List<KVNodeInfo>>(
(ref) => NodesNotifier());
// 高级配置相关
class AdvancedConfigNotifier extends StateNotifier<Map<String, dynamic>> {
final AppConfig _config;
AdvancedConfigNotifier(this._config)
: super({
'defaultProtocol': _config.advanced.defaultProtocol,
'devName': _config.advanced.devName,
'enableEncryption': _config.advanced.enableEncryption,
'enableIpv6': _config.advanced.enableIpv6,
'mtu': _config.advanced.mtu,
'latencyFirst': _config.advanced.latencyFirst,
'enableExitNode': _config.advanced.enableExitNode,
'proxyForwardBySystem': _config.advanced.proxyForwardBySystem,
'noTun': _config.advanced.noTun,
'useSmoltcp': _config.advanced.useSmoltcp,
'relayNetworkWhitelist': _config.advanced.relayNetworkWhitelist,
'disableP2p': _config.advanced.disableP2p,
'relayAllPeerRpc': _config.advanced.relayAllPeerRpc,
'disableUdpHolePunching': _config.advanced.disableUdpHolePunching,
'multiThread': _config.advanced.multiThread,
'dataCompressAlgo': _config.advanced.dataCompressAlgo,
'bindDevice': _config.advanced.bindDevice,
'enableKcpProxy': _config.advanced.enableKcpProxy,
'disableKcpInput': _config.advanced.disableKcpInput,
'disableRelayKcp': _config.advanced.disableRelayKcp,
});
// 更新单个配置项
Future<void> updateConfig(String key, dynamic value) async {
if (state.containsKey(key) && state[key] != value) {
final newState = {...state, key: value};
state = newState;
// 使用通用方法更新配置
await _config.updateAdvancedConfig(
defaultProtocol: key == 'defaultProtocol' ? value : null,
devName: key == 'devName' ? value : null,
enableEncryption: key == 'enableEncryption' ? value : null,
enableIpv6: key == 'enableIpv6' ? value : null,
mtu: key == 'mtu' ? value : null,
latencyFirst: key == 'latencyFirst' ? value : null,
enableExitNode: key == 'enableExitNode' ? value : null,
proxyForwardBySystem: key == 'proxyForwardBySystem' ? value : null,
noTun: key == 'noTun' ? value : null,
useSmoltcp: key == 'useSmoltcp' ? value : null,
relayNetworkWhitelist: key == 'relayNetworkWhitelist' ? value : null,
disableP2p: key == 'disableP2p' ? value : null,
relayAllPeerRpc: key == 'relayAllPeerRpc' ? value : null,
disableUdpHolePunching: key == 'disableUdpHolePunching' ? value : null,
multiThread: key == 'multiThread' ? value : null,
dataCompressAlgo: key == 'dataCompressAlgo' ? value : null,
bindDevice: key == 'bindDevice' ? value : null,
enableKcpProxy: key == 'enableKcpProxy' ? value : null,
disableKcpInput: key == 'disableKcpInput' ? value : null,
disableRelayKcp: key == 'disableRelayKcp' ? value : null,
);
}
}
// 批量更新配置
Future<void> updateMultipleConfigs(Map<String, dynamic> updates) async {
final newState = {...state, ...updates};
state = newState;
// 使用通用方法更新配置
await _config.updateAdvancedConfig(
defaultProtocol: updates.containsKey('defaultProtocol')
? updates['defaultProtocol']
: null,
devName: updates.containsKey('devName') ? updates['devName'] : null,
enableEncryption: updates.containsKey('enableEncryption')
? updates['enableEncryption']
: null,
enableIpv6:
updates.containsKey('enableIpv6') ? updates['enableIpv6'] : null,
mtu: updates.containsKey('mtu') ? updates['mtu'] : null,
latencyFirst:
updates.containsKey('latencyFirst') ? updates['latencyFirst'] : null,
enableExitNode: updates.containsKey('enableExitNode')
? updates['enableExitNode']
: null,
proxyForwardBySystem: updates.containsKey('proxyForwardBySystem')
? updates['proxyForwardBySystem']
: null,
noTun: updates.containsKey('noTun') ? updates['noTun'] : null,
useSmoltcp:
updates.containsKey('useSmoltcp') ? updates['useSmoltcp'] : null,
relayNetworkWhitelist: updates.containsKey('relayNetworkWhitelist')
? updates['relayNetworkWhitelist']
: null,
disableP2p:
updates.containsKey('disableP2p') ? updates['disableP2p'] : null,
relayAllPeerRpc: updates.containsKey('relayAllPeerRpc')
? updates['relayAllPeerRpc']
: null,
disableUdpHolePunching: updates.containsKey('disableUdpHolePunching')
? updates['disableUdpHolePunching']
: null,
multiThread:
updates.containsKey('multiThread') ? updates['multiThread'] : null,
dataCompressAlgo: updates.containsKey('dataCompressAlgo')
? updates['dataCompressAlgo']
: null,
bindDevice:
updates.containsKey('bindDevice') ? updates['bindDevice'] : null,
enableKcpProxy: updates.containsKey('enableKcpProxy')
? updates['enableKcpProxy']
: null,
disableKcpInput: updates.containsKey('disableKcpInput')
? updates['disableKcpInput']
: null,
disableRelayKcp: updates.containsKey('disableRelayKcp')
? updates['disableRelayKcp']
: null,
);
}
// 为每个配置项提供单独的更新方法
Future<void> setDefaultProtocol(String value) async =>
updateConfig('defaultProtocol', value);
Future<void> setDevName(String value) async => updateConfig('devName', value);
Future<void> setEnableEncryption(bool value) async =>
updateConfig('enableEncryption', value);
Future<void> setEnableIpv6(bool value) async =>
updateConfig('enableIpv6', value);
Future<void> setMtu(int value) async => updateConfig('mtu', value);
Future<void> setLatencyFirst(bool value) async =>
updateConfig('latencyFirst', value);
Future<void> setEnableExitNode(bool value) async =>
updateConfig('enableExitNode', value);
Future<void> setProxyForwardBySystem(bool value) async =>
updateConfig('proxyForwardBySystem', value);
Future<void> setNoTun(bool value) async => updateConfig('noTun', value);
Future<void> setUseSmoltcp(bool value) async =>
updateConfig('useSmoltcp', value);
Future<void> setRelayNetworkWhitelist(String value) async =>
updateConfig('relayNetworkWhitelist', value);
Future<void> setDisableP2p(bool value) async =>
updateConfig('disableP2p', value);
Future<void> setRelayAllPeerRpc(bool value) async =>
updateConfig('relayAllPeerRpc', value);
Future<void> setDisableUdpHolePunching(bool value) async =>
updateConfig('disableUdpHolePunching', value);
Future<void> setMultiThread(bool value) async =>
updateConfig('multiThread', value);
Future<void> setDataCompressAlgo(String value) async =>
updateConfig('dataCompressAlgo', value);
Future<void> setBindDevice(bool value) async =>
updateConfig('bindDevice', value);
Future<void> setEnableKcpProxy(bool value) async =>
updateConfig('enableKcpProxy', value);
Future<void> setDisableKcpInput(bool value) async =>
updateConfig('disableKcpInput', value);
Future<void> setDisableRelayKcp(bool value) async =>
updateConfig('disableRelayKcp', value);
}
final advancedConfigProvider =
StateNotifierProvider<AdvancedConfigNotifier, Map<String, dynamic>>((ref) {
return AdvancedConfigNotifier(ref.watch(appConfigProvider));
});
// 为每个高级配置项创建单独的Provider,方便在UI中使用
final defaultProtocolProvider = Provider<String>((ref) {
return ref.watch(advancedConfigProvider)['defaultProtocol'];
});
final devNameProvider = Provider<String>((ref) {
return ref.watch(advancedConfigProvider)['devName'];
});
final enableEncryptionProvider = Provider<bool>((ref) {
return ref.watch(advancedConfigProvider)['enableEncryption'];
});
final enableIpv6Provider = Provider<bool>((ref) {
return ref.watch(advancedConfigProvider)['enableIpv6'];
});
final mtuProvider = Provider<int>((ref) {
return ref.watch(advancedConfigProvider)['mtu'];
});
final latencyFirstProvider = Provider<bool>((ref) {
return ref.watch(advancedConfigProvider)['latencyFirst'];
});
final enableExitNodeProvider = Provider<bool>((ref) {
return ref.watch(advancedConfigProvider)['enableExitNode'];
});
final proxyForwardBySystemProvider = Provider<bool>((ref) {
return ref.watch(advancedConfigProvider)['proxyForwardBySystem'];
});
final noTunProvider = Provider<bool>((ref) {
return ref.watch(advancedConfigProvider)['noTun'];
});
final useSmoltcpProvider = Provider<bool>((ref) {
return ref.watch(advancedConfigProvider)['useSmoltcp'];
});
final relayNetworkWhitelistProvider = Provider<String>((ref) {
return ref.watch(advancedConfigProvider)['relayNetworkWhitelist'];
});
final disableP2pProvider = Provider<bool>((ref) {
return ref.watch(advancedConfigProvider)['disableP2p'];
});
final relayAllPeerRpcProvider = Provider<bool>((ref) {
return ref.watch(advancedConfigProvider)['relayAllPeerRpc'];
});
final disableUdpHolePunchingProvider = Provider<bool>((ref) {
return ref.watch(advancedConfigProvider)['disableUdpHolePunching'];
});
final multiThreadProvider = Provider<bool>((ref) {
return ref.watch(advancedConfigProvider)['multiThread'];
});
final dataCompressAlgoProvider = Provider<String>((ref) {
return ref.watch(advancedConfigProvider)['dataCompressAlgo'];
});
final bindDeviceProvider = Provider<bool>((ref) {
return ref.watch(advancedConfigProvider)['bindDevice'];
});
final enableKcpProxyProvider = Provider<bool>((ref) {
return ref.watch(advancedConfigProvider)['enableKcpProxy'];
});
final disableKcpInputProvider = Provider<bool>((ref) {
return ref.watch(advancedConfigProvider)['disableKcpInput'];
});
final disableRelayKcpProvider = Provider<bool>((ref) {
return ref.watch(advancedConfigProvider)['disableRelayKcp'];
});
+8 -5
View File
@@ -3,25 +3,28 @@ import 'dart:async';
class PingUtil {
static Future<int?> ping(String host) async {
Socket? socket;
try {
// 从 host:port 格式中提取主机名
final hostname = host.split(':')[0];
final post = host.split(':')[1];
final port = host.split(':')[1]; // 修正变量名 post -> port
// 使用 Socket 连接来测量实际网络延迟
final startTime = DateTime.now();
final socket = await Socket.connect(hostname, int.parse(post),
socket = await Socket.connect(hostname, int.parse(port),
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;
} finally {
// 确保在所有情况下都关闭 socket 连接
socket?.destroy();
socket = null;
}
}
}
+198
View File
@@ -0,0 +1,198 @@
import 'dart:convert';
// 主入口类
class StatusPageData {
final Config config;
final Incident incident;
final List<PublicGroup> publicGroupList;
StatusPageData({
required this.config,
required this.incident,
required this.publicGroupList,
});
factory StatusPageData.fromJson(Map<String, dynamic> json) => StatusPageData(
config: Config.fromJson(json['config']),
incident: Incident.fromJson(json['incident']),
publicGroupList: List<PublicGroup>.from(
json['publicGroupList'].map((x) => PublicGroup.fromJson(x))),
);
}
// 配置信息
class Config {
final String slug;
final String title;
final String description;
final String icon;
final String theme;
final bool published;
final bool showTags;
final String customCSS;
final String footerText;
final bool showPoweredBy;
final dynamic googleAnalyticsId;
final bool showCertificateExpiry;
Config({
required this.slug,
required this.title,
required this.description,
required this.icon,
required this.theme,
required this.published,
required this.showTags,
required this.customCSS,
required this.footerText,
required this.showPoweredBy,
this.googleAnalyticsId,
required this.showCertificateExpiry,
});
factory Config.fromJson(Map<String, dynamic> json) => Config(
slug: json['slug'],
title: json['title'],
description: json['description'],
icon: json['icon'],
theme: json['theme'],
published: json['published'],
showTags: json['showTags'],
customCSS: json['customCSS'],
footerText: json['footerText'],
showPoweredBy: json['showPoweredBy'],
googleAnalyticsId: json['googleAnalyticsId'],
showCertificateExpiry: json['showCertificateExpiry'],
);
}
// 事件信息
class Incident {
final int id;
final String style;
final String title;
final String content;
final int pin;
final DateTime createdDate;
final DateTime lastUpdatedDate;
Incident({
required this.id,
required this.style,
required this.title,
required this.content,
required this.pin,
required this.createdDate,
required this.lastUpdatedDate,
});
factory Incident.fromJson(Map<String, dynamic> json) => Incident(
id: json['id'],
style: json['style'],
title: json['title'],
content: json['content'],
pin: json['pin'],
createdDate: DateTime.parse(json['createdDate']),
lastUpdatedDate: DateTime.parse(json['lastUpdatedDate']),
);
}
// 公共服务器组
class PublicGroup {
final int id;
final String name;
final int weight;
final List<Monitor> monitorList;
PublicGroup({
required this.id,
required this.name,
required this.weight,
required this.monitorList,
});
factory PublicGroup.fromJson(Map<String, dynamic> json) => PublicGroup(
id: json['id'],
name: json['name'],
weight: json['weight'],
monitorList: List<Monitor>.from(
json['monitorList'].map((x) => Monitor.fromJson(x))),
);
}
// 监控项
class Monitor {
final int id;
final String name;
final int sendUrl;
final String type;
final List<Tag> tags;
final int? certExpiryDaysRemaining;
final bool? validCert;
Monitor({
required this.id,
required this.name,
required this.sendUrl,
required this.type,
required this.tags,
this.certExpiryDaysRemaining,
this.validCert,
});
factory Monitor.fromJson(Map<String, dynamic> json) => Monitor(
id: json['id'],
name: json['name'],
sendUrl: json['sendUrl'],
type: json['type'],
tags: List<Tag>.from(json['tags'].map((x) => Tag.fromJson(x))),
certExpiryDaysRemaining: json['certExpiryDaysRemaining'],
validCert: json['validCert'],
);
}
// 标签
class Tag {
final int id;
final int monitorId;
final int tagId;
final String value;
final String name;
final String color;
Tag({
required this.id,
required this.monitorId,
required this.tagId,
required this.value,
required this.name,
required this.color,
});
factory Tag.fromJson(Map<String, dynamic> json) => Tag(
id: json['id'],
monitorId: json['monitor_id'],
tagId: json['tag_id'],
value: json['value'],
name: json['name'],
color: json['color'],
);
}
// // 使用示例
// void main() {
// final jsonString = '''{/* 你的原始JSON数据 */}''';
// final data = StatusPageData.fromJson(jsonDecode(jsonString));
// // 示例:获取第一个服务器组的名称
// print('第一组名称: ${data.publicGroupList.first.name}');
// // 示例:列出所有可中转的服务器
// final transferableServers = data.publicGroupList
// .expand((group) => group.monitorList)
// .where((monitor) => monitor.tags.any((tag) => tag.name == '可中转'))
// .toList();
// print('可中转服务器数量: ${transferableServers.length}');
// }
+1
View File
@@ -3,6 +3,7 @@ import 'dart:math';
import 'package:astral/utils/app_info.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:fl_chart/fl_chart.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
+3 -78
View File
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'dart:math' as math;
class FloatingCard extends StatefulWidget {
final ColorScheme colorScheme;
@@ -8,14 +7,8 @@ class FloatingCard extends StatefulWidget {
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; // 控制悬浮时是升起还是降下
final double? maxWidth;
final double? height;
const FloatingCard({
super.key,
@@ -27,12 +20,6 @@ class FloatingCard extends StatefulWidget {
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
@@ -41,83 +28,21 @@ class FloatingCard extends StatefulWidget {
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,
minHeight: widget.height ?? 74, // 默认最小高度为74
),
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(
+64 -5
View File
@@ -16,11 +16,63 @@ NotificationMessage message = NotificationMessage.fromPluginTemplate(
// image: file_path
);
class WindowControls extends StatelessWidget {
class WindowControls extends StatefulWidget {
const WindowControls({super.key});
@override
State<WindowControls> createState() => _WindowControlsState();
}
class _WindowControlsState extends State<WindowControls> with WindowListener {
bool _isMaximized = false;
@override
void initState() {
super.initState();
windowManager.addListener(this);
_init();
}
@override
void dispose() {
windowManager.removeListener(this);
super.dispose();
}
void _init() async {
final isMaximized = await windowManager.isMaximized();
if (mounted) {
setState(() {
_isMaximized = isMaximized;
});
}
}
@override
void onWindowMaximize() {
if (mounted) {
setState(() {
_isMaximized = true;
});
print("窗口已最大化: $_isMaximized"); // 添加调试信息
}
}
@override
void onWindowUnmaximize() {
if (mounted) {
setState(() {
_isMaximized = false;
});
print("窗口已还原: $_isMaximized"); // 添加调试信息
}
}
@override
Widget build(BuildContext context) {
// 添加调试信息
print("构建窗口控制按钮,当前最大化状态: $_isMaximized");
return Row(
children: [
IconButton(
@@ -29,12 +81,19 @@ class WindowControls extends StatelessWidget {
tooltip: '最小化',
),
IconButton(
icon: const Icon(Icons.crop_square),
icon: Icon(_isMaximized ? Icons.filter_none : Icons.crop_square),
onPressed: () async {
if (await windowManager.isMaximized()) {
windowManager.unmaximize();
if (_isMaximized) {
await windowManager.unmaximize();
} else {
windowManager.maximize();
await windowManager.maximize();
}
// 手动更新状态,以防事件监听不工作
if (mounted) {
final isMaximized = await windowManager.isMaximized();
setState(() {
_isMaximized = isMaximized;
});
}
},
tooltip: '最大化/还原',
+329 -140
View File
File diff suppressed because it is too large Load Diff
+13 -3
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.1
version: 1.0.6
environment:
sdk: ^3.5.4
@@ -42,7 +42,6 @@ dependencies:
flutter_rust_bridge: 2.9.0
flutter_colorpicker: ^1.1.0
window_manager: ^0.4.3
fl_chart: ^0.70.2
flutter_staggered_grid_view: ^0.7.0
json_annotation: ^4.8.1
tray_manager: ^0.3.2
@@ -52,11 +51,20 @@ dependencies:
yaml: ^3.1.2
flutter_local_notifications: ^19.0.0
windows_notification: ^1.3.0
fl_chart: ^0.70.2
sqflite: ^2.4.2
path: ^1.9.1
flutter_riverpod: ^2.6.1
floor_generator: ^1.5.0
build_runner: ^2.4.15
hive: ^2.2.3
hive_flutter: ^1.1.0
http: ^1.1.0
path_provider: ^2.1.5
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.4.6
json_serializable: ^6.7.1
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
@@ -64,6 +72,8 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
hive_generator: ^2.0.0
build_runner: ^2.3.3
integration_test:
sdk: flutter
+11 -1
View File
@@ -389,6 +389,15 @@ dependencies = [
"x25519-dalek",
]
[[package]]
name = "bounded_join_set"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae18fd8f4a623bcf416b5bc8f1e0905534d9911597ed17cc57ab9b6eed65454d"
dependencies = [
"tokio",
]
[[package]]
name = "bstr"
version = "1.8.0"
@@ -954,7 +963,7 @@ dependencies = [
[[package]]
name = "easytier"
version = "2.2.2"
version = "2.2.3"
dependencies = [
"anyhow",
"async-compression",
@@ -967,6 +976,7 @@ dependencies = [
"base64 0.22.1",
"bitflags 2.9.0",
"boringtun-easytier",
"bounded_join_set",
"bytecodec",
"byteorder",
"bytes",
+1
View File
@@ -21,3 +21,4 @@ easytier = { path = "./easytier" }
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] }
-5274
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -3,7 +3,7 @@ name = "easytier"
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
homepage = "https://github.com/EasyTier/EasyTier"
repository = "https://github.com/EasyTier/EasyTier"
version = "2.2.2"
version = "2.2.3"
edition = "2021"
authors = ["kkrainbow"]
keywords = ["vpn", "p2p", "network", "easytier"]
@@ -192,6 +192,8 @@ http_req = { git = "https://github.com/EasyTier/http_req.git", default-features
# for dns connector
hickory-resolver = "0.24.4"
bounded_join_set = "0.3.0"
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies]
machine-uid = "0.5.3"
-1
View File
@@ -27,7 +27,6 @@ pub fn gen_default_flags() -> Flags {
disable_p2p: false,
relay_all_peer_rpc: false,
disable_udp_hole_punching: false,
ipv6_listener: "udp://[::]:0".to_string(),
multi_thread: true,
data_compress_algo: CompressionAlgoPb::None.into(),
bind_device: true,
+3 -3
View File
@@ -29,7 +29,7 @@ pub const WIN_SERVICE_WORK_DIR_REG_KEY: &str = "SOFTWARE\\EasyTier\\Service\\Wor
pub const EASYTIER_VERSION: &str = git_version::git_version!(
args = ["--abbrev=8", "--always", "--dirty=~"],
prefix = concat!(env!("CARGO_PKG_VERSION"), "-@astral "),
suffix = "",
fallback = env!("CARGO_PKG_VERSION")
prefix = concat!(env!("CARGO_PKG_VERSION"), "-"),
suffix = "-astral",
fallback = concat!(env!("CARGO_PKG_VERSION"), "-astral")
);
+188 -22
View File
@@ -1,5 +1,5 @@
use std::collections::BTreeSet;
use std::net::{IpAddr, SocketAddr};
use std::net::{IpAddr, Ipv6Addr, SocketAddr};
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
@@ -8,6 +8,8 @@ use crate::proto::common::{NatType, StunInfo};
use anyhow::Context;
use chrono::Local;
use crossbeam::atomic::AtomicCell;
use hickory_resolver::config::{NameServerConfig, Protocol, ResolverConfig, ResolverOpts};
use hickory_resolver::TokioAsyncResolver;
use rand::seq::IteratorRandom;
use tokio::net::{lookup_host, UdpSocket};
use tokio::sync::{broadcast, Mutex};
@@ -22,21 +24,68 @@ use crate::common::error::Error;
use super::stun_codec_ext::*;
pub fn get_default_resolver_config() -> ResolverConfig {
let mut default_resolve_config = ResolverConfig::new();
default_resolve_config.add_name_server(NameServerConfig::new(
"223.5.5.5:53".parse().unwrap(),
Protocol::Udp,
));
default_resolve_config.add_name_server(NameServerConfig::new(
"180.184.1.1:53".parse().unwrap(),
Protocol::Udp,
));
default_resolve_config
}
pub async fn resolve_txt_record(
domain_name: &str,
resolver: &TokioAsyncResolver,
) -> Result<String, Error> {
let response = resolver.txt_lookup(domain_name).await.with_context(|| {
format!(
"txt_lookup failed, domain_name: {}",
domain_name.to_string()
)
})?;
let txt_record = response.iter().next().with_context(|| {
format!(
"no txt record found, domain_name: {}",
domain_name.to_string()
)
})?;
let txt_data = String::from_utf8_lossy(&txt_record.txt_data()[0]);
tracing::info!(?txt_data, ?domain_name, "get txt record");
Ok(txt_data.to_string())
}
struct HostResolverIter {
hostnames: Vec<String>,
ips: Vec<SocketAddr>,
max_ip_per_domain: u32,
use_ipv6: bool,
}
impl HostResolverIter {
fn new(hostnames: Vec<String>, max_ip_per_domain: u32) -> Self {
fn new(hostnames: Vec<String>, max_ip_per_domain: u32, use_ipv6: bool) -> Self {
Self {
hostnames,
ips: vec![],
max_ip_per_domain,
use_ipv6,
}
}
async fn get_txt_record(domain_name: &str) -> Result<Vec<String>, Error> {
let resolver = TokioAsyncResolver::tokio_from_system_conf().unwrap_or(
TokioAsyncResolver::tokio(get_default_resolver_config(), ResolverOpts::default()),
);
let txt_data = resolve_txt_record(domain_name, &resolver).await?;
Ok(txt_data.split(" ").map(|x| x.to_string()).collect())
}
#[async_recursion::async_recursion]
async fn next(&mut self) -> Option<SocketAddr> {
if self.ips.is_empty() {
@@ -51,10 +100,35 @@ impl HostResolverIter {
format!("{}:3478", host)
};
if host.starts_with("txt:") {
let domain_name = host.trim_start_matches("txt:");
match Self::get_txt_record(domain_name).await {
Ok(hosts) => {
tracing::info!(
?domain_name,
?hosts,
"get txt record success when resolve stun server"
);
// insert hosts to the head of hostnames
self.hostnames.splice(0..0, hosts.into_iter());
}
Err(e) => {
tracing::warn!(
?domain_name,
?e,
"get txt record failed when resolve stun server"
);
}
}
return self.next().await;
}
let use_ipv6 = self.use_ipv6;
match lookup_host(&host).await {
Ok(ips) => {
self.ips = ips
.filter(|x| x.is_ipv4())
.filter(|x| if use_ipv6 { x.is_ipv6() } else { x.is_ipv4() })
.choose_multiple(&mut rand::thread_rng(), self.max_ip_per_domain as usize);
if self.ips.is_empty() {
@@ -400,7 +474,7 @@ impl UdpNatTypeDetectResult {
// find resp with distinct stun server
self.stun_resps
.iter()
.map(|x| x.stun_server_addr)
.map(|x| x.recv_from_addr)
.collect::<BTreeSet<_>>()
.len()
}
@@ -555,8 +629,11 @@ impl UdpNatTypeDetector {
udp: Arc<UdpSocket>,
) -> Result<UdpNatTypeDetectResult, Error> {
let mut stun_servers = vec![];
let mut host_resolver =
HostResolverIter::new(self.stun_server_hosts.clone(), self.max_ip_per_domain);
let mut host_resolver = HostResolverIter::new(
self.stun_server_hosts.clone(),
self.max_ip_per_domain,
false,
);
while let Some(addr) = host_resolver.next().await {
stun_servers.push(addr);
}
@@ -602,7 +679,9 @@ pub trait StunInfoCollectorTrait: Send + Sync {
pub struct StunInfoCollector {
stun_servers: Arc<RwLock<Vec<String>>>,
stun_servers_v6: Arc<RwLock<Vec<String>>>,
udp_nat_test_result: Arc<RwLock<Option<UdpNatTypeDetectResult>>>,
public_ipv6: Arc<AtomicCell<Option<Ipv6Addr>>>,
nat_test_result_time: Arc<AtomicCell<chrono::DateTime<Local>>>,
redetect_notify: Arc<tokio::sync::Notify>,
tasks: std::sync::Mutex<JoinSet<()>>,
@@ -621,7 +700,12 @@ impl StunInfoCollectorTrait for StunInfoCollector {
udp_nat_type: result.nat_type() as i32,
tcp_nat_type: 0,
last_update_time: self.nat_test_result_time.load().timestamp(),
public_ip: result.public_ips().iter().map(|x| x.to_string()).collect(),
public_ip: result
.public_ips()
.iter()
.map(|x| x.to_string())
.chain(self.public_ipv6.load().map(|x| x.to_string()))
.collect(),
min_port: result.min_port() as u32,
max_port: result.max_port() as u32,
}
@@ -640,7 +724,7 @@ impl StunInfoCollectorTrait for StunInfoCollector {
if stun_servers.is_empty() {
let mut host_resolver =
HostResolverIter::new(self.stun_servers.read().unwrap().clone(), 2);
HostResolverIter::new(self.stun_servers.read().unwrap().clone(), 2, false);
while let Some(addr) = host_resolver.next().await {
stun_servers.push(addr);
if stun_servers.len() >= 2 {
@@ -680,7 +764,9 @@ impl StunInfoCollector {
pub fn new(stun_servers: Vec<String>) -> Self {
Self {
stun_servers: Arc::new(RwLock::new(stun_servers)),
stun_servers_v6: Arc::new(RwLock::new(Self::get_default_servers_v6())),
udp_nat_test_result: Arc::new(RwLock::new(None)),
public_ipv6: Arc::new(AtomicCell::new(None)),
nat_test_result_time: Arc::new(AtomicCell::new(Local::now())),
redetect_notify: Arc::new(tokio::sync::Notify::new()),
tasks: std::sync::Mutex::new(JoinSet::new()),
@@ -696,28 +782,42 @@ impl StunInfoCollector {
// NOTICE: we may need to choose stun stun server based on geo location
// stun server cross nation may return a external ip address with high latency and loss rate
vec![
"txt:stun.easytier.cn",
"stun.miwifi.com",
"stun.chat.bilibili.com",
"stun.hitv.com",
"stun.cdnbye.com",
"stun.douyucdn.cn:18000",
"fwa.lifesizecloud.com",
"global.turn.twilio.com",
"turn.cloudflare.com",
"stun.isp.net.au",
"stun.nextcloud.com",
"stun.freeswitch.org",
"stun.voip.blackberry.com",
"stunserver.stunprotocol.org",
"stun.sipnet.com",
"stun.radiojar.com",
"stun.sonetel.com",
]
.iter()
.map(|x| x.to_string())
.collect()
}
pub fn get_default_servers_v6() -> Vec<String> {
vec!["txt:stun-v6.easytier.cn"]
.iter()
.map(|x| x.to_string())
.collect()
}
async fn get_public_ipv6(servers: &Vec<String>) -> Option<Ipv6Addr> {
let mut ips = HostResolverIter::new(servers.to_vec(), 10, true);
while let Some(ip) = ips.next().await {
let udp = Arc::new(UdpSocket::bind(format!("[::]:0")).await.unwrap());
let ret = StunClientBuilder::new(udp.clone())
.new_stun_client(ip)
.bind_request(false, false)
.await;
tracing::debug!(?ret, "finish ipv6 udp nat type detect");
match ret.map(|x| x.mapped_socket_addr.map(|x| x.ip())) {
Ok(Some(IpAddr::V6(v6))) => {
return Some(v6);
}
_ => {}
}
}
None
}
fn start_stun_routine(&self) {
if self.started.load(std::sync::atomic::Ordering::Relaxed) {
return;
@@ -784,6 +884,30 @@ impl StunInfoCollector {
}
}
});
// for ipv6
let stun_servers = self.stun_servers_v6.clone();
let stored_ipv6 = self.public_ipv6.clone();
let redetect_notify = self.redetect_notify.clone();
self.tasks.lock().unwrap().spawn(async move {
loop {
let servers = stun_servers.read().unwrap().clone();
Self::get_public_ipv6(&servers)
.await
.map(|x| stored_ipv6.store(Some(x)));
let sleep_sec = if stored_ipv6.load().is_none() {
60
} else {
360
};
tokio::select! {
_ = redetect_notify.notified() => {}
_ = tokio::time::sleep(Duration::from_secs(sleep_sec)) => {}
}
}
});
}
pub fn update_stun_info(&self) {
@@ -862,6 +986,48 @@ mod tests {
let detector = UdpNatTypeDetector::new(stun_servers, 1);
let ret = detector.detect_nat_type(0).await;
println!("{:#?}, {:?}", ret, ret.as_ref().unwrap().nat_type());
assert_eq!(ret.unwrap().nat_type(), NatType::PortRestricted);
assert_eq!(ret.unwrap().nat_type(), NatType::Restricted);
}
#[tokio::test]
async fn test_txt_public_stun_server() {
let stun_servers = vec!["txt:stun.easytier.cn".to_string()];
let detector = UdpNatTypeDetector::new(stun_servers, 1);
let ret = detector.detect_nat_type(0).await;
println!("{:#?}, {:?}", ret, ret.as_ref().unwrap().nat_type());
assert!(!ret.unwrap().stun_resps.is_empty());
}
#[tokio::test]
async fn test_v4_stun() {
let mut udp_server = UdpTunnelListener::new("udp://0.0.0.0:55355".parse().unwrap());
let mut tasks = JoinSet::new();
tasks.spawn(async move {
udp_server.listen().await.unwrap();
loop {
udp_server.accept().await.unwrap();
}
});
let stun_servers = vec!["127.0.0.1:55355".to_string()];
let detector = UdpNatTypeDetector::new(stun_servers, 1);
let ret = detector.detect_nat_type(0).await;
println!("{:#?}, {:?}", ret, ret.as_ref().unwrap().nat_type());
assert_eq!(ret.unwrap().nat_type(), NatType::Restricted);
}
#[tokio::test]
async fn test_v6_stun() {
let mut udp_server = UdpTunnelListener::new("udp://[::]:55355".parse().unwrap());
let mut tasks = JoinSet::new();
tasks.spawn(async move {
udp_server.listen().await.unwrap();
loop {
udp_server.accept().await.unwrap();
}
});
let stun_servers = vec!["::1:55355".to_string()];
let ret = StunInfoCollector::get_public_ipv6(&stun_servers).await;
println!("{:#?}", ret);
}
}
+80 -128
View File
@@ -1,7 +1,9 @@
// try connect peers directly, with either its public ip or lan ip
use std::{
net::SocketAddr,
collections::HashSet,
net::{Ipv6Addr, SocketAddr},
str::FromStr,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
@@ -79,7 +81,6 @@ struct DstListenerUrlBlackListItem(PeerId, url::Url);
struct DirectConnectorManagerData {
global_ctx: ArcGlobalCtx,
peer_manager: Arc<PeerManager>,
dst_blacklist: timedmap::TimedMap<DstBlackListItem, ()>,
dst_listener_blacklist: timedmap::TimedMap<DstListenerUrlBlackListItem, ()>,
}
@@ -88,7 +89,6 @@ impl DirectConnectorManagerData {
Self {
global_ctx,
peer_manager,
dst_blacklist: timedmap::TimedMap::new(),
dst_listener_blacklist: timedmap::TimedMap::new(),
}
}
@@ -150,7 +150,9 @@ impl DirectConnectorManager {
let peers = data.peer_manager.list_peers().await;
let mut tasks = JoinSet::new();
for peer_id in peers {
if peer_id == my_peer_id {
if peer_id == my_peer_id
|| data.peer_manager.has_directly_connected_conn(peer_id)
{
continue;
}
tasks.spawn(Self::do_try_direct_connect(data.clone(), peer_id));
@@ -173,24 +175,13 @@ impl DirectConnectorManager {
dst_peer_id: PeerId,
addr: String,
) -> Result<(), Error> {
data.dst_blacklist.cleanup();
if data
.dst_blacklist
.contains(&DstBlackListItem(dst_peer_id.clone(), addr.clone()))
{
tracing::debug!("try_connect_to_ip failed, addr in blacklist: {}", addr);
return Err(Error::UrlInBlacklist);
}
let connector = create_connector_by_url(&addr, &data.global_ctx).await?;
let (peer_id, conn_id) = timeout(
std::time::Duration::from_secs(5),
data.peer_manager.try_connect(connector),
std::time::Duration::from_secs(3),
data.peer_manager.try_direct_connect(connector),
)
.await??;
// let (peer_id, conn_id) = data.peer_manager.try_connect(connector).await?;
if peer_id != dst_peer_id && !TESTING.load(Ordering::Relaxed) {
tracing::info!(
"connect to ip succ: {}, but peer id mismatch, expect: {}, actual: {}",
@@ -204,6 +195,7 @@ impl DirectConnectorManager {
.await?;
return Err(Error::InvalidUrl(addr));
}
Ok(())
}
@@ -214,7 +206,7 @@ impl DirectConnectorManager {
addr: String,
) -> Result<(), Error> {
let mut rand_gen = rand::rngs::OsRng::default();
let backoff_ms = vec![1000, 2000, 4000];
let backoff_ms = vec![1000, 2000];
let mut backoff_idx = 0;
loop {
@@ -237,12 +229,6 @@ impl DirectConnectorManager {
backoff_idx += 1;
continue;
} else {
data.dst_blacklist.insert(
DstBlackListItem(dst_peer_id.clone(), addr.clone()),
(),
std::time::Duration::from_secs(DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC),
);
return ret;
}
}
@@ -273,61 +259,43 @@ impl DirectConnectorManager {
tracing::debug!(?available_listeners, "got available listeners");
let mut listener = available_listeners.get(0).ok_or(anyhow::anyhow!(
"peer {} have no valid listener",
dst_peer_id
))?;
if available_listeners.is_empty() {
return Err(anyhow::anyhow!("peer {} have no valid listener", dst_peer_id).into());
}
// if have default listener, use it first
listener = available_listeners
let listener = available_listeners
.iter()
.find(|l| l.scheme() == data.global_ctx.get_flags().default_protocol)
.unwrap_or(listener);
.unwrap_or(available_listeners.get(0).unwrap());
let mut tasks = JoinSet::new();
let mut tasks = bounded_join_set::JoinSet::new(2);
let listener_host = listener.socket_addrs(|| None).unwrap().pop();
match listener_host {
Some(SocketAddr::V4(s_addr)) => {
if s_addr.ip().is_unspecified() {
ip_list.interface_ipv4s.iter().for_each(|ip| {
let mut addr = (*listener).clone();
if addr.set_host(Some(ip.to_string().as_str())).is_ok() {
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
dst_peer_id.clone(),
addr.to_string(),
));
} else {
tracing::error!(
?ip,
?listener,
?dst_peer_id,
"failed to set host for interface ipv4"
);
}
});
if let Some(public_ipv4) = ip_list.public_ipv4 {
let mut addr = (*listener).clone();
if addr
.set_host(Some(public_ipv4.to_string().as_str()))
.is_ok()
{
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
dst_peer_id.clone(),
addr.to_string(),
));
} else {
tracing::error!(
?public_ipv4,
?listener,
?dst_peer_id,
"failed to set host for public ipv4"
);
}
}
ip_list
.interface_ipv4s
.iter()
.chain(ip_list.public_ipv4.iter())
.for_each(|ip| {
let mut addr = (*listener).clone();
if addr.set_host(Some(ip.to_string().as_str())).is_ok() {
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
dst_peer_id.clone(),
addr.to_string(),
));
} else {
tracing::error!(
?ip,
?listener,
?dst_peer_id,
"failed to set host for interface ipv4"
);
}
});
} else if !s_addr.ip().is_loopback() || TESTING.load(Ordering::Relaxed) {
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
@@ -338,47 +306,42 @@ impl DirectConnectorManager {
}
Some(SocketAddr::V6(s_addr)) => {
if s_addr.ip().is_unspecified() {
ip_list.interface_ipv6s.iter().for_each(|ip| {
let mut addr = (*listener).clone();
if addr
.set_host(Some(format!("[{}]", ip.to_string()).as_str()))
.is_ok()
{
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
dst_peer_id.clone(),
addr.to_string(),
));
} else {
tracing::error!(
?ip,
?listener,
?dst_peer_id,
"failed to set host for interface ipv6"
);
}
});
if let Some(public_ipv6) = ip_list.public_ipv6 {
let mut addr = (*listener).clone();
if addr
.set_host(Some(format!("[{}]", public_ipv6.to_string()).as_str()))
.is_ok()
{
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
dst_peer_id.clone(),
addr.to_string(),
));
} else {
tracing::error!(
?public_ipv6,
?listener,
?dst_peer_id,
"failed to set host for public ipv6"
);
}
}
// for ipv6, only try public ip
ip_list
.interface_ipv6s
.iter()
.chain(ip_list.public_ipv6.iter())
.filter_map(|x| Ipv6Addr::from_str(&x.to_string()).ok())
.filter(|x| {
TESTING.load(Ordering::Relaxed)
|| (!x.is_loopback()
&& !x.is_unspecified()
&& !x.is_unique_local()
&& !x.is_unicast_link_local()
&& !x.is_multicast())
})
.collect::<HashSet<_>>()
.iter()
.for_each(|ip| {
let mut addr = (*listener).clone();
if addr
.set_host(Some(format!("[{}]", ip.to_string()).as_str()))
.is_ok()
{
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
dst_peer_id.clone(),
addr.to_string(),
));
} else {
tracing::error!(
?ip,
?listener,
?dst_peer_id,
"failed to set host for public ipv6"
);
}
});
} else if !s_addr.ip().is_loopback() || TESTING.load(Ordering::Relaxed) {
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
@@ -430,14 +393,6 @@ impl DirectConnectorManager {
dst_peer_id: PeerId,
) -> Result<(), Error> {
let peer_manager = data.peer_manager.clone();
// check if we have direct connection with dst_peer_id
if let Some(c) = peer_manager.list_peer_conns(dst_peer_id).await {
// currently if we have any type of direct connection (udp or tcp), we will not try to connect
if !c.is_empty() {
return Ok(());
}
}
tracing::debug!("try direct connect to peer: {}", dst_peer_id);
let rpc_stub = peer_manager
@@ -466,8 +421,7 @@ mod tests {
use crate::{
connector::direct::{
DirectConnectorManager, DirectConnectorManagerData, DstBlackListItem,
DstListenerUrlBlackListItem,
DirectConnectorManager, DirectConnectorManagerData, DstListenerUrlBlackListItem,
},
instance::listeners::ListenerManager,
peers::tests::{
@@ -526,9 +480,7 @@ mod tests {
#[values("tcp", "udp", "wg")] proto: &str,
#[values("true", "false")] ipv6: bool,
) {
if ipv6 && proto != "udp" {
return;
}
TESTING.store(true, std::sync::atomic::Ordering::Relaxed);
let p_a = create_mock_peer_manager().await;
let p_b = create_mock_peer_manager().await;
@@ -544,14 +496,18 @@ mod tests {
dm_a.run_as_client();
dm_c.run_as_server();
let port = if proto == "wg" { 11040 } else { 11041 };
if !ipv6 {
let port = if proto == "wg" { 11040 } else { 11041 };
p_c.get_global_ctx().config.set_listeners(vec![format!(
"{}://0.0.0.0:{}",
proto, port
)
.parse()
.unwrap()]);
} else {
p_c.get_global_ctx()
.config
.set_listeners(vec![format!("{}://[::]:{}", proto, port).parse().unwrap()]);
}
let mut f = p_c.get_global_ctx().config.get_flags();
f.enable_ipv6 = ipv6;
@@ -592,9 +548,5 @@ mod tests {
1,
"tcp://127.0.0.1:10222".parse().unwrap()
)));
assert!(data
.dst_blacklist
.contains(&DstBlackListItem(1, ip_list.listeners[0].to_string())));
}
}
+33 -41
View File
@@ -1,13 +1,17 @@
use std::{net::SocketAddr, sync::Arc};
use crate::{
common::{error::Error, global_ctx::ArcGlobalCtx},
tunnel::{Tunnel, TunnelConnector, TunnelError, PROTO_PORT_OFFSET},
common::{
error::Error,
global_ctx::ArcGlobalCtx,
stun::{get_default_resolver_config, resolve_txt_record},
},
tunnel::{IpVersion, Tunnel, TunnelConnector, TunnelError, PROTO_PORT_OFFSET},
};
use anyhow::Context;
use dashmap::DashSet;
use hickory_resolver::{
config::{NameServerConfig, Protocol, ResolverConfig, ResolverOpts},
config::{ResolverConfig, ResolverOpts},
proto::rr::rdata::SRV,
TokioAsyncResolver,
};
@@ -38,6 +42,7 @@ pub struct DNSTunnelConnector {
addr: url::Url,
bind_addrs: Vec<SocketAddr>,
global_ctx: ArcGlobalCtx,
ip_version: IpVersion,
default_resolve_config: ResolverConfig,
default_resolve_opts: ResolverOpts,
@@ -45,21 +50,13 @@ pub struct DNSTunnelConnector {
impl DNSTunnelConnector {
pub fn new(addr: url::Url, global_ctx: ArcGlobalCtx) -> Self {
let mut default_resolve_config = ResolverConfig::new();
default_resolve_config.add_name_server(NameServerConfig::new(
"223.5.5.5:53".parse().unwrap(),
Protocol::Udp,
));
default_resolve_config.add_name_server(NameServerConfig::new(
"180.184.1.1:53".parse().unwrap(),
Protocol::Udp,
));
Self {
addr,
bind_addrs: Vec::new(),
global_ctx,
ip_version: IpVersion::Both,
default_resolve_config,
default_resolve_config: get_default_resolver_config(),
default_resolve_opts: ResolverOpts::default(),
}
}
@@ -69,26 +66,14 @@ impl DNSTunnelConnector {
&self,
domain_name: &str,
) -> Result<Box<dyn TunnelConnector>, Error> {
let resolver = TokioAsyncResolver::tokio_from_system_conf().unwrap_or(
TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default()),
);
let response = resolver.txt_lookup(domain_name).await.with_context(|| {
format!(
"txt_lookup failed, domain_name: {}",
domain_name.to_string()
)
})?;
let txt_record = response.iter().next().with_context(|| {
format!(
"no txt record found, domain_name: {}",
domain_name.to_string()
)
})?;
let txt_data = String::from_utf8_lossy(&txt_record.txt_data()[0]);
tracing::info!(?txt_data, ?domain_name, "get txt record");
let resolver =
TokioAsyncResolver::tokio_from_system_conf().unwrap_or(TokioAsyncResolver::tokio(
self.default_resolve_config.clone(),
self.default_resolve_opts.clone(),
));
let txt_data = resolve_txt_record(domain_name, &resolver)
.await
.with_context(|| format!("resolve txt record failed, domain_name: {}", domain_name))?;
let candidate_urls = txt_data
.split(" ")
@@ -106,9 +91,9 @@ impl DNSTunnelConnector {
)
})?;
let connector = create_connector_by_url(url.as_str(), &self.global_ctx).await;
connector
let mut connector = create_connector_by_url(url.as_str(), &self.global_ctx).await?;
connector.set_ip_version(self.ip_version);
Ok(connector)
}
fn handle_one_srv_record(record: &SRV, protocol: &str) -> Result<(url::Url, u64), Error> {
@@ -141,9 +126,11 @@ impl DNSTunnelConnector {
) -> Result<Box<dyn TunnelConnector>, Error> {
tracing::info!("handle_srv_record: {}", domain_name);
let resolver = TokioAsyncResolver::tokio_from_system_conf().unwrap_or(
TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default()),
);
let resolver =
TokioAsyncResolver::tokio_from_system_conf().unwrap_or(TokioAsyncResolver::tokio(
self.default_resolve_config.clone(),
self.default_resolve_opts.clone(),
));
let srv_domains = PROTO_PORT_OFFSET
.iter()
@@ -192,8 +179,9 @@ impl DNSTunnelConnector {
)
})?;
let connector = create_connector_by_url(url.as_str(), &self.global_ctx).await;
connector
let mut connector = create_connector_by_url(url.as_str(), &self.global_ctx).await?;
connector.set_ip_version(self.ip_version);
Ok(connector)
}
}
@@ -238,6 +226,10 @@ impl super::TunnelConnector for DNSTunnelConnector {
fn set_bind_addrs(&mut self, addrs: Vec<SocketAddr>) {
self.bind_addrs = addrs;
}
fn set_ip_version(&mut self, ip_version: IpVersion) {
self.ip_version = ip_version;
}
}
#[cfg(test)]
+4 -12
View File
@@ -293,7 +293,6 @@ impl ManualConnectorManager {
ip_version: IpVersion,
) -> Result<ReconnResult, Error> {
let ip_collector = data.global_ctx.get_ip_collector();
let net_ns = data.net_ns.clone();
connector.lock().await.set_ip_version(ip_version);
@@ -309,18 +308,11 @@ impl ManualConnectorManager {
data.global_ctx.issue_event(GlobalCtxEvent::Connecting(
connector.lock().await.remote_url().clone(),
));
let _g = net_ns.guard();
tracing::info!("reconnect try connect... conn: {:?}", connector);
let tunnel = connector.lock().await.connect().await?;
tracing::info!("reconnect get tunnel succ: {:?}", tunnel);
assert_eq!(
dead_url,
tunnel.info().unwrap().remote_addr.unwrap().to_string(),
"info: {:?}",
tunnel.info()
);
let (peer_id, conn_id) = data.peer_manager.add_client_tunnel(tunnel).await?;
let (peer_id, conn_id) = data
.peer_manager
.try_direct_connect(connector.lock().await.as_mut())
.await?;
tracing::info!("reconnect succ: {} {} {}", peer_id, conn_id, dead_url);
Ok(ReconnResult {
dead_url,
@@ -388,7 +388,7 @@ impl UdpHolePunchListener {
tracing::warn!(?conn, "udp hole punching listener got peer connection");
let peer_mgr = peer_mgr.clone();
tokio::spawn(async move {
if let Err(e) = peer_mgr.add_tunnel_as_server(conn).await {
if let Err(e) = peer_mgr.add_tunnel_as_server(conn, false).await {
tracing::error!(
?e,
"failed to add tunnel as server in hole punch listener"
+36 -4
View File
@@ -1,8 +1,14 @@
use std::{
ffi::OsString, fmt::Write, net::SocketAddr, path::PathBuf, sync::Mutex, time::Duration, vec,
ffi::OsString,
fmt::Write,
net::{IpAddr, SocketAddr},
path::PathBuf,
sync::Mutex,
time::Duration,
vec,
};
use anyhow::{Context, Ok};
use anyhow::Context;
use clap::{command, Args, Parser, Subcommand};
use humansize::format_size;
use service_manager::*;
@@ -311,7 +317,11 @@ impl CommandHandler {
ipv4: route.ipv4_addr.map(|ip| ip.to_string()).unwrap_or_default(),
hostname: route.hostname.clone(),
cost: cost_to_str(route.cost),
lat_ms: float_to_str(p.get_latency_ms().unwrap_or(0.0), 3),
lat_ms: if route.cost == 1 {
float_to_str(p.get_latency_ms().unwrap_or(0.0), 3)
} else {
route.path_latency_latency_first().to_string()
},
loss_rate: float_to_str(p.get_loss_rate().unwrap_or(0.0), 3),
rx_bytes: format_size(p.get_rx_bytes().unwrap_or(0), humansize::DECIMAL),
tx_bytes: format_size(p.get_tx_bytes().unwrap_or(0), humansize::DECIMAL),
@@ -1036,6 +1046,7 @@ async fn main() -> Result<(), Error> {
match sub_cmd.sub_command {
Some(NodeSubCommand::Info) | None => {
let stun_info = node_info.stun_info.clone().unwrap_or_default();
let ip_list = node_info.ip_list.clone().unwrap_or_default();
let mut builder = tabled::builder::Builder::default();
builder.push_record(vec!["Virtual IP", node_info.ipv4_addr.as_str()]);
@@ -1045,11 +1056,32 @@ async fn main() -> Result<(), Error> {
node_info.proxy_cidrs.join(", ").as_str(),
]);
builder.push_record(vec!["Peer ID", node_info.peer_id.to_string().as_str()]);
builder.push_record(vec!["Public IP", stun_info.public_ip.join(", ").as_str()]);
stun_info.public_ip.iter().for_each(|ip| {
let Ok(ip) = ip.parse::<IpAddr>() else {
return;
};
if ip.is_ipv4() {
builder.push_record(vec!["Public IPv4", ip.to_string().as_str()]);
} else {
builder.push_record(vec!["Public IPv6", ip.to_string().as_str()]);
}
});
builder.push_record(vec![
"UDP Stun Type",
format!("{:?}", stun_info.udp_nat_type()).as_str(),
]);
ip_list.interface_ipv4s.iter().for_each(|ip| {
builder.push_record(vec![
"Interface IPv4",
format!("{}", ip.to_string()).as_str(),
]);
});
ip_list.interface_ipv6s.iter().for_each(|ip| {
builder.push_record(vec![
"Interface IPv6",
format!("{}", ip.to_string()).as_str(),
]);
});
for (idx, l) in node_info.listeners.iter().enumerate() {
if l.starts_with("ring") {
continue;
-11
View File
@@ -308,12 +308,6 @@ struct Cli {
)]
socks5: Option<u16>,
#[arg(
long,
help = t!("core_clap.ipv6_listener").to_string()
)]
ipv6_listener: Option<String>,
#[arg(
long,
help = t!("core_clap.compression").to_string(),
@@ -576,11 +570,6 @@ impl TryFrom<&Cli> for TomlConfigLoader {
f.disable_p2p = cli.disable_p2p;
f.disable_udp_hole_punching = cli.disable_udp_hole_punching;
f.relay_all_peer_rpc = cli.relay_all_peer_rpc;
if let Some(ipv6_listener) = cli.ipv6_listener.as_ref() {
f.ipv6_listener = ipv6_listener
.parse()
.with_context(|| format!("failed to parse ipv6 listener: {}", ipv6_listener))?
}
f.multi_thread = cli.multi_thread;
f.data_compress_algo = match cli.compression.as_str() {
"none" => CompressionAlgoPb::None,
+32 -14
View File
@@ -1,5 +1,6 @@
use std::{fmt::Debug, sync::Arc};
use anyhow::Context;
use async_trait::async_trait;
use tokio::task::JoinSet;
@@ -49,6 +50,10 @@ pub fn get_listener_by_url(
})
}
pub fn is_url_host_ipv6(l: &url::Url) -> bool {
l.host_str().map_or(false, |h| h.contains(':'))
}
#[async_trait]
pub trait TunnelHandlerForListener {
async fn handle_tunnel(&self, tunnel: Box<dyn Tunnel>) -> Result<(), Error>;
@@ -58,7 +63,7 @@ pub trait TunnelHandlerForListener {
impl TunnelHandlerForListener for PeerManager {
#[tracing::instrument]
async fn handle_tunnel(&self, tunnel: Box<dyn Tunnel>) -> Result<(), Error> {
self.add_tunnel_as_server(tunnel).await
self.add_tunnel_as_server(tunnel, true).await
}
}
@@ -113,22 +118,26 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
continue;
};
let ctx = self.global_ctx.clone();
self.add_listener(move || get_listener_by_url(&l, ctx.clone()).unwrap(), true)
.await?;
}
if self.global_ctx.config.get_flags().enable_ipv6 {
let ipv6_listener = self.global_ctx.config.get_flags().ipv6_listener.clone();
let _ = self
.add_listener(
move || {
Box::new(UdpTunnelListener::new(
ipv6_listener.clone().parse().unwrap(),
))
},
let listener = l.clone();
self.add_listener(
move || get_listener_by_url(&listener, ctx.clone()).unwrap(),
true,
)
.await?;
if self.global_ctx.config.get_flags().enable_ipv6 && !is_url_host_ipv6(&l) {
let mut ipv6_listener = l.clone();
ipv6_listener
.set_host(Some("[::]".to_string().as_str()))
.with_context(|| format!("failed to set ipv6 host for listener: {}", l))?;
let ctx = self.global_ctx.clone();
self.add_listener(
move || get_listener_by_url(&ipv6_listener, ctx.clone()).unwrap(),
false,
)
.await?;
}
}
Ok(())
@@ -161,11 +170,11 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
global_ctx.issue_event(GlobalCtxEvent::ListenerAdded(l.local_url()));
}
Err(e) => {
tracing::error!(?e, ?l, "listener listen error");
global_ctx.issue_event(GlobalCtxEvent::ListenerAddFailed(
l.local_url(),
format!("error: {:?}, retry listen later...", e),
));
tracing::error!(?e, ?l, "listener listen error");
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
continue;
}
@@ -217,6 +226,15 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
pub async fn run(&mut self) -> Result<(), Error> {
for listener in &self.listeners {
if listener.must_succ {
// try listen once
let mut l = (listener.creator_fn)();
let _g = self.net_ns.guard();
l.listen()
.await
.with_context(|| format!("failed to listen on {}", l.local_url()))?;
}
self.tasks.spawn(Self::run_listener(
listener.creator_fn.clone(),
self.peer_manager.clone(),
@@ -695,7 +695,8 @@ mod tests {
let (a_ring, b_ring) = crate::tunnel::ring::create_ring_tunnel_pair();
let b_mgr_copy = pm_center.clone();
let s_ret = tokio::spawn(async move { b_mgr_copy.add_tunnel_as_server(b_ring).await });
let s_ret =
tokio::spawn(async move { b_mgr_copy.add_tunnel_as_server(b_ring, true).await });
pma_net1.add_client_tunnel(a_ring).await.unwrap();
+33 -9
View File
@@ -11,7 +11,7 @@ use super::{
peer_conn::{PeerConn, PeerConnId},
PacketRecvChan,
};
use crate::proto::cli::PeerConnInfo;
use crate::{common::scoped_task::ScopedTask, proto::cli::PeerConnInfo};
use crate::{
common::{
error::Error,
@@ -36,7 +36,8 @@ pub struct Peer {
shutdown_notifier: Arc<tokio::sync::Notify>,
default_conn_id: AtomicCell<PeerConnId>,
default_conn_id: Arc<AtomicCell<PeerConnId>>,
default_conn_id_clear_task: ScopedTask<()>,
}
impl Peer {
@@ -88,6 +89,19 @@ impl Peer {
)),
);
let default_conn_id = Arc::new(AtomicCell::new(PeerConnId::default()));
let conns_copy = conns.clone();
let default_conn_id_copy = default_conn_id.clone();
let default_conn_id_clear_task = ScopedTask::from(tokio::spawn(async move {
loop {
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
if conns_copy.len() > 1 {
default_conn_id_copy.store(PeerConnId::default());
}
}
}));
Peer {
peer_node_id,
conns: conns.clone(),
@@ -98,7 +112,8 @@ impl Peer {
close_event_listener,
shutdown_notifier,
default_conn_id: AtomicCell::new(PeerConnId::default()),
default_conn_id,
default_conn_id_clear_task,
}
}
@@ -117,14 +132,19 @@ impl Peer {
return Some(conn.clone());
}
let conn = self.conns.iter().next();
if conn.is_none() {
return None;
// find a conn with the smallest latency
let mut min_latency = std::u64::MAX;
for conn in self.conns.iter() {
let latency = conn.value().get_stats().latency_us;
if latency < min_latency {
min_latency = latency;
self.default_conn_id.store(conn.get_conn_id());
}
}
let conn = conn.unwrap().clone();
self.default_conn_id.store(conn.get_conn_id());
Some(conn)
self.conns
.get(&self.default_conn_id.load())
.map(|conn| conn.clone())
}
pub async fn send_msg(&self, msg: ZCPacket) -> Result<(), Error> {
@@ -158,6 +178,10 @@ impl Peer {
}
ret
}
pub fn get_default_conn_id(&self) -> PeerConnId {
self.default_conn_id.load()
}
}
// pritn on drop
+74 -7
View File
@@ -8,7 +8,7 @@ use std::{
use anyhow::Context;
use async_trait::async_trait;
use dashmap::DashMap;
use dashmap::{DashMap, DashSet};
use tokio::{
sync::{
@@ -23,7 +23,7 @@ use crate::{
compressor::{Compressor as _, DefaultCompressor},
constants::EASYTIER_VERSION,
error::Error,
global_ctx::{ArcGlobalCtx, NetworkIdentity},
global_ctx::{ArcGlobalCtx, GlobalCtxEvent, NetworkIdentity},
stun::StunInfoCollectorTrait,
PeerId,
},
@@ -141,6 +141,9 @@ pub struct PeerManager {
data_compress_algo: CompressorAlgo,
exit_nodes: Vec<Ipv4Addr>,
// conns that are directly connected (which are not hole punched)
directly_connected_conn_map: Arc<DashMap<PeerId, DashSet<uuid::Uuid>>>,
}
impl Debug for PeerManager {
@@ -267,6 +270,8 @@ impl PeerManager {
data_compress_algo,
exit_nodes,
directly_connected_conn_map: Arc::new(DashMap::new()),
}
}
@@ -325,8 +330,48 @@ impl PeerManager {
Ok((peer_id, conn_id))
}
fn add_directly_connected_conn(&self, peer_id: PeerId, conn_id: uuid::Uuid) {
let _ = self
.directly_connected_conn_map
.entry(peer_id)
.or_insert_with(DashSet::new)
.insert(conn_id);
}
pub fn has_directly_connected_conn(&self, peer_id: PeerId) -> bool {
self.directly_connected_conn_map
.get(&peer_id)
.map_or(false, |x| !x.is_empty())
}
async fn start_peer_conn_close_event_handler(&self) {
let dmap = self.directly_connected_conn_map.clone();
let mut event_recv = self.global_ctx.subscribe();
self.tasks.lock().await.spawn(async move {
while let Ok(event) = event_recv.recv().await {
match event {
GlobalCtxEvent::PeerConnRemoved(info) => {
if let Some(set) = dmap.get_mut(&info.peer_id) {
let conn_id = info.conn_id.parse().unwrap();
let old = set.remove(&conn_id);
tracing::info!(
?old,
?info,
"try remove conn id from directly connected map"
);
}
}
_ => {}
}
}
});
}
#[tracing::instrument]
pub async fn try_connect<C>(&self, mut connector: C) -> Result<(PeerId, PeerConnId), Error>
pub async fn try_direct_connect<C>(
&self,
mut connector: C,
) -> Result<(PeerId, PeerConnId), Error>
where
C: TunnelConnector + Debug,
{
@@ -334,18 +379,28 @@ impl PeerManager {
let t = ns
.run_async(|| async move { connector.connect().await })
.await?;
self.add_client_tunnel(t).await
let (peer_id, conn_id) = self.add_client_tunnel(t).await?;
self.add_directly_connected_conn(peer_id, conn_id);
Ok((peer_id, conn_id))
}
#[tracing::instrument]
pub async fn add_tunnel_as_server(&self, tunnel: Box<dyn Tunnel>) -> Result<(), Error> {
pub async fn add_tunnel_as_server(
&self,
tunnel: Box<dyn Tunnel>,
is_directly_connected: bool,
) -> Result<(), Error> {
tracing::info!("add tunnel as server start");
let mut peer = PeerConn::new(self.my_peer_id, self.global_ctx.clone(), tunnel);
peer.do_handshake_as_server().await?;
if peer.get_network_identity().network_name
== self.global_ctx.get_network_identity().network_name
{
let (peer_id, conn_id) = (peer.get_peer_id(), peer.get_conn_id());
self.add_new_peer_conn(peer).await?;
if is_directly_connected {
self.add_directly_connected_conn(peer_id, conn_id);
}
} else {
self.foreign_network_manager.add_peer_conn(peer).await?;
}
@@ -857,9 +912,11 @@ impl PeerManager {
async fn run_clean_peer_without_conn_routine(&self) {
let peer_map = self.peers.clone();
let dmap = self.directly_connected_conn_map.clone();
self.tasks.lock().await.spawn(async move {
loop {
peer_map.clean_peer_without_conn().await;
dmap.retain(|p, v| peer_map.has_peer(*p) && !v.is_empty());
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
}
});
@@ -876,6 +933,8 @@ impl PeerManager {
}
pub async fn run(&self) -> Result<(), Error> {
self.start_peer_conn_close_event_handler().await;
match &self.route_algo_inst {
RouteAlgoInst::Ospf(route) => self.add_route(route.clone()).await,
RouteAlgoInst::None => {}
@@ -924,7 +983,7 @@ impl PeerManager {
self.foreign_network_client.clone()
}
pub fn get_my_info(&self) -> cli::NodeInfo {
pub async fn get_my_info(&self) -> cli::NodeInfo {
cli::NodeInfo {
peer_id: self.my_peer_id,
ipv4_addr: self
@@ -950,6 +1009,7 @@ impl PeerManager {
config: self.global_ctx.config.dump(),
version: EASYTIER_VERSION.to_string(),
feature_flag: Some(self.global_ctx.get_feature_flags()),
ip_list: Some(self.global_ctx.get_ip_collector().collect_ip_addrs().await),
}
}
@@ -958,6 +1018,13 @@ impl PeerManager {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
pub fn get_directly_connections(&self, peer_id: PeerId) -> DashSet<uuid::Uuid> {
self.directly_connected_conn_map
.get(&peer_id)
.map(|x| x.clone())
.unwrap_or_default()
}
}
#[cfg(test)]
@@ -1026,7 +1093,7 @@ mod tests {
tokio::spawn(async move {
client.set_bind_addrs(vec![]);
client_mgr.try_connect(client).await.unwrap();
client_mgr.try_direct_connect(client).await.unwrap();
});
server_mgr
+5
View File
@@ -212,6 +212,11 @@ impl PeerMap {
}
}
pub async fn get_peer_default_conn_id(&self, peer_id: PeerId) -> Option<PeerConnId> {
self.get_peer_by_id(peer_id)
.map(|p| p.get_default_conn_id())
}
pub async fn close_peer_conn(
&self,
peer_id: PeerId,
+13 -2
View File
@@ -32,12 +32,23 @@ impl PeerManagerRpcService {
.await
.iter(),
);
let peer_map = self.peer_manager.get_peer_map();
let mut peer_infos = Vec::new();
for peer in peers {
let mut peer_info = PeerInfo::default();
peer_info.peer_id = peer;
peer_info.default_conn_id = peer_map
.get_peer_default_conn_id(peer)
.await
.map(Into::into);
peer_info.directly_connected_conns = self
.peer_manager
.get_directly_connections(peer)
.into_iter()
.map(Into::into)
.collect();
if let Some(conns) = self.peer_manager.get_peer_map().list_peer_conns(peer).await {
if let Some(conns) = peer_map.list_peer_conns(peer).await {
peer_info.conns = conns;
} else if let Some(conns) = self
.peer_manager
@@ -121,7 +132,7 @@ impl PeerManageRpc for PeerManagerRpcService {
_request: ShowNodeInfoRequest, // Accept request of type HelloRequest
) -> Result<ShowNodeInfoResponse, rpc_types::error::Error> {
Ok(ShowNodeInfoResponse {
node_info: Some(self.peer_manager.get_my_info()),
node_info: Some(self.peer_manager.get_my_info().await),
})
}
}
+1 -1
View File
@@ -45,7 +45,7 @@ pub async fn connect_peer_manager(client: Arc<PeerManager>, server: Arc<PeerMana
});
let b_mgr_copy = server.clone();
tokio::spawn(async move {
b_mgr_copy.add_tunnel_as_server(b_ring).await.unwrap();
b_mgr_copy.add_tunnel_as_server(b_ring, true).await.unwrap();
});
}
+4
View File
@@ -1,6 +1,7 @@
syntax = "proto3";
import "common.proto";
import "peer_rpc.proto";
package cli;
@@ -34,6 +35,8 @@ message PeerConnInfo {
message PeerInfo {
uint32 peer_id = 1;
repeated PeerConnInfo conns = 2;
common.UUID default_conn_id = 3;
repeated common.UUID directly_connected_conns = 4;
}
message ListPeerRequest {}
@@ -79,6 +82,7 @@ message NodeInfo {
string config = 8;
string version = 9;
common.PeerFeatureFlag feature_flag = 10;
peer_rpc.GetIpListResponse ip_list = 11;
}
message ShowNodeInfoRequest {}
+4
View File
@@ -4,10 +4,14 @@ impl PeerRoutePair {
pub fn get_latency_ms(&self) -> Option<f64> {
let mut ret = u64::MAX;
let p = self.peer.as_ref()?;
let default_conn_id = p.default_conn_id.map(|id| id.to_string());
for conn in p.conns.iter() {
let Some(stats) = &conn.stats else {
continue;
};
if default_conn_id == Some(conn.conn_id.to_string()) {
return Some(f64::from(stats.latency_us as u32) / 1000.0);
}
ret = ret.min(stats.latency_us);
}
+1 -1
View File
@@ -18,7 +18,7 @@ message FlagsInConfig {
bool disable_p2p = 11;
bool relay_all_peer_rpc = 12;
bool disable_udp_hole_punching = 13;
string ipv6_listener = 14;
// string ipv6_listener = 14; [deprecated = true]; use -l udp://[::]:12345 instead
bool multi_thread = 15;
CompressionAlgoPb data_compress_algo = 16;
bool bind_device = 17;
+7 -1
View File
@@ -360,7 +360,13 @@ pub(crate) fn setup_sokcet2_ext(
socket2_socket.set_nonblocking(true)?;
socket2_socket.set_reuse_address(true)?;
socket2_socket.bind(&socket2::SockAddr::from(*bind_addr))?;
if let Err(e) = socket2_socket.bind(&socket2::SockAddr::from(*bind_addr)) {
if bind_addr.is_ipv4() {
return Err(e.into());
} else {
tracing::warn!(?e, "bind failed, do not return error for ipv6");
}
}
// #[cfg(all(unix, not(target_os = "solaris"), not(target_os = "illumos")))]
// socket2_socket.set_reuse_port(true)?;
+1 -1
View File
@@ -126,7 +126,7 @@ pub trait TunnelListener: Send {
}
#[async_trait]
#[auto_impl::auto_impl(Box)]
#[auto_impl::auto_impl(Box, &mut)]
pub trait TunnelConnector: Send {
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError>;
fn remote_url(&self) -> url::Url;
+3 -3
View File
@@ -150,9 +150,9 @@ impl TcpTunnelConnector {
&mut self,
addr: SocketAddr,
) -> Result<Box<dyn Tunnel>, super::TunnelError> {
tracing::info!(addr = ?self.addr, "connect tcp start");
tracing::info!(url = ?self.addr, ?addr, "connect tcp start, bind addrs: {:?}", self.bind_addrs);
let stream = TcpStream::connect(addr).await?;
tracing::info!(addr = ?self.addr, "connect tcp succ");
tracing::info!(url = ?self.addr, ?addr, "connect tcp succ");
return get_tunnel_with_tcp_stream(stream, self.addr.clone().into());
}
@@ -190,7 +190,7 @@ impl super::TunnelConnector for TcpTunnelConnector {
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, super::TunnelError> {
let addr =
check_scheme_and_get_socket_addr_ext::<SocketAddr>(&self.addr, "tcp", self.ip_version)?;
if self.bind_addrs.is_empty() || addr.is_ipv6() {
if self.bind_addrs.is_empty() {
self.connect_with_default_bind(addr).await
} else {
self.connect_with_custom_bind(addr).await
+20 -5
View File
@@ -141,12 +141,27 @@ async fn respond_stun_packet(
.encode_into_bytes(resp_msg.clone())
.map_err(|e| anyhow::anyhow!("stun encode error: {:?}", e))?;
socket
.send_to(&rsp_buf, addr.clone())
.await
.with_context(|| "send stun response error")?;
let change_req = req_msg
.get_attribute::<ChangeRequest>()
.map(|r| r.ip() || r.port())
.unwrap_or(false);
tracing::debug!(?addr, ?req_msg, "udp respond stun packet done");
if !change_req {
socket
.send_to(&rsp_buf, addr.clone())
.await
.with_context(|| "send stun response error")?;
} else {
// send from a new udp socket
let socket = if addr.is_ipv4() {
UdpSocket::bind("0.0.0.0:0").await?
} else {
UdpSocket::bind("[::]:0").await?
};
socket.send_to(&rsp_buf, addr.clone()).await?;
}
tracing::debug!(?addr, ?req_msg, ?change_req, "udp respond stun packet done");
Ok(())
}
+71 -10
View File
@@ -2,7 +2,7 @@ pub use std::collections::BTreeMap;
//use BTreeMap
use dashmap::DashMap;
pub use easytier::common::global_ctx::{EventBusSubscriber, GlobalCtxEvent};
use easytier::common::scoped_task::ScopedTask;
use easytier::common::{config::Flags, scoped_task::ScopedTask};
pub use easytier::{
common::config::{ConfigLoader, TomlConfigLoader},
launcher::NetworkInstance,
@@ -158,6 +158,7 @@ fn create_config() -> TomlConfigLoader {
cfg.set_listeners(vec![
"tcp://0.0.0.0:11010".to_string().parse().unwrap(),
"udp://0.0.0.0:11010".to_string().parse().unwrap(),
"tcp://[::]:11010".to_string().parse().unwrap(),
]);
// cfg.set_inst_name("default".to_string());
// cfg.set_inst_name(name);
@@ -292,6 +293,7 @@ pub struct KVNodeInfo {
pub ipv4: String,
pub latency_ms: f64,
pub nat: String, // NAT类型
pub loss_rate: f32,
pub connections: Vec<KVNodeConnectionStats>,
pub version: String,
pub cost: i32,
@@ -330,7 +332,6 @@ pub fn get_network_status() -> KVNetworkStatus {
hostname: route.hostname.clone(),
ipv4,
latency_ms: if let Some(peer) = &pair.peer {
// 类似cli.rs中的get_latency_ms方法
let mut min_latency = u64::MAX;
for conn in &peer.conns {
if let Some(stats) = &conn.stats {
@@ -348,6 +349,15 @@ pub fn get_network_status() -> KVNetworkStatus {
// 如果没有peer信息,则使用路由路径延迟
f64::from(route.path_latency.max(0)) / 1000.0
},
loss_rate: if let Some(peer) = &pair.peer {
let mut total_loss_rate = 0.0;
for conn in &peer.conns {
total_loss_rate += conn.loss_rate;
}
total_loss_rate
} else {
0.0 // 如果没有连接信息,默认为0
},
nat: route.stun_info.as_ref().map_or_else(
|| "Unknown".to_string(),
|stun| {
@@ -557,6 +567,30 @@ pub fn get_running_info() -> String {
.unwrap_or_else(|| "{}".to_string())
}
pub struct FlagsC {
pub default_protocol: String,
pub dev_name: String,
pub enable_encryption: bool,
pub enable_ipv6: bool,
pub mtu: u32,
pub latency_first: bool,
pub enable_exit_node: bool,
pub no_tun: bool,
pub use_smoltcp: bool,
pub relay_network_whitelist: String,
pub disable_p2p: bool,
pub relay_all_peer_rpc: bool,
pub disable_udp_hole_punching: bool,
/// string ipv6_listener = 14; \[deprecated = true\]; use -l udp://\[::\]:12345 instead
pub multi_thread: bool,
pub data_compress_algo: i32,
pub bind_device: bool,
pub enable_kcp_proxy: bool,
pub disable_kcp_input: bool,
pub disable_relay_kcp: bool,
pub proxy_forward_by_system: bool,
}
// 创建服务器
pub fn create_server(
username: String,
@@ -564,20 +598,47 @@ pub fn create_server(
specified_ip: String,
room_name: String,
room_password: String,
severurl: String,
severurl: Vec<String>,
flag:FlagsC,
) {
RT.spawn(async move {
// 创建一个示例配置
let cfg = create_config();
cfg.set_hostname(Option::from(username));
cfg.set_dhcp(enable_dhcp);
let peer_config = PeerConfig {
uri: ("tcp://".to_string() + &severurl).parse().unwrap(),
};
let peer_config2 = PeerConfig {
uri: ("udp://".to_string() + &severurl).parse().unwrap(),
};
cfg.set_peers(vec![peer_config, peer_config2]);
let mut flags = cfg.get_flags();
flags.default_protocol = flag.default_protocol;
flags.dev_name = flag.dev_name;
flags.enable_encryption = flag.enable_encryption;
flags.enable_ipv6 = flag.enable_ipv6;
flags.mtu = flag.mtu;
flags.latency_first = flag.latency_first;
flags.enable_exit_node = flag.enable_exit_node;
flags.no_tun = flag.no_tun;
flags.use_smoltcp = flag.use_smoltcp;
flags.relay_network_whitelist = flag.relay_network_whitelist;
flags.disable_p2p = flag.disable_p2p;
flags.relay_all_peer_rpc = flag.relay_all_peer_rpc;
flags.disable_udp_hole_punching = flag.disable_udp_hole_punching;
flags.multi_thread = flag.multi_thread;
flags.data_compress_algo = flag.data_compress_algo;
flags.bind_device = flag.bind_device;
flags.enable_kcp_proxy = flag.enable_kcp_proxy;
flags.disable_kcp_input = flag.disable_kcp_input;
flags.disable_relay_kcp = flag.disable_relay_kcp;
flags.proxy_forward_by_system = flag.proxy_forward_by_system;
// flags.dev_name = "astral".to_string();
cfg.set_flags(flags);
// 创建TCP和UDP连接配置列表
let mut peer_configs = Vec::new();
// 为每个服务器URL创建TCP和UDP配置
for url in severurl {
peer_configs.push(PeerConfig {
uri: format!("{}", url).parse().unwrap(),
});
}
cfg.set_peers(peer_configs);
if enable_dhcp == false {
// 使用完整路径引用 cidr 模块的 Ipv4Inet
// 解析IP地址和子网掩码
+165 -1
View File
@@ -107,7 +107,8 @@ fn wire__crate__api__simple__create_server_impl(
let api_specified_ip = <String>::sse_decode(&mut deserializer);
let api_room_name = <String>::sse_decode(&mut deserializer);
let api_room_password = <String>::sse_decode(&mut deserializer);
let api_severurl = <String>::sse_decode(&mut deserializer);
let api_severurl = <Vec<String>>::sse_decode(&mut deserializer);
let api_flag = <crate::api::simple::FlagsC>::sse_decode(&mut deserializer);
deserializer.end();
move |context| {
transform_result_sse::<_, ()>((move || {
@@ -119,6 +120,7 @@ fn wire__crate__api__simple__create_server_impl(
api_room_name,
api_room_password,
api_severurl,
api_flag,
);
})?;
Ok(output_ok)
@@ -496,6 +498,13 @@ impl SseDecode for bool {
}
}
impl SseDecode for f32 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
deserializer.cursor.read_f32::<NativeEndian>().unwrap()
}
}
impl SseDecode for f64 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@@ -503,6 +512,54 @@ impl SseDecode for f64 {
}
}
impl SseDecode for crate::api::simple::FlagsC {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut var_defaultProtocol = <String>::sse_decode(deserializer);
let mut var_devName = <String>::sse_decode(deserializer);
let mut var_enableEncryption = <bool>::sse_decode(deserializer);
let mut var_enableIpv6 = <bool>::sse_decode(deserializer);
let mut var_mtu = <u32>::sse_decode(deserializer);
let mut var_latencyFirst = <bool>::sse_decode(deserializer);
let mut var_enableExitNode = <bool>::sse_decode(deserializer);
let mut var_noTun = <bool>::sse_decode(deserializer);
let mut var_useSmoltcp = <bool>::sse_decode(deserializer);
let mut var_relayNetworkWhitelist = <String>::sse_decode(deserializer);
let mut var_disableP2P = <bool>::sse_decode(deserializer);
let mut var_relayAllPeerRpc = <bool>::sse_decode(deserializer);
let mut var_disableUdpHolePunching = <bool>::sse_decode(deserializer);
let mut var_multiThread = <bool>::sse_decode(deserializer);
let mut var_dataCompressAlgo = <i32>::sse_decode(deserializer);
let mut var_bindDevice = <bool>::sse_decode(deserializer);
let mut var_enableKcpProxy = <bool>::sse_decode(deserializer);
let mut var_disableKcpInput = <bool>::sse_decode(deserializer);
let mut var_disableRelayKcp = <bool>::sse_decode(deserializer);
let mut var_proxyForwardBySystem = <bool>::sse_decode(deserializer);
return crate::api::simple::FlagsC {
default_protocol: var_defaultProtocol,
dev_name: var_devName,
enable_encryption: var_enableEncryption,
enable_ipv6: var_enableIpv6,
mtu: var_mtu,
latency_first: var_latencyFirst,
enable_exit_node: var_enableExitNode,
no_tun: var_noTun,
use_smoltcp: var_useSmoltcp,
relay_network_whitelist: var_relayNetworkWhitelist,
disable_p2p: var_disableP2P,
relay_all_peer_rpc: var_relayAllPeerRpc,
disable_udp_hole_punching: var_disableUdpHolePunching,
multi_thread: var_multiThread,
data_compress_algo: var_dataCompressAlgo,
bind_device: var_bindDevice,
enable_kcp_proxy: var_enableKcpProxy,
disable_kcp_input: var_disableKcpInput,
disable_relay_kcp: var_disableRelayKcp,
proxy_forward_by_system: var_proxyForwardBySystem,
};
}
}
impl SseDecode for i32 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@@ -547,6 +604,7 @@ impl SseDecode for crate::api::simple::KVNodeInfo {
let mut var_ipv4 = <String>::sse_decode(deserializer);
let mut var_latencyMs = <f64>::sse_decode(deserializer);
let mut var_nat = <String>::sse_decode(deserializer);
let mut var_lossRate = <f32>::sse_decode(deserializer);
let mut var_connections =
<Vec<crate::api::simple::KVNodeConnectionStats>>::sse_decode(deserializer);
let mut var_version = <String>::sse_decode(deserializer);
@@ -556,6 +614,7 @@ impl SseDecode for crate::api::simple::KVNodeInfo {
ipv4: var_ipv4,
latency_ms: var_latencyMs,
nat: var_nat,
loss_rate: var_lossRate,
connections: var_connections,
version: var_version,
cost: var_cost,
@@ -599,6 +658,18 @@ impl SseDecode for Vec<Route> {
}
}
impl SseDecode for Vec<String> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut len_ = <i32>::sse_decode(deserializer);
let mut ans_ = vec![];
for idx_ in 0..len_ {
ans_.push(<String>::sse_decode(deserializer));
}
return ans_;
}
}
impl SseDecode for Vec<crate::api::simple::KVNodeConnectionStats> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@@ -646,6 +717,13 @@ impl SseDecode for (Vec<PeerInfo>, Vec<Route>) {
}
}
impl SseDecode for u32 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
deserializer.cursor.read_u32::<NativeEndian>().unwrap()
}
}
impl SseDecode for u64 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@@ -769,6 +847,40 @@ impl flutter_rust_bridge::IntoIntoDart<FrbWrapper<Route>> for Route {
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::api::simple::FlagsC {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[
self.default_protocol.into_into_dart().into_dart(),
self.dev_name.into_into_dart().into_dart(),
self.enable_encryption.into_into_dart().into_dart(),
self.enable_ipv6.into_into_dart().into_dart(),
self.mtu.into_into_dart().into_dart(),
self.latency_first.into_into_dart().into_dart(),
self.enable_exit_node.into_into_dart().into_dart(),
self.no_tun.into_into_dart().into_dart(),
self.use_smoltcp.into_into_dart().into_dart(),
self.relay_network_whitelist.into_into_dart().into_dart(),
self.disable_p2p.into_into_dart().into_dart(),
self.relay_all_peer_rpc.into_into_dart().into_dart(),
self.disable_udp_hole_punching.into_into_dart().into_dart(),
self.multi_thread.into_into_dart().into_dart(),
self.data_compress_algo.into_into_dart().into_dart(),
self.bind_device.into_into_dart().into_dart(),
self.enable_kcp_proxy.into_into_dart().into_dart(),
self.disable_kcp_input.into_into_dart().into_dart(),
self.disable_relay_kcp.into_into_dart().into_dart(),
self.proxy_forward_by_system.into_into_dart().into_dart(),
]
.into_dart()
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::simple::FlagsC {}
impl flutter_rust_bridge::IntoIntoDart<crate::api::simple::FlagsC> for crate::api::simple::FlagsC {
fn into_into_dart(self) -> crate::api::simple::FlagsC {
self
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::api::simple::KVNetworkStatus {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
@@ -822,6 +934,7 @@ impl flutter_rust_bridge::IntoDart for crate::api::simple::KVNodeInfo {
self.ipv4.into_into_dart().into_dart(),
self.latency_ms.into_into_dart().into_dart(),
self.nat.into_into_dart().into_dart(),
self.loss_rate.into_into_dart().into_dart(),
self.connections.into_into_dart().into_dart(),
self.version.into_into_dart().into_dart(),
self.cost.into_into_dart().into_dart(),
@@ -928,6 +1041,13 @@ impl SseEncode for bool {
}
}
impl SseEncode for f32 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
serializer.cursor.write_f32::<NativeEndian>(self).unwrap();
}
}
impl SseEncode for f64 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@@ -935,6 +1055,32 @@ impl SseEncode for f64 {
}
}
impl SseEncode for crate::api::simple::FlagsC {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<String>::sse_encode(self.default_protocol, serializer);
<String>::sse_encode(self.dev_name, serializer);
<bool>::sse_encode(self.enable_encryption, serializer);
<bool>::sse_encode(self.enable_ipv6, serializer);
<u32>::sse_encode(self.mtu, serializer);
<bool>::sse_encode(self.latency_first, serializer);
<bool>::sse_encode(self.enable_exit_node, serializer);
<bool>::sse_encode(self.no_tun, serializer);
<bool>::sse_encode(self.use_smoltcp, serializer);
<String>::sse_encode(self.relay_network_whitelist, serializer);
<bool>::sse_encode(self.disable_p2p, serializer);
<bool>::sse_encode(self.relay_all_peer_rpc, serializer);
<bool>::sse_encode(self.disable_udp_hole_punching, serializer);
<bool>::sse_encode(self.multi_thread, serializer);
<i32>::sse_encode(self.data_compress_algo, serializer);
<bool>::sse_encode(self.bind_device, serializer);
<bool>::sse_encode(self.enable_kcp_proxy, serializer);
<bool>::sse_encode(self.disable_kcp_input, serializer);
<bool>::sse_encode(self.disable_relay_kcp, serializer);
<bool>::sse_encode(self.proxy_forward_by_system, serializer);
}
}
impl SseEncode for i32 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@@ -968,6 +1114,7 @@ impl SseEncode for crate::api::simple::KVNodeInfo {
<String>::sse_encode(self.ipv4, serializer);
<f64>::sse_encode(self.latency_ms, serializer);
<String>::sse_encode(self.nat, serializer);
<f32>::sse_encode(self.loss_rate, serializer);
<Vec<crate::api::simple::KVNodeConnectionStats>>::sse_encode(self.connections, serializer);
<String>::sse_encode(self.version, serializer);
<i32>::sse_encode(self.cost, serializer);
@@ -1004,6 +1151,16 @@ impl SseEncode for Vec<Route> {
}
}
impl SseEncode for Vec<String> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<i32>::sse_encode(self.len() as _, serializer);
for item in self {
<String>::sse_encode(item, serializer);
}
}
}
impl SseEncode for Vec<crate::api::simple::KVNodeConnectionStats> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@@ -1042,6 +1199,13 @@ impl SseEncode for (Vec<PeerInfo>, Vec<Route>) {
}
}
impl SseEncode for u32 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
serializer.cursor.write_u32::<NativeEndian>(self).unwrap();
}
}
impl SseEncode for u64 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
+58 -58
View File
@@ -6,7 +6,7 @@ packages:
description:
name: _fe_analyzer_shared
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "67.0.0"
adaptive_number:
@@ -14,7 +14,7 @@ packages:
description:
name: adaptive_number
sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
analyzer:
@@ -22,7 +22,7 @@ packages:
description:
name: analyzer
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "6.4.1"
args:
@@ -30,7 +30,7 @@ packages:
description:
name: args
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
async:
@@ -38,7 +38,7 @@ packages:
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.13.0"
boolean_selector:
@@ -46,7 +46,7 @@ packages:
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
collection:
@@ -54,7 +54,7 @@ packages:
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
convert:
@@ -62,7 +62,7 @@ packages:
description:
name: convert
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
coverage:
@@ -70,7 +70,7 @@ packages:
description:
name: coverage
sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
crypto:
@@ -78,7 +78,7 @@ packages:
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
ed25519_edwards:
@@ -86,7 +86,7 @@ packages:
description:
name: ed25519_edwards
sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
file:
@@ -94,7 +94,7 @@ packages:
description:
name: file
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "6.1.4"
fixnum:
@@ -102,7 +102,7 @@ packages:
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
frontend_server_client:
@@ -110,7 +110,7 @@ packages:
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
github:
@@ -118,7 +118,7 @@ packages:
description:
name: github
sha256: "9966bc13bf612342e916b0a343e95e5f046c88f602a14476440e9b75d2295411"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "9.17.0"
glob:
@@ -126,7 +126,7 @@ packages:
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
hex:
@@ -134,7 +134,7 @@ packages:
description:
name: hex
sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
http:
@@ -142,7 +142,7 @@ packages:
description:
name: http
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
http_multi_server:
@@ -150,7 +150,7 @@ packages:
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
@@ -158,7 +158,7 @@ packages:
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
io:
@@ -166,7 +166,7 @@ packages:
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
@@ -174,7 +174,7 @@ packages:
description:
name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
json_annotation:
@@ -182,7 +182,7 @@ packages:
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
lints:
@@ -190,7 +190,7 @@ packages:
description:
name: lints
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
logging:
@@ -198,7 +198,7 @@ packages:
description:
name: logging
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
matcher:
@@ -206,7 +206,7 @@ packages:
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.12.17"
meta:
@@ -214,7 +214,7 @@ packages:
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.16.0"
mime:
@@ -222,7 +222,7 @@ packages:
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
node_preamble:
@@ -230,7 +230,7 @@ packages:
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
package_config:
@@ -238,7 +238,7 @@ packages:
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
@@ -246,7 +246,7 @@ packages:
description:
name: path
sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.8.0"
petitparser:
@@ -254,7 +254,7 @@ packages:
description:
name: petitparser
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "5.4.0"
pool:
@@ -262,7 +262,7 @@ packages:
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
pub_semver:
@@ -270,7 +270,7 @@ packages:
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
shelf:
@@ -278,7 +278,7 @@ packages:
description:
name: shelf
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
shelf_packages_handler:
@@ -286,7 +286,7 @@ packages:
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
@@ -294,7 +294,7 @@ packages:
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
@@ -302,7 +302,7 @@ packages:
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
source_map_stack_trace:
@@ -310,7 +310,7 @@ packages:
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
@@ -318,7 +318,7 @@ packages:
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span:
@@ -326,7 +326,7 @@ packages:
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
stack_trace:
@@ -334,7 +334,7 @@ packages:
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
@@ -342,7 +342,7 @@ packages:
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
@@ -350,7 +350,7 @@ packages:
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
@@ -358,7 +358,7 @@ packages:
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test:
@@ -366,7 +366,7 @@ packages:
description:
name: test
sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.25.15"
test_api:
@@ -374,7 +374,7 @@ packages:
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.7.4"
test_core:
@@ -382,7 +382,7 @@ packages:
description:
name: test_core
sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.6.8"
toml:
@@ -390,7 +390,7 @@ packages:
description:
name: toml
sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.14.0"
typed_data:
@@ -398,7 +398,7 @@ packages:
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version:
@@ -406,7 +406,7 @@ packages:
description:
name: version
sha256: "2307e23a45b43f96469eeab946208ed63293e8afca9c28cd8b5241ff31c55f55"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
vm_service:
@@ -414,7 +414,7 @@ packages:
description:
name: vm_service
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "15.0.0"
watcher:
@@ -422,7 +422,7 @@ packages:
description:
name: watcher
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web:
@@ -430,7 +430,7 @@ packages:
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
@@ -438,7 +438,7 @@ packages:
description:
name: web_socket
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.1.6"
web_socket_channel:
@@ -446,7 +446,7 @@ packages:
description:
name: web_socket_channel
sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
webkit_inspection_protocol:
@@ -454,7 +454,7 @@ packages:
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
yaml:
@@ -462,7 +462,7 @@ packages:
description:
name: yaml
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
sdks:
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

-38
View File
@@ -1,38 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="astral">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>astral</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>
-35
View File
@@ -1,35 +0,0 @@
{
"name": "astral",
"short_name": "astral",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/Icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icons/Icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
-4
View File
@@ -75,10 +75,6 @@ set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
# 添加管理员权限要求(修正路径格式)
target_link_options(${BINARY_NAME} PRIVATE
"LINKER:/MANIFESTUAC:level='requireAdministrator' uiAccess='false'"
$<$<C_COMPILER_ID:MSVC>:"LINKER:/MANIFESTINPUT:${CMAKE_CURRENT_SOURCE_DIR}/runner/runner.exe.manifest">)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
+1 -1
View File
@@ -26,7 +26,7 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTT
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
SET_TARGET_PROPERTIES(${BINARY_NAME} PROPERTIES LINK_FLAGS "/MANIFESTUAC:\"level='requireAdministrator' uiAccess='false'\" /SUBSYSTEM:WINDOWS")
# Disable Windows macros that collide with C++ standard library functions.
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
+4 -4
View File
@@ -90,12 +90,12 @@ BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "com.example" "\0"
VALUE "FileDescription", "fltier" "\0"
VALUE "FileDescription", "Astral" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "fltier" "\0"
VALUE "InternalName", "Astral" "\0"
VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0"
VALUE "OriginalFilename", "fltier.exe" "\0"
VALUE "ProductName", "fltier" "\0"
VALUE "OriginalFilename", "Astral.exe" "\0"
VALUE "ProductName", "Astral" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
END
END
+33 -3
View File
@@ -1,12 +1,42 @@
#include <flutter/dart_project.h>
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <windows.h>
#include <string>
#include "flutter_window.h"
#include "utils.h"
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) {
const int bufferSize = 256;
wchar_t windowTitle[bufferSize];
if (GetWindowTextW(hwnd, windowTitle, bufferSize)) {
if (_wcsicmp(windowTitle, L"Astral") == 0) {
*((HWND*)lParam) = hwnd;
return FALSE;
}
}
return TRUE;
}
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command) {
HANDLE hMutex = CreateMutexW(NULL, TRUE, L"AstralAppSingleInstanceMutex");
if (GetLastError() == ERROR_ALREADY_EXISTS) {
HWND hWnd = NULL;
EnumWindows(EnumWindowsProc, (LPARAM)&hWnd);
if (hWnd != NULL) {
if (IsIconic(hWnd)) {
ShowWindow(hWnd, SW_RESTORE);
}
SetForegroundWindow(hWnd);
}
CloseHandle(hMutex);
return 0;
}
// Attach to console when present (e.g., 'flutter run') or create a
// new console when running with a debugger.
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
@@ -27,7 +57,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
FlutterWindow window(project);
Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720);
if (!window.Create(L"fltier", origin, size)) {
if (!window.Create(L"Astral", origin, size)) {
return EXIT_FAILURE;
}
window.SetQuitOnClose(true);
@@ -38,6 +68,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
::DispatchMessage(&msg);
}
::CoUninitialize();
CloseHandle(hMutex);
return EXIT_SUCCESS;
}
+14 -7
View File
@@ -1,15 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
<heapType xmlns="http://schemas.microsoft.com/SMI/2020/WindowsSettings">SegmentHeap</heapType>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
@@ -18,4 +15,14 @@
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel
level="requireAdministrator"
uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>