Compare commits

..

2 Commits

Author SHA1 Message Date
chen08209
be8cf039ee Support custom text scaling
Optimize the display of different text scale

Optimize windows setup experience
2025-04-25 17:38:26 +08:00
chen08209
30ee6889ab Optimize android tv experience
Optimize hyperOS freeform window

Add developer mode

Update core

Optimize more details
2025-04-22 16:05:24 +08:00
238 changed files with 10028 additions and 15729 deletions

View File

@@ -19,7 +19,7 @@ jobs:
os: windows-latest
arch: amd64
- platform: linux
os: ubuntu-22.04
os: ubuntu-latest
arch: amd64
- platform: macos
os: macos-13
@@ -63,19 +63,11 @@ jobs:
cache-dependency-path: |
core/go.sum
- name: Setup Flutter Master
if: startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')
uses: subosito/flutter-action@v2
with:
channel: 'master'
cache: true
- name: Setup Flutter
if: ${{ !(startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')) }}
uses: subosito/flutter-action@v2
with:
channel: 'stable'
channel: ${{ (startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')) && 'master' || 'stable' }}
cache: true
# flutter-version: 3.29.3
- name: Get Flutter Dependency
run: flutter pub get

View File

@@ -1,59 +1,3 @@
## v0.8.85
- Support override script
- Support proxies search
- Support svg display
- Optimize config persistence
- Add some scenes auto close connections
- Update core
- Optimize more details
## v0.8.84
- Fix windows service verify issues
- Update changelog
## v0.8.83
- Add windows server mode start process verify
- Add linux deb dependencies
- Add backup recovery strategy select
- Support custom text scaling
- Optimize the display of different text scale
- Optimize windows setup experience
- Optimize startTun performance
- Optimize android tv experience
- Optimize default option
- Optimize computed text size
- Optimize hyperOS freeform window
- Add developer mode
- Update core
- Optimize more details
- Add issues template
- Update changelog
## v0.8.82
- Optimize android vpn performance

View File

@@ -41,8 +41,8 @@ on Mobile:
⚠️ Make sure to install the following dependencies before using them
```bash
sudo apt-get install libayatana-appindicator3-dev
sudo apt-get install libkeybinder-3.0-dev
sudo apt-get install appindicator3-0.1 libappindicator3-dev
sudo apt-get install keybinder-3.0
```
### Android

View File

@@ -41,8 +41,8 @@ on Mobile:
⚠️ 使用前请确保安装以下依赖
```bash
sudo apt-get install libayatana-appindicator3-dev
sudo apt-get install libkeybinder-3.0-dev
sudo apt-get install appindicator3-0.1 libappindicator3-dev
sudo apt-get install keybinder-3.0
```
### Android

View File

@@ -1,8 +1 @@
include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- lib/l10n/intl/**
linter:
rules:
prefer_single_quotes: true

97
android/app/build.gradle Normal file
View File

@@ -0,0 +1,97 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
def defStoreFile = file("keystore.jks")
def defStorePassword = localProperties.getProperty('storePassword')
def defKeyAlias = localProperties.getProperty('keyAlias')
def defKeyPassword = localProperties.getProperty('keyPassword')
def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias != null && defKeyPassword != null
android {
namespace "com.follow.clash"
compileSdk 35
ndkVersion = "28.0.13004108"
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
signingConfigs {
if (isRelease) {
release {
storeFile defStoreFile
storePassword defStorePassword
keyAlias defKeyAlias
keyPassword defKeyPassword
}
}
}
defaultConfig {
applicationId "com.follow.clash"
minSdkVersion 21
targetSdkVersion 35
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
debug {
minifyEnabled false
applicationIdSuffix '.debug'
}
release {
if (isRelease) {
signingConfig signingConfigs.release
} else {
signingConfig signingConfigs.debug
}
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
}
}
}
flutter {
source '../..'
}
dependencies {
implementation project(":core")
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation 'com.google.code.gson:gson:2.10.1'
implementation("com.android.tools.smali:smali-dexlib2:3.0.9") {
exclude group: "com.google.guava", module: "guava"
}
}

View File

@@ -1,93 +0,0 @@
import java.util.Properties
plugins {
id("com.android.application")
id("kotlin-android")
id("dev.flutter.flutter-gradle-plugin")
}
val localPropertiesFile = rootProject.file("local.properties")
val localProperties = Properties().apply {
if (localPropertiesFile.exists()) {
localPropertiesFile.inputStream().use { load(it) }
}
}
val mStoreFile: File = file("keystore.jks")
val mStorePassword: String? = localProperties.getProperty("storePassword")
val mKeyAlias: String? = localProperties.getProperty("keyAlias")
val mKeyPassword: String? = localProperties.getProperty("keyPassword")
val isRelease = mStoreFile.exists()
&& mStorePassword != null
&& mKeyAlias != null
&& mKeyPassword != null
android {
namespace = "com.follow.clash"
compileSdk = 35
ndkVersion = "28.0.13004108"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
applicationId = "com.follow.clash"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
signingConfigs {
if (isRelease) {
create("release") {
storeFile = mStoreFile
storePassword = mStorePassword
keyAlias = mKeyAlias
keyPassword = mKeyPassword
}
}
}
buildTypes {
debug {
isMinifyEnabled = false
applicationIdSuffix = ".debug"
}
release {
isMinifyEnabled = true
isDebuggable = false
signingConfig = if (isRelease) {
signingConfigs.getByName("release")
} else {
signingConfigs.getByName("debug")
}
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
flutter {
source = "../.."
}
dependencies {
implementation(project(":core"))
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("com.google.code.gson:gson:2.10.1")
implementation("com.android.tools.smali:smali-dexlib2:3.0.9") {
exclude(group = "com.google.guava", module = "guava")
}
}

View File

@@ -7,9 +7,6 @@
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@@ -25,7 +22,6 @@
<application
android:name=".FlClashApplication"
android:banner="@mipmap/ic_banner"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="FlClash">
@@ -92,7 +88,7 @@
<service
android:name=".services.FlClashTileService"
android:exported="true"
android:icon="@drawable/ic"
android:icon="@drawable/ic_stat_name"
android:label="FlClash"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
tools:targetApi="n">

View File

@@ -7,10 +7,6 @@ import com.follow.clash.plugins.VpnPlugin
import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
@@ -37,15 +33,6 @@ object GlobalState {
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
}
fun syncStatus() {
CoroutineScope(Dispatchers.Default).launch {
val status = getCurrentVPNPlugin()?.getStatus() ?: false
withContext(Dispatchers.Main){
runState.value = if (status) RunState.START else RunState.STOP
}
}
}
suspend fun getText(text: String): String {
return getCurrentAppPlugin()?.getText(text) ?: ""
}

View File

@@ -17,7 +17,6 @@ class MainActivity : FlutterActivity() {
override fun onDestroy() {
GlobalState.flutterEngine = null
GlobalState.runState.value = RunState.STOP
super.onDestroy()
}
}

View File

@@ -104,7 +104,7 @@ fun ConnectivityManager.resolveDns(network: Network?): List<String> {
fun InetAddress.asSocketAddressText(port: Int): String {
return when (this) {
is Inet6Address ->
"[${numericToTextFormat(this)}]:$port"
"[${numericToTextFormat(this.address)}]:$port"
is Inet4Address ->
"${this.hostAddress}:$port"
@@ -141,8 +141,7 @@ fun Context.getActionPendingIntent(action: String): PendingIntent {
}
}
private fun numericToTextFormat(address: Inet6Address): String {
val src = address.address
private fun numericToTextFormat(src: ByteArray): String {
val sb = StringBuilder(39)
for (i in 0 until 8) {
sb.append(
@@ -155,10 +154,6 @@ private fun numericToTextFormat(address: Inet6Address): String {
sb.append(":")
}
}
if (address.scopeId > 0) {
sb.append("%")
sb.append(address.scopeId)
}
return sb.toString()
}

View File

@@ -51,7 +51,6 @@ data object ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}
}
private fun handleDestroy() {
GlobalState.destroyServiceEngine()
}

View File

@@ -100,7 +100,6 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}
fun handleStart(options: VpnOptions): Boolean {
onUpdateNetwork();
if (options.enable != this.options?.enable) {
this.flClashService = null
}
@@ -201,13 +200,6 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
timerJob = null
}
suspend fun getStatus(): Boolean? {
return withContext(Dispatchers.Default) {
flutterMethodChannel.awaitResult<Boolean>("status", null)
}
}
private fun handleStartService() {
if (flClashService == null) {
bindService()
@@ -256,9 +248,9 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
GlobalState.runLock.withLock {
if (GlobalState.runState.value == RunState.STOP) return
GlobalState.runState.value = RunState.STOP
flClashService?.stop()
stopForegroundJob()
Core.stopTun()
flClashService?.stop()
GlobalState.handleTryDestroy()
}
}

View File

@@ -16,6 +16,7 @@ import com.follow.clash.MainActivity
import com.follow.clash.R
import com.follow.clash.extensions.getActionPendingIntent
import com.follow.clash.models.VpnOptions
import io.flutter.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
@@ -53,7 +54,7 @@ fun Service.createFlClashNotificationBuilder(): Deferred<NotificationCompat.Buil
this@createFlClashNotificationBuilder, GlobalState.NOTIFICATION_CHANNEL
)
) {
setSmallIcon(R.drawable.ic)
setSmallIcon(R.drawable.ic_stat_name)
setContentTitle("FlClash")
setContentIntent(pendingIntent)
setCategory(NotificationCompat.CATEGORY_SERVICE)
@@ -75,8 +76,9 @@ fun Service.startForeground(notification: Notification) {
val manager = getSystemService(NotificationManager::class.java)
var channel = manager?.getNotificationChannel(GlobalState.NOTIFICATION_CHANNEL)
if (channel == null) {
Log.d("[FlClash]","createNotificationChannel===>")
channel = NotificationChannel(
GlobalState.NOTIFICATION_CHANNEL, "SERVICE_CHANNEL", NotificationManager.IMPORTANCE_LOW
GlobalState.NOTIFICATION_CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW
)
manager?.createNotificationChannel(channel)
}

View File

@@ -33,7 +33,6 @@ class FlClashTileService : TileService() {
override fun onStartListening() {
super.onStartListening()
GlobalState.syncStatus()
GlobalState.runState.value?.let { updateTile(it) }
GlobalState.runState.observeForever(observer)
}

View File

@@ -1,6 +1,7 @@
package com.follow.clash.services
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Intent
import android.net.ProxyInfo
import android.net.VpnService
@@ -33,10 +34,6 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
if (options.ipv4Address.isNotEmpty()) {
val cidr = options.ipv4Address.toCIDR()
addAddress(cidr.address, cidr.prefixLength)
Log.d(
"addAddress",
"address: ${cidr.address} prefixLength:${cidr.prefixLength}"
)
val routeAddress = options.getIpv4RouteAddress()
if (routeAddress.isNotEmpty()) {
try {
@@ -53,39 +50,26 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
} else {
addRoute("0.0.0.0", 0)
}
} else {
addRoute("0.0.0.0", 0)
}
try {
if (options.ipv6Address.isNotEmpty()) {
val cidr = options.ipv6Address.toCIDR()
Log.d(
"addAddress6",
"address: ${cidr.address} prefixLength:${cidr.prefixLength}"
)
addAddress(cidr.address, cidr.prefixLength)
val routeAddress = options.getIpv6RouteAddress()
if (routeAddress.isNotEmpty()) {
try {
routeAddress.forEach { i ->
Log.d(
"addRoute6",
"address: ${i.address} prefixLength:${i.prefixLength}"
)
addRoute(i.address, i.prefixLength)
}
} catch (_: Exception) {
addRoute("::", 0)
if (options.ipv6Address.isNotEmpty()) {
val cidr = options.ipv6Address.toCIDR()
addAddress(cidr.address, cidr.prefixLength)
val routeAddress = options.getIpv6RouteAddress()
if (routeAddress.isNotEmpty()) {
try {
routeAddress.forEach { i ->
Log.d(
"addRoute6",
"address: ${i.address} prefixLength:${i.prefixLength}"
)
addRoute(i.address, i.prefixLength)
}
} else {
} catch (_: Exception) {
addRoute("::", 0)
}
} else {
addRoute("::", 0)
}
}catch (_:Exception){
Log.d(
"addAddress6",
"IPv6 is not supported."
)
}
addDnsServer(options.dnsServerAddress)
setMtu(9000)

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,17 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="240dp"
android:height="240dp"
android:viewportWidth="240"
android:viewportHeight="240"
tools:ignore="VectorRaster">
<path
android:pathData="M48.1,80.89L168.44,11.41c11.08,-6.4 25.24,-2.6 31.64,8.48 0,0 0,0 0,0h0c6.4,11.08 2.6,25.24 -8.48,31.64 0,0 0,0 0,0l-120.34,69.48c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64 0,0 0,0 0,0Z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M78.98,134.37l60.18,-34.74c11.07,-6.39 25.23,-2.59 31.63,8.48h0c6.4,11.07 2.61,25.24 -8.47,31.64l-60.18,34.74c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64h0Z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M109.86,187.86h0c11.08,-6.4 25.24,-2.6 31.64,8.48 0,0 0,0 0,0h0c6.4,11.08 2.6,25.24 -8.48,31.64 0,0 0,0 0,0h0c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64 0,0 0,0 0,0Z"
android:fillColor="#FFFFFF"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

33
android/build.gradle Normal file
View File

@@ -0,0 +1,33 @@
buildscript {
ext.kotlin_version = "${kotlin_version}"
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:$agp_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
project.evaluationDependsOn(':core')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@@ -1,22 +0,0 @@
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)
}

View File

@@ -1,4 +1,4 @@
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.argumentsWithVarargAsSingleArray
import com.android.build.gradle.tasks.MergeSourceSetFolders
plugins {
id("com.android.library")
@@ -12,11 +12,11 @@ android {
defaultConfig {
minSdk = 21
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isJniDebuggable = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
@@ -37,17 +37,13 @@ android {
}
}
kotlinOptions {
jvmTarget = "17"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
implementation("androidx.annotation:annotation-jvm:1.9.1")
}
val copyNativeLibs by tasks.register<Copy>("copyNativeLibs") {
@@ -62,4 +58,8 @@ afterEvaluate {
tasks.named("preBuild") {
dependsOn(copyNativeLibs)
}
}
dependencies {
implementation("androidx.core:core-ktx:1.16.0")
}

View File

View File

@@ -4,8 +4,6 @@ project("core")
message("CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}")
message("CMAKE_BUILD_TYPE ${CMAKE_BUILD_TYPE}")
if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
add_compile_options(-O3)
@@ -23,8 +21,6 @@ if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
-Wl,--strip-all
-Wl,--exclude-libs=ALL
)
add_compile_options(-fvisibility=hidden -fvisibility-inlines-hidden)
endif ()
set(LIB_CLASH_PATH "${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libclash.so")

View File

@@ -1,18 +1,21 @@
#include <jni.h>
#ifdef LIBCLASH
#include <jni.h>
#include <string>
#include "jni_helper.h"
#include "libclash.h"
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject, const jint fd, jobject cb) {
const auto interface = new_global(cb);
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject thiz, jint fd, jobject cb) {
auto interface = new_global(cb);
startTUN(fd, interface);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_stopTun(JNIEnv *) {
Java_com_follow_clash_core_Core_stopTun(JNIEnv *env, jobject thiz) {
stopTun();
}
@@ -23,50 +26,50 @@ static jmethodID m_tun_interface_resolve_process;
static void release_jni_object_impl(void *obj) {
ATTACH_JNI();
del_global(static_cast<jobject>(obj));
del_global((jobject) obj);
}
static void call_tun_interface_protect_impl(void *tun_interface, const int fd) {
static void call_tun_interface_protect_impl(void *tun_interface, int fd) {
ATTACH_JNI();
env->CallVoidMethod(static_cast<jobject>(tun_interface),
m_tun_interface_protect,
fd);
env->CallVoidMethod((jobject) tun_interface,
(jmethodID) m_tun_interface_protect,
(jint) fd);
}
static const char *
static const char*
call_tun_interface_resolve_process_impl(void *tun_interface, int protocol,
const char *source,
const char *target,
const int uid) {
const char *source,
const char *target,
int uid) {
ATTACH_JNI();
const auto packageName = reinterpret_cast<jstring>(env->CallObjectMethod(static_cast<jobject>(tun_interface),
m_tun_interface_resolve_process,
protocol,
new_string(source),
new_string(target),
uid));
jstring packageName = (jstring)env->CallObjectMethod((jobject) tun_interface,
(jmethodID) m_tun_interface_resolve_process,
(jint) protocol,
(jstring) new_string(source),
(jstring) new_string(target),
(jint) uid);
return get_string(packageName);
}
extern "C"
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *) {
JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = nullptr;
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
initialize_jni(vm, env);
const auto c_tun_interface = find_class("com/follow/clash/core/TunInterface");
jclass c_tun_interface = find_class("com/follow/clash/core/TunInterface");
m_tun_interface_protect = find_method(c_tun_interface, "protect", "(I)V");
m_tun_interface_resolve_process = find_method(c_tun_interface, "resolverProcess",
"(ILjava/lang/String;Ljava/lang/String;I)Ljava/lang/String;");
"(ILjava/lang/String;Ljava/lang/String;I)Ljava/lang/String;");
registerCallbacks(&call_tun_interface_protect_impl,
&call_tun_interface_resolve_process_impl,
&release_jni_object_impl);
return JNI_VERSION_1_6;
}
#endif
#endif

View File

@@ -1,6 +1,5 @@
#include "jni_helper.h"
#include <cstdlib>
#include <malloc.h>
#include <cstring>
@@ -13,7 +12,7 @@ static jmethodID m_get_bytes;
void initialize_jni(JavaVM *vm, JNIEnv *env) {
global_vm = vm;
c_string = reinterpret_cast<jclass>(new_global(find_class("java/lang/String")));
c_string = (jclass) new_global(find_class("java/lang/String"));
m_new_string = find_method(c_string, "<init>", "([B)V");
m_get_bytes = find_method(c_string, "getBytes", "()[B");
}
@@ -23,23 +22,23 @@ JavaVM *global_java_vm() {
}
char *jni_get_string(JNIEnv *env, jstring str) {
const auto array = reinterpret_cast<jbyteArray>(env->CallObjectMethod(str, m_get_bytes));
const int length = env->GetArrayLength(array);
const auto content = static_cast<char *>(malloc(length + 1));
env->GetByteArrayRegion(array, 0, length, reinterpret_cast<jbyte *>(content));
auto array = (jbyteArray) env->CallObjectMethod(str, m_get_bytes);
int length = env->GetArrayLength(array);
char *content = (char *) malloc(length + 1);
env->GetByteArrayRegion(array, 0, length, (jbyte *) content);
content[length] = 0;
return content;
}
jstring jni_new_string(JNIEnv *env, const char *str) {
const auto length = static_cast<int>(strlen(str));
const auto array = env->NewByteArray(length);
env->SetByteArrayRegion(array, 0, length, reinterpret_cast<const jbyte *>(str));
return reinterpret_cast<jstring>(env->NewObject(c_string, m_new_string, array));
auto length = (int) strlen(str);
jbyteArray array = env->NewByteArray(length);
env->SetByteArrayRegion(array, 0, length, (const jbyte *) str);
return (jstring) env->NewObject(c_string, m_new_string, array);
}
int jni_catch_exception(JNIEnv *env) {
const int result = env->ExceptionCheck();
int result = env->ExceptionCheck();
if (result) {
env->ExceptionDescribe();
env->ExceptionClear();
@@ -47,9 +46,9 @@ int jni_catch_exception(JNIEnv *env) {
return result;
}
void jni_attach_thread(scoped_jni *jni) {
void jni_attach_thread(struct scoped_jni *jni) {
JavaVM *vm = global_java_vm();
if (vm->GetEnv(reinterpret_cast<void **>(&jni->env), JNI_VERSION_1_6) == JNI_OK) {
if (vm->GetEnv((void **) &jni->env, JNI_VERSION_1_6) == JNI_OK) {
jni->require_release = 0;
return;
}
@@ -59,9 +58,9 @@ void jni_attach_thread(scoped_jni *jni) {
jni->require_release = 1;
}
void jni_detach_thread(const scoped_jni *env) {
void jni_detach_thread(struct scoped_jni *jni) {
JavaVM *vm = global_java_vm();
if (env->require_release) {
if (jni->require_release) {
vm->DetachCurrentThread();
}
}

View File

@@ -1,6 +1,9 @@
#pragma once
#include <jni.h>
#include <cstdint>
#include <cstdlib>
#include <malloc.h>
struct scoped_jni {
JNIEnv *env;
@@ -15,14 +18,14 @@ extern char *jni_get_string(JNIEnv *env, jstring str);
extern int jni_catch_exception(JNIEnv *env);
extern void jni_attach_thread(scoped_jni *jni);
extern void jni_attach_thread(struct scoped_jni *jni);
extern void jni_detach_thread(const scoped_jni *env);
extern void jni_detach_thread(struct scoped_jni *env);
extern void release_string(char **str);
#define ATTACH_JNI() __attribute__((unused, cleanup(jni_detach_thread))) \
scoped_jni _jni{}; \
struct scoped_jni _jni; \
jni_attach_thread(&_jni); \
JNIEnv *env = _jni.env
@@ -33,4 +36,4 @@ extern void release_string(char **str);
#define new_global(obj) env->NewGlobalRef(obj)
#define del_global(obj) env->DeleteGlobalRef(obj)
#define get_string(jstr) jni_get_string(env, jstr)
#define new_string(cstr) jni_new_string(env, cstr)
#define new_string(cstr) jni_new_string(env, cstr)

View File

@@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true
kotlin_version=1.9.22
agp_version=8.9.2
agp_version=8.9.1

27
android/settings.gradle Normal file
View File

@@ -0,0 +1,27 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
includeBuild("${settings.ext.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 "$agp_version" apply false
id "org.jetbrains.kotlin.android" version "$kotlin_version" apply false
}
include ":app"
include ':core'

View File

@@ -1,29 +0,0 @@
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.9.2" apply false
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
}
include(":app")
include(":core")

View File

@@ -13,6 +13,7 @@
"resourcesDesc": "External resource related info",
"trafficUsage": "Traffic usage",
"coreInfo": "Core info",
"nullCoreInfoDesc": "Unable to obtain core info",
"networkSpeed": "Network speed",
"outboundMode": "Outbound mode",
"networkDetection": "Network detection",
@@ -21,6 +22,7 @@
"noProxy": "No proxy",
"noProxyDesc": "Please create a profile or add a valid profile",
"nullProfileDesc": "No profile, Please add a profile",
"nullLogsDesc": "No logs",
"settings": "Settings",
"language": "Language",
"defaultText": "Default",
@@ -147,6 +149,8 @@
"addressHelp": "WebDAV server address",
"addressTip": "Please enter a valid WebDAV address",
"password": "Password",
"passwordTip": "Password cannot be empty",
"accountTip": "Account cannot be empty",
"checkUpdate": "Check for updates",
"discoverNewVersion": "Discover the new version",
"checkUpdateError": "The current application is already the latest version",
@@ -181,6 +185,8 @@
"expirationTime": "Expiration time",
"connections": "Connections",
"connectionsDesc": "View current connections data",
"nullRequestsDesc": "No requests",
"nullConnectionsDesc": "No connections",
"intranetIP": "Intranet IP",
"view": "View",
"cut": "Cut",
@@ -213,6 +219,7 @@
"autoCloseConnectionsDesc": "Auto close connections after change node",
"onlyStatisticsProxy": "Only statistics proxy",
"onlyStatisticsProxyDesc": "When turned on, only statistics proxy traffic",
"deleteProfileTip": "Sure you want to delete the current profile?",
"pureBlackMode": "Pure black mode",
"keepAliveIntervalDesc": "Tcp keep alive interval",
"entries": " entries",
@@ -243,6 +250,7 @@
"dnsDesc": "Update DNS related settings",
"key": "Key",
"value": "Value",
"notEmpty": "Cannot be empty",
"hostsDesc": "Add Hosts",
"vpnTip": "Changes take effect after restarting the VPN",
"vpnEnableDesc": "Auto routes all system traffic through VpnService",
@@ -329,12 +337,15 @@
"fileIsUpdate": "The file has been modified. Do you want to save the changes?",
"profileHasUpdate": "The profile has been modified. Do you want to disable auto update?",
"hasCacheChange": "Do you want to cache the changes?",
"nullProxies": "No proxies",
"copySuccess": "Copy success",
"copyLink": "Copy link",
"exportFile": "Export file",
"cacheCorrupt": "The cache is corrupt. Do you want to clear it?",
"detectionTip": "Relying on third-party api is for reference only",
"listen": "Listen",
"keyExists": "The current key already exists",
"valueExists": "The current value already exists",
"undo": "undo",
"redo": "redo",
"none": "none",
@@ -342,21 +353,28 @@
"basicConfigDesc": "Modify the basic configuration globally",
"selectedCountTitle": "{count} items have been selected",
"addRule": "Add rule",
"ruleProviderEmptyTip": "Rule provider cannot be empty",
"ruleName": "Rule name",
"content": "Content",
"contentEmptyTip": "Content cannot be empty",
"subRule": "Sub rule",
"subRuleEmptyTip": "Sub rule content cannot be empty",
"ruleTarget": "Rule target",
"ruleTargetEmptyTip": "Rule target cannot be empty",
"sourceIp": "Source IP",
"noResolve": "No resolve IP",
"getOriginRules": "Get original rules",
"overrideOriginRules": "Override the original rule",
"addedOriginRules": "Attach on the original rules",
"enableOverride": "Enable override",
"deleteRuleTip": "Are you sure you want to delete the selected rule?",
"saveChanges": "Do you want to save the changes?",
"generalDesc": "Modify general settings",
"findProcessModeDesc": "There is a certain performance loss after opening",
"tabAnimationDesc": "Effective only in mobile view",
"saveTip": "Are you sure you want to save?",
"deleteColorTip": "Are you sure you want to delete the current color?",
"colorExists": "Current color already exists",
"colorSchemes": "Color schemes",
"palette": "Palette",
"tonalSpotScheme": "TonalSpot",
@@ -377,49 +395,5 @@
"textScale": "Text Scaling",
"internet": "Internet",
"systemApp": "System APP",
"noNetworkApp": "No network APP",
"contactMe": "Contact me",
"recoveryStrategy": "Recovery strategy",
"recoveryStrategy_override": "Override",
"recoveryStrategy_compatible": "Compatible",
"logsTest": "Logs test",
"emptyTip": "{label} cannot be empty",
"urlTip": "{label} must be a url",
"numberTip": "{label} must be a number",
"interval": "Interval",
"existsTip": "Current {label} already exists",
"deleteTip": "Are you sure you want to delete the current {label}?",
"deleteMultipTip": "Are you sure you want to delete the selected {label}?",
"nullTip": "No {label} at the moment",
"script": "Script",
"color": "Color",
"rename": "Rename",
"unnamed": "Unnamed",
"pleaseEnterScriptName": "Please enter a script name",
"overrideInvalidTip": "Does not take effect in script mode",
"mixedPort": "Mixed Port",
"socksPort": "Socks Port",
"redirPort": "Redir Port",
"tproxyPort": "Tproxy Port",
"portTip": "{label} must be between 1024 and 49151",
"portConflictTip": "Please enter a different port",
"import": "Import",
"importFile": "Import from file",
"importUrl": "Import from URL",
"autoSetSystemDns": "Auto set system DNS",
"details": "{label} details",
"creationTime": "Creation time",
"progress": "Progress",
"host": "Host",
"destination": "Destination",
"destinationGeoIP": "Destination GeoIP",
"destinationIPASN": "Destination IPASN",
"specialProxy": "Special proxy",
"specialRules": "special rules",
"remoteDestination": "Remote destination",
"networkType": "Network type",
"proxyChains": "Proxy chains",
"log": "Log",
"connection": "Connection",
"request": "Request"
"noNetworkApp": "No network App"
}

View File

@@ -13,6 +13,7 @@
"resourcesDesc": "外部リソース関連情報",
"trafficUsage": "トラフィック使用量",
"coreInfo": "コア情報",
"nullCoreInfoDesc": "コア情報を取得できません",
"networkSpeed": "ネットワーク速度",
"outboundMode": "アウトバウンドモード",
"networkDetection": "ネットワーク検出",
@@ -21,6 +22,7 @@
"noProxy": "プロキシなし",
"noProxyDesc": "プロファイルを作成するか、有効なプロファイルを追加してください",
"nullProfileDesc": "プロファイルがありません。追加してください",
"nullLogsDesc": "ログがありません",
"settings": "設定",
"language": "言語",
"defaultText": "デフォルト",
@@ -147,6 +149,8 @@
"addressHelp": "WebDAVサーバーアドレス",
"addressTip": "有効なWebDAVアドレスを入力",
"password": "パスワード",
"passwordTip": "パスワードは必須です",
"accountTip": "アカウントは必須です",
"checkUpdate": "更新を確認",
"discoverNewVersion": "新バージョンを発見",
"checkUpdateError": "アプリは最新版です",
@@ -181,6 +185,8 @@
"expirationTime": "有効期限",
"connections": "接続",
"connectionsDesc": "現在の接続データを表示",
"nullRequestsDesc": "リクエストなし",
"nullConnectionsDesc": "接続なし",
"intranetIP": "イントラネットIP",
"view": "表示",
"cut": "切り取り",
@@ -213,6 +219,7 @@
"autoCloseConnectionsDesc": "ノード変更後に接続を自動閉じる",
"onlyStatisticsProxy": "プロキシのみ統計",
"onlyStatisticsProxyDesc": "有効化するとプロキシトラフィックのみ統計",
"deleteProfileTip": "現在のプロファイルを削除しますか?",
"pureBlackMode": "純黒モード",
"keepAliveIntervalDesc": "TCPキープアライブ間隔",
"entries": " エントリ",
@@ -243,6 +250,7 @@
"dnsDesc": "DNS関連設定の更新",
"key": "キー",
"value": "値",
"notEmpty": "空欄不可",
"hostsDesc": "ホストを追加",
"vpnTip": "変更はVPN再起動後に有効",
"vpnEnableDesc": "VpnService経由で全システムトラフィックをルーティング",
@@ -329,12 +337,15 @@
"fileIsUpdate": "ファイルが変更されました。保存しますか?",
"profileHasUpdate": "プロファイルが変更されました。自動更新を無効化しますか?",
"hasCacheChange": "変更をキャッシュしますか?",
"nullProxies": "プロキシなし",
"copySuccess": "コピー成功",
"copyLink": "リンクをコピー",
"exportFile": "ファイルをエクスポート",
"cacheCorrupt": "キャッシュが破損しています。クリアしますか?",
"detectionTip": "サードパーティAPIに依存参考値",
"listen": "リスン",
"keyExists": "現在のキーは既に存在します",
"valueExists": "現在の値は既に存在します",
"undo": "元に戻す",
"redo": "やり直す",
"none": "なし",
@@ -342,21 +353,28 @@
"basicConfigDesc": "基本設定をグローバルに変更",
"selectedCountTitle": "{count} 項目が選択されています",
"addRule": "ルールを追加",
"ruleProviderEmptyTip": "ルールプロバイダーは必須です",
"ruleName": "ルール名",
"content": "内容",
"contentEmptyTip": "内容は必須です",
"subRule": "サブルール",
"subRuleEmptyTip": "サブルールの内容は必須です",
"ruleTarget": "ルール対象",
"ruleTargetEmptyTip": "ルール対象は必須です",
"sourceIp": "送信元IP",
"noResolve": "IPを解決しない",
"getOriginRules": "元のルールを取得",
"overrideOriginRules": "元のルールを上書き",
"addedOriginRules": "元のルールに追加",
"enableOverride": "上書きを有効化",
"deleteRuleTip": "選択したルールを削除しますか?",
"saveChanges": "変更を保存しますか?",
"generalDesc": "一般設定を変更",
"findProcessModeDesc": "有効化するとパフォーマンスが若干低下します",
"tabAnimationDesc": "モバイル表示でのみ有効",
"saveTip": "保存してもよろしいですか?",
"deleteColorTip": "現在の色を削除しますか?",
"colorExists": "この色は既に存在します",
"colorSchemes": "カラースキーム",
"palette": "パレット",
"tonalSpotScheme": "トーンスポット",
@@ -378,49 +396,5 @@
"textScale": "テキストスケーリング",
"internet": "インターネット",
"systemApp": "システムアプリ",
"noNetworkApp": "ネットワークなしアプリ",
"contactMe": "連絡する",
"recoveryStrategy": "リカバリー戦略",
"recoveryStrategy_override": "オーバーライド",
"recoveryStrategy_compatible": "互換性",
"logsTest": "ログテスト",
"emptyTip": "{label}は空欄にできません",
"urlTip": "{label}はURLである必要があります",
"numberTip": "{label}は数字でなければなりません",
"interval": "インターバル",
"existsTip": "現在の{label}は既に存在しています",
"deleteTip": "現在の{label}を削除してもよろしいですか?",
"deleteMultipTip": "選択された{label}を削除してもよろしいですか?",
"nullTip": "現在{label}はありません",
"script": "スクリプト",
"color": "カラー",
"rename": "リネーム",
"unnamed": "無題",
"pleaseEnterScriptName": "スクリプト名を入力してください",
"overrideInvalidTip": "スクリプトモードでは有効になりません",
"mixedPort": "混合ポート",
"socksPort": "Socksポート",
"redirPort": "Redirポート",
"tproxyPort": "Tproxyポート",
"portTip": "{label} は 1024 から 49151 の間でなければなりません",
"portConflictTip": "別のポートを入力してください",
"import": "インポート",
"importFile": "ファイルからインポート",
"importUrl": "URLからインポート",
"autoSetSystemDns": "オートセットシステムDNS",
"details": "{label}詳細",
"creationTime": "作成時間",
"progress": "進捗",
"host": "ホスト",
"destination": "宛先",
"destinationGeoIP": "宛先地理情報",
"destinationIPASN": "宛先IP ASN",
"specialProxy": "特殊プロキシ",
"specialRules": "特殊ルール",
"remoteDestination": "リモート宛先",
"networkType": "ネットワーク種別",
"proxyChains": "プロキシチェーン",
"log": "ログ",
"connection": "接続",
"request": "リクエスト"
"noNetworkApp": "ネットワークなしアプリ"
}

View File

@@ -13,6 +13,7 @@
"resourcesDesc": "Информация, связанная с внешними ресурсами",
"trafficUsage": "Использование трафика",
"coreInfo": "Информация о ядре",
"nullCoreInfoDesc": "Не удалось получить информацию о ядре",
"networkSpeed": "Скорость сети",
"outboundMode": "Режим исходящего трафика",
"networkDetection": "Обнаружение сети",
@@ -21,6 +22,7 @@
"noProxy": "Нет прокси",
"noProxyDesc": "Пожалуйста, создайте профиль или добавьте действительный профиль",
"nullProfileDesc": "Нет профиля, пожалуйста, добавьте профиль",
"nullLogsDesc": "Нет логов",
"settings": "Настройки",
"language": "Язык",
"defaultText": "По умолчанию",
@@ -147,6 +149,8 @@
"addressHelp": "Адрес сервера WebDAV",
"addressTip": "Пожалуйста, введите действительный адрес WebDAV",
"password": "Пароль",
"passwordTip": "Пароль не может быть пустым",
"accountTip": "Аккаунт не может быть пустым",
"checkUpdate": "Проверить обновления",
"discoverNewVersion": "Обнаружена новая версия",
"checkUpdateError": "Текущее приложение уже является последней версией",
@@ -181,6 +185,8 @@
"expirationTime": "Время истечения",
"connections": "Соединения",
"connectionsDesc": "Просмотр текущих данных о соединениях",
"nullRequestsDesc": "Нет запросов",
"nullConnectionsDesc": "Нет соединений",
"intranetIP": "Внутренний IP",
"view": "Просмотр",
"cut": "Вырезать",
@@ -213,6 +219,7 @@
"autoCloseConnectionsDesc": "Автоматически закрывать соединения после смены узла",
"onlyStatisticsProxy": "Только статистика прокси",
"onlyStatisticsProxyDesc": "При включении будет учитываться только трафик прокси",
"deleteProfileTip": "Вы уверены, что хотите удалить текущий профиль?",
"pureBlackMode": "Чисто черный режим",
"keepAliveIntervalDesc": "Интервал поддержания TCP-соединения",
"entries": " записей",
@@ -243,6 +250,7 @@
"dnsDesc": "Обновление настроек, связанных с DNS",
"key": "Ключ",
"value": "Значение",
"notEmpty": "Не может быть пустым",
"hostsDesc": "Добавить Hosts",
"vpnTip": "Изменения вступят в силу после перезапуска VPN",
"vpnEnableDesc": "Автоматически направляет весь системный трафик через VpnService",
@@ -329,12 +337,15 @@
"fileIsUpdate": "Файл был изменен. Хотите сохранить изменения?",
"profileHasUpdate": "Профиль был изменен. Хотите отключить автообновление?",
"hasCacheChange": "Хотите сохранить изменения в кэше?",
"nullProxies": "Нет прокси",
"copySuccess": "Копирование успешно",
"copyLink": "Копировать ссылку",
"exportFile": "Экспорт файла",
"cacheCorrupt": "Кэш поврежден. Хотите очистить его?",
"detectionTip": "Опирается на сторонний API, только для справки",
"listen": "Слушать",
"keyExists": "Текущий ключ уже существует",
"valueExists": "Текущее значение уже существует",
"undo": "Отменить",
"redo": "Повторить",
"none": "Нет",
@@ -342,21 +353,28 @@
"basicConfigDesc": "Глобальное изменение базовых настроек",
"selectedCountTitle": "Выбрано {count} элементов",
"addRule": "Добавить правило",
"ruleProviderEmptyTip": "Поставщик правил не может быть пустым",
"ruleName": "Название правила",
"content": "Содержание",
"contentEmptyTip": "Содержание не может быть пустым",
"subRule": "Подправило",
"subRuleEmptyTip": "Содержание подправила не может быть пустым",
"ruleTarget": "Цель правила",
"ruleTargetEmptyTip": "Цель правила не может быть пустой",
"sourceIp": "Исходный IP",
"noResolve": "Не разрешать IP",
"getOriginRules": "Получить оригинальные правила",
"overrideOriginRules": "Переопределить оригинальное правило",
"addedOriginRules": "Добавить к оригинальным правилам",
"enableOverride": "Включить переопределение",
"deleteRuleTip": "Вы уверены, что хотите удалить выбранное правило?",
"saveChanges": "Сохранить изменения?",
"generalDesc": "Изменение общих настроек",
"findProcessModeDesc": "При включении возможны небольшие потери производительности",
"tabAnimationDesc": "Действительно только в мобильном виде",
"saveTip": "Вы уверены, что хотите сохранить?",
"deleteColorTip": "Удалить текущий цвет?",
"colorExists": "Этот цвет уже существует",
"colorSchemes": "Цветовые схемы",
"palette": "Палитра",
"tonalSpotScheme": "Тональный акцент",
@@ -378,49 +396,5 @@
"textScale": "Масштабирование текста",
"internet": "Интернет",
"systemApp": "Системное приложение",
"noNetworkApp": "Приложение без сети",
"contactMe": "Свяжитесь со мной",
"recoveryStrategy": "Стратегия восстановления",
"recoveryStrategy_override": "Переопределение",
"recoveryStrategy_compatible": "Совместимый",
"logsTest": "Тест журналов",
"emptyTip": "{label} не может быть пустым",
"urlTip": "{label} должен быть URL",
"numberTip": "{label} должно быть числом",
"interval": "Интервал",
"existsTip": "Текущий {label} уже существует",
"deleteTip": "Вы уверены, что хотите удалить текущий {label}?",
"deleteMultipTip": "Вы уверены, что хотите удалить выбранные {label}?",
"nullTip": "Сейчас {label} нет",
"script": "Скрипт",
"color": "Цвет",
"rename": "Переименовать",
"unnamed": "Без имени",
"pleaseEnterScriptName": "Пожалуйста, введите название скрипта",
"overrideInvalidTip": "В скриптовом режиме не действует",
"mixedPort": "Смешанный порт",
"socksPort": "Socks-порт",
"redirPort": "Redir-порт",
"tproxyPort": "Tproxy-порт",
"portTip": "{label} должен быть числом от 1024 до 49151",
"portConflictTip": "Введите другой порт",
"import": "Импорт",
"importFile": "Импорт из файла",
"importUrl": "Импорт по URL",
"autoSetSystemDns": "Автоматическая настройка системного DNS",
"details": "Детали {}",
"creationTime": "Время создания",
"progress": "Прогресс",
"host": "Хост",
"destination": "Назначение",
"destinationGeoIP": "Геолокация назначения",
"destinationIPASN": "ASN назначения",
"specialProxy": "Специальный прокси",
"specialRules": "Специальные правила",
"remoteDestination": "Удалённое назначение",
"networkType": "Тип сети",
"proxyChains": "Цепочки прокси",
"log": "Журнал",
"connection": "Соединение",
"request": "Запрос"
"noNetworkApp": "Приложение без сети"
}

View File

@@ -13,6 +13,7 @@
"resourcesDesc": "外部资源相关信息",
"trafficUsage": "流量统计",
"coreInfo": "内核信息",
"nullCoreInfoDesc": "无法获取内核信息",
"networkSpeed": "网络速度",
"outboundMode": "出站模式",
"networkDetection": "网络检测",
@@ -21,6 +22,7 @@
"noProxy": "暂无代理",
"noProxyDesc": "请创建配置文件或者添加有效配置文件",
"nullProfileDesc": "没有配置文件,请先添加配置文件",
"nullLogsDesc": "暂无日志",
"settings": "设置",
"language": "语言",
"defaultText": "默认",
@@ -147,6 +149,8 @@
"addressHelp": "WebDAV服务器地址",
"addressTip": "请输入有效的WebDAV地址",
"password": "密码",
"passwordTip": "密码不能为空",
"accountTip": "账号不能为空",
"checkUpdate": "检查更新",
"discoverNewVersion": "发现新版本",
"checkUpdateError": "当前应用已经是最新版了",
@@ -181,6 +185,8 @@
"expirationTime": "到期时间",
"connections": "连接",
"connectionsDesc": "查看当前连接数据",
"nullRequestsDesc": "暂无请求",
"nullConnectionsDesc": "暂无连接",
"intranetIP": "内网 IP",
"view": "查看",
"cut": "剪切",
@@ -213,6 +219,7 @@
"autoCloseConnectionsDesc": "切换节点后自动关闭连接",
"onlyStatisticsProxy": "仅统计代理",
"onlyStatisticsProxyDesc": "开启后,将只统计代理流量",
"deleteProfileTip": "确定要删除当前配置吗?",
"pureBlackMode": "纯黑模式",
"keepAliveIntervalDesc": "TCP保持活动间隔",
"entries": "个条目",
@@ -243,6 +250,7 @@
"dnsDesc": "更新DNS相关设置",
"key": "键",
"value": "值",
"notEmpty": "不能为空",
"hostsDesc": "追加Hosts",
"vpnTip": "重启VPN后改变生效",
"vpnEnableDesc": "通过VpnService自动路由系统所有流量",
@@ -329,12 +337,15 @@
"fileIsUpdate": "文件有修改,是否保存修改",
"profileHasUpdate": "配置文件已经修改,是否关闭自动更新 ",
"hasCacheChange": "是否缓存修改",
"nullProxies": "暂无代理",
"copySuccess": "复制成功",
"copyLink": "复制链接",
"exportFile": "导出文件",
"cacheCorrupt": "缓存已损坏,是否清空?",
"detectionTip": "依赖第三方api仅供参考",
"listen": "监听",
"keyExists": "当前键已存在",
"valueExists": "当前值已存在",
"undo": "撤销",
"redo": "重做",
"none": "无",
@@ -342,21 +353,28 @@
"basicConfigDesc": "全局修改基本配置",
"selectedCountTitle": "已选择 {count} 项",
"addRule": "添加规则",
"ruleProviderEmptyTip": "规则提供者不能为空",
"ruleName": "规则名称",
"content": "内容",
"contentEmptyTip": "内容不能为空",
"subRule": "子规则",
"subRuleEmptyTip": "子规则内容不能为空",
"ruleTarget": "规则目标",
"ruleTargetEmptyTip": "规则目标不能为空",
"sourceIp": "源IP",
"noResolve": "不解析IP",
"getOriginRules": "获取原始规则",
"overrideOriginRules": "覆盖原始规则",
"addedOriginRules": "附加到原始规则",
"enableOverride": "启用覆写",
"deleteRuleTip": "确定要删除选中的规则吗?",
"saveChanges": "是否保存更改?",
"generalDesc": "修改通用设置",
"findProcessModeDesc": "开启后会有一定性能损耗",
"tabAnimationDesc": "仅在移动视图中有效",
"saveTip": "确定要保存吗?",
"deleteColorTip": "确定删除当前颜色吗?",
"colorExists": "该颜色已存在",
"colorSchemes": "配色方案",
"palette": "调色板",
"tonalSpotScheme": "调性点缀",
@@ -378,49 +396,5 @@
"textScale": "文本缩放",
"internet": "互联网",
"systemApp": "系统应用",
"noNetworkApp": "无网络应用",
"contactMe": "联系我",
"recoveryStrategy": "恢复策略",
"recoveryStrategy_override": "覆盖",
"recoveryStrategy_compatible": "兼容",
"logsTest": "日志测试",
"emptyTip": "{label}不能为空",
"urlTip": "{label}必须为URL",
"numberTip": "{label}必须为数字",
"interval": "间隔",
"existsTip": "{label}当前已存在",
"deleteTip": "确定删除当前{label}吗?",
"deleteMultipTip": "确定删除选中的{label}吗?",
"nullTip": "暂无{label}",
"script": "脚本",
"color": "颜色",
"rename": "重命名",
"unnamed": "未命名",
"pleaseEnterScriptName": "请输入脚本名称",
"overrideInvalidTip": "在脚本模式下不生效",
"mixedPort": "混合端口",
"socksPort": "Socks端口",
"redirPort": "Redir端口",
"tproxyPort": "Tproxy端口",
"portTip": "{label} 必须在 1024 到 49151 之间",
"portConflictTip": "请输入不同的端口",
"import": "导入",
"importFile": "通过文件导入",
"importUrl": "通过URL导入",
"autoSetSystemDns": "自动设置系统DNS",
"details": "{label}详情",
"creationTime": "创建时间",
"progress": "进度",
"host": "主机",
"destination": "目标地址",
"destinationGeoIP": "目标地理定位",
"destinationIPASN": "目标IP ASN",
"specialProxy": "特殊代理",
"specialRules": "特殊规则",
"remoteDestination": "远程目标",
"networkType": "网络类型",
"proxyChains": "代理链",
"log": "日志",
"connection": "连接",
"request": "请求"
"noNetworkApp": "无网络应用"
}

View File

@@ -5,17 +5,16 @@ import (
)
type Action struct {
Id string `json:"id"`
Method Method `json:"method"`
Data interface{} `json:"data"`
Id string `json:"id"`
Method Method `json:"method"`
Data interface{} `json:"data"`
DefaultValue interface{} `json:"default-value"`
}
type ActionResult struct {
Id string `json:"id"`
Method Method `json:"method"`
Data interface{} `json:"data"`
Code int `json:"code"`
Port int64
}
func (result ActionResult) Json() ([]byte, error) {
@@ -23,117 +22,99 @@ func (result ActionResult) Json() ([]byte, error) {
return data, err
}
func (result ActionResult) success(data interface{}) {
result.Code = 0
result.Data = data
result.send()
func (action Action) getResult(data interface{}) []byte {
resultAction := ActionResult{
Id: action.Id,
Method: action.Method,
Data: data,
}
res, _ := resultAction.Json()
return res
}
func (result ActionResult) error(data interface{}) {
result.Code = -1
result.Data = data
result.send()
}
func handleAction(action *Action, result ActionResult) {
func handleAction(action *Action, result func(data interface{})) {
switch action.Method {
case initClashMethod:
paramsString := action.Data.(string)
result.success(handleInitClash(paramsString))
result(handleInitClash(paramsString))
return
case getIsInitMethod:
result.success(handleGetIsInit())
result(handleGetIsInit())
return
case forceGcMethod:
handleForceGc()
result.success(true)
result(true)
return
case shutdownMethod:
result.success(handleShutdown())
result(handleShutdown())
return
case validateConfigMethod:
data := []byte(action.Data.(string))
result.success(handleValidateConfig(data))
result(handleValidateConfig(data))
return
case updateConfigMethod:
data := []byte(action.Data.(string))
result.success(handleUpdateConfig(data))
return
case setupConfigMethod:
data := []byte(action.Data.(string))
result.success(handleSetupConfig(data))
result(handleUpdateConfig(data))
return
case getProxiesMethod:
result.success(handleGetProxies())
result(handleGetProxies())
return
case changeProxyMethod:
data := action.Data.(string)
handleChangeProxy(data, func(value string) {
result.success(value)
result(value)
})
return
case getTrafficMethod:
result.success(handleGetTraffic())
result(handleGetTraffic())
return
case getTotalTrafficMethod:
result.success(handleGetTotalTraffic())
result(handleGetTotalTraffic())
return
case resetTrafficMethod:
handleResetTraffic()
result.success(true)
result(true)
return
case asyncTestDelayMethod:
data := action.Data.(string)
handleAsyncTestDelay(data, func(value string) {
result.success(value)
result(value)
})
return
case getConnectionsMethod:
result.success(handleGetConnections())
result(handleGetConnections())
return
case closeConnectionsMethod:
result.success(handleCloseConnections())
return
case resetConnectionsMethod:
result.success(handleResetConnections())
return
case getConfigMethod:
path := action.Data.(string)
config, err := handleGetConfig(path)
if err != nil {
result.error(err)
return
}
result.success(config)
result(handleCloseConnections())
return
case closeConnectionMethod:
id := action.Data.(string)
result.success(handleCloseConnection(id))
result(handleCloseConnection(id))
return
case getExternalProvidersMethod:
result.success(handleGetExternalProviders())
result(handleGetExternalProviders())
return
case getExternalProviderMethod:
externalProviderName := action.Data.(string)
result.success(handleGetExternalProvider(externalProviderName))
result(handleGetExternalProvider(externalProviderName))
case updateGeoDataMethod:
paramsString := action.Data.(string)
var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
result.success(err.Error())
result(err.Error())
return
}
geoType := params["geo-type"]
geoName := params["geo-name"]
handleUpdateGeoData(geoType, geoName, func(value string) {
result.success(value)
result(value)
})
return
case updateExternalProviderMethod:
providerName := action.Data.(string)
handleUpdateExternalProvider(providerName, func(value string) {
result.success(value)
result(value)
})
return
case sideLoadExternalProviderMethod:
@@ -141,48 +122,59 @@ func handleAction(action *Action, result ActionResult) {
var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
result.success(err.Error())
result(err.Error())
return
}
providerName := params["providerName"]
data := params["data"]
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
result.success(value)
result(value)
})
return
case startLogMethod:
handleStartLog()
result.success(true)
result(true)
return
case stopLogMethod:
handleStopLog()
result.success(true)
result(true)
return
case startListenerMethod:
result.success(handleStartListener())
result(handleStartListener())
return
case stopListenerMethod:
result.success(handleStopListener())
result(handleStopListener())
return
case getCountryCodeMethod:
ip := action.Data.(string)
handleGetCountryCode(ip, func(value string) {
result.success(value)
result(value)
})
return
case getMemoryMethod:
handleGetMemory(func(value string) {
result.success(value)
result(value)
})
return
case getProfileMethod:
profileId := action.Data.(string)
handleGetMemory(func(value string) {
result(handleGetProfile(profileId))
})
return
case setStateMethod:
data := action.Data.(string)
handleSetState(data)
result.success(true)
result(true)
case crashMethod:
result.success(true)
result(true)
handleCrash()
default:
nextHandle(action, result)
handle := nextHandle(action, result)
if handle {
return
} else {
result(action.DefaultValue)
}
}
}

View File

@@ -43,13 +43,13 @@ var (
}
)
func Protect(callback unsafe.Pointer, fd int) {
func protect(callback unsafe.Pointer, fd int) {
if globalCallbacks.protectFunc != nil {
C.protect(globalCallbacks.protectFunc, callback, C.int(fd))
}
}
func ResolveProcess(callback unsafe.Pointer, protocol int, source, target string, uid int) string {
func resolveProcess(callback unsafe.Pointer, protocol int, source, target string, uid int) string {
if globalCallbacks.resolveProcessFunc == nil {
return ""
}

View File

@@ -1,10 +1,9 @@
package main
import (
b "bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/adapter/outboundgroup"
@@ -22,16 +21,31 @@ import (
"github.com/metacubex/mihomo/log"
rp "github.com/metacubex/mihomo/rules/provider"
"github.com/metacubex/mihomo/tunnel"
"github.com/samber/lo"
"os"
"path/filepath"
"strings"
"sync"
)
func splitByMultipleSeparators(s string) interface{} {
isSeparator := func(r rune) bool {
return r == ',' || r == ' ' || r == ';'
}
parts := strings.FieldsFunc(s, isSeparator)
if len(parts) > 1 {
return parts
}
return s
}
var (
currentConfig *config.Config
version = 0
isRunning = false
runLock sync.Mutex
mBatch, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
version = 0
isRunning = false
runLock sync.Mutex
ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"}
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
)
type ExternalProviders []ExternalProvider
@@ -40,6 +54,54 @@ func (a ExternalProviders) Len() int { return len(a) }
func (a ExternalProviders) Less(i, j int) bool { return a[i].Name < a[j].Name }
func (a ExternalProviders) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func readFile(path string) ([]byte, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err
}
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return data, err
}
func getProfilePath(id string) string {
return filepath.Join(constant.Path.HomeDir(), "profiles", id+".yaml")
}
func getProfileProvidersPath(id string) string {
return filepath.Join(constant.Path.HomeDir(), "providers", id)
}
func getRawConfigWithId(id string) *config.RawConfig {
path := getProfilePath(id)
bytes, err := readFile(path)
if err != nil {
return config.DefaultRawConfig()
}
prof, err := config.UnmarshalRawConfig(bytes)
if err != nil {
log.Errorln("unmarshalRawConfig error %v", err)
return config.DefaultRawConfig()
}
for _, mapping := range prof.ProxyProvider {
value, exist := mapping["path"].(string)
if !exist {
continue
}
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
}
for _, mapping := range prof.RuleProvider {
value, exist := mapping["path"].(string)
if !exist {
continue
}
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
}
return prof
}
func getExternalProvidersRaw() map[string]cp.Provider {
eps := make(map[string]cp.Provider)
for n, p := range tunnel.Providers() {
@@ -104,15 +166,143 @@ func sideUpdateExternalProvider(p cp.Provider, bytes []byte) error {
}
}
func updateListeners() {
func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig {
prof := getRawConfigWithId(profileId)
overwriteConfig(prof, cfg)
return prof
}
func attachHosts(hosts, patchHosts map[string]any) {
for k, v := range patchHosts {
if str, ok := v.(string); ok {
hosts[k] = splitByMultipleSeparators(str)
}
}
}
func updatePatchDns(dns config.RawDNS) {
for pair := dns.NameServerPolicy.Oldest(); pair != nil; pair = pair.Next() {
if str, ok := pair.Value.(string); ok {
dns.NameServerPolicy.Set(pair.Key, splitByMultipleSeparators(str))
}
}
}
func trimArr(arr []string) (r []string) {
for _, e := range arr {
r = append(r, strings.Trim(e, " "))
}
return
}
func overrideRules(rules, patchRules []string) []string {
target := ""
for _, line := range rules {
rule := trimArr(strings.Split(line, ","))
if len(rule) != 2 {
continue
}
if strings.EqualFold(rule[0], "MATCH") {
target = rule[1]
break
}
}
if target == "" {
return rules
}
rulesExt := lo.Map(ips, func(ip string, _ int) string {
return fmt.Sprintf("DOMAIN,%s,%s", ip, target)
})
return append(append(rulesExt, patchRules...), rules...)
}
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) {
targetConfig.ExternalController = patchConfig.ExternalController
targetConfig.ExternalUI = ""
targetConfig.Interface = ""
targetConfig.ExternalUIURL = ""
targetConfig.TCPConcurrent = patchConfig.TCPConcurrent
targetConfig.UnifiedDelay = patchConfig.UnifiedDelay
targetConfig.IPv6 = patchConfig.IPv6
targetConfig.LogLevel = patchConfig.LogLevel
targetConfig.Port = 0
targetConfig.SocksPort = 0
targetConfig.KeepAliveInterval = patchConfig.KeepAliveInterval
targetConfig.MixedPort = patchConfig.MixedPort
targetConfig.FindProcessMode = patchConfig.FindProcessMode
targetConfig.AllowLan = patchConfig.AllowLan
targetConfig.Mode = patchConfig.Mode
targetConfig.Tun.Enable = patchConfig.Tun.Enable
targetConfig.Tun.Device = patchConfig.Tun.Device
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
targetConfig.Tun.Stack = patchConfig.Tun.Stack
targetConfig.Tun.RouteAddress = patchConfig.Tun.RouteAddress
targetConfig.GeodataLoader = patchConfig.GeodataLoader
targetConfig.Profile.StoreSelected = false
targetConfig.GeoXUrl = patchConfig.GeoXUrl
targetConfig.GlobalUA = patchConfig.GlobalUA
if configParams.TestURL != nil {
constant.DefaultTestURL = *configParams.TestURL
}
for idx := range targetConfig.ProxyGroup {
targetConfig.ProxyGroup[idx]["url"] = ""
}
attachHosts(targetConfig.Hosts, patchConfig.Hosts)
if configParams.OverrideDns {
updatePatchDns(patchConfig.DNS)
targetConfig.DNS = patchConfig.DNS
} else {
if targetConfig.DNS.Enable == false {
targetConfig.DNS.Enable = true
}
}
if configParams.OverrideRule {
targetConfig.Rule = overrideRules(patchConfig.Rule, []string{})
} else {
targetConfig.Rule = overrideRules(targetConfig.Rule, patchConfig.Rule)
}
}
func patchConfig() {
log.Infoln("[Apply] patch")
general := currentConfig.General
controller := currentConfig.Controller
tls := currentConfig.TLS
tunnel.SetSniffing(general.Sniffing)
tunnel.SetFindProcessMode(general.FindProcessMode)
dialer.SetTcpConcurrent(general.TCPConcurrent)
dialer.DefaultInterface.Store(general.Interface)
adapter.UnifiedDelay.Store(general.UnifiedDelay)
tunnel.SetMode(general.Mode)
log.SetLevel(general.LogLevel)
resolver.DisableIPv6 = !general.IPv6
route.ReCreateServer(&route.Config{
Addr: controller.ExternalController,
TLSAddr: controller.ExternalControllerTLS,
UnixAddr: controller.ExternalControllerUnix,
PipeAddr: controller.ExternalControllerPipe,
Secret: controller.Secret,
Certificate: tls.Certificate,
PrivateKey: tls.PrivateKey,
DohServer: controller.ExternalDohServer,
IsDebug: false,
Cors: route.Cors{
AllowOrigins: controller.Cors.AllowOrigins,
AllowPrivateNetwork: controller.Cors.AllowPrivateNetwork,
},
})
}
func updateListeners(force bool) {
if !isRunning {
return
}
if currentConfig == nil {
return
}
listeners := currentConfig.Listeners
general := currentConfig.General
listeners := currentConfig.Listeners
if force == true {
stopListeners()
}
listener.PatchInboundListeners(listeners, tunnel.Tunnel, true)
listener.SetAllowLan(general.AllowLan)
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
@@ -136,7 +326,11 @@ func stopListeners() {
listener.StopListener()
}
func patchSelectGroup(mapping map[string]string) {
func patchSelectGroup() {
mapping := configParams.SelectedMap
if mapping == nil {
return
}
for name, proxy := range tunnel.ProxiesWithProviders() {
outbound, ok := proxy.(*adapter.Proxy)
if !ok {
@@ -157,102 +351,20 @@ func patchSelectGroup(mapping map[string]string) {
}
}
func defaultSetupParams() *SetupParams {
return &SetupParams{
Config: config.DefaultRawConfig(),
TestURL: "https://www.gstatic.com/generate_204",
SelectedMap: map[string]string{},
}
}
func readFile(path string) ([]byte, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err
}
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return data, err
}
func updateConfig(params *UpdateParams) {
runLock.Lock()
defer runLock.Unlock()
general := currentConfig.General
if params.MixedPort != nil {
general.MixedPort = *params.MixedPort
}
if params.Sniffing != nil {
general.Sniffing = *params.Sniffing
tunnel.SetSniffing(general.Sniffing)
}
if params.FindProcessMode != nil {
general.FindProcessMode = *params.FindProcessMode
tunnel.SetFindProcessMode(general.FindProcessMode)
}
if params.TCPConcurrent != nil {
general.TCPConcurrent = *params.TCPConcurrent
dialer.SetTcpConcurrent(general.TCPConcurrent)
}
if params.Interface != nil {
general.Interface = *params.Interface
dialer.DefaultInterface.Store(general.Interface)
}
if params.UnifiedDelay != nil {
general.UnifiedDelay = *params.UnifiedDelay
adapter.UnifiedDelay.Store(general.UnifiedDelay)
}
if params.Mode != nil {
general.Mode = *params.Mode
tunnel.SetMode(general.Mode)
}
if params.LogLevel != nil {
general.LogLevel = *params.LogLevel
log.SetLevel(general.LogLevel)
}
if params.IPv6 != nil {
general.IPv6 = *params.IPv6
resolver.DisableIPv6 = !general.IPv6
}
if params.ExternalController != nil {
currentConfig.Controller.ExternalController = *params.ExternalController
route.ReCreateServer(&route.Config{
Addr: currentConfig.Controller.ExternalController,
})
}
if params.Tun != nil {
general.Tun.Enable = params.Tun.Enable
general.Tun.AutoRoute = *params.Tun.AutoRoute
general.Tun.Device = *params.Tun.Device
general.Tun.RouteAddress = *params.Tun.RouteAddress
general.Tun.DNSHijack = *params.Tun.DNSHijack
general.Tun.Stack = *params.Tun.Stack
}
updateListeners()
}
func setupConfig(params *SetupParams) error {
func applyConfig(rawConfig *config.RawConfig) error {
runLock.Lock()
defer runLock.Unlock()
var err error
constant.DefaultTestURL = params.TestURL
currentConfig, err = config.ParseRawConfig(params.Config)
currentConfig, err = config.ParseRawConfig(rawConfig)
if err != nil {
currentConfig, _ = config.ParseRawConfig(config.DefaultRawConfig())
}
hub.ApplyConfig(currentConfig)
patchSelectGroup(params.SelectedMap)
updateListeners()
return err
}
func UnmarshalJson(data []byte, v any) error {
decoder := json.NewDecoder(b.NewReader(data))
decoder.UseNumber()
err := decoder.Decode(v)
if configParams.IsPatch {
patchConfig()
} else {
hub.ApplyConfig(currentConfig)
patchSelectGroup()
}
updateListeners(false)
return err
}

View File

@@ -3,12 +3,7 @@ package main
import (
"encoding/json"
"github.com/metacubex/mihomo/adapter/provider"
P "github.com/metacubex/mihomo/component/process"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/tunnel"
"net/netip"
"time"
)
@@ -17,34 +12,19 @@ type InitParams struct {
Version int `json:"version"`
}
type SetupParams struct {
Config *config.RawConfig `json:"config"`
SelectedMap map[string]string `json:"selected-map"`
TestURL string `json:"test-url"`
type ConfigExtendedParams struct {
IsPatch bool `json:"is-patch"`
IsCompatible bool `json:"is-compatible"`
SelectedMap map[string]string `json:"selected-map"`
TestURL *string `json:"test-url"`
OverrideDns bool `json:"override-dns"`
OverrideRule bool `json:"override-rule"`
}
type UpdateParams struct {
Tun *tunSchema `json:"tun"`
AllowLan *bool `json:"allow-lan"`
MixedPort *int `json:"mixed-port"`
FindProcessMode *P.FindProcessMode `json:"find-process-mode"`
Mode *tunnel.TunnelMode `json:"mode"`
LogLevel *log.LogLevel `json:"log-level"`
IPv6 *bool `json:"ipv6"`
Sniffing *bool `json:"sniffing"`
TCPConcurrent *bool `json:"tcp-concurrent"`
ExternalController *string `json:"external-controller"`
Interface *string `json:"interface-name"`
UnifiedDelay *bool `json:"unified-delay"`
}
type tunSchema struct {
Enable bool `yaml:"enable" json:"enable"`
Device *string `yaml:"device" json:"device"`
Stack *constant.TUNStack `yaml:"stack" json:"stack"`
DNSHijack *[]string `yaml:"dns-hijack" json:"dns-hijack"`
AutoRoute *bool `yaml:"auto-route" json:"auto-route"`
RouteAddress *[]netip.Prefix `yaml:"route-address" json:"route-address,omitempty"`
type GenerateConfigParams struct {
ProfileId string `json:"profile-id"`
Config config.RawConfig `json:"config" `
Params ConfigExtendedParams `json:"params"`
}
type ChangeProxyParams struct {
@@ -84,7 +64,6 @@ const (
asyncTestDelayMethod Method = "asyncTestDelay"
getConnectionsMethod Method = "getConnections"
closeConnectionsMethod Method = "closeConnections"
resetConnectionsMethod Method = "resetConnectionsMethod"
closeConnectionMethod Method = "closeConnection"
getExternalProvidersMethod Method = "getExternalProviders"
getExternalProviderMethod Method = "getExternalProvider"
@@ -102,9 +81,8 @@ const (
getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions"
getRunTimeMethod Method = "getRunTime"
getCurrentProfileNameMethod Method = "getCurrentProfileName"
getProfileMethod Method = "getProfile"
crashMethod Method = "crash"
setupConfigMethod Method = "setupConfig"
getConfigMethod Method = "getConfig"
)
type Method string

View File

@@ -6,12 +6,13 @@ replace github.com/metacubex/mihomo => ./Clash.Meta
require (
github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000
github.com/samber/lo v1.49.1
golang.org/x/sync v0.11.0
)
require (
github.com/3andne/restls-client-go v0.1.6 // indirect
github.com/RyuaNerin/go-krypto v1.3.0 // indirect
github.com/RyuaNerin/go-krypto v1.2.4 // indirect
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
@@ -20,13 +21,13 @@ require (
github.com/cloudflare/circl v1.3.7 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/ebitengine/purego v0.8.3 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/enfein/mieru/v3 v3.13.0 // indirect
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect
github.com/go-chi/chi/v5 v5.2.1 // indirect
github.com/go-chi/render v1.0.3 // indirect
@@ -35,7 +36,7 @@ require (
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/gofrs/uuid/v5 v5.3.2 // indirect
github.com/gofrs/uuid/v5 v5.3.1 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
@@ -50,27 +51,22 @@ require (
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
github.com/metacubex/bart v0.20.5 // indirect
github.com/metacubex/bart v0.19.0 // indirect
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
github.com/metacubex/chacha v0.1.5 // indirect
github.com/metacubex/fswatch v0.1.1 // indirect
github.com/metacubex/chacha v0.1.2 // indirect
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 // indirect
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639 // indirect
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 // indirect
github.com/metacubex/randv2 v0.2.0 // indirect
github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29 // indirect
github.com/metacubex/sing-mux v0.3.2 // indirect
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f // indirect
github.com/metacubex/sing-shadowsocks v0.2.11-0.20250621023810-0e9ef9dd0c92 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.5-0.20250621023950-93d605a2143d // indirect
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 // indirect
github.com/metacubex/sing-tun v0.4.7-0.20250611091011-60774779fdd8 // indirect
github.com/metacubex/sing-vmess v0.2.2 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f // indirect
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee // indirect
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 // indirect
github.com/metacubex/utls v1.7.4-0.20250610022031-808d767c8c73 // indirect
github.com/metacubex/sing-quic v0.0.0-20250404030904-b2cc8aab562c // indirect
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
github.com/metacubex/sing-shadowtls v0.0.0-20250412122235-0e9005731a63 // indirect
github.com/metacubex/sing-tun v0.4.6-0.20250412144348-c426cb167db5 // indirect
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect
github.com/metacubex/utls v1.7.0-alpha.1 // indirect
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
github.com/miekg/dns v1.1.63 // indirect
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
@@ -82,9 +78,14 @@ require (
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/sagernet/cors v1.2.1 // indirect
github.com/sagernet/fswatch v0.1.1 // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/samber/lo v1.50.0 // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/sagernet/sing v0.5.2 // indirect
github.com/sagernet/sing-mux v0.2.1 // indirect
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect

View File

@@ -1,8 +1,8 @@
github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08=
github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY=
github.com/RyuaNerin/go-krypto v1.3.0 h1:smavTzSMAx8iuVlGb4pEwl9MD2qicqMzuXR2QWp2/Pg=
github.com/RyuaNerin/go-krypto v1.3.0/go.mod h1:9R9TU936laAIqAmjcHo/LsaXYOZlymudOAxjaBf62UM=
github.com/RyuaNerin/testingutil v0.1.0 h1:IYT6JL57RV3U2ml3dLHZsVtPOP6yNK7WUVdzzlpNrss=
github.com/RyuaNerin/elliptic2 v1.0.0/go.mod h1:wWB8fWrJI/6EPJkyV/r1Rj0hxUgrusmqSj8JN6yNf/A=
github.com/RyuaNerin/go-krypto v1.2.4 h1:mXuNdK6M317aPV0llW6Xpjbo4moOlPF7Yxz4tb4b4Go=
github.com/RyuaNerin/go-krypto v1.2.4/go.mod h1:QqCYkoutU3yInyD9INt2PGolVRsc3W4oraQadVGXJ/8=
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok=
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
@@ -26,12 +26,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po=
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg=
@@ -39,8 +39,8 @@ github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBE
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4=
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA=
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
@@ -59,8 +59,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=
github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gofrs/uuid/v5 v5.3.1 h1:aPx49MwJbekCzOyhZDjJVb0hx3A0KLjlbLx6p2gY0p0=
github.com/gofrs/uuid/v5 v5.3.1/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
@@ -97,49 +97,38 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
github.com/metacubex/bart v0.20.5 h1:XkgLZ17QxfxkqKdGsojoM2Zu01mmHyyQSFzt2/calTM=
github.com/metacubex/bart v0.20.5/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
github.com/metacubex/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY=
github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
github.com/metacubex/chacha v0.1.5 h1:fKWMb/5c7ZrY8Uoqi79PPFxl+qwR7X/q0OrsAubyX2M=
github.com/metacubex/chacha v0.1.5/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQuxhU=
github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI=
github.com/metacubex/chacha v0.1.2 h1:QulCq3eVm3TO6+4nVIWJtmSe7BT2GMrgVHuAoqRQnlc=
github.com/metacubex/chacha v0.1.2/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI=
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo=
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639 h1:L+1brQNzBhCCxWlicwfK1TlceemCRmrDE4HmcVHc29w=
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639/go.mod h1:Kc6h++Q/zf3AxcUCevJhJwgrskJumv+pZdR8g/E/10k=
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 h1:B+AP/Pj2/jBDS/kCYjz/x+0BCOKfd2VODYevyeIt+Ds=
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996/go.mod h1:ExVjGyEwTUjCFqx+5uxgV7MOoA3fZI+th4D40H35xmY=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29 h1:SD9q025FNTaepuFXFOKDhnGLVu6PQYChBvw2ZYPXeLo=
github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7ncw=
github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw=
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f h1:mP3vIm+9hRFI0C0Vl3pE0NESF/L85FDbuB0tGgUii6I=
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f/go.mod h1:JPTpf7fpnojsSuwRJExhSZSy63pVbp3VM39+zj+sAJM=
github.com/metacubex/sing-shadowsocks v0.2.11-0.20250621023810-0e9ef9dd0c92 h1:Y9ebcKya6ow7VHoESCN5+l4zZvg5eaL2IhI5LLCQxQA=
github.com/metacubex/sing-shadowsocks v0.2.11-0.20250621023810-0e9ef9dd0c92/go.mod h1:/squZ38pXrYjqtg8qn+joVvwbpGNYQNp8yxKsMVbCto=
github.com/metacubex/sing-shadowsocks2 v0.2.5-0.20250621023950-93d605a2143d h1:Ey3A1tA8lVkRbK1FDmwuWj/57Nr8JMdpoVqe45mFzJg=
github.com/metacubex/sing-shadowsocks2 v0.2.5-0.20250621023950-93d605a2143d/go.mod h1:+ukTd0OPFglT3bnKAYTJWYPbuox6HYNXE235r5tHdUk=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
github.com/metacubex/sing-tun v0.4.7-0.20250611091011-60774779fdd8 h1:4zWKqxTx75TbfW2EmlQ3hxM6RTRg2PYOAVMCnU4I61I=
github.com/metacubex/sing-tun v0.4.7-0.20250611091011-60774779fdd8/go.mod h1:2YywXPWW8Z97kTH7RffOeykKzU+l0aiKlglWV1PAS64=
github.com/metacubex/sing-vmess v0.2.2 h1:nG6GIKF1UOGmlzs+BIetdGHkFZ20YqFVIYp5Htqzp+4=
github.com/metacubex/sing-vmess v0.2.2/go.mod h1:CVDNcdSLVYFgTHQlubr88d8CdqupAUDqLjROos+H9xk=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f/go.mod h1:jpAkVLPnCpGSfNyVmj6Cq4YbuZsFepm/Dc+9BAOcR80=
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee h1:lp6hJ+4wCLZu113awp7P6odM2okB5s60HUyF0FMqKmo=
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE=
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 h1:j1VRTiC9JLR4nUbSikx9OGdu/3AgFDqgcLj4GoqyQkc=
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.7.4-0.20250610022031-808d767c8c73 h1:HWKsf92BqLYqugATFIJ3hYiEBZ7JF6AoqyvqF39afuI=
github.com/metacubex/utls v1.7.4-0.20250610022031-808d767c8c73/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU=
github.com/metacubex/sing-quic v0.0.0-20250404030904-b2cc8aab562c h1:OB3WmMA8YPJjE36RjD9X8xlrWGJ4orxbf2R/KAE28b0=
github.com/metacubex/sing-quic v0.0.0-20250404030904-b2cc8aab562c/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4=
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
github.com/metacubex/sing-shadowtls v0.0.0-20250412122235-0e9005731a63 h1:vy/8ZYYtWUXYnOnw/NF8ThG1W/RqM/h5rkun+OXZMH0=
github.com/metacubex/sing-shadowtls v0.0.0-20250412122235-0e9005731a63/go.mod h1:eDZ2JpkSkewGmUlCoLSn2MRFn1D0jKPIys/6aogFx7U=
github.com/metacubex/sing-tun v0.4.6-0.20250412144348-c426cb167db5 h1:hcsz5e5lqhBxn3iQQDIF60FLZ8PQT542GTQZ+1wcIGo=
github.com/metacubex/sing-tun v0.4.6-0.20250412144348-c426cb167db5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8=
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.7.0-alpha.1 h1:oMFsPh2oTlALJ7vKXPJuqgy0YeiZ+q/LLw+ZdxZ80l4=
github.com/metacubex/utls v1.7.0-alpha.1/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
@@ -170,12 +159,25 @@ github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing v0.5.2 h1:2OZQJNKGtji/66QLxbf/T/dqtK/3+fF/zuHH9tsGK7M=
github.com/sagernet/sing v0.5.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
@@ -187,16 +189,9 @@ github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
@@ -256,7 +251,7 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=

View File

@@ -10,7 +10,6 @@ import (
"github.com/metacubex/mihomo/common/observable"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/mmdb"
"github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/component/updater"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
@@ -29,8 +28,10 @@ import (
var (
isInit = false
configParams = ConfigExtendedParams{}
externalProviders = map[string]cp.Provider{}
logSubscriber observable.Subscription[log.Event]
currentConfig *config.Config
)
func handleInitClash(paramsString string) bool {
@@ -51,8 +52,7 @@ func handleStartListener() bool {
runLock.Lock()
defer runLock.Unlock()
isRunning = true
updateListeners()
resolver.ResetConnection()
updateListeners(true)
return true
}
@@ -91,10 +91,30 @@ func handleValidateConfig(bytes []byte) string {
return ""
}
func handleGetProxies() map[string]constant.Proxy {
func handleUpdateConfig(bytes []byte) string {
var params = &GenerateConfigParams{}
err := json.Unmarshal(bytes, params)
if err != nil {
return err.Error()
}
configParams = params.Params
prof := decorationConfig(params.ProfileId, params.Config)
err = applyConfig(prof)
if err != nil {
return err.Error()
}
return ""
}
func handleGetProxies() string {
runLock.Lock()
defer runLock.Unlock()
return tunnel.ProxiesWithProviders()
data, err := json.Marshal(tunnel.ProxiesWithProviders())
if err != nil {
return ""
}
return string(data)
}
func handleChangeProxy(data string, fn func(string string)) {
@@ -164,12 +184,21 @@ func handleGetTotalTraffic() string {
return string(data)
}
func handleGetProfile(profileId string) string {
prof := getRawConfigWithId(profileId)
data, err := json.Marshal(prof)
if err != nil {
return ""
}
return string(data)
}
func handleResetTraffic() {
statistic.DefaultManager.ResetStatistic()
}
func handleAsyncTestDelay(paramsString string, fn func(string)) {
mBatch.Go(paramsString, func() (bool, error) {
b.Go(paramsString, func() (bool, error) {
var params = &TestDelayParams{}
err := json.Unmarshal([]byte(paramsString), params)
if err != nil {
@@ -237,11 +266,6 @@ func handleGetConnections() string {
func handleCloseConnections() bool {
runLock.Lock()
defer runLock.Unlock()
closeConnections()
return true
}
func closeConnections() {
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
err := c.Close()
if err != nil {
@@ -249,12 +273,6 @@ func closeConnections() {
}
return true
})
}
func handleResetConnections() bool {
runLock.Lock()
defer runLock.Unlock()
resolver.ResetConnection()
return true
}
@@ -424,47 +442,10 @@ func handleSetState(params string) {
_ = json.Unmarshal([]byte(params), state.CurrentState)
}
func handleGetConfig(path string) (*config.RawConfig, error) {
bytes, err := readFile(path)
if err != nil {
return nil, err
}
prof, err := config.UnmarshalRawConfig(bytes)
if err != nil {
return nil, err
}
return prof, nil
}
func handleCrash() {
panic("handle invoke crash")
}
func handleUpdateConfig(bytes []byte) string {
var params = &UpdateParams{}
err := json.Unmarshal(bytes, params)
if err != nil {
return err.Error()
}
updateConfig(params)
return ""
}
func handleSetupConfig(bytes []byte) string {
var params = defaultSetupParams()
err := UnmarshalJson(bytes, params)
if err != nil {
log.Errorln("unmarshalRawConfig error %v", err)
_ = setupConfig(defaultSetupParams())
return err.Error()
}
err = setupConfig(params)
if err != nil {
return err.Error()
}
return ""
}
func init() {
adapter.UrlTestHook = func(url string, name string, delay uint16) {
delayData := &Delay{

View File

@@ -39,14 +39,6 @@ func freeCString(s *C.char) {
C.free(unsafe.Pointer(s))
}
func (result ActionResult) send() {
data, err := result.Json()
if err != nil {
return
}
bridge.SendToPort(result.Port, string(data))
}
//export invokeAction
func invokeAction(paramsChar *C.char, port C.longlong) {
params := C.GoString(paramsChar)
@@ -57,38 +49,22 @@ func invokeAction(paramsChar *C.char, port C.longlong) {
bridge.SendToPort(i, err.Error())
return
}
result := ActionResult{
Id: action.Id,
Method: action.Method,
Port: i,
}
go handleAction(action, result)
go handleAction(action, func(data interface{}) {
bridge.SendToPort(i, string(action.getResult(data)))
})
}
func sendMessage(message Message) {
if messagePort == -1 {
return
}
result := ActionResult{
res, err := message.Json()
if err != nil {
return
}
bridge.SendToPort(messagePort, string(Action{
Method: messageMethod,
Port: messagePort,
Data: message,
}
result.send()
}
//export getConfig
func getConfig(s *C.char) *C.char {
path := C.GoString(s)
config, err := handleGetConfig(path)
if err != nil {
return C.CString("")
}
marshal, err := json.Marshal(config)
if err != nil {
return C.CString("")
}
return C.CString(string(marshal))
}.getResult(res)))
}
//export startListener

View File

@@ -58,7 +58,7 @@ func (t *TunHandler) handleProtect(fd int) {
return
}
Protect(t.callback, fd)
protect(t.callback, fd)
}
func (t *TunHandler) handleResolveProcess(source, target net.Addr) string {
@@ -79,7 +79,7 @@ func (t *TunHandler) handleResolveProcess(source, target net.Addr) string {
if version < 29 {
uid = platform.QuerySocketUidFromProcFs(source, target)
}
return ResolveProcess(t.callback, protocol, source.String(), target.String(), uid)
return resolveProcess(t.callback, protocol, source.String(), target.String(), uid)
}
var (
@@ -98,13 +98,13 @@ func handleStopTun() {
}
}
func handleStartTun(fd int, callback unsafe.Pointer) {
func handleStartTun(fd int, callback unsafe.Pointer) bool {
handleStopTun()
tunLock.Lock()
defer tunLock.Unlock()
now := time.Now()
runTime = &now
if fd != 0 {
tunLock.Lock()
defer tunLock.Unlock()
tunHandler = &TunHandler{
callback: callback,
limit: semaphore.NewWeighted(4),
@@ -113,11 +113,13 @@ func handleStartTun(fd int, callback unsafe.Pointer) {
tunListener, _ := t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
if tunListener != nil {
log.Infoln("TUN address: %v", tunListener.Address())
tunHandler.listener = tunListener
} else {
removeTunHook()
return false
}
tunHandler.listener = tunListener
}
return true
}
func handleGetRunTime() string {
@@ -188,21 +190,21 @@ func handleGetCurrentProfileName() string {
return state.CurrentState.CurrentProfileName
}
func nextHandle(action *Action, result ActionResult) bool {
func nextHandle(action *Action, result func(data interface{})) bool {
switch action.Method {
case getAndroidVpnOptionsMethod:
result.success(handleGetAndroidVpnOptions())
result(handleGetAndroidVpnOptions())
return true
case updateDnsMethod:
data := action.Data.(string)
handleUpdateDns(data)
result.success(true)
result(true)
return true
case getRunTimeMethod:
result.success(handleGetRunTime())
result(handleGetRunTime())
return true
case getCurrentProfileNameMethod:
result.success(handleGetCurrentProfileName())
result(handleGetCurrentProfileName())
return true
}
return false
@@ -220,16 +222,13 @@ func quickStart(initParamsChar *C.char, paramsChar *C.char, stateParamsChar *C.c
bridge.SendToPort(i, "init error")
}
handleSetState(stateParams)
bridge.SendToPort(i, handleSetupConfig(bytes))
bridge.SendToPort(i, handleUpdateConfig(bytes))
}()
}
//export startTUN
func startTUN(fd C.int, callback unsafe.Pointer) bool {
go func() {
handleStartTun(int(fd), callback)
}()
return true
return handleStartTun(int(fd), callback)
}
//export getRunTime
@@ -239,9 +238,7 @@ func getRunTime() *C.char {
//export stopTun
func stopTun() {
go func() {
handleStopTun()
}()
handleStopTun()
}
//export getCurrentProfileName

View File

@@ -12,20 +12,14 @@ import (
var conn net.Conn
func (result ActionResult) send() {
data, err := result.Json()
func sendMessage(message Message) {
res, err := message.Json()
if err != nil {
return
}
send(data)
}
func sendMessage(message Message) {
result := ActionResult{
send(Action{
Method: messageMethod,
Data: message,
}
result.send()
}.getResult(res))
}
func send(data []byte) {
@@ -67,15 +61,12 @@ func startServer(arg string) {
return
}
result := ActionResult{
Id: action.Id,
Method: action.Method,
}
go handleAction(action, result)
go handleAction(action, func(data interface{}) {
send(action.getResult(data))
})
}
}
func nextHandle(action *Action, result ActionResult) bool {
func nextHandle(action *Action, result func(data interface{})) bool {
return false
}

View File

@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/l10n/l10n.dart';
@@ -62,7 +61,7 @@ class ApplicationState extends ConsumerState<Application> {
});
}
void _autoUpdateGroupTask() {
_autoUpdateGroupTask() {
_autoUpdateGroupTaskTimer = Timer(const Duration(milliseconds: 20000), () {
WidgetsBinding.instance.addPostFrameCallback((_) {
globalState.appController.updateGroupsDebounce();
@@ -71,14 +70,14 @@ class ApplicationState extends ConsumerState<Application> {
});
}
void _autoUpdateProfilesTask() {
_autoUpdateProfilesTask() {
_autoUpdateProfilesTaskTimer = Timer(const Duration(minutes: 20), () async {
await globalState.appController.autoUpdateProfiles();
_autoUpdateProfilesTask();
});
}
Widget _buildPlatformState(Widget child) {
_buildPlatformState(Widget child) {
if (system.isDesktop) {
return WindowManager(
child: TrayManager(
@@ -97,14 +96,11 @@ class ApplicationState extends ConsumerState<Application> {
);
}
Widget _buildState(Widget child) {
_buildState(Widget child) {
return AppStateManager(
child: ClashManager(
child: ConnectivityManager(
onConnectivityChanged: (results) async {
if (!results.contains(ConnectivityResult.vpn)) {
clashCore.closeConnections();
}
onConnectivityChanged: () {
globalState.appController.updateLocalIp();
globalState.appController.addCheckIpNumDebounce();
},
@@ -114,7 +110,7 @@ class ApplicationState extends ConsumerState<Application> {
);
}
Widget _buildPlatformApp(Widget child) {
_buildPlatformApp(Widget child) {
if (system.isDesktop) {
return WindowHeaderContainer(
child: child,
@@ -125,7 +121,7 @@ class ApplicationState extends ConsumerState<Application> {
);
}
Widget _buildApp(Widget child) {
_buildApp(Widget child) {
return MessageManager(
child: ThemeManager(
child: child,
@@ -153,12 +149,8 @@ class ApplicationState extends ConsumerState<Application> {
],
builder: (_, child) {
return AppEnvManager(
child: _buildApp(
AppSidebarContainer(
child: _buildPlatformApp(
child!,
),
),
child: _buildPlatformApp(
_buildApp(child!),
),
);
},
@@ -183,7 +175,7 @@ class ApplicationState extends ConsumerState<Application> {
primaryColor: themeProps.primaryColor,
).toPureBlack(themeProps.pureBlack),
),
home: child!,
home: child,
);
},
child: const HomePage(),

View File

@@ -17,7 +17,7 @@ class ClashCore {
late ClashHandlerInterface clashInterface;
ClashCore._internal() {
if (system.isAndroid) {
if (Platform.isAndroid) {
clashInterface = clashLib!;
} else {
clashInterface = clashService!;
@@ -66,11 +66,6 @@ class ClashCore {
Future<bool> init() async {
await initGeo();
if (globalState.config.appSetting.openLogs) {
clashCore.startLog();
} else {
clashCore.stopLog();
}
final homeDirPath = await appPath.homeDirPath;
return await clashInterface.init(
InitParams(
@@ -84,7 +79,7 @@ class ClashCore {
return await clashInterface.setState(state);
}
Future<void> shutdown() async {
shutdown() async {
await clashInterface.shutdown();
}
@@ -94,64 +89,60 @@ class ClashCore {
return clashInterface.validateConfig(data);
}
Future<String> updateConfig(UpdateParams updateParams) async {
return await clashInterface.updateConfig(updateParams);
}
Future<String> setupConfig(SetupParams setupParams) async {
return await clashInterface.setupConfig(setupParams);
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
return await clashInterface.updateConfig(updateConfigParams);
}
Future<List<Group>> getProxiesGroups() async {
final proxies = await clashInterface.getProxies();
if (proxies.isEmpty) return [];
final groupNames = [
UsedProxy.GLOBAL.name,
...(proxies[UsedProxy.GLOBAL.name]['all'] as List).where((e) {
final proxy = proxies[e] ?? {};
return GroupTypeExtension.valueList.contains(proxy['type']);
})
];
final groupsRaw = groupNames.map((groupName) {
final group = proxies[groupName];
group['all'] = ((group['all'] ?? []) as List)
final proxiesRawString = await clashInterface.getProxies();
return Isolate.run<List<Group>>(() {
if (proxiesRawString.isEmpty) return [];
final proxies = (json.decode(proxiesRawString) ?? {}) as Map;
if (proxies.isEmpty) return [];
final groupNames = [
UsedProxy.GLOBAL.name,
...(proxies[UsedProxy.GLOBAL.name]["all"] as List).where((e) {
final proxy = proxies[e] ?? {};
return GroupTypeExtension.valueList.contains(proxy['type']);
})
];
final groupsRaw = groupNames.map((groupName) {
final group = proxies[groupName];
group["all"] = ((group["all"] ?? []) as List)
.map(
(name) => proxies[name],
)
.where((proxy) => proxy != null)
.toList();
return group;
}).toList();
return groupsRaw
.map(
(name) => proxies[name],
(e) => Group.fromJson(e),
)
.where((proxy) => proxy != null)
.toList();
return group;
}).toList();
return groupsRaw
.map(
(e) => Group.fromJson(e),
)
.toList();
});
}
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) async {
return await clashInterface.changeProxy(changeProxyParams);
}
Future<List<TrackerInfo>> getConnections() async {
Future<List<Connection>> getConnections() async {
final res = await clashInterface.getConnections();
final connectionsData = json.decode(res) as Map;
final connectionsRaw = connectionsData['connections'] as List? ?? [];
return connectionsRaw.map((e) => TrackerInfo.fromJson(e)).toList();
return connectionsRaw.map((e) => Connection.fromJson(e)).toList();
}
void closeConnection(String id) {
closeConnection(String id) {
clashInterface.closeConnection(id);
}
void closeConnections() {
closeConnections() {
clashInterface.closeConnections();
}
void resetConnections() {
clashInterface.resetConnections();
}
Future<List<ExternalProvider>> getExternalProviders() async {
final externalProvidersRawString =
await clashInterface.getExternalProviders();
@@ -202,11 +193,11 @@ class ClashCore {
return clashInterface.updateExternalProvider(providerName);
}
Future<void> startListener() async {
startListener() async {
await clashInterface.startListener();
}
Future<void> stopListener() async {
stopListener() async {
await clashInterface.stopListener();
}
@@ -215,16 +206,6 @@ class ClashCore {
return Delay.fromJson(json.decode(data));
}
Future<Map<String, dynamic>> getConfig(String id) async {
final profilePath = await appPath.getProfilePath(id);
final res = await clashInterface.getConfig(profilePath);
if (res.isSuccess) {
return res.data as Map<String, dynamic>;
} else {
throw res.message;
}
}
Future<Traffic> getTraffic() async {
final trafficString = await clashInterface.getTraffic();
if (trafficString.isEmpty) {
@@ -260,23 +241,31 @@ class ClashCore {
return int.parse(value);
}
void resetTraffic() {
Future<ClashConfigSnippet?> getProfile(String id) async {
final res = await clashInterface.getProfile(id);
if (res.isEmpty) {
return null;
}
return Isolate.run(() => ClashConfigSnippet.fromJson(json.decode(res)));
}
resetTraffic() {
clashInterface.resetTraffic();
}
void startLog() {
startLog() {
clashInterface.startLog();
}
void stopLog() {
stopLog() {
clashInterface.stopLog();
}
Future<void> requestGc() async {
await clashInterface.forceGc();
requestGc() {
clashInterface.forceGc();
}
Future<void> destroy() async {
destroy() async {
await clashInterface.destroy();
}
}

View File

@@ -154,18 +154,18 @@ class ClashFFI {
late final _setrlimit =
_setrlimitPtr.asFunction<int Function(int, ffi.Pointer<rlimit>)>();
int wait$1(
int wait1(
ffi.Pointer<ffi.Int> arg0,
) {
return _wait$1(
return _wait1(
arg0,
);
}
late final _wait$1Ptr =
late final _wait1Ptr =
_lookup<ffi.NativeFunction<pid_t Function(ffi.Pointer<ffi.Int>)>>('wait');
late final _wait$1 =
_wait$1Ptr.asFunction<int Function(ffi.Pointer<ffi.Int>)>();
late final _wait1 =
_wait1Ptr.asFunction<int Function(ffi.Pointer<ffi.Int>)>();
int waitpid(
int arg0,
@@ -2518,20 +2518,6 @@ class ClashFFI {
late final _invokeAction =
_invokeActionPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
ffi.Pointer<ffi.Char> getConfig(
ffi.Pointer<ffi.Char> s,
) {
return _getConfig(
s,
);
}
late final _getConfigPtr = _lookup<
ffi.NativeFunction<
ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>>('getConfig');
late final _getConfig = _getConfigPtr
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
void startListener() {
return _startListener();
}
@@ -2653,29 +2639,6 @@ class ClashFFI {
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
}
typedef __int8_t = ffi.SignedChar;
typedef Dart__int8_t = int;
typedef __uint8_t = ffi.UnsignedChar;
typedef Dart__uint8_t = int;
typedef __int16_t = ffi.Short;
typedef Dart__int16_t = int;
typedef __uint16_t = ffi.UnsignedShort;
typedef Dart__uint16_t = int;
typedef __int32_t = ffi.Int;
typedef Dart__int32_t = int;
typedef __uint32_t = ffi.UnsignedInt;
typedef Dart__uint32_t = int;
typedef __int64_t = ffi.LongLong;
typedef Dart__int64_t = int;
typedef __uint64_t = ffi.UnsignedLongLong;
typedef Dart__uint64_t = int;
typedef __darwin_intptr_t = ffi.Long;
typedef Dart__darwin_intptr_t = int;
typedef __darwin_natural_t = ffi.UnsignedInt;
typedef Dart__darwin_natural_t = int;
typedef __darwin_ct_rune_t = ffi.Int;
typedef Dart__darwin_ct_rune_t = int;
final class __mbstate_t extends ffi.Union {
@ffi.Array.multi([128])
external ffi.Array<ffi.Char> __mbstate8;
@@ -2684,46 +2647,6 @@ final class __mbstate_t extends ffi.Union {
external int _mbstateL;
}
typedef __darwin_mbstate_t = __mbstate_t;
typedef __darwin_ptrdiff_t = ffi.Long;
typedef Dart__darwin_ptrdiff_t = int;
typedef __darwin_size_t = ffi.UnsignedLong;
typedef Dart__darwin_size_t = int;
typedef __builtin_va_list = ffi.Pointer<ffi.Char>;
typedef __darwin_va_list = __builtin_va_list;
typedef __darwin_wchar_t = ffi.Int;
typedef Dart__darwin_wchar_t = int;
typedef __darwin_rune_t = __darwin_wchar_t;
typedef __darwin_wint_t = ffi.Int;
typedef Dart__darwin_wint_t = int;
typedef __darwin_clock_t = ffi.UnsignedLong;
typedef Dart__darwin_clock_t = int;
typedef __darwin_socklen_t = __uint32_t;
typedef __darwin_ssize_t = ffi.Long;
typedef Dart__darwin_ssize_t = int;
typedef __darwin_time_t = ffi.Long;
typedef Dart__darwin_time_t = int;
typedef __darwin_blkcnt_t = __int64_t;
typedef __darwin_blksize_t = __int32_t;
typedef __darwin_dev_t = __int32_t;
typedef __darwin_fsblkcnt_t = ffi.UnsignedInt;
typedef Dart__darwin_fsblkcnt_t = int;
typedef __darwin_fsfilcnt_t = ffi.UnsignedInt;
typedef Dart__darwin_fsfilcnt_t = int;
typedef __darwin_gid_t = __uint32_t;
typedef __darwin_id_t = __uint32_t;
typedef __darwin_ino64_t = __uint64_t;
typedef __darwin_ino_t = __darwin_ino64_t;
typedef __darwin_mach_port_name_t = __darwin_natural_t;
typedef __darwin_mach_port_t = __darwin_mach_port_name_t;
typedef __darwin_mode_t = __uint16_t;
typedef __darwin_off_t = __int64_t;
typedef __darwin_pid_t = __int32_t;
typedef __darwin_sigset_t = __uint32_t;
typedef __darwin_suseconds_t = __int32_t;
typedef __darwin_uid_t = __uint32_t;
typedef __darwin_useconds_t = __uint32_t;
final class __darwin_pthread_handler_rec extends ffi.Struct {
external ffi
.Pointer<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>
@@ -2808,48 +2731,6 @@ final class _opaque_pthread_t extends ffi.Struct {
external ffi.Array<ffi.Char> __opaque;
}
typedef __darwin_pthread_attr_t = _opaque_pthread_attr_t;
typedef __darwin_pthread_cond_t = _opaque_pthread_cond_t;
typedef __darwin_pthread_condattr_t = _opaque_pthread_condattr_t;
typedef __darwin_pthread_key_t = ffi.UnsignedLong;
typedef Dart__darwin_pthread_key_t = int;
typedef __darwin_pthread_mutex_t = _opaque_pthread_mutex_t;
typedef __darwin_pthread_mutexattr_t = _opaque_pthread_mutexattr_t;
typedef __darwin_pthread_once_t = _opaque_pthread_once_t;
typedef __darwin_pthread_rwlock_t = _opaque_pthread_rwlock_t;
typedef __darwin_pthread_rwlockattr_t = _opaque_pthread_rwlockattr_t;
typedef __darwin_pthread_t = ffi.Pointer<_opaque_pthread_t>;
typedef __darwin_nl_item = ffi.Int;
typedef Dart__darwin_nl_item = int;
typedef __darwin_wctrans_t = ffi.Int;
typedef Dart__darwin_wctrans_t = int;
typedef __darwin_wctype_t = __uint32_t;
typedef u_int8_t = ffi.UnsignedChar;
typedef Dartu_int8_t = int;
typedef u_int16_t = ffi.UnsignedShort;
typedef Dartu_int16_t = int;
typedef u_int32_t = ffi.UnsignedInt;
typedef Dartu_int32_t = int;
typedef u_int64_t = ffi.UnsignedLongLong;
typedef Dartu_int64_t = int;
typedef register_t = ffi.Int64;
typedef Dartregister_t = int;
typedef user_addr_t = u_int64_t;
typedef user_size_t = u_int64_t;
typedef user_ssize_t = ffi.Int64;
typedef Dartuser_ssize_t = int;
typedef user_long_t = ffi.Int64;
typedef Dartuser_long_t = int;
typedef user_ulong_t = u_int64_t;
typedef user_time_t = ffi.Int64;
typedef Dartuser_time_t = int;
typedef user_off_t = ffi.Int64;
typedef Dartuser_off_t = int;
typedef syscall_arg_t = u_int64_t;
typedef ptrdiff_t = __darwin_ptrdiff_t;
typedef rsize_t = __darwin_size_t;
typedef wint_t = __darwin_wint_t;
final class _GoString_ extends ffi.Struct {
external ffi.Pointer<ffi.Char> p;
@@ -2857,6 +2738,10 @@ final class _GoString_ extends ffi.Struct {
external int n;
}
typedef ptrdiff_t = __darwin_ptrdiff_t;
typedef __darwin_ptrdiff_t = ffi.Long;
typedef Dart__darwin_ptrdiff_t = int;
enum idtype_t {
P_ALL(0),
P_PID(1),
@@ -2869,15 +2754,10 @@ enum idtype_t {
0 => P_ALL,
1 => P_PID,
2 => P_PGID,
_ => throw ArgumentError('Unknown value for idtype_t: $value'),
_ => throw ArgumentError("Unknown value for idtype_t: $value"),
};
}
typedef pid_t = __darwin_pid_t;
typedef id_t = __darwin_id_t;
typedef sig_atomic_t = ffi.Int;
typedef Dartsig_atomic_t = int;
final class __darwin_arm_exception_state extends ffi.Struct {
@__uint32_t()
external int __exception;
@@ -2889,6 +2769,9 @@ final class __darwin_arm_exception_state extends ffi.Struct {
external int __far;
}
typedef __uint32_t = ffi.UnsignedInt;
typedef Dart__uint32_t = int;
final class __darwin_arm_exception_state64 extends ffi.Struct {
@__uint64_t()
external int __far;
@@ -2900,6 +2783,9 @@ final class __darwin_arm_exception_state64 extends ffi.Struct {
external int __exception;
}
typedef __uint64_t = ffi.UnsignedLongLong;
typedef Dart__uint64_t = int;
final class __darwin_arm_exception_state64_v2 extends ffi.Struct {
@__uint64_t()
external int __far;
@@ -3028,9 +2914,6 @@ final class __darwin_mcontext32 extends ffi.Struct {
final class __darwin_mcontext64 extends ffi.Opaque {}
typedef mcontext_t = ffi.Pointer<__darwin_mcontext64>;
typedef pthread_attr_t = __darwin_pthread_attr_t;
final class __darwin_sigaltstack extends ffi.Struct {
external ffi.Pointer<ffi.Void> ss_sp;
@@ -3041,7 +2924,8 @@ final class __darwin_sigaltstack extends ffi.Struct {
external int ss_flags;
}
typedef stack_t = __darwin_sigaltstack;
typedef __darwin_size_t = ffi.UnsignedLong;
typedef Dart__darwin_size_t = int;
final class __darwin_ucontext extends ffi.Struct {
@ffi.Int()
@@ -3060,9 +2944,7 @@ final class __darwin_ucontext extends ffi.Struct {
external ffi.Pointer<__darwin_mcontext64> uc_mcontext;
}
typedef ucontext_t = __darwin_ucontext;
typedef sigset_t = __darwin_sigset_t;
typedef uid_t = __darwin_uid_t;
typedef __darwin_sigset_t = __uint32_t;
final class sigval extends ffi.Union {
@ffi.Int()
@@ -3086,6 +2968,9 @@ final class sigevent extends ffi.Struct {
external ffi.Pointer<pthread_attr_t> sigev_notify_attributes;
}
typedef pthread_attr_t = __darwin_pthread_attr_t;
typedef __darwin_pthread_attr_t = _opaque_pthread_attr_t;
final class __siginfo extends ffi.Struct {
@ffi.Int()
external int si_signo;
@@ -3116,7 +3001,12 @@ final class __siginfo extends ffi.Struct {
external ffi.Array<ffi.UnsignedLong> __pad;
}
typedef siginfo_t = __siginfo;
typedef pid_t = __darwin_pid_t;
typedef __darwin_pid_t = __int32_t;
typedef __int32_t = ffi.Int;
typedef Dart__int32_t = int;
typedef uid_t = __darwin_uid_t;
typedef __darwin_uid_t = __uint32_t;
final class __sigaction_u extends ffi.Union {
external ffi.Pointer<ffi.NativeFunction<ffi.Void Function(ffi.Int)>>
@@ -3130,7 +3020,7 @@ final class __sigaction_u extends ffi.Union {
}
final class __sigaction extends ffi.Struct {
external __sigaction_u __sigaction_u$1;
external __sigaction_u __sigaction_u1;
external ffi.Pointer<
ffi.NativeFunction<
@@ -3144,8 +3034,11 @@ final class __sigaction extends ffi.Struct {
external int sa_flags;
}
typedef siginfo_t = __siginfo;
typedef sigset_t = __darwin_sigset_t;
final class sigaction extends ffi.Struct {
external __sigaction_u __sigaction_u$1;
external __sigaction_u __sigaction_u1;
@sigset_t()
external int sa_mask;
@@ -3154,10 +3047,6 @@ final class sigaction extends ffi.Struct {
external int sa_flags;
}
typedef sig_tFunction = ffi.Void Function(ffi.Int);
typedef Dartsig_tFunction = void Function(int);
typedef sig_t = ffi.Pointer<ffi.NativeFunction<sig_tFunction>>;
final class sigvec extends ffi.Struct {
external ffi.Pointer<ffi.NativeFunction<ffi.Void Function(ffi.Int)>>
sv_handler;
@@ -3176,43 +3065,6 @@ final class sigstack extends ffi.Struct {
external int ss_onstack;
}
typedef int_least8_t = ffi.Int8;
typedef Dartint_least8_t = int;
typedef int_least16_t = ffi.Int16;
typedef Dartint_least16_t = int;
typedef int_least32_t = ffi.Int32;
typedef Dartint_least32_t = int;
typedef int_least64_t = ffi.Int64;
typedef Dartint_least64_t = int;
typedef uint_least8_t = ffi.Uint8;
typedef Dartuint_least8_t = int;
typedef uint_least16_t = ffi.Uint16;
typedef Dartuint_least16_t = int;
typedef uint_least32_t = ffi.Uint32;
typedef Dartuint_least32_t = int;
typedef uint_least64_t = ffi.Uint64;
typedef Dartuint_least64_t = int;
typedef int_fast8_t = ffi.Int8;
typedef Dartint_fast8_t = int;
typedef int_fast16_t = ffi.Int16;
typedef Dartint_fast16_t = int;
typedef int_fast32_t = ffi.Int32;
typedef Dartint_fast32_t = int;
typedef int_fast64_t = ffi.Int64;
typedef Dartint_fast64_t = int;
typedef uint_fast8_t = ffi.Uint8;
typedef Dartuint_fast8_t = int;
typedef uint_fast16_t = ffi.Uint16;
typedef Dartuint_fast16_t = int;
typedef uint_fast32_t = ffi.Uint32;
typedef Dartuint_fast32_t = int;
typedef uint_fast64_t = ffi.Uint64;
typedef Dartuint_fast64_t = int;
typedef intmax_t = ffi.Long;
typedef Dartintmax_t = int;
typedef uintmax_t = ffi.UnsignedLong;
typedef Dartuintmax_t = int;
final class timeval extends ffi.Struct {
@__darwin_time_t()
external int tv_sec;
@@ -3221,7 +3073,9 @@ final class timeval extends ffi.Struct {
external int tv_usec;
}
typedef rlim_t = __uint64_t;
typedef __darwin_time_t = ffi.Long;
typedef Dart__darwin_time_t = int;
typedef __darwin_suseconds_t = __int32_t;
final class rusage extends ffi.Struct {
external timeval ru_utime;
@@ -3271,8 +3125,6 @@ final class rusage extends ffi.Struct {
external int ru_nivcsw;
}
typedef rusage_info_t = ffi.Pointer<ffi.Void>;
final class rusage_info_v0 extends ffi.Struct {
@ffi.Array.multi([16])
external ffi.Array<ffi.Uint8> ri_uuid;
@@ -3878,8 +3730,6 @@ final class rusage_info_v6 extends ffi.Struct {
external ffi.Array<ffi.Uint64> ri_reserved;
}
typedef rusage_info_current = rusage_info_v6;
final class rlimit extends ffi.Struct {
@rlim_t()
external int rlim_cur;
@@ -3888,6 +3738,8 @@ final class rlimit extends ffi.Struct {
external int rlim_max;
}
typedef rlim_t = __uint64_t;
final class proc_rlimit_control_wakeupmon extends ffi.Struct {
@ffi.Uint32()
external int wm_flags;
@@ -3896,10 +3748,10 @@ final class proc_rlimit_control_wakeupmon extends ffi.Struct {
external int wm_rate;
}
final class wait extends ffi.Opaque {}
typedef id_t = __darwin_id_t;
typedef __darwin_id_t = __uint32_t;
typedef ct_rune_t = __darwin_ct_rune_t;
typedef rune_t = __darwin_rune_t;
final class wait extends ffi.Opaque {}
final class div_t extends ffi.Struct {
@ffi.Int()
@@ -3932,18 +3784,18 @@ final class _malloc_zone_t extends ffi.Opaque {}
typedef malloc_zone_t = _malloc_zone_t;
typedef dev_t = __darwin_dev_t;
typedef __darwin_dev_t = __int32_t;
typedef mode_t = __darwin_mode_t;
typedef release_object_funcFunction = ffi.Void Function(
ffi.Pointer<ffi.Void> obj);
typedef Dartrelease_object_funcFunction = void Function(
ffi.Pointer<ffi.Void> obj);
typedef release_object_func
= ffi.Pointer<ffi.NativeFunction<release_object_funcFunction>>;
typedef __darwin_mode_t = __uint16_t;
typedef __uint16_t = ffi.UnsignedShort;
typedef Dart__uint16_t = int;
typedef protect_func = ffi.Pointer<ffi.NativeFunction<protect_funcFunction>>;
typedef protect_funcFunction = ffi.Void Function(
ffi.Pointer<ffi.Void> tun_interface, ffi.Int fd);
typedef Dartprotect_funcFunction = void Function(
ffi.Pointer<ffi.Void> tun_interface, int fd);
typedef protect_func = ffi.Pointer<ffi.NativeFunction<protect_funcFunction>>;
typedef resolve_process_func
= ffi.Pointer<ffi.NativeFunction<resolve_process_funcFunction>>;
typedef resolve_process_funcFunction = ffi.Pointer<ffi.Char> Function(
ffi.Pointer<ffi.Void> tun_interface,
ffi.Int protocol,
@@ -3956,35 +3808,12 @@ typedef Dartresolve_process_funcFunction = ffi.Pointer<ffi.Char> Function(
ffi.Pointer<ffi.Char> source,
ffi.Pointer<ffi.Char> target,
int uid);
typedef resolve_process_func
= ffi.Pointer<ffi.NativeFunction<resolve_process_funcFunction>>;
typedef GoInt8 = ffi.SignedChar;
typedef DartGoInt8 = int;
typedef GoUint8 = ffi.UnsignedChar;
typedef DartGoUint8 = int;
typedef GoInt16 = ffi.Short;
typedef DartGoInt16 = int;
typedef GoUint16 = ffi.UnsignedShort;
typedef DartGoUint16 = int;
typedef GoInt32 = ffi.Int;
typedef DartGoInt32 = int;
typedef GoUint32 = ffi.UnsignedInt;
typedef DartGoUint32 = int;
typedef GoInt64 = ffi.LongLong;
typedef DartGoInt64 = int;
typedef GoUint64 = ffi.UnsignedLongLong;
typedef DartGoUint64 = int;
typedef GoInt = GoInt64;
typedef GoUint = GoUint64;
typedef GoUintptr = ffi.Size;
typedef DartGoUintptr = int;
typedef GoFloat32 = ffi.Float;
typedef DartGoFloat32 = double;
typedef GoFloat64 = ffi.Double;
typedef DartGoFloat64 = double;
typedef GoString = _GoString_;
typedef GoMap = ffi.Pointer<ffi.Void>;
typedef GoChan = ffi.Pointer<ffi.Void>;
typedef release_object_func
= ffi.Pointer<ffi.NativeFunction<release_object_funcFunction>>;
typedef release_object_funcFunction = ffi.Void Function(
ffi.Pointer<ffi.Void> obj);
typedef Dartrelease_object_funcFunction = void Function(
ffi.Pointer<ffi.Void> obj);
final class GoInterface extends ffi.Struct {
external ffi.Pointer<ffi.Void> t;
@@ -4002,6 +3831,12 @@ final class GoSlice extends ffi.Struct {
external int cap;
}
typedef GoInt = GoInt64;
typedef GoInt64 = ffi.LongLong;
typedef DartGoInt64 = int;
typedef GoUint8 = ffi.UnsignedChar;
typedef DartGoUint8 = int;
const int __has_safe_buffers = 1;
const int __DARWIN_ONLY_64_BIT_INO_T = 1;

View File

@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
import 'package:fl_clash/clash/message.dart';
import 'package:fl_clash/common/common.dart';
@@ -20,15 +19,11 @@ mixin ClashInterface {
FutureOr<String> validateConfig(String data);
FutureOr<Result> getConfig(String path);
Future<String> asyncTestDelay(String url, String proxyName);
FutureOr<String> updateConfig(UpdateParams updateParams);
FutureOr<String> updateConfig(UpdateConfigParams updateConfigParams);
FutureOr<String> setupConfig(SetupParams setupParams);
FutureOr<Map> getProxies();
FutureOr<String> getProxies();
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams);
@@ -57,11 +52,11 @@ mixin ClashInterface {
FutureOr<String> getMemory();
FutureOr<void> resetTraffic();
resetTraffic();
FutureOr<void> startLog();
startLog();
FutureOr<void> stopLog();
stopLog();
Future<bool> crash();
@@ -71,12 +66,18 @@ mixin ClashInterface {
FutureOr<bool> closeConnections();
FutureOr<bool> resetConnections();
FutureOr<String> getProfile(String id);
Future<bool> setState(CoreState state);
}
mixin AndroidClashInterface {
Future<bool> setFdMap(int fd);
Future<bool> setProcessMap(ProcessMapItem item);
// Future<bool> stopTun();
Future<bool> updateDns(String value);
Future<AndroidVpnOptions?> getAndroidVpnOptions();
@@ -89,29 +90,61 @@ mixin AndroidClashInterface {
abstract class ClashHandlerInterface with ClashInterface {
Map<String, Completer> callbackCompleterMap = {};
Future<void> handleResult(ActionResult result) async {
Future<bool> nextHandleResult(ActionResult result, Completer? completer) =>
Future.value(false);
handleResult(ActionResult result) async {
final completer = callbackCompleterMap[result.id];
try {
switch (result.method) {
case ActionMethod.initClash:
case ActionMethod.shutdown:
case ActionMethod.getIsInit:
case ActionMethod.startListener:
case ActionMethod.resetTraffic:
case ActionMethod.closeConnections:
case ActionMethod.closeConnection:
case ActionMethod.stopListener:
case ActionMethod.setState:
case ActionMethod.crash:
completer?.complete(result.data as bool);
return;
case ActionMethod.changeProxy:
case ActionMethod.getProxies:
case ActionMethod.getTraffic:
case ActionMethod.getTotalTraffic:
case ActionMethod.asyncTestDelay:
case ActionMethod.getConnections:
case ActionMethod.getExternalProviders:
case ActionMethod.getExternalProvider:
case ActionMethod.validateConfig:
case ActionMethod.updateConfig:
case ActionMethod.updateGeoData:
case ActionMethod.updateExternalProvider:
case ActionMethod.sideLoadExternalProvider:
case ActionMethod.getCountryCode:
case ActionMethod.getMemory:
completer?.complete(result.data as String);
return;
case ActionMethod.message:
clashMessage.controller.add(result.data);
clashMessage.controller.add(result.data as String);
completer?.complete(true);
return;
case ActionMethod.getConfig:
completer?.complete(result.toResult);
return;
default:
final isHandled = await nextHandleResult(result, completer);
if (isHandled) {
return;
}
completer?.complete(result.data);
return;
}
} catch (e) {
commonPrint.log('${result.id} error $e');
} catch (_) {
commonPrint.log(result.id);
}
}
void sendMessage(String message);
sendMessage(String message);
FutureOr<void> reStart();
reStart();
FutureOr<bool> destroy();
@@ -120,21 +153,18 @@ abstract class ClashHandlerInterface with ClashInterface {
dynamic data,
Duration? timeout,
FutureOr<T> Function()? onTimeout,
T? defaultValue,
}) async {
final id = '${method.name}#${utils.id}';
final id = "${method.name}#${utils.id}";
callbackCompleterMap[id] = Completer<T>();
dynamic mDefaultValue = defaultValue;
if (mDefaultValue == null) {
if (T == String) {
mDefaultValue = '';
} else if (T == bool) {
mDefaultValue = false;
} else if (T == Map) {
mDefaultValue = {};
}
dynamic defaultValue;
if (T == String) {
defaultValue = "";
}
if (T == bool) {
defaultValue = false;
}
sendMessage(
@@ -143,6 +173,7 @@ abstract class ClashHandlerInterface with ClashInterface {
id: id,
method: method,
data: data,
defaultValue: defaultValue,
),
),
);
@@ -154,7 +185,7 @@ abstract class ClashHandlerInterface with ClashInterface {
},
onTimeout: onTimeout ??
() {
return mDefaultValue;
return defaultValue;
},
functionName: id,
);
@@ -180,7 +211,6 @@ abstract class ClashHandlerInterface with ClashInterface {
shutdown() async {
return await invoke<bool>(
method: ActionMethod.shutdown,
timeout: Duration(seconds: 1),
);
}
@@ -207,34 +237,14 @@ abstract class ClashHandlerInterface with ClashInterface {
}
@override
Future<String> updateConfig(UpdateParams updateParams) async {
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
return await invoke<String>(
method: ActionMethod.updateConfig,
data: json.encode(updateParams),
data: json.encode(updateConfigParams),
timeout: Duration(minutes: 2),
);
}
@override
Future<Result> getConfig(String path) async {
final res = await invoke<Result>(
method: ActionMethod.getConfig,
data: path,
timeout: Duration(minutes: 2),
defaultValue: Result.success({}),
);
return res;
}
@override
Future<String> setupConfig(SetupParams setupParams) async {
final data = await Isolate.run(() => json.encode(setupParams));
return await invoke<String>(
method: ActionMethod.setupConfig,
data: data,
timeout: Duration(minutes: 2),
);
}
@override
Future<bool> crash() {
@@ -244,8 +254,8 @@ abstract class ClashHandlerInterface with ClashInterface {
}
@override
Future<Map> getProxies() {
return invoke<Map>(
Future<String> getProxies() {
return invoke<String>(
method: ActionMethod.getProxies,
timeout: Duration(seconds: 5),
);
@@ -290,8 +300,8 @@ abstract class ClashHandlerInterface with ClashInterface {
return invoke<String>(
method: ActionMethod.sideLoadExternalProvider,
data: json.encode({
'providerName': providerName,
'data': data,
"providerName": providerName,
"data": data,
}),
);
}
@@ -320,16 +330,17 @@ abstract class ClashHandlerInterface with ClashInterface {
}
@override
Future<bool> resetConnections() {
Future<bool> closeConnection(String id) {
return invoke<bool>(
method: ActionMethod.resetConnections,
method: ActionMethod.closeConnection,
data: id,
);
}
@override
Future<bool> closeConnection(String id) {
return invoke<bool>(
method: ActionMethod.closeConnection,
Future<String> getProfile(String id) {
return invoke<String>(
method: ActionMethod.getProfile,
data: id,
);
}
@@ -382,9 +393,9 @@ abstract class ClashHandlerInterface with ClashInterface {
@override
Future<String> asyncTestDelay(String url, String proxyName) {
final delayParams = {
'proxy-name': proxyName,
'timeout': httpTimeoutDuration.inMilliseconds,
'test-url': url,
"proxy-name": proxyName,
"timeout": httpTimeoutDuration.inMilliseconds,
"test-url": url,
};
return invoke<String>(
method: ActionMethod.asyncTestDelay,

View File

@@ -1,11 +1,12 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'dart:ui';
import 'package:ffi/ffi.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/constant.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/service.dart';
@@ -29,7 +30,7 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
return _canSendCompleter.future;
}
Future<void> _initService() async {
_initService() async {
await service?.destroy();
_registerMainPort(receiverPort.sendPort);
receiverPort.listen((message) {
@@ -51,7 +52,7 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
await service?.init();
}
void _registerMainPort(SendPort sendPort) {
_registerMainPort(SendPort sendPort) {
IsolateNameServer.removePortNameMapping(mainIsolate);
IsolateNameServer.registerPortWithName(sendPort, mainIsolate);
}
@@ -61,6 +62,26 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
return _instance!;
}
@override
Future<bool> nextHandleResult(result, completer) async {
switch (result.method) {
case ActionMethod.setFdMap:
case ActionMethod.setProcessMap:
case ActionMethod.stopTun:
case ActionMethod.updateDns:
completer?.complete(result.data as bool);
return true;
case ActionMethod.getRunTime:
case ActionMethod.startTun:
case ActionMethod.getAndroidVpnOptions:
case ActionMethod.getCurrentProfileName:
completer?.complete(result.data as String);
return true;
default:
return false;
}
}
@override
destroy() async {
await service?.destroy();
@@ -85,6 +106,22 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
sendPort?.send(message);
}
@override
Future<bool> setFdMap(int fd) {
return invoke<bool>(
method: ActionMethod.setFdMap,
data: json.encode(fd),
);
}
@override
Future<bool> setProcessMap(item) {
return invoke<bool>(
method: ActionMethod.setProcessMap,
data: item,
);
}
// @override
// Future<bool> stopTun() {
// return invoke<bool>(
@@ -138,7 +175,7 @@ class ClashLibHandler {
late final DynamicLibrary lib;
ClashLibHandler._internal() {
lib = DynamicLibrary.open('libclash.so');
lib = DynamicLibrary.open("libclash.so");
clashFFI = ClashFFI(lib);
clashFFI.initNativeApiBridge(
NativeApi.initializeApiDLData,
@@ -168,19 +205,19 @@ class ClashLibHandler {
return completer.future;
}
void attachMessagePort(int messagePort) {
attachMessagePort(int messagePort) {
clashFFI.attachMessagePort(
messagePort,
);
}
void updateDns(String dns) {
updateDns(String dns) {
final dnsChar = dns.toNativeUtf8().cast<Char>();
clashFFI.updateDns(dnsChar);
malloc.free(dnsChar);
}
void setState(CoreState state) {
setState(CoreState state) {
final stateChar = json.encode(state).toNativeUtf8().cast<Char>();
clashFFI.setState(stateChar);
malloc.free(stateChar);
@@ -220,42 +257,19 @@ class ClashLibHandler {
return Traffic.fromMap(json.decode(trafficString));
}
Future<bool> startListener() async {
startListener() async {
clashFFI.startListener();
return true;
}
Future<bool> stopListener() async {
stopListener() async {
clashFFI.stopListener();
return true;
}
DateTime? getRunTime() {
final runTimeRaw = clashFFI.getRunTime();
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
if (runTimeString.isEmpty) {
return null;
}
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
}
Future<Map<String, dynamic>> getConfig(String id) async {
final path = await appPath.getProfilePath(id);
final pathChar = path.toNativeUtf8().cast<Char>();
final configRaw = clashFFI.getConfig(pathChar);
final configString = configRaw.cast<Utf8>().toDartString();
if (configString.isEmpty) {
return {};
}
final config = json.decode(configString);
malloc.free(pathChar);
clashFFI.freeCString(configRaw);
return config;
}
Future<String> quickStart(
InitParams initParams,
SetupParams setupParams,
UpdateConfigParams updateConfigParams,
CoreState state,
) {
final completer = Completer<String>();
@@ -266,7 +280,7 @@ class ClashLibHandler {
receiver.close();
}
});
final params = json.encode(setupParams);
final params = json.encode(updateConfigParams);
final initValue = json.encode(initParams);
final stateParams = json.encode(state);
final initParamsChar = initValue.toNativeUtf8().cast<Char>();
@@ -283,10 +297,15 @@ class ClashLibHandler {
malloc.free(stateParamsChar);
return completer.future;
}
DateTime? getRunTime() {
final runTimeRaw = clashFFI.getRunTime();
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(runTimeRaw);
if (runTimeString.isEmpty) return null;
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
}
}
ClashLib? get clashLib =>
system.isAndroid && !globalState.isService ? ClashLib() : null;
ClashLibHandler? get clashLibHandler =>
system.isAndroid && globalState.isService ? ClashLibHandler() : null;
Platform.isAndroid && !globalState.isService ? ClashLib() : null;

View File

@@ -1,19 +1,20 @@
import 'dart:async';
import 'dart:convert';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/foundation.dart';
class ClashMessage {
final controller = StreamController<Map<String, Object?>>();
final controller = StreamController<String>();
ClashMessage._() {
controller.stream.listen(
(message) {
if (message.isEmpty) {
if(message.isEmpty){
return;
}
final m = AppMessage.fromJson(message);
final m = AppMessage.fromJson(json.decode(message));
for (final AppMessageListener listener in _listeners) {
switch (m.type) {
case AppMessageType.log:
@@ -23,7 +24,7 @@ class ClashMessage {
listener.onDelay(Delay.fromJson(m.data));
break;
case AppMessageType.request:
listener.onRequest(TrackerInfo.fromJson(m.data));
listener.onRequest(Connection.fromJson(m.data));
break;
case AppMessageType.loaded:
listener.onLoaded(m.data);

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:fl_clash/clash/interface.dart';
import 'package:fl_clash/common/common.dart';
@@ -28,9 +29,9 @@ class ClashService extends ClashHandlerInterface {
reStart();
}
Future<void> _initServer() async {
_initServer() async {
runZonedGuarded(() async {
final address = !system.isWindows
final address = !Platform.isWindows
? InternetAddress(
unixSocketPath,
type: InternetAddressType.unix,
@@ -50,24 +51,29 @@ class ClashService extends ClashHandlerInterface {
await _destroySocket();
socketCompleter.complete(socket);
socket
.transform(uint8ListToListIntConverter)
.transform(utf8.decoder)
.transform(
StreamTransformer<Uint8List, String>.fromHandlers(
handleData: (Uint8List data, EventSink<String> sink) {
sink.add(utf8.decode(data, allowMalformed: true));
},
),
)
.transform(LineSplitter())
.listen(
(data) {
handleResult(
ActionResult.fromJson(
json.decode(data.trim()),
),
(data) {
handleResult(
ActionResult.fromJson(
json.decode(data.trim()),
),
);
},
);
},
);
}
}, (error, stack) {
commonPrint.log(error.toString());
if (error is SocketException) {
if(error is SocketException){
globalState.showNotifier(error.toString());
// globalState.appController.restartCore();
globalState.appController.restartCore();
}
});
}
@@ -83,14 +89,15 @@ class ClashService extends ClashHandlerInterface {
await shutdown();
}
final serverSocket = await serverCompleter.future;
final arg = system.isWindows
? '${serverSocket.port}'
final arg = Platform.isWindows
? "${serverSocket.port}"
: serverSocket.address.address;
if (system.isWindows && await system.checkIsAdmin()) {
final isSuccess = await request.startCoreByHelper(arg);
if (isSuccess) {
return;
}
bool isSuccess = false;
if (Platform.isWindows && await system.checkIsAdmin()) {
isSuccess = await request.startCoreByHelper(arg);
}
if (isSuccess) {
return;
}
process = await Process.start(
appPath.corePath,
@@ -98,13 +105,7 @@ class ClashService extends ClashHandlerInterface {
arg,
],
);
process?.stdout.listen((_) {});
process?.stderr.listen((e) {
final error = utf8.decode(e);
if (error.isNotEmpty) {
commonPrint.log(error);
}
});
process!.stdout.listen((_) {});
isStarting = false;
}
@@ -122,8 +123,8 @@ class ClashService extends ClashHandlerInterface {
socket.writeln(message);
}
Future<void> _deleteSocketFile() async {
if (!system.isWindows) {
_deleteSocketFile() async {
if (!Platform.isWindows) {
final file = File(unixSocketPath);
if (await file.exists()) {
await file.delete();
@@ -131,7 +132,7 @@ class ClashService extends ClashHandlerInterface {
}
}
Future<void> _destroySocket() async {
_destroySocket() async {
if (socketCompleter.isCompleted) {
final lastSocket = await socketCompleter.future;
await lastSocket.close();
@@ -141,7 +142,7 @@ class ClashService extends ClashHandlerInterface {
@override
shutdown() async {
if (system.isWindows) {
if (Platform.isWindows) {
await request.stopCoreByHelper();
}
await _destroySocket();

View File

@@ -1,14 +1,14 @@
import 'dart:io';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/state.dart';
import 'system.dart';
class Android {
Future<void> init() async {
init() async {
app?.onExit = () async {
await globalState.appController.savePreferences();
};
}
}
final android = system.isAndroid ? Android() : null;
final android = Platform.isAndroid ? Android() : null;

View File

@@ -1,11 +1,10 @@
import 'dart:convert';
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:path/path.dart';
extension ArchiveExt on Archive {
void addDirectoryToArchive(String dirPath, String parentPath) {
addDirectoryToArchive(String dirPath, String parentPath) {
final dir = Directory(dirPath);
final entities = dir.listSync(recursive: false);
for (final entity in entities) {
@@ -20,7 +19,7 @@ extension ArchiveExt on Archive {
}
}
void add<T>(String name, T raw) {
add<T>(String name, T raw) {
final data = json.encode(raw);
addFile(
ArchiveFile(name, data.length, data),

View File

@@ -23,10 +23,6 @@ extension ColorExtension on Color {
return withAlpha(77);
}
Color get opacity12 {
return withAlpha(31);
}
Color get opacity15 {
return withAlpha(38);
}

View File

@@ -3,9 +3,7 @@ export 'app_localizations.dart';
export 'color.dart';
export 'constant.dart';
export 'context.dart';
export 'converter.dart';
export 'datetime.dart';
export 'fixed.dart';
export 'function.dart';
export 'future.dart';
export 'http.dart';
@@ -14,26 +12,28 @@ export 'iterable.dart';
export 'keyboard.dart';
export 'launch.dart';
export 'link.dart';
export 'list.dart';
export 'lock.dart';
export 'measure.dart';
export 'mixin.dart';
export 'navigation.dart';
export 'navigator.dart';
export 'network.dart';
export 'num.dart';
export 'utils.dart';
export 'package.dart';
export 'path.dart';
export 'picker.dart';
export 'preferences.dart';
export 'print.dart';
export 'protocol.dart';
export 'proxy.dart';
export 'render.dart';
export 'request.dart';
export 'scroll.dart';
export 'string.dart';
export 'system.dart';
export 'text.dart';
export 'tray.dart';
export 'utils.dart';
export 'window.dart';
export 'windows.dart';
export 'render.dart';
export 'mixin.dart';
export 'print.dart';

View File

@@ -1,3 +1,4 @@
import 'dart:io';
import 'dart:math';
import 'dart:ui';
@@ -7,48 +8,45 @@ import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
const appName = 'FlClash';
const appHelperService = 'FlClashHelperService';
const coreName = 'clash.meta';
const appName = "FlClash";
const appHelperService = "FlClashHelperService";
const coreName = "clash.meta";
const browserUa =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
const packageName = 'com.follow.clash';
final unixSocketPath = '/tmp/FlClashSocket_${Random().nextInt(10000)}.sock';
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
const packageName = "com.follow.clash";
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
const helperPort = 47890;
const maxTextScale = 1.4;
const minTextScale = 0.8;
const helperTag = "2024125";
final baseInfoEdgeInsets = EdgeInsets.symmetric(
vertical: 16.ap,
horizontal: 16.ap,
);
final defaultTextScaleFactor =
WidgetsBinding.instance.platformDispatcher.textScaleFactor;
final defaultTextScaleFactor = WidgetsBinding.instance.platformDispatcher.textScaleFactor;
const httpTimeoutDuration = Duration(milliseconds: 5000);
const moreDuration = Duration(milliseconds: 100);
const animateDuration = Duration(milliseconds: 100);
const midDuration = Duration(milliseconds: 200);
const commonDuration = Duration(milliseconds: 300);
const defaultUpdateDuration = Duration(days: 1);
const mmdbFileName = 'geoip.metadb';
const asnFileName = 'ASN.mmdb';
const geoIpFileName = 'GeoIP.dat';
const geoSiteFileName = 'GeoSite.dat';
const mmdbFileName = "geoip.metadb";
const asnFileName = "ASN.mmdb";
const geoIpFileName = "GeoIP.dat";
const geoSiteFileName = "GeoSite.dat";
final double kHeaderHeight = system.isDesktop
? !system.isMacOS
? !Platform.isMacOS
? 40
: 28
: 0;
const profilesDirectoryName = 'profiles';
const localhost = '127.0.0.1';
const clashConfigKey = 'clash_config';
const configKey = 'config';
const profilesDirectoryName = "profiles";
const localhost = "127.0.0.1";
const clashConfigKey = "clash_config";
const configKey = "config";
const double dialogCommonWidth = 300;
const repository = 'chen08209/FlClash';
const defaultExternalController = '127.0.0.1:9090';
const repository = "chen08209/FlClash";
const defaultExternalController = "127.0.0.1:9090";
const maxMobileWidth = 600;
const maxLaptopWidth = 840;
const defaultTestUrl = 'https://www.gstatic.com/generate_204';
const defaultTestUrl = "https://www.gstatic.com/generate_204";
final commonFilter = ImageFilter.blur(
sigmaX: 5,
sigmaY: 5,
@@ -56,7 +54,7 @@ final commonFilter = ImageFilter.blur(
);
const navigationItemListEquality = ListEquality<NavigationItem>();
const trackerInfoListEquality = ListEquality<TrackerInfo>();
const connectionListEquality = ListEquality<Connection>();
const stringListEquality = ListEquality<String>();
const intListEquality = ListEquality<int>();
const logListEquality = ListEquality<Log>();
@@ -77,33 +75,22 @@ const viewModeColumnsMap = {
ViewMode.desktop: [4, 3],
};
const proxiesListStoreKey = PageStorageKey<String>('proxies_list');
const toolsStoreKey = PageStorageKey<String>('tools');
const profilesStoreKey = PageStorageKey<String>('profiles');
const defaultPrimaryColor = 0XFFD8C0C3;
const defaultPrimaryColor = 0xFF795548;
double getWidgetHeight(num lines) {
return max(lines * 84 + (lines - 1) * 16, 0).ap;
}
const maxLength = 1000;
final mainIsolate = "FlClashMainIsolate";
final mainIsolate = 'FlClashMainIsolate';
final serviceIsolate = 'FlClashServiceIsolate';
final serviceIsolate = "FlClashServiceIsolate";
const defaultPrimaryColors = [
0xFF795548,
defaultPrimaryColor,
0xFF03A9F4,
0xFFFFFF00,
0XFFBBC9CC,
0XFFABD397,
defaultPrimaryColor,
0XFFD8C0C3,
0XFF665390,
];
const scriptTemplate = '''
const main = (config) => {
return config;
}''';

View File

@@ -7,11 +7,11 @@ extension BuildContextExtension on BuildContext {
return findAncestorStateOfType<CommonScaffoldState>();
}
Future<void>? showNotifier(String text) {
showNotifier(String text) {
return findAncestorStateOfType<MessageManagerState>()?.message(text);
}
void showSnackBar(
showSnackBar(
String message, {
SnackBarAction? action,
}) {
@@ -72,18 +72,3 @@ extension BuildContextExtension on BuildContext {
return state;
}
}
class BackHandleInherited extends InheritedWidget {
final Function handleBack;
const BackHandleInherited(
{super.key, required this.handleBack, required super.child});
static BackHandleInherited? of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<BackHandleInherited>();
@override
bool updateShouldNotify(BackHandleInherited oldWidget) {
return handleBack != oldWidget.handleBack;
}
}

View File

@@ -1,32 +0,0 @@
import 'dart:convert';
import 'dart:typed_data';
class Uint8ListToListIntConverter extends Converter<Uint8List, List<int>> {
@override
List<int> convert(Uint8List input) {
return input.toList();
}
@override
Sink<Uint8List> startChunkedConversion(Sink<List<int>> sink) {
return _Uint8ListToListIntConverterSink(sink);
}
}
class _Uint8ListToListIntConverterSink implements Sink<Uint8List> {
const _Uint8ListToListIntConverterSink(this._target);
final Sink<List<int>> _target;
@override
void add(Uint8List data) {
_target.add(data.toList());
}
@override
void close() {
_target.close();
}
}
final uint8ListToListIntConverter = Uint8ListToListIntConverter();

View File

@@ -17,34 +17,26 @@ extension DateTimeExtension on DateTime {
final difference = currentDateTime.difference(this);
final days = difference.inDays;
if (days >= 365) {
return '${(days / 365).floor()} ${appLocalizations.years}${appLocalizations.ago}';
return "${(days / 365).floor()} ${appLocalizations.years}${appLocalizations.ago}";
}
if (days >= 30) {
return '${(days / 30).floor()} ${appLocalizations.months}${appLocalizations.ago}';
return "${(days / 30).floor()} ${appLocalizations.months}${appLocalizations.ago}";
}
if (days >= 1) {
return '$days ${appLocalizations.days}${appLocalizations.ago}';
return "$days ${appLocalizations.days}${appLocalizations.ago}";
}
final hours = difference.inHours;
if (hours >= 1) {
return '$hours ${appLocalizations.hours}${appLocalizations.ago}';
return "$hours ${appLocalizations.hours}${appLocalizations.ago}";
}
final minutes = difference.inMinutes;
if (minutes >= 1) {
return '$minutes ${appLocalizations.minutes}${appLocalizations.ago}';
return "$minutes ${appLocalizations.minutes}${appLocalizations.ago}";
}
return appLocalizations.just;
}
String get show {
return toString().substring(0, 10);
}
String get showFull {
return toString().substring(0, 19);
}
String get showTime {
return toString().substring(10, 19);
return toIso8601String().substring(0, 10);
}
}

View File

@@ -38,18 +38,18 @@ class DAVClient {
}
}
String get root => '/$appName';
get root => "/$appName";
String get backupFile => '$root/$fileName';
get backupFile => "$root/$fileName";
Future<bool> backup(Uint8List data) async {
await client.mkdir(root);
await client.write(backupFile, data);
backup(Uint8List data) async {
await client.mkdir("$root");
await client.write("$backupFile", data);
return true;
}
Future<List<int>> recovery() async {
await client.mkdir(root);
await client.mkdir("$root");
final data = await client.read(backupFile);
return data;
}

View File

@@ -1,81 +0,0 @@
import 'iterable.dart';
typedef ValueCallback<T> = T Function();
class FixedList<T> {
final int maxLength;
final List<T> _list;
FixedList(this.maxLength, {List<T>? list})
: _list = (list ?? [])..truncate(maxLength);
void add(T item) {
_list.add(item);
_list.truncate(maxLength);
}
void clear() {
_list.clear();
}
List<T> get list => List.unmodifiable(_list);
int get length => _list.length;
T operator [](int index) => _list[index];
FixedList<T> copyWith() {
return FixedList(
maxLength,
list: _list,
);
}
}
class FixedMap<K, V> {
int maxLength;
late Map<K, V> _map;
FixedMap(this.maxLength, {Map<K, V>? map}) {
_map = map ?? {};
}
V updateCacheValue(K key, ValueCallback<V> callback) {
final realValue = _map.updateCacheValue(
key,
callback,
);
_adjustMap();
return realValue;
}
void clear() {
_map.clear();
}
void updateMaxLength(int size) {
maxLength = size;
_adjustMap();
}
void updateMap(Map<K, V> map) {
_map = map;
_adjustMap();
}
void _adjustMap() {
if (_map.length > maxLength) {
_map = Map.fromEntries(
map.entries.toList()..truncate(maxLength),
);
}
}
V? get(K key) => _map[key];
bool containsKey(K key) => _map.containsKey(key);
int get length => _map.length;
Map<K, V> get map => Map.unmodifiable(_map);
}

View File

@@ -1,12 +1,10 @@
import 'dart:async';
import 'package:fl_clash/enum/enum.dart';
class Debouncer {
final Map<FunctionTag, Timer?> _operations = {};
final Map<dynamic, Timer?> _operations = {};
void call(
FunctionTag tag,
call(
dynamic tag,
Function func, {
List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600),
@@ -28,17 +26,17 @@ class Debouncer {
);
}
void cancel(dynamic tag) {
cancel(dynamic tag) {
_operations[tag]?.cancel();
_operations[tag] = null;
}
}
class Throttler {
final Map<FunctionTag, Timer?> _operations = {};
final Map<dynamic, Timer?> _operations = {};
bool call(
FunctionTag tag,
call(
dynamic tag,
Function func, {
List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600),
@@ -61,7 +59,7 @@ class Throttler {
return false;
}
void cancel(dynamic tag) {
cancel(dynamic tag) {
_operations[tag]?.cancel();
_operations[tag] = null;
}
@@ -81,7 +79,7 @@ Future<T> retry<T>({
}
attempts++;
}
throw 'unknown error';
throw "unknown error";
}
final debouncer = Debouncer();

View File

@@ -4,7 +4,7 @@ import 'dart:ui';
import 'package:fl_clash/common/common.dart';
extension CompleterExt<T> on Completer<T> {
Future<T> safeFuture({
safeFuture({
Duration? timeout,
VoidCallback? onLast,
FutureOr<T> Function()? onTimeout,

View File

@@ -6,19 +6,18 @@ import 'package:fl_clash/state.dart';
class FlClashHttpOverrides extends HttpOverrides {
static String handleFindProxy(Uri url) {
if ([localhost].contains(url.host)) {
return 'DIRECT';
return "DIRECT";
}
final port = globalState.config.patchClashConfig.mixedPort;
final isStart = globalState.appState.runTime != null;
commonPrint.log('find $url proxy:$isStart');
if (!isStart) return 'DIRECT';
return 'PROXY localhost:$port';
commonPrint.log("find $url proxy:$isStart");
if (!isStart) return "DIRECT";
return "PROXY localhost:$port";
}
@override
HttpClient createHttpClient(SecurityContext? context) {
final client = super.createHttpClient(context);
client.badCertificateCallback = (_, __, ___) => true;
client.findProxy = handleFindProxy;
return client;
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
class IconsExt {
static const IconData target = IconData(0xe900, fontFamily: 'Icons');
}
class IconsExt{
static const IconData target =
IconData(0xe900, fontFamily: "Icons");
}

View File

@@ -38,53 +38,6 @@ extension IterableExt<T> on Iterable<T> {
count++;
}
}
Iterable<T> takeLast({int count = 50}) {
if (count <= 0) return Iterable.empty();
return count >= length ? this : toList().skip(length - count);
}
}
extension ListExt<T> on List<T> {
void truncate(int maxLength) {
if (maxLength == 0) {
return;
}
if (length > maxLength) {
removeRange(0, length - maxLength);
}
}
List<T> intersection(List<T> list) {
return where((item) => list.contains(item)).toList();
}
List<List<T>> batch(int maxConcurrent) {
final batches = (length / maxConcurrent).ceil();
final List<List<T>> res = [];
for (int i = 0; i < batches; i++) {
if (i != batches - 1) {
res.add(sublist(i * maxConcurrent, maxConcurrent * (i + 1)));
} else {
res.add(sublist(i * maxConcurrent, length));
}
}
return res;
}
List<T> safeSublist(int start, [int? end]) {
if (start <= 0) return this;
if (start > length) return [];
if (end != null) {
return sublist(start, end.clamp(start, length));
}
return sublist(start);
}
T safeGet(int index) {
if (length > index) return this[index];
return last;
}
}
extension DoubleListExt on List<double> {
@@ -114,10 +67,10 @@ extension DoubleListExt on List<double> {
}
extension MapExt<K, V> on Map<K, V> {
V updateCacheValue(K key, V Function() callback) {
getCacheValue(K key, V defaultValue) {
if (this[key] == null) {
this[key] = callback();
this[key] = defaultValue;
}
return this[key]!;
return this[key];
}
}

View File

@@ -1,8 +1,8 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:uni_platform/uni_platform.dart';
import 'system.dart';
final Map<PhysicalKeyboardKey, String> _knownKeyLabels =
<PhysicalKeyboardKey, String>{
PhysicalKeyboardKey.keyA: 'A',
@@ -79,14 +79,14 @@ final Map<PhysicalKeyboardKey, String> _knownKeyLabels =
PhysicalKeyboardKey.arrowLeft: '',
PhysicalKeyboardKey.arrowDown: '',
PhysicalKeyboardKey.arrowUp: '',
PhysicalKeyboardKey.controlLeft: 'CTRL',
PhysicalKeyboardKey.controlLeft: "CTRL",
PhysicalKeyboardKey.shiftLeft: 'SHIFT',
PhysicalKeyboardKey.altLeft: 'ALT',
PhysicalKeyboardKey.metaLeft: system.isMacOS ? '' : 'WIN',
PhysicalKeyboardKey.controlRight: 'CTRL',
PhysicalKeyboardKey.altLeft: "ALT",
PhysicalKeyboardKey.metaLeft: Platform.isMacOS ? '' : 'WIN',
PhysicalKeyboardKey.controlRight: "CTRL",
PhysicalKeyboardKey.shiftRight: 'SHIFT',
PhysicalKeyboardKey.altRight: 'ALT',
PhysicalKeyboardKey.metaRight: system.isMacOS ? '' : 'WIN',
PhysicalKeyboardKey.altRight: "ALT",
PhysicalKeyboardKey.metaRight: Platform.isMacOS ? '' : 'WIN',
PhysicalKeyboardKey.fn: 'FN',
};
@@ -101,3 +101,6 @@ extension KeyboardKeyExt on KeyboardKey {
return _knownKeyLabels[physicalKey] ?? physicalKey?.debugName ?? 'Unknown';
}
}

View File

@@ -34,8 +34,8 @@ class AutoLaunch {
return await launchAtStartup.disable();
}
Future<void> updateStatus(bool isAutoLaunch) async {
if (kDebugMode) {
updateStatus(bool isAutoLaunch) async {
if(kDebugMode){
return;
}
if (await isEnable == isAutoLaunch) return;

View File

@@ -15,9 +15,8 @@ class LinkManager {
_appLinks = AppLinks();
}
Future<void> initAppLinksListen(
Function(String url) installConfigCallBack) async {
commonPrint.log('initAppLinksListen');
initAppLinksListen(installConfigCallBack) async {
commonPrint.log("initAppLinksListen");
destroy();
subscription = _appLinks.uriLinkStream.listen(
(uri) {
@@ -33,7 +32,7 @@ class LinkManager {
);
}
void destroy() {
destroy() {
if (subscription != null) {
subscription?.cancel();
subscription = null;

93
lib/common/list.dart Normal file
View File

@@ -0,0 +1,93 @@
import 'dart:collection';
class FixedList<T> {
final int maxLength;
final List<T> _list;
FixedList(this.maxLength, {List<T>? list}) : _list = list ?? [];
add(T item) {
if (_list.length == maxLength) {
_list.removeAt(0);
}
_list.add(item);
}
clear() {
_list.clear();
}
List<T> get list => List.unmodifiable(_list);
int get length => _list.length;
T operator [](int index) => _list[index];
FixedList<T> copyWith() {
return FixedList(
maxLength,
list: _list,
);
}
}
class FixedMap<K, V> {
int maxSize;
final Map<K, V> _map = {};
final Queue<K> _queue = Queue<K>();
FixedMap(this.maxSize);
put(K key, V value) {
if (_map.length == maxSize) {
final oldestKey = _queue.removeFirst();
_map.remove(oldestKey);
}
_map[key] = value;
_queue.add(key);
return value;
}
clear() {
_map.clear();
_queue.clear();
}
updateMaxSize(int size){
maxSize = size;
}
V? get(K key) => _map[key];
bool containsKey(K key) => _map.containsKey(key);
int get length => _map.length;
Map<K, V> get map => Map.unmodifiable(_map);
}
extension ListExtension<T> on List<T> {
List<T> intersection(List<T> list) {
return where((item) => list.contains(item)).toList();
}
List<List<T>> batch(int maxConcurrent) {
final batches = (length / maxConcurrent).ceil();
final List<List<T>> res = [];
for (int i = 0; i < batches; i++) {
if (i != batches - 1) {
res.add(sublist(i * maxConcurrent, maxConcurrent * (i + 1)));
} else {
res.add(sublist(i * maxConcurrent, length));
}
}
return res;
}
List<T> safeSublist(int start) {
if (start <= 0) return this;
if (start > length) return [];
return sublist(start);
}
}

View File

@@ -32,35 +32,23 @@ class Measure {
}
double get bodyMediumHeight {
return _measureMap.updateCacheValue(
'bodyMediumHeight',
() => computeTextSize(
return _measureMap.getCacheValue(
"bodyMediumHeight",
computeTextSize(
Text(
'X',
"X",
style: context.textTheme.bodyMedium,
),
).height,
);
}
double get bodyLargeHeight {
return _measureMap.updateCacheValue(
'bodyLargeHeight',
() => computeTextSize(
Text(
'X',
style: context.textTheme.bodyLarge,
),
).height,
);
}
double get bodySmallHeight {
return _measureMap.updateCacheValue(
'bodySmallHeight',
() => computeTextSize(
return _measureMap.getCacheValue(
"bodySmallHeight",
computeTextSize(
Text(
'X',
"X",
style: context.textTheme.bodySmall,
),
).height,
@@ -68,11 +56,11 @@ class Measure {
}
double get labelSmallHeight {
return _measureMap.updateCacheValue(
'labelSmallHeight',
() => computeTextSize(
return _measureMap.getCacheValue(
"labelSmallHeight",
computeTextSize(
Text(
'X',
"X",
style: context.textTheme.labelSmall,
),
).height,
@@ -80,11 +68,11 @@ class Measure {
}
double get labelMediumHeight {
return _measureMap.updateCacheValue(
'labelMediumHeight',
() => computeTextSize(
return _measureMap.getCacheValue(
"labelMediumHeight",
computeTextSize(
Text(
'X',
"X",
style: context.textTheme.labelMedium,
),
).height,
@@ -92,11 +80,11 @@ class Measure {
}
double get titleLargeHeight {
return _measureMap.updateCacheValue(
'titleLargeHeight',
() => computeTextSize(
return _measureMap.getCacheValue(
"titleLargeHeight",
computeTextSize(
Text(
'X',
"X",
style: context.textTheme.titleLarge,
),
).height,
@@ -104,11 +92,11 @@ class Measure {
}
double get titleMediumHeight {
return _measureMap.updateCacheValue(
'titleMediumHeight',
() => computeTextSize(
return _measureMap.getCacheValue(
"titleMediumHeight",
computeTextSize(
Text(
'X',
"X",
style: context.textTheme.titleMedium,
),
).height,

View File

@@ -1,4 +1,7 @@
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'package:riverpod/riverpod.dart';
import 'context.dart';
mixin AutoDisposeNotifierMixin<T> on AutoDisposeNotifier<T> {
set value(T value) {
@@ -14,31 +17,37 @@ mixin AutoDisposeNotifierMixin<T> on AutoDisposeNotifier<T> {
return res;
}
void onUpdate(T value) {}
onUpdate(T value) {}
}
// mixin PageMixin<T extends StatefulWidget> on State<T> {
// initPageState() {
// WidgetsBinding.instance.addPostFrameCallback((_) {
// final commonScaffoldState = context.commonScaffoldState;
// commonScaffoldState?.actions = actions;
// commonScaffoldState?.floatingActionButton = floatingActionButton;
// commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
// commonScaffoldState?.updateSearchState(
// (_) => onSearch != null
// ? AppBarSearchState(
// onSearch: onSearch!,
// )
// : null,
// );
// });
// }
//
// List<Widget> get actions => [];
//
// Widget? get floatingActionButton => null;
//
// Function(String)? get onSearch => null;
//
// Function(List<String>)? get onKeywordsUpdate => null;
// }
mixin PageMixin<T extends StatefulWidget> on State<T> {
void onPageShow() {
initPageState();
}
initPageState() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState = context.commonScaffoldState;
commonScaffoldState?.actions = actions;
commonScaffoldState?.floatingActionButton = floatingActionButton;
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
commonScaffoldState?.updateSearchState(
(_) => onSearch != null
? AppBarSearchState(
onSearch: onSearch!,
)
: null,
);
});
}
void onPageHidden() {}
List<Widget> get actions => [];
Widget? get floatingActionButton => null;
Function(String)? get onSearch => null;
Function(List<String>)? get onKeywordsUpdate => null;
}

View File

@@ -1,9 +1,7 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/fragments.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/views/views.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class Navigation {
static Navigation? _instance;
@@ -13,69 +11,63 @@ class Navigation {
bool hasProxies = false,
}) {
return [
NavigationItem(
keep: false,
const NavigationItem(
icon: Icon(Icons.space_dashboard),
label: PageLabel.dashboard,
builder: (_) => const DashboardView(
keep: false,
fragment: DashboardFragment(
key: GlobalObjectKey(PageLabel.dashboard),
),
),
NavigationItem(
icon: const Icon(Icons.article),
label: PageLabel.proxies,
builder: (_) => ProviderScope(
overrides: [
queryProvider.overrideWith(
() => Query(),
),
],
child: const ProxiesView(
key: GlobalObjectKey(
PageLabel.proxies,
),
fragment: const ProxiesFragment(
key: GlobalObjectKey(
PageLabel.proxies,
),
),
modes: hasProxies
? [NavigationItemMode.mobile, NavigationItemMode.desktop]
: [],
),
NavigationItem(
const NavigationItem(
icon: Icon(Icons.folder),
label: PageLabel.profiles,
builder: (_) => const ProfilesView(
fragment: ProfilesFragment(
key: GlobalObjectKey(
PageLabel.profiles,
),
),
),
NavigationItem(
const NavigationItem(
icon: Icon(Icons.view_timeline),
label: PageLabel.requests,
builder: (_) => const RequestsView(
fragment: RequestsFragment(
key: GlobalObjectKey(
PageLabel.requests,
),
),
description: 'requestsDesc',
description: "requestsDesc",
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
),
NavigationItem(
const NavigationItem(
icon: Icon(Icons.ballot),
label: PageLabel.connections,
builder: (_) => const ConnectionsView(
fragment: ConnectionsFragment(
key: GlobalObjectKey(
PageLabel.connections,
),
),
description: 'connectionsDesc',
description: "connectionsDesc",
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
),
NavigationItem(
const NavigationItem(
icon: Icon(Icons.storage),
label: PageLabel.resources,
description: 'resourcesDesc',
builder: (_) => const ResourcesView(
description: "resourcesDesc",
keep: false,
fragment: Resources(
key: GlobalObjectKey(
PageLabel.resources,
),
@@ -85,20 +77,20 @@ class Navigation {
NavigationItem(
icon: const Icon(Icons.adb),
label: PageLabel.logs,
builder: (_) => const LogsView(
fragment: const LogsFragment(
key: GlobalObjectKey(
PageLabel.logs,
),
),
description: 'logsDesc',
description: "logsDesc",
modes: openLogs
? [NavigationItemMode.desktop, NavigationItemMode.more]
: [],
),
NavigationItem(
const NavigationItem(
icon: Icon(Icons.construction),
label: PageLabel.tools,
builder: (_) => const ToolsView(
fragment: ToolsFragment(
key: GlobalObjectKey(
PageLabel.tools,
),

View File

@@ -19,21 +19,6 @@ class BaseNavigator {
),
);
}
// static Future<T?> modal<T>(BuildContext context, Widget child) async {
// if (globalState.appState.viewMode != ViewMode.mobile) {
// return await globalState.showCommonDialog<T>(
// child: CommonModal(
// child: child,
// ),
// );
// }
// return await Navigator.of(context).push<T>(
// CommonRoute(
// builder: (context) => child,
// ),
// );
// }
}
class CommonDesktopRoute<T> extends PageRoute<T> {
@@ -233,7 +218,8 @@ class _CommonPageTransitionState extends State<CommonPageTransition> {
begin: const _CommonEdgeShadowDecoration(),
end: _CommonEdgeShadowDecoration(
<Color>[
widget.context.colorScheme.inverseSurface.withValues(alpha: 0.02),
widget.context.colorScheme.inverseSurface
.withValues(alpha: 0.02),
Colors.transparent,
],
),

View File

@@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
extension NumExt on num {
String fixed({int decimals = 2}) {
String fixed({decimals = 2}) {
String formatted = toStringAsFixed(decimals);
if (formatted.contains('.')) {
formatted = formatted.replaceAll(RegExp(r'0*$'), '');
@@ -20,7 +20,7 @@ extension NumExt on num {
}
extension DoubleExt on double {
bool moreOrEqual(double value) {
moreOrEqual(double value) {
return this > value || (value - this).abs() < precisionErrorTolerance + 1;
}
}
@@ -46,7 +46,7 @@ extension OffsetExt on Offset {
}
extension RectExt on Rect {
bool doRectIntersect(Rect rect) {
doRectIntersect(Rect rect) {
return left < rect.right &&
right > rect.left &&
top < rect.bottom &&

View File

@@ -6,8 +6,8 @@ import 'common.dart';
extension PackageInfoExtension on PackageInfo {
String get ua => [
'$appName/v$version',
'clash-verge',
'Platform/${Platform.operatingSystem}',
].join(' ');
"$appName/v$version",
"clash-verge",
"Platform/${Platform.operatingSystem}",
].join(" ");
}

View File

@@ -1,10 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'constant.dart';
class AppPath {
static AppPath? _instance;
Completer<Directory> dataDir = Completer();
@@ -31,7 +32,7 @@ class AppPath {
}
String get executableExtension {
return system.isWindows ? '.exe' : '';
return Platform.isWindows ? ".exe" : "";
}
String get executableDirPath {
@@ -40,11 +41,11 @@ class AppPath {
}
String get corePath {
return join(executableDirPath, 'FlClashCore$executableExtension');
return join(executableDirPath, "FlClashCore$executableExtension");
}
String get helperPath {
return join(executableDirPath, '$appHelperService$executableExtension');
return join(executableDirPath, "$appHelperService$executableExtension");
}
Future<String> get downloadDirPath async {
@@ -59,12 +60,12 @@ class AppPath {
Future<String> get lockFilePath async {
final directory = await dataDir.future;
return join(directory.path, 'FlClash.lock');
return join(directory.path, "FlClash.lock");
}
Future<String> get sharedPreferencesPath async {
final directory = await dataDir.future;
return join(directory.path, 'shared_preferences.json');
return join(directory.path, "shared_preferences.json");
}
Future<String> get profilesPath async {
@@ -72,33 +73,16 @@ class AppPath {
return join(directory.path, profilesDirectoryName);
}
Future<String> getProfilePath(String id) async {
Future<String?> getProfilePath(String? id) async {
if (id == null) return null;
final directory = await profilesPath;
return join(directory, '$id.yaml');
return join(directory, "$id.yaml");
}
Future<String> getProvidersDirPath(String id) async {
Future<String?> getProvidersPath(String? id) async {
if (id == null) return null;
final directory = await profilesPath;
return join(
directory,
'providers',
id,
);
}
Future<String> getProvidersFilePath(
String id,
String type,
String url,
) async {
final directory = await profilesPath;
return join(
directory,
'providers',
id,
type,
url.toMd5(),
);
return join(directory, "providers", id);
}
Future<String> get tempPath async {

View File

@@ -20,9 +20,9 @@ class Picker {
final path = await FilePicker.platform.saveFile(
fileName: fileName,
initialDirectory: await appPath.downloadDirPath,
bytes: system.isAndroid ? bytes : null,
bytes: Platform.isAndroid ? bytes : null,
);
if (!system.isAndroid && path != null) {
if (!Platform.isAndroid && path != null) {
final file = await File(path).create(recursive: true);
await file.writeAsBytes(bytes);
}

View File

@@ -49,12 +49,12 @@ class Preferences {
false;
}
Future<void> clearClashConfig() async {
clearClashConfig() async {
final preferences = await sharedPreferencesCompleter.future;
preferences?.remove(clashConfigKey);
}
Future<void> clearPreferences() async {
clearPreferences() async {
final sharedPreferencesIns = await sharedPreferencesCompleter.future;
sharedPreferencesIns?.clear();
}

View File

@@ -12,10 +12,10 @@ class CommonPrint {
return _instance!;
}
void log(String? text) {
final payload = '[APP] $text';
log(String? text) {
final payload = "[FlClash] $text";
debugPrint(payload);
if (!globalState.isInit) {
if (globalState.isService) {
return;
}
globalState.appController.addLog(

View File

@@ -16,21 +16,21 @@ class Render {
return _instance!;
}
void active() {
active() {
resume();
pause();
}
void pause() {
pause() {
throttler.call(
FunctionTag.renderPause,
DebounceTag.renderPause,
_pause,
duration: Duration(seconds: 5),
);
}
void resume() {
throttler.cancel(FunctionTag.renderPause);
resume() {
throttler.cancel(DebounceTag.renderPause);
_resume();
}
@@ -41,7 +41,7 @@ class Render {
_drawFrame = _dispatcher.onDrawFrame;
_dispatcher.onBeginFrame = null;
_dispatcher.onDrawFrame = null;
commonPrint.log('pause');
commonPrint.log("pause");
}
void _resume() {
@@ -50,7 +50,7 @@ class Render {
_dispatcher.onBeginFrame = _beginFrame;
_dispatcher.onDrawFrame = _drawFrame;
_dispatcher.scheduleFrame();
commonPrint.log('resume');
commonPrint.log("resume");
}
}

View File

@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
@@ -19,7 +18,7 @@ class Request {
_dio = Dio(
BaseOptions(
headers: {
'User-Agent': browserUa,
"User-Agent": browserUa,
},
),
);
@@ -44,16 +43,6 @@ class Request {
return response;
}
Future<Response> getTextResponseForUrl(String url) async {
final response = await _clashDio.get(
url,
options: Options(
responseType: ResponseType.plain,
),
);
return response;
}
Future<MemoryImage?> getImage(String url) async {
if (url.isEmpty) return null;
final response = await _dio.get<Uint8List>(
@@ -69,7 +58,7 @@ class Request {
Future<Map<String, dynamic>?> checkForUpdate() async {
final response = await _dio.get(
'https://api.github.com/repos/$repository/releases/latest',
"https://api.github.com/repos/$repository/releases/latest",
options: Options(
responseType: ResponseType.json,
),
@@ -85,55 +74,50 @@ class Request {
}
final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
'https://ipwho.is/': IpInfo.fromIpwhoIsJson,
'https://api.ip.sb/geoip/': IpInfo.fromIpSbJson,
'https://ipapi.co/json/': IpInfo.fromIpApiCoJson,
'https://ipinfo.io/json/': IpInfo.fromIpInfoIoJson,
"https://ipwho.is/": IpInfo.fromIpwhoIsJson,
"https://api.ip.sb/geoip/": IpInfo.fromIpSbJson,
"https://ipapi.co/json/": IpInfo.fromIpApiCoJson,
"https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
};
Future<Result<IpInfo?>> checkIp({CancelToken? cancelToken}) async {
var failureCount = 0;
final futures = _ipInfoSources.entries.map((source) async {
final Completer<Result<IpInfo?>> completer = Completer();
handleFailRes() {
if (!completer.isCompleted && failureCount == _ipInfoSources.length) {
completer.complete(Result.success(null));
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
for (final source in _ipInfoSources.entries) {
try {
final response = await Dio()
.get<Map<String, dynamic>>(
source.key,
cancelToken: cancelToken,
options: Options(
responseType: ResponseType.json,
),
)
.timeout(
Duration(
seconds: 30,
),
);
if (response.statusCode != 200 || response.data == null) {
continue;
}
if (response.data == null) {
continue;
}
return source.value(response.data!);
} catch (e) {
commonPrint.log("checkIp error ===> $e");
if (e is DioException && e.type == DioExceptionType.cancel) {
throw "cancelled";
}
}
final future = Dio().get<Map<String, dynamic>>(
source.key,
cancelToken: cancelToken,
options: Options(
responseType: ResponseType.json,
),
);
future.then((res) {
if (res.statusCode == HttpStatus.ok && res.data != null) {
completer.complete(Result.success(source.value(res.data!)));
} else {
failureCount++;
handleFailRes();
}
}).catchError((e) {
failureCount++;
if (e == DioExceptionType.cancel) {
completer.complete(Result.error('cancelled'));
}
handleFailRes();
});
return completer.future;
});
final res = await Future.any(futures);
cancelToken?.cancel();
return res;
}
return null;
}
Future<bool> pingHelper() async {
try {
final response = await _dio
.get(
'http://$localhost:$helperPort/ping',
"http://$localhost:$helperPort/ping",
options: Options(
responseType: ResponseType.plain,
),
@@ -146,7 +130,7 @@ class Request {
if (response.statusCode != HttpStatus.ok) {
return false;
}
return (response.data as String) == globalState.coreSHA256;
return (response.data as String) == helperTag;
} catch (_) {
return false;
}
@@ -156,10 +140,10 @@ class Request {
try {
final response = await _dio
.post(
'http://$localhost:$helperPort/start',
"http://$localhost:$helperPort/start",
data: json.encode({
'path': appPath.corePath,
'arg': arg,
"path": appPath.corePath,
"arg": arg,
}),
options: Options(
responseType: ResponseType.plain,
@@ -184,7 +168,7 @@ class Request {
try {
final response = await _dio
.post(
'http://$localhost:$helperPort/stop',
"http://$localhost:$helperPort/stop",
options: Options(
responseType: ResponseType.plain,
),

View File

@@ -35,7 +35,7 @@ class ShowBarScrollBehavior extends BaseScrollBehavior {
Widget child,
ScrollableDetails details,
) {
return CommonScrollBar(
return CommonAutoHiddenScrollBar(
controller: details.controller,
child: child,
);
@@ -88,54 +88,6 @@ class NextClampingScrollPhysics extends ClampingScrollPhysics {
}
}
// class CacheScrollPositionController extends ScrollController {
// final String key;
//
// CacheScrollPositionController({
// required this.key,
// double initialScrollOffset = 0.0,
// super.keepScrollOffset = true,
// super.debugLabel,
// super.onAttach,
// super.onDetach,
// });
//
// @override
// ScrollPosition createScrollPosition(
// ScrollPhysics physics,
// ScrollContext context,
// ScrollPosition? oldPosition,
// ) {
// return ScrollPositionWithSingleContext(
// physics: physics,
// context: context,
// initialPixels:
// globalState.scrollPositionCache[key] ?? initialScrollOffset,
// keepScrollOffset: keepScrollOffset,
// oldPosition: oldPosition,
// debugLabel: debugLabel,
// );
// }
//
// double? get cacheOffset => globalState.scrollPositionCache[key];
//
// _handleScroll() {
// globalState.scrollPositionCache[key] = position.pixels;
// }
//
// @override
// void attach(ScrollPosition position) {
// super.attach(position);
// addListener(_handleScroll);
// }
//
// @override
// void detach(ScrollPosition position) {
// removeListener(_handleScroll);
// super.detach(position);
// }
// }
class ReverseScrollController extends ScrollController {
ReverseScrollController({
super.initialScrollOffset,

View File

@@ -1,8 +1,6 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'print.dart';
extension StringExtension on String {
@@ -10,13 +8,6 @@ extension StringExtension on String {
return RegExp(r'^(http|https|ftp)://').hasMatch(this);
}
dynamic get splitByMultipleSeparators {
final parts =
split(RegExp(r'[, ;]+')).where((part) => part.isNotEmpty).toList();
return parts.length > 1 ? parts : this;
}
int compareToLower(String other) {
return toLowerCase().compareTo(
other.toLowerCase(),
@@ -47,10 +38,6 @@ extension StringExtension on String {
}
}
bool get isSvg {
return endsWith('.svg');
}
bool get isRegex {
try {
RegExp(this);
@@ -60,15 +47,6 @@ extension StringExtension on String {
return false;
}
}
String toMd5() {
final bytes = utf8.encode(this);
return md5.convert(bytes).toString();
}
// bool containsToLower(String target) {
// return toLowerCase().contains(target);
// }
}
extension StringExtensionSafe on String? {

View File

@@ -1,15 +1,12 @@
import 'dart:ffi';
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:ffi/ffi.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/input.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
class System {
static System? _instance;
@@ -21,32 +18,25 @@ class System {
return _instance!;
}
bool get isDesktop => isWindows || isMacOS || isLinux;
bool get isWindows => Platform.isWindows;
bool get isMacOS => Platform.isMacOS;
bool get isAndroid => Platform.isAndroid;
bool get isLinux => Platform.isLinux;
bool get isDesktop =>
Platform.isWindows || Platform.isMacOS || Platform.isLinux;
Future<int> get version async {
final deviceInfo = await DeviceInfoPlugin().deviceInfo;
return switch (Platform.operatingSystem) {
'macos' => (deviceInfo as MacOsDeviceInfo).majorVersion,
'android' => (deviceInfo as AndroidDeviceInfo).version.sdkInt,
'windows' => (deviceInfo as WindowsDeviceInfo).majorVersion,
"macos" => (deviceInfo as MacOsDeviceInfo).majorVersion,
"android" => (deviceInfo as AndroidDeviceInfo).version.sdkInt,
"windows" => (deviceInfo as WindowsDeviceInfo).majorVersion,
String() => 0
};
}
Future<bool> checkIsAdmin() async {
final corePath = appPath.corePath.replaceAll(' ', '\\\\ ');
if (system.isWindows) {
if (Platform.isWindows) {
final result = await windows?.checkService();
return result == WindowsHelperServiceStatus.running;
} else if (system.isMacOS) {
} else if (Platform.isMacOS) {
final result = await Process.run('stat', ['-f', '%Su:%Sg %Sp', corePath]);
final output = result.stdout.trim();
if (output.startsWith('root:admin') && output.contains('rws')) {
@@ -65,30 +55,24 @@ class System {
}
Future<AuthorizeCode> authorizeCore() async {
if (system.isAndroid) {
return AuthorizeCode.error;
}
final corePath = appPath.corePath.replaceAll(' ', '\\\\ ');
final isAdmin = await checkIsAdmin();
if (isAdmin) {
return AuthorizeCode.none;
}
if (system.isWindows) {
if (Platform.isWindows) {
final result = await windows?.registerService();
if (result == true) {
return AuthorizeCode.success;
}
return AuthorizeCode.error;
}
if (system.isMacOS) {
} else if (Platform.isMacOS) {
final shell = 'chown root:admin $corePath; chmod +sx $corePath';
final arguments = [
'-e',
"-e",
'do shell script "$shell" with administrator privileges',
];
final result = await Process.run('osascript', arguments);
final result = await Process.run("osascript", arguments);
if (result.exitCode != 0) {
return AuthorizeCode.error;
}
@@ -102,7 +86,7 @@ class System {
),
);
final arguments = [
'-c',
"-c",
'echo "$password" | sudo -S chown root:root "$corePath" && echo "$password" | sudo -S chmod +sx "$corePath"'
];
final result = await Process.run(shell, arguments);
@@ -114,13 +98,13 @@ class System {
return AuthorizeCode.error;
}
Future<void> back() async {
back() async {
await app?.moveTaskToBack();
await window?.hide();
}
Future<void> exit() async {
if (system.isAndroid) {
exit() async {
if (Platform.isAndroid) {
await SystemNavigator.pop();
}
await window?.close();
@@ -128,287 +112,3 @@ class System {
}
final system = System();
class Windows {
static Windows? _instance;
late DynamicLibrary _shell32;
Windows._internal() {
_shell32 = DynamicLibrary.open('shell32.dll');
}
factory Windows() {
_instance ??= Windows._internal();
return _instance!;
}
bool runas(String command, String arguments) {
final commandPtr = command.toNativeUtf16();
final argumentsPtr = arguments.toNativeUtf16();
final operationPtr = 'runas'.toNativeUtf16();
final shellExecute = _shell32.lookupFunction<
Int32 Function(
Pointer<Utf16> hwnd,
Pointer<Utf16> lpOperation,
Pointer<Utf16> lpFile,
Pointer<Utf16> lpParameters,
Pointer<Utf16> lpDirectory,
Int32 nShowCmd),
int Function(
Pointer<Utf16> hwnd,
Pointer<Utf16> lpOperation,
Pointer<Utf16> lpFile,
Pointer<Utf16> lpParameters,
Pointer<Utf16> lpDirectory,
int nShowCmd)>('ShellExecuteW');
final result = shellExecute(
nullptr,
operationPtr,
commandPtr,
argumentsPtr,
nullptr,
1,
);
calloc.free(commandPtr);
calloc.free(argumentsPtr);
calloc.free(operationPtr);
commonPrint.log('windows runas: $command $arguments resultCode:$result');
if (result < 42) {
return false;
}
return true;
}
Future<void> _killProcess(int port) async {
final result = await Process.run('netstat', ['-ano']);
final lines = result.stdout.toString().trim().split('\n');
for (final line in lines) {
if (!line.contains(':$port') || !line.contains('LISTENING')) {
continue;
}
final parts = line.trim().split(RegExp(r'\s+'));
final pid = int.tryParse(parts.last);
if (pid != null) {
await Process.run('taskkill', ['/PID', pid.toString(), '/F']);
}
}
}
Future<WindowsHelperServiceStatus> checkService() async {
// final qcResult = await Process.run('sc', ['qc', appHelperService]);
// final qcOutput = qcResult.stdout.toString();
// if (qcResult.exitCode != 0 || !qcOutput.contains(appPath.helperPath)) {
// return WindowsHelperServiceStatus.none;
// }
final result = await Process.run('sc', ['query', appHelperService]);
if (result.exitCode != 0) {
return WindowsHelperServiceStatus.none;
}
final output = result.stdout.toString();
if (output.contains('RUNNING') && await request.pingHelper()) {
return WindowsHelperServiceStatus.running;
}
return WindowsHelperServiceStatus.presence;
}
Future<bool> registerService() async {
final status = await checkService();
if (status == WindowsHelperServiceStatus.running) {
return true;
}
await _killProcess(helperPort);
final command = [
'/c',
if (status == WindowsHelperServiceStatus.presence) ...[
'sc',
'delete',
appHelperService,
'/force',
'&&',
],
'sc',
'create',
appHelperService,
'binPath= "${appPath.helperPath}"',
'start= auto',
'&&',
'sc',
'start',
appHelperService,
].join(' ');
final res = runas('cmd.exe', command);
await Future.delayed(
Duration(milliseconds: 300),
);
return res;
}
Future<bool> registerTask(String appName) async {
final taskXml = '''
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<Principals>
<Principal id="Author">
<LogonType>InteractiveToken</LogonType>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Triggers>
<LogonTrigger/>
</Triggers>
<Settings>
<MultipleInstancesPolicy>Parallel</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>false</AllowHardTerminate>
<StartWhenAvailable>false</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>false</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>"${Platform.resolvedExecutable}"</Command>
</Exec>
</Actions>
</Task>''';
final taskPath = join(await appPath.tempPath, 'task.xml');
await File(taskPath).create(recursive: true);
await File(taskPath)
.writeAsBytes(taskXml.encodeUtf16LeWithBom, flush: true);
final commandLine = [
'/Create',
'/TN',
appName,
'/XML',
'%s',
'/F',
].join(' ');
return runas(
'schtasks',
commandLine.replaceFirst('%s', taskPath),
);
}
}
final windows = system.isWindows ? Windows() : null;
class MacOS {
static MacOS? _instance;
List<String>? originDns;
MacOS._internal();
factory MacOS() {
_instance ??= MacOS._internal();
return _instance!;
}
Future<String?> get defaultServiceName async {
final result = await Process.run('route', ['-n', 'get', 'default']);
final output = result.stdout.toString();
final deviceLine = output
.split('\n')
.firstWhere((s) => s.contains('interface:'), orElse: () => '');
final lineSplits = deviceLine.trim().split(' ');
if (lineSplits.length != 2) {
return null;
}
final device = lineSplits[1];
final serviceResult = await Process.run(
'networksetup',
['-listnetworkserviceorder'],
);
final serviceResultOutput = serviceResult.stdout.toString();
final currentService = serviceResultOutput.split('\n\n').firstWhere(
(s) => s.contains('Device: $device'),
orElse: () => '',
);
if (currentService.isEmpty) {
return null;
}
final currentServiceNameLine = currentService.split('\n').firstWhere(
(line) => RegExp(r'^\(\d+\).*').hasMatch(line),
orElse: () => '');
final currentServiceNameLineSplits =
currentServiceNameLine.trim().split(' ');
if (currentServiceNameLineSplits.length < 2) {
return null;
}
return currentServiceNameLineSplits[1];
}
Future<List<String>?> get systemDns async {
final deviceServiceName = await defaultServiceName;
if (deviceServiceName == null) {
return null;
}
final result = await Process.run(
'networksetup',
['-getdnsservers', deviceServiceName],
);
final output = result.stdout.toString().trim();
if (output.startsWith("There aren't any DNS Servers set on")) {
originDns = [];
} else {
originDns = output.split('\n');
}
return originDns;
}
Future<void> updateDns(bool restore) async {
final serviceName = await defaultServiceName;
if (serviceName == null) {
return;
}
List<String>? nextDns;
if (restore) {
nextDns = originDns;
} else {
final originDns = await systemDns;
if (originDns == null) {
return;
}
final needAddDns = '223.5.5.5';
if (originDns.contains(needAddDns)) {
return;
}
nextDns = List.from(originDns)..add(needAddDns);
}
if (nextDns == null) {
return;
}
await Process.run(
'networksetup',
[
'-setdnsservers',
serviceName,
if (nextDns.isNotEmpty) ...nextDns,
if (nextDns.isEmpty) 'Empty',
],
);
}
}
final macOS = system.isMacOS ? MacOS() : null;

View File

@@ -12,35 +12,32 @@ class CommonTheme {
) : _colorMap = {};
Color get darkenSecondaryContainer {
return _colorMap.updateCacheValue(
'darkenSecondaryContainer',
() => context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.1),
return _colorMap.getCacheValue(
"darkenSecondaryContainer",
context.colorScheme.secondaryContainer.blendDarken(context, factor: 0.1),
);
}
Color get darkenSecondaryContainerLighter {
return _colorMap.updateCacheValue(
'darkenSecondaryContainerLighter',
() => context.colorScheme.secondaryContainer
return _colorMap.getCacheValue(
"darkenSecondaryContainerLighter",
context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.1)
.opacity60,
);
}
Color get darken2SecondaryContainer {
return _colorMap.updateCacheValue(
'darken2SecondaryContainer',
() => context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.2),
return _colorMap.getCacheValue(
"darken2SecondaryContainer",
context.colorScheme.secondaryContainer.blendDarken(context, factor: 0.2),
);
}
Color get darken3PrimaryContainer {
return _colorMap.updateCacheValue(
'darken3PrimaryContainer',
() => context.colorScheme.primaryContainer
.blendDarken(context, factor: 0.3),
return _colorMap.getCacheValue(
"darken3PrimaryContainer",
context.colorScheme.primaryContainer.blendDarken(context, factor: 0.3),
);
}
}

View File

@@ -11,7 +11,6 @@ import 'package:tray_manager/tray_manager.dart';
import 'app_localizations.dart';
import 'constant.dart';
import 'system.dart';
import 'window.dart';
class Tray {
@@ -19,7 +18,7 @@ class Tray {
required Brightness? brightness,
bool force = false,
}) async {
if (system.isAndroid) {
if (Platform.isAndroid) {
return;
}
if (Platform.isLinux || force) {
@@ -39,11 +38,11 @@ class Tray {
}
}
Future<void> update({
update({
required TrayState trayState,
bool focus = false,
}) async {
if (system.isAndroid) {
if (Platform.isAndroid) {
return;
}
if (!Platform.isLinux) {
@@ -81,7 +80,7 @@ class Tray {
);
}
menuItems.add(MenuItem.separator());
if (system.isMacOS) {
if (Platform.isMacOS) {
for (final group in trayState.groups) {
List<MenuItem> subMenuItems = [];
for (final proxy in group.all) {
@@ -170,8 +169,8 @@ class Tray {
}
}
Future<void> updateTrayTitle([Traffic? traffic]) async {
// if (!system.isMacOS) {
updateTrayTitle([Traffic? traffic]) async {
// if (!Platform.isMacOS) {
// return;
// }
// if (traffic == null) {
@@ -184,10 +183,11 @@ class Tray {
}
Future<void> _copyEnv(int port) async {
final url = 'http://127.0.0.1:$port';
final url = "http://127.0.0.1:$port";
final cmdline =
system.isWindows ? 'set \$env:all_proxy=$url' : 'export all_proxy=$url';
final cmdline = Platform.isWindows
? "set \$env:all_proxy=$url"
: "export all_proxy=$url";
await Clipboard.setData(
ClipboardData(

View File

@@ -1,13 +1,10 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:ui';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:lpinyin/lpinyin.dart';
class Utils {
@@ -23,11 +20,11 @@ class Utils {
final random = Random();
final randomStr =
String.fromCharCodes(List.generate(8, (_) => random.nextInt(26) + 97));
return '$timestamp$randomStr';
return "$timestamp$randomStr";
}
String getDateStringLast2(int value) {
var valueRaw = '0$value';
var valueRaw = "0$value";
return valueRaw.substring(
valueRaw.length - 2,
);
@@ -73,7 +70,7 @@ class Utils {
var inMinutes = difference.inMinutes;
var inSeconds = difference.inSeconds;
return '${getDateStringLast2(inHours)}:${getDateStringLast2(inMinutes)}:${getDateStringLast2(inSeconds)}';
return "${getDateStringLast2(inHours)}:${getDateStringLast2(inMinutes)}:${getDateStringLast2(inSeconds)}";
}
String getTimeText(int? timeStamp) {
@@ -83,17 +80,17 @@ class Utils {
final diff = timeStamp / 1000;
final inHours = (diff / 3600).floor();
if (inHours > 99) {
return '99:59:59';
return "99:59:59";
}
final inMinutes = (diff / 60 % 60).floor();
final inSeconds = (diff % 60).floor();
return '${getDateStringLast2(inHours)}:${getDateStringLast2(inMinutes)}:${getDateStringLast2(inSeconds)}';
return "${getDateStringLast2(inHours)}:${getDateStringLast2(inMinutes)}:${getDateStringLast2(inSeconds)}";
}
Locale? getLocaleForString(String? localString) {
if (localString == null) return null;
var localSplit = localString.split('_');
var localSplit = localString.split("_");
if (localSplit.length == 1) {
return Locale(localSplit[0]);
}
@@ -137,18 +134,18 @@ class Utils {
final number = int.parse(match[1] ?? '0') + 1;
return label.replaceFirst(reg, '($number)', label.length - 3 - 1);
} else {
return '$label(1)';
return "$label(1)";
}
}
String getTrayIconPath({
required Brightness brightness,
}) {
if (system.isMacOS) {
return 'assets/images/icon_white.png';
if (Platform.isMacOS) {
return "assets/images/icon_white.png";
}
final suffix = system.isWindows ? 'ico' : 'png';
return 'assets/images/icon.$suffix';
final suffix = Platform.isWindows ? "ico" : "png";
return "assets/images/icon.$suffix";
// return switch (brightness) {
// Brightness.dark => "assets/images/icon_white.$suffix",
// Brightness.light => "assets/images/icon_black.$suffix",
@@ -181,7 +178,7 @@ class Utils {
String getPinyin(String value) {
return value.isNotEmpty
? PinyinHelper.getFirstWordPinyin(value.substring(0, 1))
: '';
: "";
}
String? getFileNameForDisposition(String? disposition) {
@@ -189,7 +186,7 @@ class Utils {
final parseValue = HeaderValue.parse(disposition);
final parameters = parseValue.parameters;
final fileNamePointKey = parameters.keys
.firstWhere((key) => key == 'filename*', orElse: () => '');
.firstWhere((key) => key == "filename*", orElse: () => "");
if (fileNamePointKey.isNotEmpty) {
final res = parameters[fileNamePointKey]?.split("''") ?? [];
if (res.length >= 2) {
@@ -197,7 +194,7 @@ class Utils {
}
}
final fileNameKey = parameters.keys
.firstWhere((key) => key == 'filename', orElse: () => '');
.firstWhere((key) => key == "filename", orElse: () => "");
if (fileNameKey.isEmpty) return null;
return parameters[fileNameKey];
}
@@ -233,7 +230,7 @@ class Utils {
}
int getProfilesColumns(double viewWidth) {
return max((viewWidth / 320).floor(), 1);
return max((viewWidth / 350).floor(), 1);
}
final _indexPrimary = [
@@ -250,7 +247,7 @@ class Utils {
900,
];
MaterialColor _createPrimarySwatch(Color color) {
_createPrimarySwatch(Color color) {
final Map<int, Color> swatch = <int, Color>{};
final int a = color.alpha8bit;
final int r = color.red8bit;
@@ -294,11 +291,11 @@ class Utils {
}
String getBackupFileName() {
return '${appName}_backup_${DateTime.now().show}.zip';
return "${appName}_backup_${DateTime.now().show}.zip";
}
String get logFile {
return '${appName}_${DateTime.now().show}.log';
return "${appName}_${DateTime.now().show}.log";
}
Future<String?> getLocalIpAddress() async {
@@ -324,73 +321,7 @@ class Utils {
});
return addresses.first.address;
}
return '';
}
SingleActivator controlSingleActivator(LogicalKeyboardKey trigger) {
final control = system.isMacOS ? false : true;
return SingleActivator(
trigger,
control: control,
meta: !control,
);
}
// dynamic convertYamlNode(dynamic node) {
// if (node is YamlMap) {
// final map = <String, dynamic>{};
// YamlNode? mergeKeyNode;
// for (final entry in node.nodes.entries) {
// if (entry.key is YamlScalar &&
// (entry.key as YamlScalar).value == '<<') {
// mergeKeyNode = entry.value;
// break;
// }
// }
// if (mergeKeyNode != null) {
// final mergeValue = mergeKeyNode.value;
// if (mergeValue is YamlMap) {
// map.addAll(convertYamlNode(mergeValue) as Map<String, dynamic>);
// } else if (mergeValue is YamlList) {
// for (final node in mergeValue.nodes) {
// if (node.value is YamlMap) {
// map.addAll(convertYamlNode(node.value) as Map<String, dynamic>);
// }
// }
// }
// }
//
// node.nodes.forEach((key, value) {
// String stringKey;
// if (key is YamlScalar) {
// stringKey = key.value.toString();
// } else {
// stringKey = key.toString();
// }
// map[stringKey] = convertYamlNode(value.value);
// });
// return map;
// } else if (node is YamlList) {
// final list = <dynamic>[];
// for (final item in node.nodes) {
// list.add(convertYamlNode(item.value));
// }
// return list;
// } else if (node is YamlScalar) {
// return node.value;
// }
// return node;
// }
FutureOr<T> handleWatch<T>(Function function) async {
if (kDebugMode) {
final stopwatch = Stopwatch()..start();
final res = await function();
stopwatch.stop();
commonPrint.log('耗时:${stopwatch.elapsedMilliseconds} ms');
return res;
}
return await function();
return "";
}
}

View File

@@ -3,34 +3,30 @@ import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_acrylic/flutter_acrylic.dart' as acrylic;
import 'package:screen_retriever/screen_retriever.dart';
import 'package:window_manager/window_manager.dart';
class Window {
Future<void> init(int version) async {
init(int version) async {
final props = globalState.config.windowProps;
final acquire = await singleInstanceLock.acquire();
if (!acquire) {
exit(0);
}
if (system.isWindows) {
protocol.register('clash');
protocol.register('clashmeta');
protocol.register('flclash');
}
if ((version > 10 && system.isMacOS)) {
await acrylic.Window.initialize();
if (Platform.isWindows) {
protocol.register("clash");
protocol.register("clashmeta");
protocol.register("flclash");
}
await windowManager.ensureInitialized();
WindowOptions windowOptions = WindowOptions(
size: Size(props.width, props.height),
minimumSize: const Size(380, 400),
);
if (!system.isMacOS || version > 10) {
if (!Platform.isMacOS || version > 10) {
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
}
if (!system.isMacOS) {
if (!Platform.isMacOS) {
final left = props.left ?? 0;
final top = props.top ?? 0;
final right = left + props.width;
@@ -66,14 +62,7 @@ class Window {
});
}
void updateMacOSBrightness(Brightness brightness) {
if (!system.isMacOS) {
return;
}
acrylic.Window.overrideMacOSBrightness(dark: brightness == Brightness.dark);
}
Future<void> show() async {
show() async {
render?.resume();
await windowManager.show();
await windowManager.focus();
@@ -82,15 +71,15 @@ class Window {
Future<bool> get isVisible async {
final value = await windowManager.isVisible();
commonPrint.log('window visible check: $value');
commonPrint.log("window visible check: $value");
return value;
}
Future<void> close() async {
close() async {
exit(0);
}
Future<void> hide() async {
hide() async {
render?.pause();
await windowManager.hide();
await windowManager.setSkipTaskbar(true);

191
lib/common/windows.dart Normal file
View File

@@ -0,0 +1,191 @@
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:path/path.dart';
class Windows {
static Windows? _instance;
late DynamicLibrary _shell32;
Windows._internal() {
_shell32 = DynamicLibrary.open('shell32.dll');
}
factory Windows() {
_instance ??= Windows._internal();
return _instance!;
}
bool runas(String command, String arguments) {
final commandPtr = command.toNativeUtf16();
final argumentsPtr = arguments.toNativeUtf16();
final operationPtr = 'runas'.toNativeUtf16();
final shellExecute = _shell32.lookupFunction<
Int32 Function(
Pointer<Utf16> hwnd,
Pointer<Utf16> lpOperation,
Pointer<Utf16> lpFile,
Pointer<Utf16> lpParameters,
Pointer<Utf16> lpDirectory,
Int32 nShowCmd),
int Function(
Pointer<Utf16> hwnd,
Pointer<Utf16> lpOperation,
Pointer<Utf16> lpFile,
Pointer<Utf16> lpParameters,
Pointer<Utf16> lpDirectory,
int nShowCmd)>('ShellExecuteW');
final result = shellExecute(
nullptr,
operationPtr,
commandPtr,
argumentsPtr,
nullptr,
1,
);
calloc.free(commandPtr);
calloc.free(argumentsPtr);
calloc.free(operationPtr);
commonPrint.log("windows runas: $command $arguments resultCode:$result");
if (result < 42) {
return false;
}
return true;
}
_killProcess(int port) async {
final result = await Process.run('netstat', ['-ano']);
final lines = result.stdout.toString().trim().split('\n');
for (final line in lines) {
if (!line.contains(":$port") || !line.contains("LISTENING")) {
continue;
}
final parts = line.trim().split(RegExp(r'\s+'));
final pid = int.tryParse(parts.last);
if (pid != null) {
await Process.run('taskkill', ['/PID', pid.toString(), '/F']);
}
}
}
Future<WindowsHelperServiceStatus> checkService() async {
// final qcResult = await Process.run('sc', ['qc', appHelperService]);
// final qcOutput = qcResult.stdout.toString();
// if (qcResult.exitCode != 0 || !qcOutput.contains(appPath.helperPath)) {
// return WindowsHelperServiceStatus.none;
// }
final result = await Process.run('sc', ['query', appHelperService]);
if(result.exitCode != 0){
return WindowsHelperServiceStatus.none;
}
final output = result.stdout.toString();
if (output.contains("RUNNING") && await request.pingHelper()) {
return WindowsHelperServiceStatus.running;
}
return WindowsHelperServiceStatus.presence;
}
Future<bool> registerService() async {
final status = await checkService();
if (status == WindowsHelperServiceStatus.running) {
return true;
}
await _killProcess(helperPort);
final command = [
"/c",
if (status == WindowsHelperServiceStatus.presence) ...[
"sc",
"delete",
appHelperService,
"/force",
"&&",
],
"sc",
"create",
appHelperService,
'binPath= "${appPath.helperPath}"',
'start= auto',
"&&",
"sc",
"start",
appHelperService,
].join(" ");
final res = runas("cmd.exe", command);
await Future.delayed(
Duration(milliseconds: 300),
);
return res;
}
Future<bool> registerTask(String appName) async {
final taskXml = '''
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<Principals>
<Principal id="Author">
<LogonType>InteractiveToken</LogonType>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Triggers>
<LogonTrigger/>
</Triggers>
<Settings>
<MultipleInstancesPolicy>Parallel</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>false</AllowHardTerminate>
<StartWhenAvailable>false</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>false</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>"${Platform.resolvedExecutable}"</Command>
</Exec>
</Actions>
</Task>''';
final taskPath = join(await appPath.tempPath, "task.xml");
await File(taskPath).create(recursive: true);
await File(taskPath)
.writeAsBytes(taskXml.encodeUtf16LeWithBom, flush: true);
final commandLine = [
'/Create',
'/TN',
appName,
'/XML',
"%s",
'/F',
].join(" ");
return runas(
'schtasks',
commandLine.replaceFirst("%s", taskPath),
);
}
}
final windows = Platform.isWindows ? Windows() : null;

View File

@@ -2,27 +2,26 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:archive/archive.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/archive.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/dialog.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path/path.dart';
import 'package:url_launcher/url_launcher.dart';
import 'common/common.dart';
import 'fragments/profiles/override_profile.dart';
import 'models/models.dart';
import 'views/profiles/override_profile.dart';
class AppController {
bool lastTunEnable = false;
int? lastProfileModified;
final BuildContext context;
@@ -30,42 +29,37 @@ class AppController {
AppController(this.context, WidgetRef ref) : _ref = ref;
void setupClashConfigDebounce() {
debouncer.call(FunctionTag.setupClashConfig, () async {
await setupClashConfig();
updateClashConfigDebounce() {
debouncer.call(DebounceTag.updateClashConfig, () async {
final isPatch = globalState.appState.needApply ? false : true;
await updateClashConfig(isPatch);
});
}
Future<void> updateClashConfigDebounce() async {
debouncer.call(FunctionTag.updateClashConfig, () async {
await updateClashConfig();
});
updateGroupsDebounce() {
debouncer.call(DebounceTag.updateGroups, updateGroups);
}
void updateGroupsDebounce() {
debouncer.call(FunctionTag.updateGroups, updateGroups);
}
void addCheckIpNumDebounce() {
debouncer.call(FunctionTag.addCheckIpNum, () {
addCheckIpNumDebounce() {
debouncer.call(DebounceTag.addCheckIpNum, () {
_ref.read(checkIpNumProvider.notifier).add();
});
}
void applyProfileDebounce({
applyProfileDebounce({
bool silence = false,
}) {
debouncer.call(FunctionTag.applyProfile, (silence) {
debouncer.call(DebounceTag.applyProfile, (silence) {
applyProfile(silence: silence);
}, args: [silence]);
}
void savePreferencesDebounce() {
debouncer.call(FunctionTag.savePreferences, savePreferences);
savePreferencesDebounce() {
debouncer.call(DebounceTag.savePreferences, savePreferences);
}
void changeProxyDebounce(String groupName, String proxyName) {
debouncer.call(FunctionTag.changeProxy,
changeProxyDebounce(String groupName, String proxyName) {
debouncer.call(DebounceTag.changeProxy,
(String groupName, String proxyName) async {
await changeProxy(
groupName: groupName,
@@ -75,16 +69,16 @@ class AppController {
}, args: [groupName, proxyName]);
}
Future<void> restartCore() async {
commonPrint.log('restart core');
restartCore() async {
await clashService?.reStart();
await _initCore();
if (_ref.read(runTimeProvider.notifier).isStart) {
await globalState.handleStart();
}
}
Future<void> updateStatus(bool isStart) async {
updateStatus(bool isStart) async {
if (isStart) {
await globalState.handleStart([
updateRunTime,
@@ -103,7 +97,7 @@ class AppController {
applyProfileDebounce();
} else {
await globalState.handleStop();
clashCore.resetTraffic();
await clashCore.resetTraffic();
_ref.read(trafficsProvider.notifier).clear();
_ref.read(totalTrafficProvider.notifier).value = Traffic();
_ref.read(runTimeProvider.notifier).value = null;
@@ -111,7 +105,7 @@ class AppController {
}
}
void updateRunTime() {
updateRunTime() {
final startTime = globalState.startTime;
if (startTime != null) {
final startTimeStamp = startTime.millisecondsSinceEpoch;
@@ -122,20 +116,20 @@ class AppController {
}
}
Future<void> updateTraffic() async {
updateTraffic() async {
final traffic = await clashCore.getTraffic();
_ref.read(trafficsProvider.notifier).addTraffic(traffic);
_ref.read(totalTrafficProvider.notifier).value =
await clashCore.getTotalTraffic();
}
Future<void> addProfile(Profile profile) async {
addProfile(Profile profile) async {
_ref.read(profilesProvider.notifier).setProfile(profile);
if (_ref.read(currentProfileIdProvider) != null) return;
_ref.read(currentProfileIdProvider.notifier).value = profile.id;
}
Future<void> deleteProfile(String id) async {
deleteProfile(String id) async {
_ref.read(profilesProvider.notifier).deleteProfileById(id);
clearEffect(id);
if (globalState.config.currentProfileId == id) {
@@ -151,12 +145,12 @@ class AppController {
}
}
Future<void> updateProviders() async {
updateProviders() async {
_ref.read(providersProvider.notifier).value =
await clashCore.getExternalProviders();
}
Future<void> updateLocalIp() async {
updateLocalIp() async {
_ref.read(localIpProvider.notifier).value = null;
await Future.delayed(commonDuration);
_ref.read(localIpProvider.notifier).value = await utils.getLocalIpAddress();
@@ -172,26 +166,26 @@ class AppController {
}
}
void setProfile(Profile profile) {
setProfile(Profile profile) {
_ref.read(profilesProvider.notifier).setProfile(profile);
}
void setProfileAndAutoApply(Profile profile) {
setProfileAndAutoApply(Profile profile) {
_ref.read(profilesProvider.notifier).setProfile(profile);
if (profile.id == _ref.read(currentProfileIdProvider)) {
applyProfileDebounce(silence: true);
}
}
void setProfiles(List<Profile> profiles) {
setProfiles(List<Profile> profiles) {
_ref.read(profilesProvider.notifier).value = profiles;
}
void addLog(Log log) {
addLog(Log log) {
_ref.read(logsProvider).add(log);
}
void updateOrAddHotKeyAction(HotKeyAction hotKeyAction) {
updateOrAddHotKeyAction(HotKeyAction hotKeyAction) {
final hotKeyActions = _ref.read(hotKeyActionsProvider);
final index =
hotKeyActions.indexWhere((item) => item.action == hotKeyAction.action);
@@ -220,26 +214,26 @@ class AppController {
return _ref.read(getProxiesColumnsProvider);
}
dynamic addSortNum() {
addSortNum() {
return _ref.read(sortNumProvider.notifier).add();
}
String? getCurrentGroupName() {
getCurrentGroupName() {
final currentGroupName = _ref.read(currentProfileProvider.select(
(state) => state?.currentGroupName,
));
return currentGroupName;
}
ProxyCardState getProxyCardState(String proxyName) {
ProxyCardState getProxyCardState(proxyName) {
return _ref.read(getProxyCardStateProvider(proxyName));
}
String? getSelectedProxyName(String groupName) {
getSelectedProxyName(groupName) {
return _ref.read(getSelectedProxyNameProvider(groupName));
}
void updateCurrentGroupName(String groupName) {
updateCurrentGroupName(String groupName) {
final profile = _ref.read(currentProfileProvider);
if (profile == null || profile.currentGroupName == groupName) {
return;
@@ -249,87 +243,57 @@ class AppController {
);
}
Future<void> updateClashConfig() async {
await safeRun(
() async {
await _updateClashConfig();
},
needLoading: true,
);
Future<void> updateClashConfig([bool? isPatch]) async {
commonPrint.log("update clash patch: ${isPatch ?? false}");
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
await commonScaffoldState?.loadingRun(() async {
await _updateClashConfig(
isPatch,
);
});
}
Future<void> _updateClashConfig() async {
final updateParams = _ref.read(updateParamsProvider);
final res = await _requestAdmin(updateParams.tun.enable);
if (res.isError) {
return;
}
final realTunEnable = _ref.read(realTunEnableProvider);
final message = await clashCore.updateConfig(
updateParams.copyWith.tun(
enable: realTunEnable,
),
);
if (message.isNotEmpty) throw message;
}
Future<Result<bool>> _requestAdmin(bool enableTun) async {
if(system.isWindows && kDebugMode){
return Result.success(false);
}
final realTunEnable = _ref.read(realTunEnableProvider);
if (enableTun != realTunEnable && realTunEnable == false) {
final code = await system.authorizeCore();
switch (code) {
case AuthorizeCode.success:
await restartCore();
return Result.error('');
case AuthorizeCode.none:
break;
case AuthorizeCode.error:
enableTun = false;
break;
}
}
_ref.read(realTunEnableProvider.notifier).value = enableTun;
return Result.success(enableTun);
}
Future<void> setupClashConfig() async {
await safeRun(
() async {
await _setupClashConfig();
},
needLoading: true,
);
}
Future<void> _setupClashConfig() async {
Future<void> _updateClashConfig([bool? isPatch]) async {
final profile = _ref.watch(currentProfileProvider);
await _ref.read(currentProfileProvider)?.checkAndUpdate();
final patchConfig = _ref.read(patchClashConfigProvider);
final res = await _requestAdmin(patchConfig.tun.enable);
if (res.isError) {
return;
final appSetting = _ref.read(appSettingProvider);
bool enableTun = patchConfig.tun.enable;
if (enableTun != lastTunEnable &&
lastTunEnable == false &&
!Platform.isAndroid) {
final code = await system.authorizeCore();
switch (code) {
case AuthorizeCode.none:
break;
case AuthorizeCode.success:
lastTunEnable = enableTun;
await restartCore();
return;
case AuthorizeCode.error:
enableTun = false;
}
}
final realTunEnable = _ref.read(realTunEnableProvider);
final realPatchConfig = patchConfig.copyWith.tun(enable: realTunEnable);
final params = await globalState.getSetupParams(
pathConfig: realPatchConfig,
);
final message = await clashCore.setupConfig(params);
lastProfileModified = await _ref.read(
currentProfileProvider.select(
(state) => state?.profileLastModified,
),
);
if (message.isNotEmpty) {
throw message;
if (appSetting.openLogs) {
clashCore.startLog();
} else {
clashCore.stopLog();
}
final res = await clashCore.updateConfig(
globalState.getUpdateConfigParams(isPatch),
);
if (isPatch == false) {
_ref.read(needApplyProvider.notifier).value = false;
}
if (res.isNotEmpty) throw res;
lastTunEnable = enableTun;
lastProfileModified = await profile?.profileLastModified;
}
Future _applyProfile() async {
await clashCore.requestGc();
await setupClashConfig();
await updateClashConfig();
await updateGroups();
await updateProviders();
}
@@ -338,32 +302,25 @@ class AppController {
if (silence) {
await _applyProfile();
} else {
await safeRun(
() async {
await _applyProfile();
},
needLoading: true,
);
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
await commonScaffoldState?.loadingRun(() async {
await _applyProfile();
});
}
addCheckIpNumDebounce();
}
void handleChangeProfile() {
handleChangeProfile() {
_ref.read(delayDataSourceProvider.notifier).value = {};
applyProfile();
_ref.read(logsProvider.notifier).value = FixedList(500);
_ref.read(requestsProvider.notifier).value = FixedList(500);
globalState.computeHeightMapCache = {};
}
void updateBrightness() {
WidgetsBinding.instance.addPostFrameCallback((_) {
_ref.read(systemBrightnessProvider.notifier).value =
WidgetsBinding.instance.platformDispatcher.platformBrightness;
});
updateBrightness(Brightness brightness) {
_ref.read(appBrightnessProvider.notifier).value = brightness;
}
Future<void> autoUpdateProfiles() async {
autoUpdateProfiles() async {
for (final profile in _ref.read(profilesProvider)) {
if (!profile.autoUpdate) continue;
final isNotNeedUpdate = profile.lastUpdateDate
@@ -395,7 +352,7 @@ class AppController {
}
}
Future<void> updateProfiles() async {
updateProfiles() async {
for (final profile in _ref.read(profilesProvider)) {
if (profile.type == ProfileType.file) {
continue;
@@ -404,12 +361,12 @@ class AppController {
}
}
Future<void> savePreferences() async {
commonPrint.log('save preferences');
savePreferences() async {
commonPrint.log("save preferences");
await preferences.saveConfig(globalState.config);
}
Future<void> changeProxy({
changeProxy({
required String groupName,
required String proxyName,
}) async {
@@ -425,13 +382,10 @@ class AppController {
addCheckIpNumDebounce();
}
Future<void> handleBackOrExit() async {
if (_ref.read(backBlockProvider)) {
return;
}
handleBackOrExit() async {
if (_ref.read(appSettingProvider).minimizeOnExit) {
if (system.isDesktop) {
await savePreferences();
await savePreferencesDebounce();
}
await system.back();
} else {
@@ -439,24 +393,13 @@ class AppController {
}
}
void backBlock() {
_ref.read(backBlockProvider.notifier).value = true;
}
void unBackBlock() {
_ref.read(backBlockProvider.notifier).value = false;
}
Future<void> handleExit() async {
Future.delayed(commonDuration, () {
system.exit();
});
handleExit() async {
try {
await savePreferences();
await macOS?.updateDns(true);
await proxy?.stopProxy();
await updateStatus(false);
await clashCore.shutdown();
await clashService?.destroy();
await proxy?.stopProxy();
await savePreferences();
} finally {
system.exit();
}
@@ -464,19 +407,19 @@ class AppController {
Future handleClear() async {
await preferences.clearPreferences();
commonPrint.log('clear preferences');
commonPrint.log("clear preferences");
globalState.config = Config(
themeProps: defaultThemeProps,
);
}
Future<void> autoCheckUpdate() async {
autoCheckUpdate() async {
if (!_ref.read(appSettingProvider).autoCheckUpdate) return;
final res = await request.checkForUpdate();
checkUpdateResultHandle(data: res);
}
Future<void> checkUpdateResultHandle({
checkUpdateResultHandle({
Map<String, dynamic>? data,
bool handleError = false,
}) async {
@@ -491,16 +434,16 @@ class AppController {
final res = await globalState.showMessage(
title: appLocalizations.discoverNewVersion,
message: TextSpan(
text: '$tagName \n',
text: "$tagName \n",
style: textTheme.headlineSmall,
children: [
TextSpan(
text: '\n',
text: "\n",
style: textTheme.bodyMedium,
),
for (final submit in submits)
TextSpan(
text: '- $submit \n',
text: "- $submit \n",
style: textTheme.bodyMedium,
),
],
@@ -511,7 +454,7 @@ class AppController {
return;
}
launchUrl(
Uri.parse('https://github.com/$repository/releases/latest'),
Uri.parse("https://github.com/$repository/releases/latest"),
);
} else if (handleError) {
globalState.showMessage(
@@ -523,7 +466,7 @@ class AppController {
}
}
Future<void> _handlePreference() async {
_handlePreference() async {
if (await preferences.isInit) {
return;
}
@@ -544,23 +487,20 @@ class AppController {
Future<void> _initCore() async {
final isInit = await clashCore.isInit;
if (!isInit) {
await clashCore.init();
await clashCore.setState(
globalState.getCoreState(),
);
await clashCore.init();
}
await applyProfile();
}
Future<void> init() async {
FlutterError.onError = (details) {
if (kDebugMode) {
commonPrint.log(details.stack.toString());
}
};
updateTray(true);
init() async {
await _handlePreference();
await _handlerDisclaimer();
await _initCore();
await _initStatus();
updateTray(true);
autoLaunch?.updateStatus(
_ref.read(appSettingProvider).autoLaunch,
);
@@ -571,13 +511,11 @@ class AppController {
} else {
window?.hide();
}
await _handlePreference();
await _handlerDisclaimer();
_ref.read(initProvider.notifier).value = true;
}
Future<void> _initStatus() async {
if (system.isAndroid) {
_initStatus() async {
if (Platform.isAndroid) {
await globalState.updateStartTime();
}
final status = globalState.isStart == true
@@ -590,28 +528,28 @@ class AppController {
}
}
void setDelay(Delay delay) {
setDelay(Delay delay) {
_ref.read(delayDataSourceProvider.notifier).setDelay(delay);
}
void toPage(PageLabel pageLabel) {
toPage(PageLabel pageLabel) {
_ref.read(currentPageLabelProvider.notifier).value = pageLabel;
}
void toProfiles() {
toProfiles() {
toPage(PageLabel.profiles);
}
void initLink() {
initLink() {
linkManager.initAppLinksListen(
(url) async {
final res = await globalState.showMessage(
title: '${appLocalizations.add}${appLocalizations.profile}',
title: "${appLocalizations.add}${appLocalizations.profile}",
message: TextSpan(
children: [
TextSpan(text: appLocalizations.doYouWantToPass),
TextSpan(
text: ' $url ',
text: " $url ",
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
@@ -620,7 +558,7 @@ class AppController {
),
TextSpan(
text:
'${appLocalizations.create}${appLocalizations.profile}'),
"${appLocalizations.create}${appLocalizations.profile}"),
],
),
);
@@ -663,7 +601,7 @@ class AppController {
false;
}
Future<void> _handlerDisclaimer() async {
_handlerDisclaimer() async {
if (_ref.read(appSettingProvider).disclaimerAccepted) {
return;
}
@@ -674,64 +612,62 @@ class AppController {
return;
}
Future<void> addProfileFormURL(String url) async {
addProfileFormURL(String url) async {
if (globalState.navigatorKey.currentState?.canPop() ?? false) {
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
}
toProfiles();
final profile = await safeRun(
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
final profile = await commonScaffoldState?.loadingRun<Profile>(
() async {
return await Profile.normal(
url: url,
).update();
},
needLoading: true,
title: '${appLocalizations.add}${appLocalizations.profile}',
title: "${appLocalizations.add}${appLocalizations.profile}",
);
if (profile != null) {
await addProfile(profile);
}
}
Future<void> addProfileFormFile() async {
final platformFile = await safeRun(picker.pickerFile);
addProfileFormFile() async {
final platformFile = await globalState.safeRun(picker.pickerFile);
final bytes = platformFile?.bytes;
if (bytes == null) {
return;
return null;
}
if (!context.mounted) return;
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
toProfiles();
final profile = await safeRun(
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
final profile = await commonScaffoldState?.loadingRun<Profile?>(
() async {
await Future.delayed(const Duration(milliseconds: 300));
return await Profile.normal(label: platformFile?.name).saveFile(bytes);
},
needLoading: true,
title: '${appLocalizations.add}${appLocalizations.profile}',
title: "${appLocalizations.add}${appLocalizations.profile}",
);
if (profile != null) {
await addProfile(profile);
}
}
Future<void> addProfileFormQrCode() async {
final url = await safeRun(
picker.pickerConfigQRCode,
);
addProfileFormQrCode() async {
final url = await globalState.safeRun(picker.pickerConfigQRCode);
if (url == null) return;
addProfileFormURL(url);
}
void updateViewSize(Size size) {
updateViewSize(Size size) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_ref.read(viewSizeProvider.notifier).value = size;
});
}
void setProvider(ExternalProvider? provider) {
setProvider(ExternalProvider? provider) {
_ref.read(providersProvider.notifier).setProvider(provider);
}
@@ -752,16 +688,10 @@ class AppController {
return List.of(proxies)
..sort(
(a, b) {
final aDelay = _ref.read(getDelayProvider(
proxyName: a.name,
testUrl: testUrl,
));
final bDelay = _ref.read(
getDelayProvider(
proxyName: b.name,
testUrl: testUrl,
),
);
final aDelay =
_ref.read(getDelayProvider(proxyName: a.name, testUrl: testUrl));
final bDelay =
_ref.read(getDelayProvider(proxyName: b.name, testUrl: testUrl));
if (aDelay == null && bDelay == null) {
return 0;
}
@@ -776,45 +706,45 @@ class AppController {
);
}
List<Proxy> getSortProxies({
required List<Proxy> proxies,
required ProxiesSortType sortType,
String? testUrl,
}) {
return switch (sortType) {
List<Proxy> getSortProxies(List<Proxy> proxies, [String? url]) {
return switch (_ref.read(proxiesStyleSettingProvider).sortType) {
ProxiesSortType.none => proxies,
ProxiesSortType.delay => _sortOfDelay(
proxies: proxies,
testUrl: testUrl,
testUrl: url,
),
ProxiesSortType.name => _sortOfName(proxies),
};
}
Future<Null> clearEffect(String profileId) async {
clearEffect(String profileId) async {
final profilePath = await appPath.getProfilePath(profileId);
final providersDirPath = await appPath.getProvidersDirPath(profileId);
final providersPath = await appPath.getProvidersPath(profileId);
return await Isolate.run(() async {
final profileFile = File(profilePath);
final isExists = await profileFile.exists();
if (isExists) {
profileFile.delete(recursive: true);
if (profilePath != null) {
final profileFile = File(profilePath);
final isExists = await profileFile.exists();
if (isExists) {
profileFile.delete(recursive: true);
}
}
final providersFileDir = File(providersDirPath);
final providersFileIsExists = await providersFileDir.exists();
if (providersFileIsExists) {
providersFileDir.delete(recursive: true);
if (providersPath != null) {
final providersFileDir = File(providersPath);
final isExists = await providersFileDir.exists();
if (isExists) {
providersFileDir.delete(recursive: true);
}
}
});
}
void updateTun() {
updateTun() {
_ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.tun(enable: !state.tun.enable),
);
}
void updateSystemProxy() {
updateSystemProxy() {
_ref.read(networkSettingProvider.notifier).updateState(
(state) => state.copyWith(
systemProxy: !state.systemProxy,
@@ -822,22 +752,11 @@ class AppController {
);
}
Future<List<Package>> getPackages() async {
if (_ref.read(isMobileViewProvider)) {
await Future.delayed(commonDuration);
}
if (_ref.read(packagesProvider).isEmpty) {
_ref.read(packagesProvider.notifier).value =
await app?.getPackages() ?? [];
}
return _ref.read(packagesProvider);
}
void updateStart() {
updateStart() {
updateStatus(!_ref.read(runTimeProvider.notifier).isStart);
}
void updateCurrentSelectedMap(String groupName, String proxyName) {
updateCurrentSelectedMap(String groupName, String proxyName) {
final currentProfile = _ref.read(currentProfileProvider);
if (currentProfile != null &&
currentProfile.selectedMap[groupName] != proxyName) {
@@ -852,7 +771,7 @@ class AppController {
}
}
void updateCurrentUnfoldSet(Set<String> value) {
updateCurrentUnfoldSet(Set<String> value) {
final currentProfile = _ref.read(currentProfileProvider);
if (currentProfile == null) {
return;
@@ -864,7 +783,7 @@ class AppController {
);
}
void changeMode(Mode mode) {
changeMode(Mode mode) {
_ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(mode: mode),
);
@@ -874,7 +793,7 @@ class AppController {
addCheckIpNumDebounce();
}
void updateAutoLaunch() {
updateAutoLaunch() {
_ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
autoLaunch: !state.autoLaunch,
@@ -882,7 +801,7 @@ class AppController {
);
}
Future<void> updateVisible() async {
updateVisible() async {
final visible = await window?.isVisible;
if (visible != null && !visible) {
window?.show();
@@ -891,7 +810,7 @@ class AppController {
}
}
void updateMode() {
updateMode() {
_ref.read(patchClashConfigProvider.notifier).updateState(
(state) {
final index = Mode.values.indexWhere((item) => item == state.mode);
@@ -906,7 +825,7 @@ class AppController {
);
}
Future<void> handleAddOrUpdate(WidgetRef ref, [Rule? rule]) async {
handleAddOrUpdate(WidgetRef ref, [Rule? rule]) async {
final res = await globalState.showCommonDialog<Rule>(
child: AddRuleDialog(
rule: rule,
@@ -943,7 +862,7 @@ class AppController {
(item) => item.toString(),
);
final data = await Isolate.run<List<int>>(() async {
final logsRawString = logsRaw.join('\n');
final logsRawString = logsRaw.join("\n");
return utf8.encode(logsRawString);
});
return await picker.saveFile(
@@ -959,20 +878,20 @@ class AppController {
final configJson = globalState.config.toJson();
return Isolate.run<List<int>>(() async {
final archive = Archive();
archive.add('config.json', configJson);
archive.addDirectoryToArchive(profilesPath, homeDirPath);
archive.add("config.json", configJson);
await archive.addDirectoryToArchive(profilesPath, homeDirPath);
final zipEncoder = ZipEncoder();
return zipEncoder.encode(archive) ?? [];
});
}
Future<void> updateTray([bool focus = false]) async {
updateTray([bool focus = false]) async {
tray.update(
trayState: _ref.read(trayStateProvider),
);
}
Future<void> recoveryData(
recoveryData(
List<int> data,
RecoveryOption recoveryOption,
) async {
@@ -982,12 +901,12 @@ class AppController {
});
final homeDirPath = await appPath.homeDirPath;
final configs =
archive.files.where((item) => item.name.endsWith('.json')).toList();
archive.files.where((item) => item.name.endsWith(".json")).toList();
final profiles =
archive.files.where((item) => !item.name.endsWith('.json'));
archive.files.where((item) => !item.name.endsWith(".json"));
final configIndex =
configs.indexWhere((config) => config.name == 'config.json');
if (configIndex == -1) throw 'invalid backup file';
configs.indexWhere((config) => config.name == "config.json");
if (configIndex == -1) throw "invalid backup file";
final configFile = configs[configIndex];
var tempConfig = Config.compatibleFromJson(
json.decode(
@@ -1001,7 +920,7 @@ class AppController {
await file.writeAsBytes(profile.content);
}
final clashConfigIndex =
configs.indexWhere((config) => config.name == 'clashConfig.json');
configs.indexWhere((config) => config.name == "clashConfig.json");
if (clashConfigIndex != -1) {
final clashConfigFile = configs[clashConfigIndex];
tempConfig = tempConfig.copyWith(
@@ -1020,19 +939,15 @@ class AppController {
);
}
void _recovery(Config config, RecoveryOption recoveryOption) {
final recoveryStrategy = _ref.read(appSettingProvider.select(
(state) => state.recoveryStrategy,
));
_recovery(Config config, RecoveryOption recoveryOption) {
final profiles = config.profiles;
if (recoveryStrategy == RecoveryStrategy.override) {
_ref.read(profilesProvider.notifier).value = profiles;
} else {
for (final profile in profiles) {
_ref.read(profilesProvider.notifier).setProfile(
profile,
);
}
final oldProfiles = List.from(globalState.config.profiles);
_ref.read(profilesProvider.notifier).value = profiles;
for (final profile in oldProfiles) {
_ref.read(profilesProvider.notifier).setProfile(
profile,
force: false,
);
}
final onlyProfiles = recoveryOption == RecoveryOption.onlyProfiles;
if (!onlyProfiles) {
@@ -1050,42 +965,10 @@ class AppController {
_ref.read(overrideDnsProvider.notifier).value = config.overrideDns;
_ref.read(networkSettingProvider.notifier).value = config.networkProps;
_ref.read(hotKeyActionsProvider.notifier).value = config.hotKeyActions;
_ref.read(scriptStateProvider.notifier).value = config.scriptProps;
}
final currentProfile = _ref.read(currentProfileProvider);
if (currentProfile == null) {
_ref.read(currentProfileIdProvider.notifier).value = profiles.first.id;
}
}
Future<T?> safeRun<T>(
FutureOr<T> Function() futureFunction, {
String? title,
bool needLoading = false,
bool silence = true,
}) async {
final realSilence = needLoading == true ? true : silence;
try {
if (needLoading) {
_ref.read(loadingProvider.notifier).value = true;
}
final res = await futureFunction();
return res;
} catch (e) {
commonPrint.log('$e');
if (realSilence) {
globalState.showNotifier(e.toString());
} else {
globalState.showMessage(
title: title ?? appLocalizations.tip,
message: TextSpan(
text: e.toString(),
),
);
}
return null;
} finally {
_ref.read(loadingProvider.notifier).value = false;
}
}
}

View File

@@ -2,10 +2,8 @@
import 'dart:io';
import 'package:fl_clash/common/system.dart';
import 'package:fl_clash/views/dashboard/widgets/widgets.dart';
import 'package:fl_clash/fragments/dashboard/widgets/widgets.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
@@ -17,16 +15,16 @@ enum SupportPlatform {
Android;
static SupportPlatform get currentPlatform {
if (system.isWindows) {
if (Platform.isWindows) {
return SupportPlatform.Windows;
} else if (system.isMacOS) {
} else if (Platform.isMacOS) {
return SupportPlatform.MacOS;
} else if (Platform.isLinux) {
return SupportPlatform.Linux;
} else if (system.isAndroid) {
} else if (Platform.isAndroid) {
return SupportPlatform.Android;
}
throw 'invalid platform';
throw "invalid platform";
}
}
@@ -45,11 +43,11 @@ enum GroupType {
static GroupType parseProfileType(String type) {
return switch (type) {
'url-test' => URLTest,
'select' => Selector,
'fallback' => Fallback,
'load-balance' => LoadBalance,
'relay' => Relay,
"url-test" => URLTest,
"select" => Selector,
"fallback" => Fallback,
"load-balance" => LoadBalance,
"relay" => Relay,
String() => throw UnimplementedError(),
};
}
@@ -60,7 +58,7 @@ enum GroupName { GLOBAL, Proxy, Auto, Fallback }
extension GroupTypeExtension on GroupType {
static List<String> get valueList => GroupType.values
.map(
(e) => e.toString().split('.').last,
(e) => e.toString().split(".").last,
)
.toList();
@@ -82,7 +80,7 @@ enum UsedProxy { GLOBAL, DIRECT, REJECT }
extension UsedProxyExtension on UsedProxy {
static List<String> get valueList => UsedProxy.values
.map(
(e) => e.toString().split('.').last,
(e) => e.toString().split(".").last,
)
.toList();
@@ -99,18 +97,7 @@ enum LogLevel {
warning,
error,
silent,
}
extension LogLevelExt on LogLevel {
Color? get color {
return switch (this) {
LogLevel.silent => Colors.grey.shade700,
LogLevel.debug => Colors.grey.shade400,
LogLevel.info => null,
LogLevel.warning => Colors.yellowAccent,
LogLevel.error => Colors.redAccent,
};
}
app,
}
enum TransportProtocol { udp, tcp }
@@ -131,12 +118,7 @@ enum AccessSortType { none, name, time }
enum ProfileType { file, url }
enum ResultType {
@JsonValue(0)
success,
@JsonValue(-1)
error,
}
enum ResultType { success, error }
enum AppMessageType {
log,
@@ -173,22 +155,18 @@ enum ProxyCardType { expand, shrink, min }
enum DnsMode {
normal,
@JsonValue('fake-ip')
@JsonValue("fake-ip")
fakeIp,
@JsonValue('redir-host')
@JsonValue("redir-host")
redirHost,
hosts
}
enum ExternalControllerStatus {
@JsonValue('')
close(''),
@JsonValue('127.0.0.1:9090')
open('127.0.0.1:9090');
final String value;
const ExternalControllerStatus(this.value);
@JsonValue("")
close,
@JsonValue("127.0.0.1:9090")
open
}
enum KeyboardModifier {
@@ -248,9 +226,9 @@ enum ProxiesIconStyle {
}
enum FontFamily {
twEmoji('Twemoji'),
jetBrainsMono('JetBrainsMono'),
icon('Icons');
twEmoji("Twemoji"),
jetBrainsMono("JetBrainsMono"),
icon("Icons");
final String value;
@@ -270,7 +248,6 @@ enum ActionMethod {
shutdown,
validateConfig,
updateConfig,
getConfig,
getProxies,
changeProxy,
getTraffic,
@@ -279,7 +256,6 @@ enum ActionMethod {
asyncTestDelay,
getConnections,
closeConnections,
resetConnections,
closeConnection,
getExternalProviders,
getExternalProvider,
@@ -292,10 +268,12 @@ enum ActionMethod {
stopListener,
getCountryCode,
getMemory,
getProfile,
crash,
setupConfig,
///Android,
setFdMap,
setProcessMap,
setState,
startTun,
stopTun,
@@ -313,10 +291,8 @@ enum WindowsHelperServiceStatus {
running,
}
enum FunctionTag {
enum DebounceTag {
updateClashConfig,
setupClashConfig,
updateStatus,
updateGroups,
addCheckIpNum,
applyProfile,
@@ -331,9 +307,6 @@ enum FunctionTag {
updatePageIndex,
pageChange,
proxiesTabChange,
logs,
requests,
autoScrollToEnd,
}
enum DashboardWidget {
@@ -437,39 +410,39 @@ enum PageLabel {
}
enum RuleAction {
DOMAIN('DOMAIN'),
DOMAIN_SUFFIX('DOMAIN-SUFFIX'),
DOMAIN_KEYWORD('DOMAIN-KEYWORD'),
DOMAIN_REGEX('DOMAIN-REGEX'),
GEOSITE('GEOSITE'),
IP_CIDR('IP-CIDR'),
IP_CIDR6('IP-CIDR6'),
IP_SUFFIX('IP-SUFFIX'),
IP_ASN('IP-ASN'),
GEOIP('GEOIP'),
SRC_GEOIP('SRC-GEOIP'),
SRC_IP_ASN('SRC-IP-ASN'),
SRC_IP_CIDR('SRC-IP-CIDR'),
SRC_IP_SUFFIX('SRC-IP-SUFFIX'),
DST_PORT('DST-PORT'),
SRC_PORT('SRC-PORT'),
IN_PORT('IN-PORT'),
IN_TYPE('IN-TYPE'),
IN_USER('IN-USER'),
IN_NAME('IN-NAME'),
PROCESS_PATH('PROCESS-PATH'),
PROCESS_PATH_REGEX('PROCESS-PATH-REGEX'),
PROCESS_NAME('PROCESS-NAME'),
PROCESS_NAME_REGEX('PROCESS-NAME-REGEX'),
UID('UID'),
NETWORK('NETWORK'),
DSCP('DSCP'),
RULE_SET('RULE-SET'),
AND('AND'),
OR('OR'),
NOT('NOT'),
SUB_RULE('SUB-RULE'),
MATCH('MATCH');
DOMAIN("DOMAIN"),
DOMAIN_SUFFIX("DOMAIN-SUFFIX"),
DOMAIN_KEYWORD("DOMAIN-KEYWORD"),
DOMAIN_REGEX("DOMAIN-REGEX"),
GEOSITE("GEOSITE"),
IP_CIDR("IP-CIDR"),
IP_CIDR6("IP-CIDR6"),
IP_SUFFIX("IP-SUFFIX"),
IP_ASN("IP-ASN"),
GEOIP("GEOIP"),
SRC_GEOIP("SRC-GEOIP"),
SRC_IP_ASN("SRC-IP-ASN"),
SRC_IP_CIDR("SRC-IP-CIDR"),
SRC_IP_SUFFIX("SRC-IP-SUFFIX"),
DST_PORT("DST-PORT"),
SRC_PORT("SRC-PORT"),
IN_PORT("IN-PORT"),
IN_TYPE("IN-TYPE"),
IN_USER("IN-USER"),
IN_NAME("IN-NAME"),
PROCESS_PATH("PROCESS-PATH"),
PROCESS_PATH_REGEX("PROCESS-PATH-REGEX"),
PROCESS_NAME("PROCESS-NAME"),
PROCESS_NAME_REGEX("PROCESS-NAME-REGEX"),
UID("UID"),
NETWORK("NETWORK"),
DSCP("DSCP"),
RULE_SET("RULE-SET"),
AND("AND"),
OR("OR"),
NOT("NOT"),
SUB_RULE("SUB-RULE"),
MATCH("MATCH");
final String value;
@@ -497,32 +470,3 @@ enum RuleTarget {
DIRECT,
REJECT,
}
enum RecoveryStrategy {
compatible,
override,
}
enum CacheTag {
logs,
rules,
requests,
proxiesList,
}
enum Language {
yaml,
javaScript,
}
enum ImportOption {
file,
url,
}
enum ScrollPositionCacheKeys {
tools,
profiles,
proxiesList,
proxiesTabList,
}

View File

@@ -20,16 +20,15 @@ class Contributor {
});
}
class AboutView extends StatelessWidget {
const AboutView({super.key});
class AboutFragment extends StatelessWidget {
const AboutFragment({super.key});
Future<void> _checkUpdate(BuildContext context) async {
_checkUpdate(BuildContext context) async {
final commonScaffoldState = context.commonScaffoldState;
if (commonScaffoldState?.mounted != true) return;
final data = await globalState.appController.safeRun<Map<String, dynamic>?>(
final data = await commonScaffoldState?.loadingRun<Map<String, dynamic>?>(
request.checkForUpdate,
title: appLocalizations.checkUpdate,
needLoading: true,
);
globalState.appController.checkUpdateResultHandle(
data: data,
@@ -49,10 +48,10 @@ class AboutView extends StatelessWidget {
},
),
ListItem(
title: const Text('Telegram'),
title: const Text("Telegram"),
onTap: () {
globalState.openUrl(
'https://t.me/FlClash',
"https://t.me/FlClash",
);
},
trailing: const Icon(Icons.launch),
@@ -61,7 +60,7 @@ class AboutView extends StatelessWidget {
title: Text(appLocalizations.project),
onTap: () {
globalState.openUrl(
'https://github.com/$repository',
"https://github.com/$repository",
);
},
trailing: const Icon(Icons.launch),
@@ -70,7 +69,7 @@ class AboutView extends StatelessWidget {
title: Text(appLocalizations.core),
onTap: () {
globalState.openUrl(
'https://github.com/chen08209/Clash.Meta/tree/FlClash',
"https://github.com/chen08209/Clash.Meta/tree/FlClash",
);
},
trailing: const Icon(Icons.launch),
@@ -82,14 +81,14 @@ class AboutView extends StatelessWidget {
List<Widget> _buildContributorsSection() {
const contributors = [
Contributor(
avatar: 'assets/images/avatars/june2.jpg',
name: 'June2',
link: 'https://t.me/Jibadong',
avatar: "assets/images/avatars/june2.jpg",
name: "June2",
link: "https://t.me/Jibadong",
),
Contributor(
avatar: 'assets/images/avatars/arue.jpg',
name: 'Arue',
link: 'https://t.me/xrcm6868',
avatar: "assets/images/avatars/arue.jpg",
name: "Arue",
link: "https://t.me/xrcm6868",
),
];
return generateSection(
@@ -194,7 +193,10 @@ class Avatar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
return InkWell(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
hoverColor: Colors.transparent,
child: Column(
children: [
SizedBox(

View File

@@ -1,10 +1,9 @@
import 'dart:async';
import 'dart:convert';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
@@ -12,25 +11,32 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class AccessView extends ConsumerStatefulWidget {
const AccessView({super.key});
class AccessFragment extends ConsumerStatefulWidget {
const AccessFragment({super.key});
@override
ConsumerState<AccessView> createState() => _AccessViewState();
ConsumerState<AccessFragment> createState() => _AccessFragmentState();
}
class _AccessViewState extends ConsumerState<AccessView> {
class _AccessFragmentState extends ConsumerState<AccessFragment> {
List<String> acceptList = [];
List<String> rejectList = [];
late ScrollController _controller;
final _completer = Completer();
@override
void initState() {
super.initState();
_updateInitList();
_controller = ScrollController();
_completer.complete(globalState.appController.getPackages());
WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appState;
if (appState.packages.isEmpty) {
Future.delayed(const Duration(milliseconds: 300), () async {
ref.read(packagesProvider.notifier).value =
await app?.getPackages() ?? [];
});
}
});
}
@override
@@ -39,7 +45,7 @@ class _AccessViewState extends ConsumerState<AccessView> {
super.dispose();
}
void _updateInitList() {
_updateInitList() {
acceptList = globalState.config.vpnProps.accessControl.acceptList;
rejectList = globalState.config.vpnProps.accessControl.rejectList;
}
@@ -106,7 +112,7 @@ class _AccessViewState extends ConsumerState<AccessView> {
);
}
Future<void> _intelligentSelected() async {
_intelligentSelected() async {
final packageNames = ref.read(
packageListSelectorStateProvider.select(
(state) => state.list.map((item) => item.packageName),
@@ -115,8 +121,7 @@ class _AccessViewState extends ConsumerState<AccessView> {
final commonScaffoldState = context.commonScaffoldState;
if (commonScaffoldState?.mounted != true) return;
final selectedPackageNames =
(await globalState.appController.safeRun<List<String>>(
needLoading: true,
(await commonScaffoldState?.loadingRun<List<String>>(
() async {
return await app?.getChinaPackageNames() ?? [];
},
@@ -161,7 +166,7 @@ class _AccessViewState extends ConsumerState<AccessView> {
);
}
void _handleSelected(List<String> valueList, Package package, bool? value) {
_handleSelected(List<String> valueList, Package package, bool? value) {
if (value == true) {
valueList.add(package.packageName);
} else {
@@ -268,7 +273,7 @@ class _AccessViewState extends ConsumerState<AccessView> {
),
Flexible(
child: Text(
'${valueList.length}',
"${valueList.length}",
style: Theme.of(context)
.textTheme
.labelLarge
@@ -314,42 +319,31 @@ class _AccessViewState extends ConsumerState<AccessView> {
),
Expanded(
flex: 1,
child: FutureBuilder(
future: _completer.future,
builder: (_, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return Center(
child: CircularProgressIndicator(),
);
}
return packages.isEmpty
? NullStatus(
label: appLocalizations.noData,
)
: CommonScrollBar(
controller: _controller,
child: ListView.builder(
controller: _controller,
itemCount: packages.length,
itemExtent: 72,
itemBuilder: (_, index) {
final package = packages[index];
return PackageListItem(
key: Key(package.packageName),
package: package,
value: valueList
.contains(package.packageName),
isActive: accessControl.enable,
onChanged: (value) {
_handleSelected(
valueList, package, value);
},
);
},
),
child: packages.isEmpty
? const Center(
child: CircularProgressIndicator(),
)
: CommonScrollBar(
controller: _controller,
child: ListView.builder(
controller: _controller,
itemCount: packages.length,
itemExtent: 72,
itemBuilder: (_, index) {
final package = packages[index];
return PackageListItem(
key: Key(package.packageName),
package: package,
value: valueList.contains(package.packageName),
isActive: accessControl.enable,
onChanged: (value) {
_handleSelected(valueList, package, value);
},
);
}),
)
},
),
),
),
],
),
),
@@ -460,7 +454,7 @@ class AccessControlSearchDelegate extends SearchDelegate {
);
}
void _handleSelected(
_handleSelected(
WidgetRef ref, List<String> valueList, Package package, bool? value) {
if (value == true) {
valueList.add(package.packageName);
@@ -716,8 +710,8 @@ class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
);
}
Future<void> _copyToClipboard() async {
await globalState.appController.safeRun(() {
_copyToClipboard() async {
await globalState.safeRun(() {
final data = globalState.config.vpnProps.accessControl.toJson();
Clipboard.setData(
ClipboardData(
@@ -729,8 +723,8 @@ class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
Navigator.of(context).pop();
}
Future<void> _pasteToClipboard() async {
await globalState.appController.safeRun(
_pasteToClipboard() async {
await globalState.safeRun(
() async {
final data = await Clipboard.getData('text/plain');
final text = data?.text;

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/widgets/widgets.dart';
@@ -255,8 +257,8 @@ class AutoCheckUpdateItem extends ConsumerWidget {
}
}
class ApplicationSettingView extends StatelessWidget {
const ApplicationSettingView({super.key});
class ApplicationSettingFragment extends StatelessWidget {
const ApplicationSettingFragment({super.key});
String getLocaleString(Locale? locale) {
if (locale == null) return appLocalizations.defaultText;
@@ -272,7 +274,7 @@ class ApplicationSettingView extends StatelessWidget {
SilentLaunchItem(),
],
AutoRunItem(),
if (system.isAndroid) ...[
if (Platform.isAndroid) ...[
HiddenItem(),
],
AnimateTabItem(),

Some files were not shown because too many files have changed in this diff Show More