diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index d2383e2a..13b39ae4 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -2,11 +2,11 @@
xmlns:tools="http://schemas.android.com/tools">
+ android:name="android.hardware.touchscreen"
+ android:required="false" />
+ android:name="android.hardware.camera"
+ android:required="false" />
@@ -14,18 +14,20 @@
-
-
-
-
-
+
+
+
-
+
-
-
-
+
+
+
+ android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
@@ -114,13 +115,17 @@
android:name=".services.FlClashVpnService"
android:exported="false"
android:foregroundServiceType="specialUse"
- android:permission="android.permission.BIND_VPN_SERVICE"
- >
+ android:permission="android.permission.BIND_VPN_SERVICE">
+
+
diff --git a/android/app/src/main/kotlin/com/follow/clash/BaseServiceInterface.kt b/android/app/src/main/kotlin/com/follow/clash/BaseServiceInterface.kt
new file mode 100644
index 00000000..f616424b
--- /dev/null
+++ b/android/app/src/main/kotlin/com/follow/clash/BaseServiceInterface.kt
@@ -0,0 +1,9 @@
+package com.follow.clash
+
+import com.follow.clash.models.Props
+
+interface BaseServiceInterface {
+ fun start(port: Int, props: Props?): Int?
+ fun stop()
+ fun startForeground(title: String, content: String)
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt b/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt
index 237e3df7..f1d815e0 100644
--- a/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt
+++ b/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt
@@ -1,10 +1,10 @@
package com.follow.clash
import android.content.Context
-import android.util.Log
import androidx.lifecycle.MutableLiveData
import com.follow.clash.plugins.AppPlugin
-import com.follow.clash.plugins.ProxyPlugin
+import com.follow.clash.plugins.ServicePlugin
+import com.follow.clash.plugins.VpnPlugin
import com.follow.clash.plugins.TilePlugin
import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine
@@ -22,6 +22,7 @@ enum class RunState {
object GlobalState {
private val lock = ReentrantLock()
+ val runLock = ReentrantLock()
val runState: MutableLiveData = MutableLiveData(RunState.STOP)
var flutterEngine: FlutterEngine? = null
@@ -37,6 +38,11 @@ object GlobalState {
return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
}
+ fun getCurrentVPNPlugin(): VpnPlugin? {
+ val currentEngine = if (serviceEngine != null) serviceEngine else flutterEngine
+ return currentEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
+ }
+
fun destroyServiceEngine() {
serviceEngine?.destroy()
serviceEngine = null
@@ -47,9 +53,10 @@ object GlobalState {
lock.withLock {
destroyServiceEngine()
serviceEngine = FlutterEngine(context)
- serviceEngine?.plugins?.add(ProxyPlugin())
+ serviceEngine?.plugins?.add(VpnPlugin())
serviceEngine?.plugins?.add(AppPlugin())
serviceEngine?.plugins?.add(TilePlugin())
+ serviceEngine?.plugins?.add(ServicePlugin())
val vpnService = DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"vpnService"
diff --git a/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt b/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt
index bd507669..4fef0d78 100644
--- a/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt
+++ b/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt
@@ -2,7 +2,8 @@ package com.follow.clash
import com.follow.clash.plugins.AppPlugin
-import com.follow.clash.plugins.ProxyPlugin
+import com.follow.clash.plugins.ServicePlugin
+import com.follow.clash.plugins.VpnPlugin
import com.follow.clash.plugins.TilePlugin
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
@@ -12,7 +13,8 @@ class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
flutterEngine.plugins.add(AppPlugin())
- flutterEngine.plugins.add(ProxyPlugin())
+ flutterEngine.plugins.add(VpnPlugin())
+ flutterEngine.plugins.add(ServicePlugin())
flutterEngine.plugins.add(TilePlugin())
GlobalState.flutterEngine = flutterEngine
}
diff --git a/android/app/src/main/kotlin/com/follow/clash/extensions/Drawable.kt b/android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt
similarity index 62%
rename from android/app/src/main/kotlin/com/follow/clash/extensions/Drawable.kt
rename to android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt
index 2390adcd..cf607c18 100644
--- a/android/app/src/main/kotlin/com/follow/clash/extensions/Drawable.kt
+++ b/android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt
@@ -1,18 +1,28 @@
package com.follow.clash.extensions
+import android.annotation.SuppressLint
+import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
+import android.os.Build
import android.system.OsConstants.IPPROTO_TCP
import android.system.OsConstants.IPPROTO_UDP
import android.util.Base64
-import java.net.URL
+import androidx.core.app.NotificationCompat
import androidx.core.graphics.drawable.toBitmap
+import com.follow.clash.MainActivity
+import com.follow.clash.R
import com.follow.clash.models.Metadata
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
-import java.net.InetAddress
-import java.net.InetSocketAddress
suspend fun Drawable.getBase64(): String {
@@ -31,7 +41,6 @@ fun Metadata.getProtocol(): Int? {
return null
}
-fun String.getInetSocketAddress(): InetSocketAddress {
- val url = URL("https://$this")
- return InetSocketAddress(InetAddress.getByName(url.host), url.port)
-}
+private val CHANNEL = "FlClash"
+
+private val notificationId: Int = 1
diff --git a/android/app/src/main/kotlin/com/follow/clash/models/Props.kt b/android/app/src/main/kotlin/com/follow/clash/models/Props.kt
index 2b94ba57..082483f1 100644
--- a/android/app/src/main/kotlin/com/follow/clash/models/Props.kt
+++ b/android/app/src/main/kotlin/com/follow/clash/models/Props.kt
@@ -12,6 +12,7 @@ data class AccessControl(
)
data class Props(
+ val enable: Boolean?,
val accessControl: AccessControl?,
val allowBypass: Boolean?,
val systemProxy: Boolean?,
diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt
index 94978531..5ff83cb4 100644
--- a/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt
+++ b/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt
@@ -9,8 +9,11 @@ import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo
import android.content.pm.PackageManager
import android.net.ConnectivityManager
+import android.net.VpnService
import android.os.Build
import android.widget.Toast
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
import androidx.core.content.FileProvider
@@ -21,6 +24,7 @@ import com.follow.clash.extensions.getProtocol
import com.follow.clash.models.Package
import com.follow.clash.models.Process
import com.google.gson.Gson
+import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
@@ -36,14 +40,13 @@ import java.io.File
import java.net.InetSocketAddress
import java.util.zip.ZipFile
-
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
private var activity: Activity? = null
private var toast: Toast? = null
- private var context: Context? = null
+ private lateinit var context: Context
private lateinit var channel: MethodChannel
@@ -51,6 +54,8 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private var connectivity: ConnectivityManager? = null
+ private var vpnCallBack: (() -> Unit)? = null
+
private val iconMap = mutableMapOf()
private val packages = mutableListOf()
@@ -109,12 +114,18 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex()
}
+
+ val VPN_PERMISSION_REQUEST_CODE = 1001
+
+ val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
+
+ private var isBlockNotification: Boolean = false
+
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
scope = CoroutineScope(Dispatchers.Default)
context = flutterPluginBinding.applicationContext;
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
channel.setMethodCallHandler(this)
-
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
@@ -172,7 +183,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
if (iconMap["default"] == null) {
iconMap["default"] =
- context?.packageManager?.defaultActivityIcon?.getBase64()
+ context.packageManager?.defaultActivityIcon?.getBase64()
}
result.success(iconMap["default"])
return@launch
@@ -199,12 +210,8 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
result.success(null)
return@withContext
}
- if (context == null) {
- result.success(null)
- return@withContext
- }
if (connectivity == null) {
- connectivity = context!!.getSystemService()
+ connectivity = context.getSystemService()
}
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
val dst = InetSocketAddress(
@@ -220,7 +227,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
result.success(null)
return@withContext
}
- val packages = context?.packageManager?.getPackagesForUid(uid)
+ val packages = context.packageManager?.getPackagesForUid(uid)
result.success(packages?.first())
}
}
@@ -245,46 +252,43 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
private fun openFile(path: String) {
- context?.let {
- val file = File(path)
- val uri = FileProvider.getUriForFile(
- it,
- "${it.packageName}.fileProvider",
- file
- )
-
- val intent = Intent(Intent.ACTION_VIEW).setDataAndType(
+ val file = File(path)
+ val uri = FileProvider.getUriForFile(
+ context,
+ "${context.packageName}.fileProvider",
+ file
+ )
+
+ val intent = Intent(Intent.ACTION_VIEW).setDataAndType(
+ uri,
+ "text/plain"
+ )
+
+ val flags =
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
+
+ val resInfoList = context.packageManager.queryIntentActivities(
+ intent, PackageManager.MATCH_DEFAULT_ONLY
+ )
+
+ for (resolveInfo in resInfoList) {
+ val packageName = resolveInfo.activityInfo.packageName
+ context.grantUriPermission(
+ packageName,
uri,
- "text/plain"
- )
-
- val flags =
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
-
- val resInfoList = it.packageManager.queryIntentActivities(
- intent, PackageManager.MATCH_DEFAULT_ONLY
+ flags
)
+ }
- for (resolveInfo in resInfoList) {
- val packageName = resolveInfo.activityInfo.packageName
- it.grantUriPermission(
- packageName,
- uri,
- flags
- )
- }
-
- try {
- activity?.startActivity(intent)
- } catch (e: Exception) {
- println(e)
- }
+ try {
+ activity?.startActivity(intent)
+ } catch (e: Exception) {
+ println(e)
}
}
private fun updateExcludeFromRecents(value: Boolean?) {
- if (context == null) return
- val am = getSystemService(context!!, ActivityManager::class.java)
+ val am = getSystemService(context, ActivityManager::class.java)
val task = am?.appTasks?.firstOrNull {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
it.taskInfo.taskId == activity?.taskId
@@ -301,7 +305,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
private suspend fun getPackageIcon(packageName: String): String? {
- val packageManager = context?.packageManager
+ val packageManager = context.packageManager
if (iconMap[packageName] == null) {
iconMap[packageName] = try {
packageManager?.getApplicationIcon(packageName)?.getBase64()
@@ -314,10 +318,10 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
private fun getPackages(): List {
- val packageManager = context?.packageManager
+ val packageManager = context.packageManager
if (packages.isNotEmpty()) return packages;
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
- it.packageName != context?.packageName
+ it.packageName != context.packageName
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|| it.packageName == "android"
@@ -346,8 +350,38 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
}
+ fun requestVpnPermission(context: Context, callBack: () -> Unit) {
+ vpnCallBack = callBack
+ val intent = VpnService.prepare(context)
+ if (intent != null) {
+ activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
+ return;
+ }
+ vpnCallBack?.invoke()
+ }
+
+ fun requestNotificationsPermission(context: Context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ val permission = ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.POST_NOTIFICATIONS
+ )
+ if (permission != PackageManager.PERMISSION_GRANTED) {
+ if (isBlockNotification) return
+ if (activity == null) return
+ ActivityCompat.requestPermissions(
+ activity!!,
+ arrayOf(Manifest.permission.POST_NOTIFICATIONS),
+ NOTIFICATION_PERMISSION_REQUEST_CODE
+ )
+ return
+ }
+ }
+ }
+
+
private fun isChinaPackage(packageName: String): Boolean {
- val packageManager = context?.packageManager ?: return false
+ val packageManager = context.packageManager ?: return false
skipPrefixList.forEach {
if (packageName == it || packageName.startsWith("$it.")) return false
}
@@ -419,6 +453,8 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity;
+ binding.addActivityResultListener(::onActivityResult)
+ binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
}
override fun onDetachedFromActivityForConfigChanges() {
@@ -433,4 +469,25 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
channel.invokeMethod("exit", null)
activity = null
}
+
+ private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
+ if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
+ if (resultCode == FlutterActivity.RESULT_OK) {
+ GlobalState.initServiceEngine(context)
+ vpnCallBack?.invoke()
+ }
+ }
+ return true
+ }
+
+ private fun onRequestPermissionsResultListener(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ): Boolean {
+ if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
+ isBlockNotification = true
+ }
+ return true
+ }
}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/ProxyPlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/ProxyPlugin.kt
deleted file mode 100644
index 4667522f..00000000
--- a/android/app/src/main/kotlin/com/follow/clash/plugins/ProxyPlugin.kt
+++ /dev/null
@@ -1,220 +0,0 @@
-package com.follow.clash.plugins
-
-import android.Manifest
-import android.app.Activity
-import android.content.ComponentName
-import android.content.Context
-import android.content.Intent
-import android.content.ServiceConnection
-import android.content.pm.PackageManager
-import android.net.VpnService
-import android.os.Build
-import android.os.IBinder
-import android.util.Log
-import androidx.core.app.ActivityCompat
-import androidx.core.content.ContextCompat
-import com.follow.clash.GlobalState
-import com.follow.clash.RunState
-import com.follow.clash.models.Props
-import com.follow.clash.services.FlClashVpnService
-import com.google.gson.Gson
-import io.flutter.embedding.android.FlutterActivity
-import io.flutter.embedding.engine.plugins.FlutterPlugin
-import io.flutter.embedding.engine.plugins.activity.ActivityAware
-import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
-import io.flutter.plugin.common.MethodCall
-import io.flutter.plugin.common.MethodChannel
-
-
-class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
-
- private lateinit var flutterMethodChannel: MethodChannel
-
- val VPN_PERMISSION_REQUEST_CODE = 1001
- val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
-
- private var activity: Activity? = null
- private var context: Context? = null
- private var flClashVpnService: FlClashVpnService? = null
- private var port: Int = 7890
- private var props: Props? = null
- private var isBlockNotification: Boolean = false
- private var isStart: Boolean = false
-
- private val connection = object : ServiceConnection {
- override fun onServiceConnected(className: ComponentName, service: IBinder) {
- val binder = service as FlClashVpnService.LocalBinder
- flClashVpnService = binder.getService()
- if (isStart) {
- startVpn()
- } else {
- flClashVpnService?.initServiceEngine()
- }
- }
-
- override fun onServiceDisconnected(arg: ComponentName) {
- flClashVpnService = null
- }
- }
-
- override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
- context = flutterPluginBinding.applicationContext
- flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "proxy")
- flutterMethodChannel.setMethodCallHandler(this)
- }
-
- override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
- flutterMethodChannel.setMethodCallHandler(null)
- }
-
- override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
- "initService" -> {
- isStart = false
- initService()
- requestNotificationsPermission()
- result.success(true)
- }
-
- "startProxy" -> {
- isStart = true
- port = call.argument("port")!!
- val args = call.argument("args")
- props =
- if (args != null) Gson().fromJson(args, Props::class.java) else null
- startVpn()
- result.success(true)
- }
-
- "stopProxy" -> {
- stopVpn()
- result.success(true)
- }
-
- "setProtect" -> {
- val fd = call.argument("fd")
- if (fd != null) {
- flClashVpnService?.protect(fd)
- result.success(true)
- } else {
- result.success(false)
- }
- }
-
- "startForeground" -> {
- val title = call.argument("title") as String
- val content = call.argument("content") as String
- startForeground(title, content)
- result.success(true)
- }
-
- else -> {
- result.notImplemented()
- }
- }
-
- private fun initService() {
- val intent = VpnService.prepare(context)
- if (intent != null) {
- activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
- } else {
- if (flClashVpnService != null) {
- flClashVpnService!!.initServiceEngine()
- } else {
- bindService()
- }
- }
- }
-
- private fun startVpn() {
- if (flClashVpnService == null) {
- bindService()
- return
- }
- if (GlobalState.runState.value == RunState.START) return
- GlobalState.runState.value = RunState.START
- val intent = VpnService.prepare(context)
- if (intent != null) {
- stopVpn()
- return
- }
- val fd = flClashVpnService?.start(port, props)
- flutterMethodChannel.invokeMethod("started", fd)
- }
-
- private fun stopVpn() {
- if (GlobalState.runState.value == RunState.STOP) return
- GlobalState.runState.value = RunState.STOP
- flClashVpnService?.stop()
- GlobalState.destroyServiceEngine()
- }
-
- private fun startForeground(title: String, content: String) {
- if (GlobalState.runState.value != RunState.START) return
- flClashVpnService?.startForeground(title, content)
- }
-
- override fun onAttachedToActivity(binding: ActivityPluginBinding) {
- activity = binding.activity
- binding.addActivityResultListener(::onActivityResult)
- binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
- }
-
- private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
- if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
- if (resultCode == FlutterActivity.RESULT_OK) {
- bindService()
- } else {
- stopVpn()
- }
- }
- return true
- }
-
- private fun onRequestPermissionsResultListener(
- requestCode: Int,
- permissions: Array,
- grantResults: IntArray
- ): Boolean {
- if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
- isBlockNotification = true
- }
- return false
- }
-
- private fun requestNotificationsPermission() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- val permission = context?.let {
- ContextCompat.checkSelfPermission(
- it,
- Manifest.permission.POST_NOTIFICATIONS
- )
- }
- if (permission != PackageManager.PERMISSION_GRANTED) {
- if (isBlockNotification) return
- if (activity == null) return
- ActivityCompat.requestPermissions(
- activity!!,
- arrayOf(Manifest.permission.POST_NOTIFICATIONS),
- NOTIFICATION_PERMISSION_REQUEST_CODE
- )
- }
- }
- }
-
- override fun onDetachedFromActivityForConfigChanges() {
- activity = null
- }
-
- override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
- activity = binding.activity
- }
-
- override fun onDetachedFromActivity() {
- activity = null
- }
-
- private fun bindService() {
- val intent = Intent(context, FlClashVpnService::class.java)
- context?.bindService(intent, connection, Context.BIND_AUTO_CREATE)
- }
-}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt
new file mode 100644
index 00000000..2e0c8eb9
--- /dev/null
+++ b/android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt
@@ -0,0 +1,47 @@
+package com.follow.clash.plugins
+
+import android.content.Context
+import com.follow.clash.GlobalState
+import io.flutter.embedding.engine.plugins.FlutterPlugin
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+
+
+class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
+
+ private lateinit var flutterMethodChannel: MethodChannel
+
+ private lateinit var context: Context
+
+ override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
+ context = flutterPluginBinding.applicationContext
+ flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "service")
+ flutterMethodChannel.setMethodCallHandler(this)
+ }
+
+ override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
+ flutterMethodChannel.setMethodCallHandler(null)
+ }
+
+ override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
+ "init" -> {
+ GlobalState.getCurrentAppPlugin()?.requestNotificationsPermission(context)
+ GlobalState.initServiceEngine(context)
+ result.success(true)
+ }
+
+ "destroy" -> {
+ handleDestroy()
+ result.success(true)
+ }
+
+ else -> {
+ result.notImplemented()
+ }
+ }
+
+ private fun handleDestroy() {
+ GlobalState.getCurrentVPNPlugin()?.stop()
+ GlobalState.destroyServiceEngine()
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/VpnPlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/VpnPlugin.kt
new file mode 100644
index 00000000..4d60bde0
--- /dev/null
+++ b/android/app/src/main/kotlin/com/follow/clash/plugins/VpnPlugin.kt
@@ -0,0 +1,143 @@
+package com.follow.clash.plugins
+
+import android.annotation.SuppressLint
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import android.util.Log
+import com.follow.clash.BaseServiceInterface
+import com.follow.clash.GlobalState
+import com.follow.clash.RunState
+import com.follow.clash.models.Props
+import com.follow.clash.services.FlClashService
+import com.follow.clash.services.FlClashVpnService
+import com.google.gson.Gson
+import io.flutter.embedding.engine.plugins.FlutterPlugin
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+import kotlin.concurrent.withLock
+
+
+class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
+ private lateinit var flutterMethodChannel: MethodChannel
+ private lateinit var context: Context
+ private var flClashService: BaseServiceInterface? = null
+ private var port: Int = 7890
+ private var props: Props? = null
+
+ private val connection = object : ServiceConnection {
+ override fun onServiceConnected(className: ComponentName, service: IBinder) {
+ flClashService = when (service) {
+ is FlClashVpnService.LocalBinder -> service.getService()
+ is FlClashService.LocalBinder -> service.getService()
+ else -> throw Exception("invalid binder")
+ }
+ start()
+ }
+
+ override fun onServiceDisconnected(arg: ComponentName) {
+ flClashService = null
+ }
+ }
+
+ override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
+ context = flutterPluginBinding.applicationContext
+ flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "vpn")
+ flutterMethodChannel.setMethodCallHandler(this)
+ }
+
+ override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
+ flutterMethodChannel.setMethodCallHandler(null)
+ }
+
+ override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
+
+ "start" -> {
+ port = call.argument("port")!!
+ val args = call.argument("args")
+ props =
+ if (args != null) Gson().fromJson(args, Props::class.java) else null
+ when (props?.enable == true) {
+ true -> handleStartVpn()
+ false -> start()
+ }
+ result.success(true)
+ }
+
+ "stop" -> {
+ stop()
+ result.success(true)
+ }
+
+ "setProtect" -> {
+ val fd = call.argument("fd")
+ if (fd != null) {
+ if (flClashService is FlClashVpnService) {
+ (flClashService as FlClashVpnService).protect(fd)
+ }
+ result.success(true)
+ } else {
+ result.success(false)
+ }
+ }
+
+ "startForeground" -> {
+ val title = call.argument("title") as String
+ val content = call.argument("content") as String
+ startForeground(title, content)
+ result.success(true)
+ }
+
+ else -> {
+ result.notImplemented()
+ }
+ }
+
+ @SuppressLint("ForegroundServiceType")
+ fun handleStartVpn() {
+ GlobalState.getCurrentAppPlugin()?.requestVpnPermission(context) {
+ start()
+ }
+ }
+
+ @SuppressLint("ForegroundServiceType")
+ private fun startForeground(title: String, content: String) {
+ GlobalState.runLock.withLock {
+ if (GlobalState.runState.value != RunState.START) return
+ flClashService?.startForeground(title, content)
+ }
+ }
+
+ private fun start() {
+ if (flClashService == null) {
+ bindService()
+ return
+ }
+ GlobalState.runLock.withLock {
+ if (GlobalState.runState.value == RunState.START) return
+ GlobalState.runState.value = RunState.START
+ val fd = flClashService?.start(port, props)
+ flutterMethodChannel.invokeMethod("started", fd)
+ }
+ }
+
+ fun stop() {
+ GlobalState.runLock.withLock {
+ if (GlobalState.runState.value == RunState.STOP) return
+ GlobalState.runState.value = RunState.STOP
+ flClashService?.stop()
+ }
+ GlobalState.destroyServiceEngine()
+ }
+
+ private fun bindService() {
+ val intent = when (props?.enable == true) {
+ true -> Intent(context, FlClashVpnService::class.java)
+ false -> Intent(context, FlClashService::class.java)
+ }
+ context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
+ }
+
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/com/follow/clash/services/FlClashService.kt b/android/app/src/main/kotlin/com/follow/clash/services/FlClashService.kt
new file mode 100644
index 00000000..47e04928
--- /dev/null
+++ b/android/app/src/main/kotlin/com/follow/clash/services/FlClashService.kt
@@ -0,0 +1,104 @@
+package com.follow.clash.services
+
+import android.annotation.SuppressLint
+import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Intent
+import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
+import android.os.Binder
+import android.os.Build
+import android.os.IBinder
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import com.follow.clash.BaseServiceInterface
+import com.follow.clash.MainActivity
+import com.follow.clash.models.Props
+
+
+@SuppressLint("WrongConstant")
+class FlClashService : Service(), BaseServiceInterface {
+
+ private val binder = LocalBinder()
+
+ inner class LocalBinder : Binder() {
+ fun getService(): FlClashService = this@FlClashService
+ }
+
+ override fun onBind(intent: Intent): IBinder {
+ return binder
+ }
+
+ override fun onUnbind(intent: Intent?): Boolean {
+ return super.onUnbind(intent)
+ }
+
+ private val CHANNEL = "FlClash"
+
+ private val notificationId: Int = 1
+
+ private val notificationBuilder: NotificationCompat.Builder by lazy {
+ val intent = Intent(this, MainActivity::class.java)
+
+ val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
+ PendingIntent.getActivity(
+ this,
+ 0,
+ intent,
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ } else {
+ PendingIntent.getActivity(
+ this,
+ 0,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ }
+ with(NotificationCompat.Builder(this, CHANNEL)) {
+ setSmallIcon(com.follow.clash.R.drawable.ic_stat_name)
+ setContentTitle("FlClash")
+ setContentIntent(pendingIntent)
+ setCategory(NotificationCompat.CATEGORY_SERVICE)
+ priority = NotificationCompat.PRIORITY_MIN
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
+ }
+ setOngoing(true)
+ setShowWhen(false)
+ setOnlyAlertOnce(true)
+ setAutoCancel(true)
+ }
+ }
+
+ override fun start(port: Int, props: Props?): Int? = null
+
+ override fun stop() {
+ stopSelf()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ }
+ }
+
+ @SuppressLint("ForegroundServiceType", "WrongConstant")
+ override fun startForeground(title: String, content: String) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val manager = getSystemService(NotificationManager::class.java)
+ var channel = manager?.getNotificationChannel(CHANNEL)
+ if (channel == null) {
+ channel =
+ NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
+ manager?.createNotificationChannel(channel)
+ }
+ }
+ val notification =
+ notificationBuilder.setContentTitle(title).setContentText(content).build()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
+ } else {
+ startForeground(notificationId, notification)
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt b/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt
index f5d1480b..4616b1b2 100644
--- a/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt
+++ b/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt
@@ -1,5 +1,6 @@
package com.follow.clash.services
+import android.annotation.SuppressLint
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
import android.app.NotificationChannel
import android.app.NotificationManager
@@ -15,6 +16,7 @@ import android.os.Parcel
import android.os.RemoteException
import android.util.Log
import androidx.core.app.NotificationCompat
+import com.follow.clash.BaseServiceInterface
import com.follow.clash.GlobalState
import com.follow.clash.MainActivity
import com.follow.clash.R
@@ -25,10 +27,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-class FlClashVpnService : VpnService() {
- private val CHANNEL = "FlClash"
-
- private val notificationId: Int = 1
+@SuppressLint("WrongConstant")
+class FlClashVpnService : VpnService(), BaseServiceInterface {
private val passList = listOf(
"*zhihu.com",
@@ -52,10 +52,10 @@ class FlClashVpnService : VpnService() {
override fun onCreate() {
super.onCreate()
- initServiceEngine()
+ GlobalState.initServiceEngine(applicationContext)
}
- fun start(port: Int, props: Props?): Int? {
+ override fun start(port: Int, props: Props?): Int? {
return with(Builder()) {
addAddress("172.16.0.1", 30)
setMtu(9000)
@@ -97,11 +97,18 @@ class FlClashVpnService : VpnService() {
}
}
- fun stop() {
+
+ override fun stop() {
stopSelf()
- stopForeground()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ }
}
+ private val CHANNEL = "FlClash"
+
+ private val notificationId: Int = 1
+
private val notificationBuilder: NotificationCompat.Builder by lazy {
val intent = Intent(this, MainActivity::class.java)
@@ -136,16 +143,8 @@ class FlClashVpnService : VpnService() {
}
}
- fun initServiceEngine() {
- GlobalState.initServiceEngine(applicationContext)
- }
-
- override fun onTrimMemory(level: Int) {
- super.onTrimMemory(level)
- GlobalState.getCurrentAppPlugin()?.requestGc()
- }
-
- fun startForeground(title: String, content: String) {
+ @SuppressLint("ForegroundServiceType", "WrongConstant")
+ override fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
var channel = manager?.getNotificationChannel(CHANNEL)
@@ -157,17 +156,16 @@ class FlClashVpnService : VpnService() {
}
val notification =
notificationBuilder.setContentTitle(title).setContentText(content).build()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(notificationId, notification)
}
}
- private fun stopForeground() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- stopForeground(STOP_FOREGROUND_REMOVE)
- }
+ override fun onTrimMemory(level: Int) {
+ super.onTrimMemory(level)
+ GlobalState.getCurrentAppPlugin()?.requestGc()
}
private val binder = LocalBinder()
@@ -190,7 +188,6 @@ class FlClashVpnService : VpnService() {
}
}
-
override fun onBind(intent: Intent): IBinder {
return binder
}
diff --git a/core/Clash.Meta b/core/Clash.Meta
index 44d4b6da..0125a90a 160000
--- a/core/Clash.Meta
+++ b/core/Clash.Meta
@@ -1 +1 @@
-Subproject commit 44d4b6dab23ec596f5df9acc7fadb66f4eb30bd0
+Subproject commit 0125a90a77353ebfe3554bf027ff7a285e1db8cc
diff --git a/core/common.go b/core/common.go
index 0a82f49d..6b0798ff 100644
--- a/core/common.go
+++ b/core/common.go
@@ -4,6 +4,16 @@ import "C"
import (
"context"
"errors"
+ "math"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "sync"
+ "syscall"
+ "time"
+
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/adapter/outboundgroup"
@@ -21,51 +31,8 @@ import (
"github.com/metacubex/mihomo/log"
rp "github.com/metacubex/mihomo/rules/provider"
"github.com/metacubex/mihomo/tunnel"
- "os"
- "os/exec"
- "path/filepath"
- "runtime"
- "strings"
- "sync"
- "syscall"
- "time"
)
-//type healthCheckSchema struct {
-// Enable bool `provider:"enable"`
-// URL string `provider:"url"`
-// Interval int `provider:"interval"`
-// TestTimeout int `provider:"timeout,omitempty"`
-// Lazy bool `provider:"lazy,omitempty"`
-// ExpectedStatus string `provider:"expected-status,omitempty"`
-//}
-
-//type proxyProviderSchema struct {
-// Type string `provider:"type"`
-// Path string `provider:"path,omitempty"`
-// URL string `provider:"url,omitempty"`
-// Proxy string `provider:"proxy,omitempty"`
-// Interval int `provider:"interval,omitempty"`
-// Filter string `provider:"filter,omitempty"`
-// ExcludeFilter string `provider:"exclude-filter,omitempty"`
-// ExcludeType string `provider:"exclude-type,omitempty"`
-// DialerProxy string `provider:"dialer-proxy,omitempty"`
-//
-// HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
-// Override ap.OverrideSchema `provider:"override,omitempty"`
-// Header map[string][]string `provider:"header,omitempty"`
-//}
-//
-//type ruleProviderSchema struct {
-// Type string `provider:"type"`
-// Behavior string `provider:"behavior"`
-// Path string `provider:"path,omitempty"`
-// URL string `provider:"url,omitempty"`
-// Proxy string `provider:"proxy,omitempty"`
-// Format string `provider:"format,omitempty"`
-// Interval int `provider:"interval,omitempty"`
-//}
-
type ConfigExtendedParams struct {
IsPatch bool `json:"is-patch"`
IsCompatible bool `json:"is-compatible"`
@@ -455,30 +422,63 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
func patchConfig(general *config.General) {
log.Infoln("[Apply] patch")
route.ReStartServer(general.ExternalController)
- listener.SetAllowLan(general.AllowLan)
- inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
- inbound.SetAllowedIPs(general.LanAllowedIPs)
- inbound.SetDisAllowedIPs(general.LanDisAllowedIPs)
- listener.SetBindAddress(general.BindAddress)
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
+}
+
+var isRunning = false
+
+var runLock sync.Mutex
+
+func updateListeners(general *config.General, listeners map[string]constant.InboundListener) {
+ listener.PatchInboundListeners(listeners, tunnel.Tunnel, true)
+ listener.SetAllowLan(general.AllowLan)
+ inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
+ inbound.SetAllowedIPs(general.LanAllowedIPs)
+ inbound.SetDisAllowedIPs(general.LanDisAllowedIPs)
+ listener.SetBindAddress(general.BindAddress)
listener.ReCreateHTTP(general.Port, tunnel.Tunnel)
listener.ReCreateSocks(general.SocksPort, tunnel.Tunnel)
listener.ReCreateRedir(general.RedirPort, tunnel.Tunnel)
listener.ReCreateAutoRedir(general.EBpf.AutoRedir, tunnel.Tunnel)
listener.ReCreateTProxy(general.TProxyPort, tunnel.Tunnel)
- listener.ReCreateTun(general.Tun, tunnel.Tunnel)
listener.ReCreateMixed(general.MixedPort, tunnel.Tunnel)
listener.ReCreateShadowSocks(general.ShadowSocksConfig, tunnel.Tunnel)
listener.ReCreateVmess(general.VmessConfig, tunnel.Tunnel)
listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel)
- tunnel.SetMode(general.Mode)
- log.SetLevel(general.LogLevel)
+ listener.ReCreateTun(general.Tun, tunnel.Tunnel)
+ listener.ReCreateRedirToTun(general.EBpf.RedirectToTun)
+}
+
+func stopListeners() {
+ listener.StopListener()
+}
+
+func hcCompatibleProvider(proxyProviders map[string]cp.ProxyProvider) {
+ wg := sync.WaitGroup{}
+ ch := make(chan struct{}, math.MaxInt)
+ for _, proxyProvider := range proxyProviders {
+ proxyProvider := proxyProvider
+ if proxyProvider.VehicleType() == cp.Compatible {
+ log.Infoln("Start initial Compatible provider %s", proxyProvider.Name())
+ wg.Add(1)
+ ch <- struct{}{}
+ go func() {
+ defer func() { <-ch; wg.Done() }()
+ if err := proxyProvider.Initial(); err != nil {
+ log.Errorln("initial Compatible provider %s error: %v", proxyProvider.Name(), err)
+ }
+ }()
+ }
+
+ }
- resolver.DisableIPv6 = !general.IPv6
}
func patchSelectGroup() {
@@ -506,12 +506,8 @@ func patchSelectGroup() {
}
}
-var applyLock sync.Mutex
-
func applyConfig() error {
- applyLock.Lock()
- defer applyLock.Unlock()
- cfg, err := config.ParseRawConfig(currentConfig)
+ cfg, err := config.ParseRawConfig(currentRawConfig)
if err != nil {
cfg, _ = config.ParseRawConfig(config.DefaultRawConfig())
}
@@ -523,9 +519,13 @@ func applyConfig() error {
} else {
closeConnections()
runtime.GC()
- hub.UltraApplyConfig(cfg, true)
+ hub.UltraApplyConfig(cfg)
patchSelectGroup()
}
+ if isRunning {
+ updateListeners(cfg.General, cfg.Listeners)
+ hcCompatibleProvider(cfg.Providers)
+ }
externalProviders = getExternalProvidersRaw()
return err
}
diff --git a/core/hub.go b/core/hub.go
index 8ad4fc6d..b3ae314c 100644
--- a/core/hub.go
+++ b/core/hub.go
@@ -8,6 +8,13 @@ import (
bridge "core/dart-bridge"
"encoding/json"
"fmt"
+ "os"
+ "runtime"
+ "sort"
+ "sync"
+ "time"
+ "unsafe"
+
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/outboundgroup"
"github.com/metacubex/mihomo/adapter/provider"
@@ -21,14 +28,9 @@ import (
"github.com/metacubex/mihomo/tunnel"
"github.com/metacubex/mihomo/tunnel/statistic"
"golang.org/x/net/context"
- "os"
- "runtime"
- "sort"
- "time"
- "unsafe"
)
-var currentConfig = config.DefaultRawConfig()
+var currentRawConfig = config.DefaultRawConfig()
var configParams = ConfigExtendedParams{}
@@ -36,6 +38,21 @@ var externalProviders = map[string]cp.Provider{}
var isInit = false
+//export start
+func start() {
+ runLock.Lock()
+ defer runLock.Unlock()
+ isRunning = true
+}
+
+//export stop
+func stop() {
+ runLock.Lock()
+ defer runLock.Unlock()
+ isRunning = false
+ stopListeners()
+}
+
//export initClash
func initClash(homeDirStr *C.char) bool {
if !isInit {
@@ -59,10 +76,10 @@ func restartClash() bool {
//export shutdownClash
func shutdownClash() bool {
+ stopListeners()
executor.Shutdown()
runtime.GC()
isInit = false
- currentConfig = nil
return true
}
@@ -88,11 +105,15 @@ func validateConfig(s *C.char, port C.longlong) {
}()
}
+var updateLock sync.Mutex
+
//export updateConfig
func updateConfig(s *C.char, port C.longlong) {
i := int64(port)
paramsString := C.GoString(s)
go func() {
+ updateLock.Lock()
+ defer updateLock.Unlock()
var params = &GenerateConfigParams{}
err := json.Unmarshal([]byte(paramsString), params)
if err != nil {
@@ -101,7 +122,7 @@ func updateConfig(s *C.char, port C.longlong) {
}
configParams = params.Params
prof := decorationConfig(params.ProfileId, params.Config)
- currentConfig = prof
+ currentRawConfig = prof
err = applyConfig()
if err != nil {
bridge.SendToPort(i, err.Error())
diff --git a/core/state.go b/core/state.go
index 3034a92f..72343de6 100644
--- a/core/state.go
+++ b/core/state.go
@@ -14,6 +14,7 @@ type AccessControl struct {
}
type AndroidProps struct {
+ Enable bool `json:"enable"`
AccessControl *AccessControl `json:"accessControl"`
AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"`
diff --git a/core/tun.go b/core/tun.go
index d8cde589..a4daad6f 100644
--- a/core/tun.go
+++ b/core/tun.go
@@ -7,14 +7,15 @@ import (
"core/platform"
t "core/tun"
"errors"
- "github.com/metacubex/mihomo/component/dialer"
- "github.com/metacubex/mihomo/log"
- "golang.org/x/sync/semaphore"
"strconv"
"sync"
"sync/atomic"
"syscall"
"time"
+
+ "github.com/metacubex/mihomo/component/dialer"
+ "github.com/metacubex/mihomo/log"
+ "golang.org/x/sync/semaphore"
)
var tunLock sync.Mutex
@@ -40,6 +41,18 @@ var fdMap FdMap
func startTUN(fd C.int, port C.longlong) {
i := int64(port)
ServicePort = i
+ if fd == 0 {
+ tunLock.Lock()
+ defer tunLock.Unlock()
+ now := time.Now()
+ runTime = &now
+ SendMessage(Message{
+ Type: StartedMessage,
+ Data: strconv.FormatInt(runTime.UnixMilli(), 10),
+ })
+ return
+ }
+ initSocketHook()
go func() {
tunLock.Lock()
defer tunLock.Unlock()
@@ -88,6 +101,7 @@ func getRunTime() *C.char {
//export stopTun
func stopTun() {
+ removeSocketHook()
go func() {
tunLock.Lock()
defer tunLock.Unlock()
@@ -95,6 +109,7 @@ func stopTun() {
runTime = nil
if tun != nil {
+ log.Errorln("[Tun] stopTun")
tun.Close()
tun = nil
}
@@ -125,7 +140,7 @@ func markSocket(fd Fd) {
var fdCounter int64 = 0
-func init() {
+func initSocketHook() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() {
return errBlocked
@@ -159,3 +174,7 @@ func init() {
})
}
}
+
+func removeSocketHook() {
+ dialer.DefaultSocketHook = nil
+}
diff --git a/lib/application.dart b/lib/application.dart
index 29021b92..fa3e6182 100644
--- a/lib/application.dart
+++ b/lib/application.dart
@@ -4,6 +4,7 @@ import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
+import 'package:fl_clash/widgets/proxy_container.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
@@ -95,7 +96,9 @@ class ApplicationState extends State {
if (system.isDesktop) {
return WindowContainer(
child: TrayContainer(
- child: app,
+ child: ProxyContainer(
+ child: app,
+ ),
),
);
}
@@ -121,59 +124,65 @@ class ApplicationState extends State {
@override
Widget build(context) {
- return AppStateContainer(
- child: ClashContainer(
- child: Selector2(
- selector: (_, appState, config) => ApplicationSelectorState(
- locale: config.locale,
- themeMode: config.themeMode,
- primaryColor: config.primaryColor,
- prueBlack: config.prueBlack,
- ),
- builder: (_, state, child) {
- return DynamicColorBuilder(
- builder: (lightDynamic, darkDynamic) {
- _updateSystemColorSchemes(lightDynamic, darkDynamic);
- return MaterialApp(
- navigatorKey: globalState.navigatorKey,
- localizationsDelegates: const [
- AppLocalizations.delegate,
- GlobalMaterialLocalizations.delegate,
- GlobalCupertinoLocalizations.delegate,
- GlobalWidgetsLocalizations.delegate
- ],
- builder: (_, child) {
- return _buildApp(child!);
- },
- scrollBehavior: BaseScrollBehavior(),
- title: appName,
- locale: other.getLocaleForString(state.locale),
- supportedLocales: AppLocalizations.delegate.supportedLocales,
- themeMode: state.themeMode,
- theme: ThemeData(
- useMaterial3: true,
- pageTransitionsTheme: _pageTransitionsTheme,
- colorScheme: _getAppColorScheme(
- brightness: Brightness.light,
- systemColorSchemes: systemColorSchemes,
- primaryColor: state.primaryColor,
+ return _buildApp(
+ AppStateContainer(
+ child: ClashContainer(
+ child: Selector2(
+ selector: (_, appState, config) => ApplicationSelectorState(
+ locale: config.locale,
+ themeMode: config.themeMode,
+ primaryColor: config.primaryColor,
+ prueBlack: config.prueBlack,
+ ),
+ builder: (_, state, child) {
+ return DynamicColorBuilder(
+ builder: (lightDynamic, darkDynamic) {
+ _updateSystemColorSchemes(lightDynamic, darkDynamic);
+ return MaterialApp(
+ navigatorKey: globalState.navigatorKey,
+ localizationsDelegates: const [
+ AppLocalizations.delegate,
+ GlobalMaterialLocalizations.delegate,
+ GlobalCupertinoLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate
+ ],
+ builder: (_, child) {
+ if (system.isDesktop) {
+ return WindowHeaderContainer(child: child!);
+ }
+ return child!;
+ },
+ scrollBehavior: BaseScrollBehavior(),
+ title: appName,
+ locale: other.getLocaleForString(state.locale),
+ supportedLocales:
+ AppLocalizations.delegate.supportedLocales,
+ themeMode: state.themeMode,
+ theme: ThemeData(
+ useMaterial3: true,
+ pageTransitionsTheme: _pageTransitionsTheme,
+ colorScheme: _getAppColorScheme(
+ brightness: Brightness.light,
+ systemColorSchemes: systemColorSchemes,
+ primaryColor: state.primaryColor,
+ ),
),
- ),
- darkTheme: ThemeData(
- useMaterial3: true,
- pageTransitionsTheme: _pageTransitionsTheme,
- colorScheme: _getAppColorScheme(
- brightness: Brightness.dark,
- systemColorSchemes: systemColorSchemes,
- primaryColor: state.primaryColor,
- ).toPrueBlack(state.prueBlack),
- ),
- home: child,
- );
- },
- );
- },
- child: const HomePage(),
+ darkTheme: ThemeData(
+ useMaterial3: true,
+ pageTransitionsTheme: _pageTransitionsTheme,
+ colorScheme: _getAppColorScheme(
+ brightness: Brightness.dark,
+ systemColorSchemes: systemColorSchemes,
+ primaryColor: state.primaryColor,
+ ).toPrueBlack(state.prueBlack),
+ ),
+ home: child,
+ );
+ },
+ );
+ },
+ child: const HomePage(),
+ ),
),
),
);
diff --git a/lib/clash/core.dart b/lib/clash/core.dart
index 8f5add83..6c40d3f6 100644
--- a/lib/clash/core.dart
+++ b/lib/clash/core.dart
@@ -237,6 +237,14 @@ class ClashCore {
malloc.free(paramsChar);
}
+ start() {
+ clashFFI.start();
+ }
+
+ stop() {
+ clashFFI.stop();
+ }
+
Future getDelay(String proxyName) {
final delayParams = {
"proxy-name": proxyName,
diff --git a/lib/clash/generated/clash_ffi.dart b/lib/clash/generated/clash_ffi.dart
index 366dc766..236f435a 100644
--- a/lib/clash/generated/clash_ffi.dart
+++ b/lib/clash/generated/clash_ffi.dart
@@ -5144,6 +5144,22 @@ class ClashFFI {
late final __FCmulcr =
__FCmulcrPtr.asFunction<_Fcomplex Function(_Fcomplex, double)>();
+ void start() {
+ return _start();
+ }
+
+ late final _startPtr =
+ _lookup>('start');
+ late final _start = _startPtr.asFunction();
+
+ void stop() {
+ return _stop();
+ }
+
+ late final _stopPtr =
+ _lookup>('stop');
+ late final _stop = _stopPtr.asFunction();
+
int initClash(
ffi.Pointer homeDirStr,
) {
diff --git a/lib/common/common.dart b/lib/common/common.dart
index 4db663c2..91c1468d 100644
--- a/lib/common/common.dart
+++ b/lib/common/common.dart
@@ -23,6 +23,6 @@ export 'app_localizations.dart';
export 'function.dart';
export 'package.dart';
export 'measure.dart';
-export 'service.dart';
+export 'windows.dart';
export 'iterable.dart';
export 'scroll.dart';
\ No newline at end of file
diff --git a/lib/common/launch.dart b/lib/common/launch.dart
index 20947572..05daf344 100644
--- a/lib/common/launch.dart
+++ b/lib/common/launch.dart
@@ -24,17 +24,71 @@ class AutoLaunch {
return await launchAtStartup.isEnabled();
}
+ Future get windowsIsEnable async {
+ final res = await Process.run(
+ 'schtasks',
+ ['/Query', '/TN', appName, '/V', "/FO", "LIST"],
+ runInShell: true,
+ );
+ return res.stdout.toString().contains(Platform.resolvedExecutable);
+ }
+
Future enable() async {
return await launchAtStartup.enable();
}
+ windowsDisable() async {
+ final res = await Process.run(
+ 'schtasks',
+ [
+ '/Delete',
+ '/TN',
+ appName,
+ '/F',
+ ],
+ runInShell: true,
+ );
+ return res.exitCode == 0;
+ }
+
+ windowsEnable() async {
+ await Process.run(
+ 'schtasks',
+ [
+ '/Create',
+ '/SC',
+ 'ONLOGON',
+ '/TN',
+ appName,
+ '/TR',
+ Platform.resolvedExecutable,
+ '/RL',
+ 'HIGHEST',
+ '/F'
+ ],
+ runInShell: true,
+ );
+ }
+
Future disable() async {
return await launchAtStartup.disable();
}
updateStatus(bool value) async {
- final isEnable = await this.isEnable;
- if (isEnable == value) return;
+ final currentEnable =
+ Platform.isWindows ? await windowsIsEnable : await isEnable;
+ if (value == currentEnable) {
+ return;
+ }
+ if (Platform.isWindows) {
+ if (value) {
+ enable();
+ windowsEnable();
+ } else {
+ windowsDisable();
+ }
+ return;
+ }
if (value == true) {
enable();
} else {
diff --git a/lib/common/other.dart b/lib/common/other.dart
index 1cb4572e..98d28202 100644
--- a/lib/common/other.dart
+++ b/lib/common/other.dart
@@ -6,7 +6,6 @@ import 'dart:typed_data';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart';
-import 'package:lpinyin/lpinyin.dart';
import 'package:zxing2/qrcode.dart';
import 'package:image/image.dart' as img;
@@ -192,7 +191,6 @@ class Other {
return ViewMode.desktop;
}
-
int getProxiesColumns(double viewWidth, ProxiesLayout proxiesLayout) {
final columns = max((viewWidth / 300).ceil(), 2);
return switch (proxiesLayout) {
diff --git a/lib/common/proxy.dart b/lib/common/proxy.dart
index fb32d176..6e66f495 100644
--- a/lib/common/proxy.dart
+++ b/lib/common/proxy.dart
@@ -1,37 +1,4 @@
-import 'package:fl_clash/common/datetime.dart';
-import 'package:fl_clash/plugins/proxy.dart';
-import 'package:proxy/proxy.dart' as proxy_plugin;
-import 'package:proxy/proxy_platform_interface.dart';
+import 'package:fl_clash/common/system.dart';
+import 'package:proxy/proxy.dart';
-class ProxyManager {
- static ProxyManager? _instance;
- late ProxyPlatform _proxy;
-
- ProxyManager._internal() {
- _proxy = proxy ?? proxy_plugin.Proxy();
- }
-
- bool get isStart => startTime != null && startTime!.isBeforeNow;
-
- DateTime? get startTime => _proxy.startTime;
-
- Future startProxy({required int port}) async {
- return await _proxy.startProxy(port);
- }
-
- Future stopProxy() async {
- return await _proxy.stopProxy();
- }
-
- Future updateStartTime() async {
- if (_proxy is! Proxy) return null;
- return await (_proxy as Proxy).updateStartTime();
- }
-
- factory ProxyManager() {
- _instance ??= ProxyManager._internal();
- return _instance!;
- }
-}
-
-final proxyManager = ProxyManager();
+final proxy = system.isDesktop ? Proxy() : null;
diff --git a/lib/common/request.dart b/lib/common/request.dart
index ab0502e6..83c43ef0 100644
--- a/lib/common/request.dart
+++ b/lib/common/request.dart
@@ -101,6 +101,9 @@ class Request {
return source.value(response.data!);
}
} catch (e) {
+ if(cancelToken?.isCancelled == true){
+ throw "cancelled";
+ }
continue;
}
}
diff --git a/lib/common/service.dart b/lib/common/service.dart
deleted file mode 100644
index 1d40dfa9..00000000
--- a/lib/common/service.dart
+++ /dev/null
@@ -1,110 +0,0 @@
-import 'dart:ffi';
-import 'dart:io';
-import 'package:ffi/ffi.dart';
-import 'package:win32/win32.dart';
-
-typedef CreateServiceNative = IntPtr Function(
- IntPtr hSCManager,
- Pointer lpServiceName,
- Pointer lpDisplayName,
- Uint32 dwDesiredAccess,
- Uint32 dwServiceType,
- Uint32 dwStartType,
- Uint32 dwErrorControl,
- Pointer lpBinaryPathName,
- Pointer lpLoadOrderGroup,
- Pointer lpdwTagId,
- Pointer lpDependencies,
- Pointer lpServiceStartName,
- Pointer lpPassword,
-);
-
-typedef CreateServiceDart = int Function(
- int hSCManager,
- Pointer lpServiceName,
- Pointer lpDisplayName,
- int dwDesiredAccess,
- int dwServiceType,
- int dwStartType,
- int dwErrorControl,
- Pointer lpBinaryPathName,
- Pointer lpLoadOrderGroup,
- Pointer lpdwTagId,
- Pointer lpDependencies,
- Pointer lpServiceStartName,
- Pointer lpPassword,
-);
-
-const _SERVICE_ALL_ACCESS = 0xF003F;
-
-const _SERVICE_WIN32_OWN_PROCESS = 0x00000010;
-
-const _SERVICE_AUTO_START = 0x00000002;
-
-const _SERVICE_ERROR_NORMAL = 0x00000001;
-
-typedef GetLastErrorNative = Uint32 Function();
-typedef GetLastErrorDart = int Function();
-
-class Service {
- static Service? _instance;
- late DynamicLibrary _advapi32;
-
- Service._internal() {
- _advapi32 = DynamicLibrary.open('advapi32.dll');
- }
-
- factory Service() {
- _instance ??= Service._internal();
- return _instance!;
- }
-
- Future createService() async {
- final int scManager = OpenSCManager(nullptr, nullptr, _SERVICE_ALL_ACCESS);
- if (scManager == 0) return;
- final serviceName = 'FlClash Service'.toNativeUtf16();
- final displayName = 'FlClash Service'.toNativeUtf16();
- final binaryPathName = "C:\\Application\\Clash.Verge_1.6.6_x64_portable\\resources\\clash-verge-service.exe".toNativeUtf16();
- final createService =
- _advapi32.lookupFunction(
- 'CreateServiceW',
- );
- final getLastError = DynamicLibrary.open('kernel32.dll')
- .lookupFunction('GetLastError');
-
- final serviceHandle = createService(
- scManager,
- serviceName,
- displayName,
- _SERVICE_ALL_ACCESS,
- _SERVICE_WIN32_OWN_PROCESS,
- _SERVICE_AUTO_START,
- _SERVICE_ERROR_NORMAL,
- binaryPathName,
- nullptr,
- nullptr,
- nullptr,
- nullptr,
- nullptr,
- );
-
- print("serviceHandle $serviceHandle");
-
- final errorCode = GetLastError();
- print('Error code: $errorCode');
-
- final result = StartService(serviceHandle, 0, nullptr);
-
- if (result == 0) {
- print('Failed to start the service.');
- } else {
- print('Service started successfully.');
- }
-
- calloc.free(serviceName);
- calloc.free(displayName);
- calloc.free(binaryPathName);
- }
-}
-
-final service = Platform.isWindows ? Service() : null;
diff --git a/lib/common/window.dart b/lib/common/window.dart
index c4aa637a..dba27d16 100644
--- a/lib/common/window.dart
+++ b/lib/common/window.dart
@@ -34,7 +34,7 @@ class Window {
// await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
// }
await windowManager.waitUntilReadyToShow(windowOptions, () async {
- await windowManager.setPreventClose(true);
+ // await windowManager.setPreventClose(true);
});
}
diff --git a/lib/common/windows.dart b/lib/common/windows.dart
new file mode 100644
index 00000000..b7a0b14e
--- /dev/null
+++ b/lib/common/windows.dart
@@ -0,0 +1,58 @@
+import 'dart:ffi';
+import 'dart:io';
+import 'package:ffi/ffi.dart';
+
+class Windows {
+ static Windows? _instance;
+ late DynamicLibrary _shell32;
+
+ Windows._internal() {
+ _shell32 = DynamicLibrary.open('shell32.dll');
+ }
+
+ factory Windows() {
+ _instance ??= Windows._internal();
+ return _instance!;
+ }
+
+ void runAsAdministrator(String command, String arguments) async {
+ final commandPtr = command.toNativeUtf16();
+ final argumentsPtr = arguments.toNativeUtf16();
+ final operationPtr = 'runas'.toNativeUtf16();
+
+ final shellExecute = _shell32.lookupFunction<
+ Int32 Function(
+ Pointer hwnd,
+ Pointer lpOperation,
+ Pointer lpFile,
+ Pointer lpParameters,
+ Pointer lpDirectory,
+ Int32 nShowCmd),
+ int Function(
+ Pointer hwnd,
+ Pointer lpOperation,
+ Pointer lpFile,
+ Pointer lpParameters,
+ Pointer lpDirectory,
+ int nShowCmd)>('ShellExecuteW');
+
+ final result = shellExecute(
+ nullptr,
+ operationPtr,
+ commandPtr,
+ argumentsPtr,
+ nullptr,
+ 1,
+ );
+
+ calloc.free(commandPtr);
+ calloc.free(argumentsPtr);
+ calloc.free(operationPtr);
+
+ if (result <= 32) {
+ throw Exception('Failed to launch $command with UAC');
+ }
+ }
+}
+
+final windows = Platform.isWindows ? Windows() : null;
diff --git a/lib/controller.dart b/lib/controller.dart
index 7fad229a..f4d013d3 100644
--- a/lib/controller.dart
+++ b/lib/controller.dart
@@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
-import 'dart:math';
import 'package:archive/archive.dart';
import 'package:fl_clash/common/archive.dart';
@@ -44,10 +43,9 @@ class AppController {
measure = Measure.of(context);
}
- Future updateSystemProxy(bool isStart) async {
+ updateStatus(bool isStart) async {
if (isStart) {
- await globalState.startSystemProxy(
- appState: appState,
+ await globalState.handleStart(
config: config,
clashConfig: clashConfig,
);
@@ -60,7 +58,7 @@ class AppController {
if (Platform.isAndroid) return;
await applyProfile(isPrue: true);
} else {
- await globalState.stopSystemProxy();
+ await globalState.handleStop();
clashCore.resetTraffic();
appState.traffics = [];
appState.totalTraffic = Traffic();
@@ -74,8 +72,9 @@ class AppController {
}
updateRunTime() {
- if (proxyManager.startTime != null) {
- final startTimeStamp = proxyManager.startTime!.millisecondsSinceEpoch;
+ final startTime = globalState.startTime;
+ if (startTime != null) {
+ final startTimeStamp = startTime.millisecondsSinceEpoch;
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
appState.runTime = nowTimeStamp - startTimeStamp;
} else {
@@ -103,7 +102,7 @@ class AppController {
final updateId = config.profiles.first.id;
changeProfile(updateId);
} else {
- updateSystemProxy(false);
+ updateStatus(false);
}
}
}
@@ -229,7 +228,7 @@ class AppController {
}
handleExit() async {
- await updateSystemProxy(false);
+ await updateStatus(false);
await savePreferences();
clashCore.shutdown();
system.exit();
@@ -298,11 +297,13 @@ class AppController {
if (!config.silentLaunch) {
window?.show();
}
- await proxyManager.updateStartTime();
- if (proxyManager.isStart) {
- await updateSystemProxy(true);
+ if (Platform.isAndroid) {
+ globalState.updateStartTime();
+ }
+ if (globalState.isStart) {
+ await updateStatus(true);
} else {
- await updateSystemProxy(config.autoRun);
+ await updateStatus(config.autoRun);
}
autoUpdateProfiles();
autoCheckUpdate();
@@ -415,7 +416,6 @@ class AppController {
addProfileFormURL(url);
}
-
updateViewWidth(double width) {
WidgetsBinding.instance.addPostFrameCallback((_) {
appState.viewWidth = width;
diff --git a/lib/fragments/application_setting.dart b/lib/fragments/application_setting.dart
index 1d097c69..07e1429f 100644
--- a/lib/fragments/application_setting.dart
+++ b/lib/fragments/application_setting.dart
@@ -76,7 +76,7 @@ class ApplicationSettingFragment extends StatelessWidget {
selector: (_, config) => config.autoRun,
builder: (_, autoRun, child) {
return ListItem.switchItem(
- leading: const Icon(Icons.start),
+ leading: const Icon(Icons.not_started),
title: Text(appLocalizations.autoRun),
subtitle: Text(appLocalizations.autoRunDesc),
delegate: SwitchDelegate(
diff --git a/lib/fragments/config.dart b/lib/fragments/config.dart
index 19438de9..a9762151 100644
--- a/lib/fragments/config.dart
+++ b/lib/fragments/config.dart
@@ -27,7 +27,6 @@ class _ConfigFragmentState extends State {
final mixedPort = int.parse(port);
if (mixedPort < 1024 || mixedPort > 49151) throw "Invalid port";
globalState.appController.clashConfig.mixedPort = mixedPort;
- globalState.appController.updateClashConfigDebounce();
} catch (e) {
globalState.showMessage(
title: appLocalizations.proxyPort,
@@ -62,7 +61,6 @@ class _ConfigFragmentState extends State {
}
final appController = globalState.appController;
appController.clashConfig.logLevel = value;
- appController.updateClashConfigDebounce();
Navigator.of(context).pop();
},
),
@@ -100,7 +98,6 @@ class _ConfigFragmentState extends State {
onChanged: (String? value) {
final appController = globalState.appController;
appController.clashConfig.globalRealUa = value;
- appController.updateClashConfigDebounce();
Navigator.of(context).pop();
},
),
@@ -125,7 +122,6 @@ class _ConfigFragmentState extends State {
throw "Invalid url";
}
globalState.appController.config.testUrl = newTestUrl;
- globalState.appController.updateClashConfigDebounce();
} catch (e) {
globalState.showMessage(
title: appLocalizations.testUrl,
@@ -172,7 +168,7 @@ class _ConfigFragmentState extends State {
items: [
if (Platform.isAndroid) ...[
Selector(
- selector: (_, config) => config.allowBypass,
+ selector: (_, config) => config.vpnProps.allowBypass,
builder: (_, allowBypass, __) {
return ListItem.switchItem(
leading: const Icon(Icons.arrow_forward_outlined),
@@ -181,15 +177,18 @@ class _ConfigFragmentState extends State {
delegate: SwitchDelegate(
value: allowBypass,
onChanged: (bool value) async {
- final appController = globalState.appController;
- appController.config.allowBypass = value;
+ final config = globalState.appController.config;
+ final vpnProps = config.vpnProps;
+ config.vpnProps = vpnProps.copyWith(
+ allowBypass: value,
+ );
},
),
);
},
),
Selector(
- selector: (_, config) => config.systemProxy,
+ selector: (_, config) => config.vpnProps.systemProxy,
builder: (_, systemProxy, __) {
return ListItem.switchItem(
leading: const Icon(Icons.settings_ethernet),
@@ -198,8 +197,11 @@ class _ConfigFragmentState extends State {
delegate: SwitchDelegate(
value: systemProxy,
onChanged: (bool value) async {
- final appController = globalState.appController;
- appController.config.systemProxy = value;
+ final config = globalState.appController.config;
+ final vpnProps = config.vpnProps;
+ config.vpnProps = vpnProps.copyWith(
+ systemProxy: value,
+ );
},
),
);
@@ -351,7 +353,6 @@ class _ConfigFragmentState extends State {
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.ipv6 = value;
- appController.updateClashConfigDebounce();
},
),
);
@@ -369,7 +370,6 @@ class _ConfigFragmentState extends State {
onChanged: (bool value) async {
final clashConfig = context.read();
clashConfig.allowLan = value;
- globalState.appController.updateClashConfigDebounce();
},
),
);
@@ -387,7 +387,6 @@ class _ConfigFragmentState extends State {
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.unifiedDelay = value;
- appController.updateClashConfigDebounce();
},
),
);
@@ -407,7 +406,6 @@ class _ConfigFragmentState extends State {
final appController = globalState.appController;
appController.clashConfig.findProcessMode =
value ? FindProcessMode.always : FindProcessMode.off;
- appController.updateClashConfigDebounce();
},
),
);
@@ -425,7 +423,6 @@ class _ConfigFragmentState extends State {
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.tcpConcurrent = value;
- appController.updateClashConfigDebounce();
},
),
);
@@ -446,7 +443,6 @@ class _ConfigFragmentState extends State {
appController.clashConfig.geodataLoader = value
? geodataLoaderMemconservative
: geodataLoaderStandard;
- appController.updateClashConfigDebounce();
},
),
);
@@ -466,7 +462,6 @@ class _ConfigFragmentState extends State {
final appController = globalState.appController;
appController.clashConfig.externalController =
value ? defaultExternalController : '';
- appController.updateClashConfigDebounce();
},
),
);
@@ -493,7 +488,6 @@ class _ConfigFragmentState extends State {
onChanged: (bool value) async {
final clashConfig = context.read();
clashConfig.tun = Tun(enable: value);
- globalState.appController.updateClashConfigDebounce();
},
),
);
@@ -652,8 +646,9 @@ class _KeepAliveIntervalFormDialogState
@override
void initState() {
super.initState();
- keepAliveIntervalController =
- TextEditingController(text: "${widget.keepAliveInterval}");
+ keepAliveIntervalController = TextEditingController(
+ text: "${widget.keepAliveInterval}",
+ );
}
_handleUpdate() async {
diff --git a/lib/fragments/dashboard/dashboard.dart b/lib/fragments/dashboard/dashboard.dart
index 610335f8..08c15677 100644
--- a/lib/fragments/dashboard/dashboard.dart
+++ b/lib/fragments/dashboard/dashboard.dart
@@ -1,6 +1,9 @@
+import 'dart:io';
import 'dart:math';
+import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/fragments/dashboard/intranet_ip.dart';
+import 'package:fl_clash/fragments/dashboard/status_switch.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'package:fl_clash/widgets/widgets.dart';
@@ -28,34 +31,51 @@ class _DashboardFragmentState extends State {
child: Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(16).copyWith(
+ bottom: 88,
+ ),
child: Selector(
selector: (_, appState) => appState.viewWidth,
builder: (_, viewWidth, ___) {
- // final viewMode = other.getViewMode(viewWidth);
- // final isDesktop = viewMode == ViewMode.desktop;
+ final columns = max(4 * ((viewWidth / 350).ceil()), 8);
+ final int switchCount = (4 / columns) * viewWidth < 200 ? 8 : 4;
return Grid(
- crossAxisCount: max(4 * ((viewWidth / 350).ceil()), 8),
+ crossAxisCount: columns,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
- children: const [
- GridItem(
+ children: [
+ const GridItem(
crossAxisCellCount: 8,
child: NetworkSpeed(),
),
- GridItem(
+ if (Platform.isAndroid)
+ GridItem(
+ crossAxisCellCount: switchCount,
+ child: const VPNSwitch(),
+ ),
+ if (system.isDesktop) ...[
+ GridItem(
+ crossAxisCellCount: switchCount,
+ child: const TUNSwitch(),
+ ),
+ GridItem(
+ crossAxisCellCount: switchCount,
+ child: const ProxySwitch(),
+ ),
+ ],
+ const GridItem(
crossAxisCellCount: 4,
child: OutboundMode(),
),
- GridItem(
+ const GridItem(
crossAxisCellCount: 4,
child: NetworkDetection(),
),
- GridItem(
+ const GridItem(
crossAxisCellCount: 4,
child: TrafficUsage(),
),
- GridItem(
+ const GridItem(
crossAxisCellCount: 4,
child: IntranetIP(),
),
diff --git a/lib/fragments/dashboard/network_detection.dart b/lib/fragments/dashboard/network_detection.dart
index ec0cc2ab..a0886b2f 100644
--- a/lib/fragments/dashboard/network_detection.dart
+++ b/lib/fragments/dashboard/network_detection.dart
@@ -14,8 +14,12 @@ class NetworkDetection extends StatefulWidget {
}
class _NetworkDetectionState extends State {
- final ipInfoNotifier = ValueNotifier(null);
- final timeoutNotifier = ValueNotifier(false);
+ final networkDetectionState = ValueNotifier(
+ const NetworkDetectionState(
+ isTesting: true,
+ ipInfo: null,
+ ),
+ );
bool? _preIsStart;
Function? _checkIpDebounce;
CancelToken? cancelToken;
@@ -23,26 +27,28 @@ class _NetworkDetectionState extends State {
_checkIp() async {
final appState = globalState.appController.appState;
final isInit = appState.isInit;
- final isStart = appState.isStart;
if (!isInit) return;
- timeoutNotifier.value = false;
+ final isStart = appState.isStart;
if (_preIsStart == false && _preIsStart == isStart) return;
+ networkDetectionState.value = networkDetectionState.value.copyWith(
+ isTesting: true,
+ ipInfo: null,
+ );
_preIsStart = isStart;
- ipInfoNotifier.value = null;
if (cancelToken != null) {
cancelToken!.cancel();
- _preIsStart = null;
- timeoutNotifier.value == false;
cancelToken = null;
}
cancelToken = CancelToken();
- final ipInfo = await request.checkIp(cancelToken: cancelToken);
- if (ipInfo == null) {
- timeoutNotifier.value = true;
- return;
+ try {
+ final ipInfo = await request.checkIp(cancelToken: cancelToken);
+ networkDetectionState.value = networkDetectionState.value.copyWith(
+ isTesting: false,
+ ipInfo: ipInfo,
+ );
+ } catch (_) {
+
}
- timeoutNotifier.value = false;
- ipInfoNotifier.value = ipInfo;
}
_checkIpContainer(Widget child) {
@@ -63,8 +69,7 @@ class _NetworkDetectionState extends State {
@override
void dispose() {
super.dispose();
- ipInfoNotifier.dispose();
- timeoutNotifier.dispose();
+ networkDetectionState.dispose();
}
String countryCodeToEmoji(String countryCode) {
@@ -81,9 +86,11 @@ class _NetworkDetectionState extends State {
Widget build(BuildContext context) {
_checkIpDebounce ??= debounce(_checkIp);
return _checkIpContainer(
- ValueListenableBuilder(
- valueListenable: ipInfoNotifier,
- builder: (_, ipInfo, __) {
+ ValueListenableBuilder(
+ valueListenable: networkDetectionState,
+ builder: (_, state, __) {
+ final ipInfo = state.ipInfo;
+ final isTesting = state.isTesting;
return CommonCard(
onPressed: () {},
child: Column(
@@ -104,46 +111,38 @@ class _NetworkDetectionState extends State {
Flexible(
flex: 1,
child: FadeBox(
- child: ipInfo != null
- ? Container(
- alignment: Alignment.centerLeft,
- height: globalState.appController.measure
- .titleMediumHeight,
- child: Text(
- countryCodeToEmoji(ipInfo.countryCode),
- style: Theme.of(context)
- .textTheme
- .titleLarge
- ?.copyWith(
- fontFamily: "Twemoji",
- ),
- ),
+ child: isTesting
+ ? Text(
+ appLocalizations.checking,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style:
+ Theme.of(context).textTheme.titleMedium,
)
- : ValueListenableBuilder(
- valueListenable: timeoutNotifier,
- builder: (_, timeout, __) {
- if (timeout) {
- return Text(
- appLocalizations.checkError,
- style: Theme.of(context)
- .textTheme
- .titleMedium,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- );
- }
- return TooltipText(
- text: Text(
- appLocalizations.checking,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
+ : ipInfo != null
+ ? Container(
+ alignment: Alignment.centerLeft,
+ height: globalState.appController
+ .measure.titleMediumHeight,
+ child: Text(
+ countryCodeToEmoji(
+ ipInfo.countryCode),
style: Theme.of(context)
.textTheme
- .titleMedium,
+ .titleLarge
+ ?.copyWith(
+ fontFamily: "Twemoji",
+ ),
),
- );
- },
- ),
+ )
+ : Text(
+ appLocalizations.checkError,
+ style: Theme.of(context)
+ .textTheme
+ .titleMedium,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
),
),
],
@@ -176,28 +175,24 @@ class _NetworkDetectionState extends State {
),
],
)
- : ValueListenableBuilder(
- valueListenable: timeoutNotifier,
- builder: (_, timeout, __) {
- if (timeout) {
- return Text(
- "timeout",
- style: context.textTheme.titleLarge
- ?.copyWith(color: Colors.red)
- .toSoftBold
- .toMinus,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- );
- }
- return Container(
- padding: const EdgeInsets.all(2),
- child: const AspectRatio(
- aspectRatio: 1,
- child: CircularProgressIndicator(),
- ),
- );
- },
+ : FadeBox(
+ child: isTesting == false && ipInfo == null
+ ? Text(
+ "timeout",
+ style: context.textTheme.titleLarge
+ ?.copyWith(color: Colors.red)
+ .toSoftBold
+ .toMinus,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ )
+ : Container(
+ padding: const EdgeInsets.all(2),
+ child: const AspectRatio(
+ aspectRatio: 1,
+ child: CircularProgressIndicator(),
+ ),
+ ),
),
),
)
diff --git a/lib/fragments/dashboard/network_speed.dart b/lib/fragments/dashboard/network_speed.dart
index 28f60cb3..dca5d562 100644
--- a/lib/fragments/dashboard/network_speed.dart
+++ b/lib/fragments/dashboard/network_speed.dart
@@ -114,7 +114,7 @@ class _NetworkSpeedState extends State {
onPressed: () {},
info: Info(
label: appLocalizations.networkSpeed,
- iconData: Icons.speed,
+ iconData: Icons.speed_sharp,
),
child: Selector>(
selector: (_, appState) => appState.traffics,
diff --git a/lib/fragments/dashboard/outbound_mode.dart b/lib/fragments/dashboard/outbound_mode.dart
index 7cfc6ab9..e09a4af0 100644
--- a/lib/fragments/dashboard/outbound_mode.dart
+++ b/lib/fragments/dashboard/outbound_mode.dart
@@ -15,7 +15,6 @@ class OutboundMode extends StatelessWidget {
final clashConfig = appController.clashConfig;
if (value == null || clashConfig.mode == value) return;
clashConfig.mode = value;
- await appController.updateClashConfig();
appController.addCheckIpNumDebounce();
}
@@ -28,7 +27,7 @@ class OutboundMode extends StatelessWidget {
onPressed: () {},
info: Info(
label: appLocalizations.outboundMode,
- iconData: Icons.call_split,
+ iconData: Icons.call_split_sharp,
),
child: Padding(
padding: const EdgeInsets.only(bottom: 16),
diff --git a/lib/fragments/dashboard/start_button.dart b/lib/fragments/dashboard/start_button.dart
index 66f926c0..2bb0b5d2 100644
--- a/lib/fragments/dashboard/start_button.dart
+++ b/lib/fragments/dashboard/start_button.dart
@@ -37,7 +37,7 @@ class _StartButtonState extends State
if (isStart == appController.appState.isStart) {
isStart = !isStart;
updateController();
- appController.updateSystemProxy(isStart);
+ appController.updateStatus(isStart);
}
}
@@ -53,7 +53,7 @@ class _StartButtonState extends State
return Selector(
selector: (_, appState) => appState.isStart,
builder: (_, isStart, child) {
- if(isStart != this.isStart){
+ if (isStart != this.isStart) {
this.isStart = isStart;
updateController();
}
diff --git a/lib/fragments/dashboard/status_switch.dart b/lib/fragments/dashboard/status_switch.dart
new file mode 100644
index 00000000..2e107664
--- /dev/null
+++ b/lib/fragments/dashboard/status_switch.dart
@@ -0,0 +1,121 @@
+import 'package:fl_clash/common/app_localizations.dart';
+import 'package:fl_clash/models/models.dart';
+import 'package:fl_clash/state.dart';
+import 'package:fl_clash/widgets/widgets.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+class VPNSwitch extends StatelessWidget {
+ const VPNSwitch({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return SwitchContainer(
+ info: const Info(
+ label: "VPN",
+ iconData: Icons.stacked_line_chart,
+ ),
+ child: Selector(
+ selector: (_, config) => config.vpnProps.enable,
+ builder: (_, enable, __) {
+ return Switch(
+ materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ value: enable,
+ onChanged: (value) {
+ final config = globalState.appController.config;
+ config.vpnProps = config.vpnProps.copyWith(
+ enable: value,
+ );
+ },
+ );
+ },
+ ),
+ );
+ }
+}
+
+class TUNSwitch extends StatelessWidget {
+ const TUNSwitch({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return SwitchContainer(
+ info: Info(
+ label: appLocalizations.tun,
+ iconData: Icons.stacked_line_chart,
+ ),
+ child: Selector(
+ selector: (_, clashConfig) => clashConfig.tun.enable,
+ builder: (_, enable, __) {
+ return Switch(
+ value: enable,
+ onChanged: (value) {
+ final clashConfig = globalState.appController.clashConfig;
+ clashConfig.tun = clashConfig.tun.copyWith(
+ enable: value,
+ );
+ },
+ );
+ },
+ ),
+ );
+ }
+}
+
+class ProxySwitch extends StatelessWidget {
+ const ProxySwitch({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return SwitchContainer(
+ info: Info(
+ label: appLocalizations.systemProxy,
+ iconData: Icons.shuffle,
+ ),
+ child: Selector(
+ selector: (_, config) => config.desktopProps.systemProxy,
+ builder: (_, systemProxy, __) {
+ return Switch(
+ materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ value: systemProxy,
+ onChanged: (value) {
+ final config = globalState.appController.config;
+ config.desktopProps =
+ config.desktopProps.copyWith(systemProxy: value);
+ },
+ );
+ },
+ ),
+ );
+ }
+}
+
+class SwitchContainer extends StatelessWidget {
+ final Info info;
+ final Widget child;
+
+ const SwitchContainer({
+ super.key,
+ required this.info,
+ required this.child,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return CommonCard(
+ onPressed: () {},
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ InfoHeader(
+ info: info,
+ actions: [
+ child,
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/fragments/resources.dart b/lib/fragments/resources.dart
index 86a9e99d..98e94e1f 100644
--- a/lib/fragments/resources.dart
+++ b/lib/fragments/resources.dart
@@ -91,7 +91,6 @@ class _GeoDataListItemState extends State {
final appController = globalState.appController;
appController.clashConfig.geoXUrl =
Map.from(appController.clashConfig.geoXUrl)..[geoItem.key] = newUrl;
- appController.updateClashConfigDebounce();
} catch (e) {
globalState.showMessage(
title: geoItem.label,
diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb
index c2578064..3ff46e31 100644
--- a/lib/l10n/arb/intl_en.arb
+++ b/lib/l10n/arb/intl_en.arb
@@ -37,7 +37,7 @@
"overrideDesc": "Override Proxy related config",
"allowLan": "AllowLan",
"allowLanDesc": "Allow access proxy through the LAN",
- "tun": "TUN mode",
+ "tun": "TUN",
"tunDesc": "only effective in administrator mode",
"minimizeOnExit": "Minimize on exit",
"minimizeOnExitDesc": "Modify the default system exit event",
@@ -117,7 +117,7 @@
"logLevel": "LogLevel",
"show": "Show",
"exit": "Exit",
- "systemProxy": "SystemProxy",
+ "systemProxy": "System proxy",
"project": "Project",
"core": "Core",
"tabAnimation": "Tab animation",
diff --git a/lib/l10n/arb/intl_zh_CN.arb b/lib/l10n/arb/intl_zh_CN.arb
index 4cec5c18..e59e8b67 100644
--- a/lib/l10n/arb/intl_zh_CN.arb
+++ b/lib/l10n/arb/intl_zh_CN.arb
@@ -37,7 +37,7 @@
"overrideDesc": "覆写代理相关配置",
"allowLan": "局域网代理",
"allowLanDesc": "允许通过局域网访问代理",
- "tun": "TUN模式",
+ "tun": "虚拟网卡",
"tunDesc": "仅在管理员模式生效",
"minimizeOnExit": "退出时最小化",
"minimizeOnExitDesc": "修改系统默认退出事件",
diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart
index aff6aac0..f1096fb7 100644
--- a/lib/l10n/intl/messages_en.dart
+++ b/lib/l10n/intl/messages_en.dart
@@ -321,7 +321,7 @@ class MessageLookup extends MessageLookupByLibrary {
"style": MessageLookupByLibrary.simpleMessage("Style"),
"submit": MessageLookupByLibrary.simpleMessage("Submit"),
"sync": MessageLookupByLibrary.simpleMessage("Sync"),
- "systemProxy": MessageLookupByLibrary.simpleMessage("SystemProxy"),
+ "systemProxy": MessageLookupByLibrary.simpleMessage("System proxy"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage(
"Attach HTTP proxy to VpnService"),
"tab": MessageLookupByLibrary.simpleMessage("Tab"),
@@ -343,7 +343,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tip": MessageLookupByLibrary.simpleMessage("tip"),
"tools": MessageLookupByLibrary.simpleMessage("Tools"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"),
- "tun": MessageLookupByLibrary.simpleMessage("TUN mode"),
+ "tun": MessageLookupByLibrary.simpleMessage("TUN"),
"tunDesc": MessageLookupByLibrary.simpleMessage(
"only effective in administrator mode"),
"twoColumns": MessageLookupByLibrary.simpleMessage("Two columns"),
diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart
index c7d58030..73461d78 100644
--- a/lib/l10n/intl/messages_zh_CN.dart
+++ b/lib/l10n/intl/messages_zh_CN.dart
@@ -278,7 +278,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tip": MessageLookupByLibrary.simpleMessage("提示"),
"tools": MessageLookupByLibrary.simpleMessage("工具"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"),
- "tun": MessageLookupByLibrary.simpleMessage("TUN模式"),
+ "tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),
"tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"),
"twoColumns": MessageLookupByLibrary.simpleMessage("两列"),
"unableToUpdateCurrentProfileDesc":
diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart
index 0a0476b4..d42b7328 100644
--- a/lib/l10n/l10n.dart
+++ b/lib/l10n/l10n.dart
@@ -430,10 +430,10 @@ class AppLocalizations {
);
}
- /// `TUN mode`
+ /// `TUN`
String get tun {
return Intl.message(
- 'TUN mode',
+ 'TUN',
name: 'tun',
desc: '',
args: [],
@@ -1230,10 +1230,10 @@ class AppLocalizations {
);
}
- /// `SystemProxy`
+ /// `System proxy`
String get systemProxy {
return Intl.message(
- 'SystemProxy',
+ 'System proxy',
name: 'systemProxy',
desc: '',
args: [],
diff --git a/lib/main.dart b/lib/main.dart
index 5879be1b..4c2b1660 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -3,8 +3,8 @@ import 'dart:io';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/plugins/app.dart';
-import 'package:fl_clash/plugins/proxy.dart';
import 'package:fl_clash/plugins/tile.dart';
+import 'package:fl_clash/plugins/vpn.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
@@ -61,14 +61,14 @@ Future vpnService() async {
clashConfig: clashConfig,
);
- proxy?.setServiceMessageHandler(
+ vpn?.setServiceMessageHandler(
ServiceMessageHandler(
onProtect: (Fd fd) async {
- await proxy?.setProtect(fd.value);
+ await vpn?.setProtect(fd.value);
clashCore.setFdMap(fd.id);
},
onProcess: (Process process) async {
- var packageName = await app?.resolverProcess(process);
+ final packageName = await app?.resolverProcess(process);
clashCore.setProcessMap(
ProcessMapItem(
id: process.id,
@@ -76,8 +76,8 @@ Future vpnService() async {
),
);
},
- onStarted: (String runTime) {
- globalState.applyProfile(
+ onStarted: (String runTime) async {
+ await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
@@ -100,8 +100,7 @@ Future vpnService() async {
WidgetsBinding.instance.platformDispatcher.locale,
);
await app?.tip(appLocalizations.startVpn);
- await globalState.startSystemProxy(
- appState: appState,
+ await globalState.handleStart(
config: config,
clashConfig: clashConfig,
);
@@ -110,7 +109,7 @@ Future vpnService() async {
TileListenerWithVpn(
onStop: () async {
await app?.tip(appLocalizations.stopVpn);
- await globalState.stopSystemProxy();
+ await globalState.handleStop();
clashCore.shutdown();
exit(0);
},
diff --git a/lib/models/config.dart b/lib/models/config.dart
index 1caeb985..1a845872 100644
--- a/lib/models/config.dart
+++ b/lib/models/config.dart
@@ -38,6 +38,7 @@ class CoreState with _$CoreState {
const factory CoreState({
AccessControl? accessControl,
required String currentProfileName,
+ required bool enable,
required bool allowBypass,
required bool systemProxy,
required int mixedPort,
@@ -58,10 +59,30 @@ class WindowProps with _$WindowProps {
}) = _WindowProps;
factory WindowProps.fromJson(Map? json) =>
- json == null ? defaultWindowProps : _$WindowPropsFromJson(json);
+ json == null ? const WindowProps() : _$WindowPropsFromJson(json);
}
-const defaultWindowProps = WindowProps();
+@freezed
+class VpnProps with _$VpnProps {
+ const factory VpnProps({
+ @Default(true) bool enable,
+ @Default(false) bool systemProxy,
+ @Default(true) bool allowBypass,
+ }) = _VpnProps;
+
+ factory VpnProps.fromJson(Map? json) =>
+ json == null ? const VpnProps() : _$VpnPropsFromJson(json);
+}
+
+@freezed
+class DesktopProps with _$DesktopProps {
+ const factory DesktopProps({
+ @Default(true) bool systemProxy,
+ }) = _DesktopProps;
+
+ factory DesktopProps.fromJson(Map? json) =>
+ json == null ? const DesktopProps() : _$DesktopPropsFromJson(json);
+}
@JsonSerializable()
class Config extends ChangeNotifier {
@@ -81,8 +102,6 @@ class Config extends ChangeNotifier {
AccessControl _accessControl;
bool _isAnimateToPage;
bool _autoCheckUpdate;
- bool _allowBypass;
- bool _systemProxy;
bool _isExclude;
DAV? _dav;
bool _isCloseConnections;
@@ -93,6 +112,9 @@ class Config extends ChangeNotifier {
WindowProps _windowProps;
bool _onlyProxy;
bool _prueBlack;
+ VpnProps _vpnProps;
+ DesktopProps _desktopProps;
+ bool _showLabel;
Config()
: _profiles = [],
@@ -108,18 +130,19 @@ class Config extends ChangeNotifier {
_isMinimizeOnExit = true,
_isAccessControl = false,
_autoCheckUpdate = true,
- _systemProxy = false,
_testUrl = defaultTestUrl,
_accessControl = const AccessControl(),
_isAnimateToPage = true,
- _allowBypass = true,
_isExclude = false,
_proxyCardType = ProxyCardType.expand,
- _windowProps = defaultWindowProps,
+ _windowProps = const WindowProps(),
_proxiesType = ProxiesType.tab,
_prueBlack = false,
_onlyProxy = false,
- _proxiesLayout = ProxiesLayout.standard;
+ _proxiesLayout = ProxiesLayout.standard,
+ _vpnProps = const VpnProps(),
+ _desktopProps = const DesktopProps(),
+ _showLabel = false;
deleteProfileById(String id) {
_profiles = profiles.where((element) => element.id != id).toList();
@@ -409,30 +432,6 @@ class Config extends ChangeNotifier {
}
}
- @JsonKey(defaultValue: true)
- bool get allowBypass {
- return _allowBypass;
- }
-
- set allowBypass(bool value) {
- if (_allowBypass != value) {
- _allowBypass = value;
- notifyListeners();
- }
- }
-
- @JsonKey(defaultValue: false)
- bool get systemProxy {
- return _systemProxy;
- }
-
- set systemProxy(bool value) {
- if (_systemProxy != value) {
- _systemProxy = value;
- notifyListeners();
- }
- }
-
@JsonKey(defaultValue: false)
bool get onlyProxy {
return _onlyProxy;
@@ -521,6 +520,33 @@ class Config extends ChangeNotifier {
}
}
+ VpnProps get vpnProps => _vpnProps;
+
+ set vpnProps(VpnProps value) {
+ if (_vpnProps != value) {
+ _vpnProps = value;
+ notifyListeners();
+ }
+ }
+
+ DesktopProps get desktopProps => _desktopProps;
+
+ set desktopProps(DesktopProps value) {
+ if (_desktopProps != value) {
+ _desktopProps = value;
+ notifyListeners();
+ }
+ }
+
+ bool get showLabel => _showLabel;
+
+ set showLabel(bool value) {
+ if (_showLabel != value) {
+ _showLabel = value;
+ notifyListeners();
+ }
+ }
+
update([
Config? config,
RecoveryOption recoveryOptions = RecoveryOption.all,
@@ -545,7 +571,6 @@ class Config extends ChangeNotifier {
_openLog = config._openLog;
_themeMode = config._themeMode;
_locale = config._locale;
- _allowBypass = config._allowBypass;
_primaryColor = config._primaryColor;
_proxiesSortType = config._proxiesSortType;
_isMinimizeOnExit = config._isMinimizeOnExit;
@@ -557,6 +582,8 @@ class Config extends ChangeNotifier {
_testUrl = config._testUrl;
_isExclude = config._isExclude;
_windowProps = config._windowProps;
+ _vpnProps = config._vpnProps;
+ _desktopProps = config._desktopProps;
}
notifyListeners();
}
diff --git a/lib/models/generated/config.freezed.dart b/lib/models/generated/config.freezed.dart
index 8eb42c9f..abe8fdc3 100644
--- a/lib/models/generated/config.freezed.dart
+++ b/lib/models/generated/config.freezed.dart
@@ -270,6 +270,7 @@ CoreState _$CoreStateFromJson(Map json) {
mixin _$CoreState {
AccessControl? get accessControl => throw _privateConstructorUsedError;
String get currentProfileName => throw _privateConstructorUsedError;
+ bool get enable => throw _privateConstructorUsedError;
bool get allowBypass => throw _privateConstructorUsedError;
bool get systemProxy => throw _privateConstructorUsedError;
int get mixedPort => throw _privateConstructorUsedError;
@@ -289,6 +290,7 @@ abstract class $CoreStateCopyWith<$Res> {
$Res call(
{AccessControl? accessControl,
String currentProfileName,
+ bool enable,
bool allowBypass,
bool systemProxy,
int mixedPort,
@@ -312,6 +314,7 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
$Res call({
Object? accessControl = freezed,
Object? currentProfileName = null,
+ Object? enable = null,
Object? allowBypass = null,
Object? systemProxy = null,
Object? mixedPort = null,
@@ -326,6 +329,10 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
? _value.currentProfileName
: currentProfileName // ignore: cast_nullable_to_non_nullable
as String,
+ enable: null == enable
+ ? _value.enable
+ : enable // ignore: cast_nullable_to_non_nullable
+ as bool,
allowBypass: null == allowBypass
? _value.allowBypass
: allowBypass // ignore: cast_nullable_to_non_nullable
@@ -369,6 +376,7 @@ abstract class _$$CoreStateImplCopyWith<$Res>
$Res call(
{AccessControl? accessControl,
String currentProfileName,
+ bool enable,
bool allowBypass,
bool systemProxy,
int mixedPort,
@@ -391,6 +399,7 @@ class __$$CoreStateImplCopyWithImpl<$Res>
$Res call({
Object? accessControl = freezed,
Object? currentProfileName = null,
+ Object? enable = null,
Object? allowBypass = null,
Object? systemProxy = null,
Object? mixedPort = null,
@@ -405,6 +414,10 @@ class __$$CoreStateImplCopyWithImpl<$Res>
? _value.currentProfileName
: currentProfileName // ignore: cast_nullable_to_non_nullable
as String,
+ enable: null == enable
+ ? _value.enable
+ : enable // ignore: cast_nullable_to_non_nullable
+ as bool,
allowBypass: null == allowBypass
? _value.allowBypass
: allowBypass // ignore: cast_nullable_to_non_nullable
@@ -431,6 +444,7 @@ class _$CoreStateImpl implements _CoreState {
const _$CoreStateImpl(
{this.accessControl,
required this.currentProfileName,
+ required this.enable,
required this.allowBypass,
required this.systemProxy,
required this.mixedPort,
@@ -444,6 +458,8 @@ class _$CoreStateImpl implements _CoreState {
@override
final String currentProfileName;
@override
+ final bool enable;
+ @override
final bool allowBypass;
@override
final bool systemProxy;
@@ -454,7 +470,7 @@ class _$CoreStateImpl implements _CoreState {
@override
String toString() {
- return 'CoreState(accessControl: $accessControl, currentProfileName: $currentProfileName, allowBypass: $allowBypass, systemProxy: $systemProxy, mixedPort: $mixedPort, onlyProxy: $onlyProxy)';
+ return 'CoreState(accessControl: $accessControl, currentProfileName: $currentProfileName, enable: $enable, allowBypass: $allowBypass, systemProxy: $systemProxy, mixedPort: $mixedPort, onlyProxy: $onlyProxy)';
}
@override
@@ -466,6 +482,7 @@ class _$CoreStateImpl implements _CoreState {
other.accessControl == accessControl) &&
(identical(other.currentProfileName, currentProfileName) ||
other.currentProfileName == currentProfileName) &&
+ (identical(other.enable, enable) || other.enable == enable) &&
(identical(other.allowBypass, allowBypass) ||
other.allowBypass == allowBypass) &&
(identical(other.systemProxy, systemProxy) ||
@@ -478,8 +495,15 @@ class _$CoreStateImpl implements _CoreState {
@JsonKey(ignore: true)
@override
- int get hashCode => Object.hash(runtimeType, accessControl,
- currentProfileName, allowBypass, systemProxy, mixedPort, onlyProxy);
+ int get hashCode => Object.hash(
+ runtimeType,
+ accessControl,
+ currentProfileName,
+ enable,
+ allowBypass,
+ systemProxy,
+ mixedPort,
+ onlyProxy);
@JsonKey(ignore: true)
@override
@@ -499,6 +523,7 @@ abstract class _CoreState implements CoreState {
const factory _CoreState(
{final AccessControl? accessControl,
required final String currentProfileName,
+ required final bool enable,
required final bool allowBypass,
required final bool systemProxy,
required final int mixedPort,
@@ -512,6 +537,8 @@ abstract class _CoreState implements CoreState {
@override
String get currentProfileName;
@override
+ bool get enable;
+ @override
bool get allowBypass;
@override
bool get systemProxy;
@@ -715,3 +742,318 @@ abstract class _WindowProps implements WindowProps {
_$$WindowPropsImplCopyWith<_$WindowPropsImpl> get copyWith =>
throw _privateConstructorUsedError;
}
+
+VpnProps _$VpnPropsFromJson(Map json) {
+ return _VpnProps.fromJson(json);
+}
+
+/// @nodoc
+mixin _$VpnProps {
+ bool get enable => throw _privateConstructorUsedError;
+ bool get systemProxy => throw _privateConstructorUsedError;
+ bool get allowBypass => throw _privateConstructorUsedError;
+
+ Map toJson() => throw _privateConstructorUsedError;
+ @JsonKey(ignore: true)
+ $VpnPropsCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $VpnPropsCopyWith<$Res> {
+ factory $VpnPropsCopyWith(VpnProps value, $Res Function(VpnProps) then) =
+ _$VpnPropsCopyWithImpl<$Res, VpnProps>;
+ @useResult
+ $Res call({bool enable, bool systemProxy, bool allowBypass});
+}
+
+/// @nodoc
+class _$VpnPropsCopyWithImpl<$Res, $Val extends VpnProps>
+ implements $VpnPropsCopyWith<$Res> {
+ _$VpnPropsCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? enable = null,
+ Object? systemProxy = null,
+ Object? allowBypass = null,
+ }) {
+ return _then(_value.copyWith(
+ enable: null == enable
+ ? _value.enable
+ : enable // ignore: cast_nullable_to_non_nullable
+ as bool,
+ systemProxy: null == systemProxy
+ ? _value.systemProxy
+ : systemProxy // ignore: cast_nullable_to_non_nullable
+ as bool,
+ allowBypass: null == allowBypass
+ ? _value.allowBypass
+ : allowBypass // ignore: cast_nullable_to_non_nullable
+ as bool,
+ ) as $Val);
+ }
+}
+
+/// @nodoc
+abstract class _$$VpnPropsImplCopyWith<$Res>
+ implements $VpnPropsCopyWith<$Res> {
+ factory _$$VpnPropsImplCopyWith(
+ _$VpnPropsImpl value, $Res Function(_$VpnPropsImpl) then) =
+ __$$VpnPropsImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({bool enable, bool systemProxy, bool allowBypass});
+}
+
+/// @nodoc
+class __$$VpnPropsImplCopyWithImpl<$Res>
+ extends _$VpnPropsCopyWithImpl<$Res, _$VpnPropsImpl>
+ implements _$$VpnPropsImplCopyWith<$Res> {
+ __$$VpnPropsImplCopyWithImpl(
+ _$VpnPropsImpl _value, $Res Function(_$VpnPropsImpl) _then)
+ : super(_value, _then);
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? enable = null,
+ Object? systemProxy = null,
+ Object? allowBypass = null,
+ }) {
+ return _then(_$VpnPropsImpl(
+ enable: null == enable
+ ? _value.enable
+ : enable // ignore: cast_nullable_to_non_nullable
+ as bool,
+ systemProxy: null == systemProxy
+ ? _value.systemProxy
+ : systemProxy // ignore: cast_nullable_to_non_nullable
+ as bool,
+ allowBypass: null == allowBypass
+ ? _value.allowBypass
+ : allowBypass // ignore: cast_nullable_to_non_nullable
+ as bool,
+ ));
+ }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$VpnPropsImpl implements _VpnProps {
+ const _$VpnPropsImpl(
+ {this.enable = true, this.systemProxy = false, this.allowBypass = true});
+
+ factory _$VpnPropsImpl.fromJson(Map json) =>
+ _$$VpnPropsImplFromJson(json);
+
+ @override
+ @JsonKey()
+ final bool enable;
+ @override
+ @JsonKey()
+ final bool systemProxy;
+ @override
+ @JsonKey()
+ final bool allowBypass;
+
+ @override
+ String toString() {
+ return 'VpnProps(enable: $enable, systemProxy: $systemProxy, allowBypass: $allowBypass)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$VpnPropsImpl &&
+ (identical(other.enable, enable) || other.enable == enable) &&
+ (identical(other.systemProxy, systemProxy) ||
+ other.systemProxy == systemProxy) &&
+ (identical(other.allowBypass, allowBypass) ||
+ other.allowBypass == allowBypass));
+ }
+
+ @JsonKey(ignore: true)
+ @override
+ int get hashCode =>
+ Object.hash(runtimeType, enable, systemProxy, allowBypass);
+
+ @JsonKey(ignore: true)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$VpnPropsImplCopyWith<_$VpnPropsImpl> get copyWith =>
+ __$$VpnPropsImplCopyWithImpl<_$VpnPropsImpl>(this, _$identity);
+
+ @override
+ Map toJson() {
+ return _$$VpnPropsImplToJson(
+ this,
+ );
+ }
+}
+
+abstract class _VpnProps implements VpnProps {
+ const factory _VpnProps(
+ {final bool enable,
+ final bool systemProxy,
+ final bool allowBypass}) = _$VpnPropsImpl;
+
+ factory _VpnProps.fromJson(Map json) =
+ _$VpnPropsImpl.fromJson;
+
+ @override
+ bool get enable;
+ @override
+ bool get systemProxy;
+ @override
+ bool get allowBypass;
+ @override
+ @JsonKey(ignore: true)
+ _$$VpnPropsImplCopyWith<_$VpnPropsImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+DesktopProps _$DesktopPropsFromJson(Map json) {
+ return _DesktopProps.fromJson(json);
+}
+
+/// @nodoc
+mixin _$DesktopProps {
+ bool get systemProxy => throw _privateConstructorUsedError;
+
+ Map toJson() => throw _privateConstructorUsedError;
+ @JsonKey(ignore: true)
+ $DesktopPropsCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $DesktopPropsCopyWith<$Res> {
+ factory $DesktopPropsCopyWith(
+ DesktopProps value, $Res Function(DesktopProps) then) =
+ _$DesktopPropsCopyWithImpl<$Res, DesktopProps>;
+ @useResult
+ $Res call({bool systemProxy});
+}
+
+/// @nodoc
+class _$DesktopPropsCopyWithImpl<$Res, $Val extends DesktopProps>
+ implements $DesktopPropsCopyWith<$Res> {
+ _$DesktopPropsCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? systemProxy = null,
+ }) {
+ return _then(_value.copyWith(
+ systemProxy: null == systemProxy
+ ? _value.systemProxy
+ : systemProxy // ignore: cast_nullable_to_non_nullable
+ as bool,
+ ) as $Val);
+ }
+}
+
+/// @nodoc
+abstract class _$$DesktopPropsImplCopyWith<$Res>
+ implements $DesktopPropsCopyWith<$Res> {
+ factory _$$DesktopPropsImplCopyWith(
+ _$DesktopPropsImpl value, $Res Function(_$DesktopPropsImpl) then) =
+ __$$DesktopPropsImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({bool systemProxy});
+}
+
+/// @nodoc
+class __$$DesktopPropsImplCopyWithImpl<$Res>
+ extends _$DesktopPropsCopyWithImpl<$Res, _$DesktopPropsImpl>
+ implements _$$DesktopPropsImplCopyWith<$Res> {
+ __$$DesktopPropsImplCopyWithImpl(
+ _$DesktopPropsImpl _value, $Res Function(_$DesktopPropsImpl) _then)
+ : super(_value, _then);
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? systemProxy = null,
+ }) {
+ return _then(_$DesktopPropsImpl(
+ systemProxy: null == systemProxy
+ ? _value.systemProxy
+ : systemProxy // ignore: cast_nullable_to_non_nullable
+ as bool,
+ ));
+ }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$DesktopPropsImpl implements _DesktopProps {
+ const _$DesktopPropsImpl({this.systemProxy = true});
+
+ factory _$DesktopPropsImpl.fromJson(Map json) =>
+ _$$DesktopPropsImplFromJson(json);
+
+ @override
+ @JsonKey()
+ final bool systemProxy;
+
+ @override
+ String toString() {
+ return 'DesktopProps(systemProxy: $systemProxy)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$DesktopPropsImpl &&
+ (identical(other.systemProxy, systemProxy) ||
+ other.systemProxy == systemProxy));
+ }
+
+ @JsonKey(ignore: true)
+ @override
+ int get hashCode => Object.hash(runtimeType, systemProxy);
+
+ @JsonKey(ignore: true)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$DesktopPropsImplCopyWith<_$DesktopPropsImpl> get copyWith =>
+ __$$DesktopPropsImplCopyWithImpl<_$DesktopPropsImpl>(this, _$identity);
+
+ @override
+ Map toJson() {
+ return _$$DesktopPropsImplToJson(
+ this,
+ );
+ }
+}
+
+abstract class _DesktopProps implements DesktopProps {
+ const factory _DesktopProps({final bool systemProxy}) = _$DesktopPropsImpl;
+
+ factory _DesktopProps.fromJson(Map json) =
+ _$DesktopPropsImpl.fromJson;
+
+ @override
+ bool get systemProxy;
+ @override
+ @JsonKey(ignore: true)
+ _$$DesktopPropsImplCopyWith<_$DesktopPropsImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart
index 621c9141..ca3b4137 100644
--- a/lib/models/generated/config.g.dart
+++ b/lib/models/generated/config.g.dart
@@ -36,8 +36,6 @@ Config _$ConfigFromJson(Map json) => Config()
..isAnimateToPage = json['isAnimateToPage'] as bool? ?? true
..isCompatible = json['isCompatible'] as bool? ?? true
..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true
- ..allowBypass = json['allowBypass'] as bool? ?? true
- ..systemProxy = json['systemProxy'] as bool? ?? false
..onlyProxy = json['onlyProxy'] as bool? ?? false
..prueBlack = json['prueBlack'] as bool? ?? false
..isCloseConnections = json['isCloseConnections'] as bool? ?? false
@@ -51,7 +49,10 @@ Config _$ConfigFromJson(Map json) => Config()
json['test-url'] as String? ?? 'https://www.gstatic.com/generate_204'
..isExclude = json['isExclude'] as bool? ?? false
..windowProps =
- WindowProps.fromJson(json['windowProps'] as Map?);
+ WindowProps.fromJson(json['windowProps'] as Map?)
+ ..vpnProps = VpnProps.fromJson(json['vpnProps'] as Map?)
+ ..desktopProps =
+ DesktopProps.fromJson(json['desktopProps'] as Map?);
Map _$ConfigToJson(Config instance) => {
'profiles': instance.profiles,
@@ -72,8 +73,6 @@ Map _$ConfigToJson(Config instance) => {
'isAnimateToPage': instance.isAnimateToPage,
'isCompatible': instance.isCompatible,
'autoCheckUpdate': instance.autoCheckUpdate,
- 'allowBypass': instance.allowBypass,
- 'systemProxy': instance.systemProxy,
'onlyProxy': instance.onlyProxy,
'prueBlack': instance.prueBlack,
'isCloseConnections': instance.isCloseConnections,
@@ -82,6 +81,8 @@ Map _$ConfigToJson(Config instance) => {
'test-url': instance.testUrl,
'isExclude': instance.isExclude,
'windowProps': instance.windowProps,
+ 'vpnProps': instance.vpnProps,
+ 'desktopProps': instance.desktopProps,
};
const _$ThemeModeEnumMap = {
@@ -157,6 +158,7 @@ _$CoreStateImpl _$$CoreStateImplFromJson(Map json) =>
: AccessControl.fromJson(
json['accessControl'] as Map),
currentProfileName: json['currentProfileName'] as String,
+ enable: json['enable'] as bool,
allowBypass: json['allowBypass'] as bool,
systemProxy: json['systemProxy'] as bool,
mixedPort: (json['mixedPort'] as num).toInt(),
@@ -167,6 +169,7 @@ Map _$$CoreStateImplToJson(_$CoreStateImpl instance) =>
{
'accessControl': instance.accessControl,
'currentProfileName': instance.currentProfileName,
+ 'enable': instance.enable,
'allowBypass': instance.allowBypass,
'systemProxy': instance.systemProxy,
'mixedPort': instance.mixedPort,
@@ -188,3 +191,27 @@ Map _$$WindowPropsImplToJson(_$WindowPropsImpl instance) =>
'top': instance.top,
'left': instance.left,
};
+
+_$VpnPropsImpl _$$VpnPropsImplFromJson(Map json) =>
+ _$VpnPropsImpl(
+ enable: json['enable'] as bool? ?? true,
+ systemProxy: json['systemProxy'] as bool? ?? false,
+ allowBypass: json['allowBypass'] as bool? ?? true,
+ );
+
+Map _$$VpnPropsImplToJson(_$VpnPropsImpl instance) =>
+ {
+ 'enable': instance.enable,
+ 'systemProxy': instance.systemProxy,
+ 'allowBypass': instance.allowBypass,
+ };
+
+_$DesktopPropsImpl _$$DesktopPropsImplFromJson(Map json) =>
+ _$DesktopPropsImpl(
+ systemProxy: json['systemProxy'] as bool? ?? true,
+ );
+
+Map _$$DesktopPropsImplToJson(_$DesktopPropsImpl instance) =>
+ {
+ 'systemProxy': instance.systemProxy,
+ };
diff --git a/lib/models/generated/selector.freezed.dart b/lib/models/generated/selector.freezed.dart
index f048579b..181082e3 100644
--- a/lib/models/generated/selector.freezed.dart
+++ b/lib/models/generated/selector.freezed.dart
@@ -625,6 +625,147 @@ abstract class _ProfilesSelectorState implements ProfilesSelectorState {
get copyWith => throw _privateConstructorUsedError;
}
+/// @nodoc
+mixin _$NetworkDetectionState {
+ bool get isTesting => throw _privateConstructorUsedError;
+ IpInfo? get ipInfo => throw _privateConstructorUsedError;
+
+ @JsonKey(ignore: true)
+ $NetworkDetectionStateCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $NetworkDetectionStateCopyWith<$Res> {
+ factory $NetworkDetectionStateCopyWith(NetworkDetectionState value,
+ $Res Function(NetworkDetectionState) then) =
+ _$NetworkDetectionStateCopyWithImpl<$Res, NetworkDetectionState>;
+ @useResult
+ $Res call({bool isTesting, IpInfo? ipInfo});
+}
+
+/// @nodoc
+class _$NetworkDetectionStateCopyWithImpl<$Res,
+ $Val extends NetworkDetectionState>
+ implements $NetworkDetectionStateCopyWith<$Res> {
+ _$NetworkDetectionStateCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? isTesting = null,
+ Object? ipInfo = freezed,
+ }) {
+ return _then(_value.copyWith(
+ isTesting: null == isTesting
+ ? _value.isTesting
+ : isTesting // ignore: cast_nullable_to_non_nullable
+ as bool,
+ ipInfo: freezed == ipInfo
+ ? _value.ipInfo
+ : ipInfo // ignore: cast_nullable_to_non_nullable
+ as IpInfo?,
+ ) as $Val);
+ }
+}
+
+/// @nodoc
+abstract class _$$NetworkDetectionStateImplCopyWith<$Res>
+ implements $NetworkDetectionStateCopyWith<$Res> {
+ factory _$$NetworkDetectionStateImplCopyWith(
+ _$NetworkDetectionStateImpl value,
+ $Res Function(_$NetworkDetectionStateImpl) then) =
+ __$$NetworkDetectionStateImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({bool isTesting, IpInfo? ipInfo});
+}
+
+/// @nodoc
+class __$$NetworkDetectionStateImplCopyWithImpl<$Res>
+ extends _$NetworkDetectionStateCopyWithImpl<$Res,
+ _$NetworkDetectionStateImpl>
+ implements _$$NetworkDetectionStateImplCopyWith<$Res> {
+ __$$NetworkDetectionStateImplCopyWithImpl(_$NetworkDetectionStateImpl _value,
+ $Res Function(_$NetworkDetectionStateImpl) _then)
+ : super(_value, _then);
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? isTesting = null,
+ Object? ipInfo = freezed,
+ }) {
+ return _then(_$NetworkDetectionStateImpl(
+ isTesting: null == isTesting
+ ? _value.isTesting
+ : isTesting // ignore: cast_nullable_to_non_nullable
+ as bool,
+ ipInfo: freezed == ipInfo
+ ? _value.ipInfo
+ : ipInfo // ignore: cast_nullable_to_non_nullable
+ as IpInfo?,
+ ));
+ }
+}
+
+/// @nodoc
+
+class _$NetworkDetectionStateImpl implements _NetworkDetectionState {
+ const _$NetworkDetectionStateImpl(
+ {required this.isTesting, required this.ipInfo});
+
+ @override
+ final bool isTesting;
+ @override
+ final IpInfo? ipInfo;
+
+ @override
+ String toString() {
+ return 'NetworkDetectionState(isTesting: $isTesting, ipInfo: $ipInfo)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$NetworkDetectionStateImpl &&
+ (identical(other.isTesting, isTesting) ||
+ other.isTesting == isTesting) &&
+ (identical(other.ipInfo, ipInfo) || other.ipInfo == ipInfo));
+ }
+
+ @override
+ int get hashCode => Object.hash(runtimeType, isTesting, ipInfo);
+
+ @JsonKey(ignore: true)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$NetworkDetectionStateImplCopyWith<_$NetworkDetectionStateImpl>
+ get copyWith => __$$NetworkDetectionStateImplCopyWithImpl<
+ _$NetworkDetectionStateImpl>(this, _$identity);
+}
+
+abstract class _NetworkDetectionState implements NetworkDetectionState {
+ const factory _NetworkDetectionState(
+ {required final bool isTesting,
+ required final IpInfo? ipInfo}) = _$NetworkDetectionStateImpl;
+
+ @override
+ bool get isTesting;
+ @override
+ IpInfo? get ipInfo;
+ @override
+ @JsonKey(ignore: true)
+ _$$NetworkDetectionStateImplCopyWith<_$NetworkDetectionStateImpl>
+ get copyWith => throw _privateConstructorUsedError;
+}
+
/// @nodoc
mixin _$ApplicationSelectorState {
String? get locale => throw _privateConstructorUsedError;
@@ -2845,3 +2986,772 @@ abstract class _ProxiesActionsState implements ProxiesActionsState {
_$$ProxiesActionsStateImplCopyWith<_$ProxiesActionsStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}
+
+/// @nodoc
+mixin _$AutoLaunchState {
+ bool get isAutoLaunch => throw _privateConstructorUsedError;
+ bool get isOpenTun => throw _privateConstructorUsedError;
+
+ @JsonKey(ignore: true)
+ $AutoLaunchStateCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $AutoLaunchStateCopyWith<$Res> {
+ factory $AutoLaunchStateCopyWith(
+ AutoLaunchState value, $Res Function(AutoLaunchState) then) =
+ _$AutoLaunchStateCopyWithImpl<$Res, AutoLaunchState>;
+ @useResult
+ $Res call({bool isAutoLaunch, bool isOpenTun});
+}
+
+/// @nodoc
+class _$AutoLaunchStateCopyWithImpl<$Res, $Val extends AutoLaunchState>
+ implements $AutoLaunchStateCopyWith<$Res> {
+ _$AutoLaunchStateCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? isAutoLaunch = null,
+ Object? isOpenTun = null,
+ }) {
+ return _then(_value.copyWith(
+ isAutoLaunch: null == isAutoLaunch
+ ? _value.isAutoLaunch
+ : isAutoLaunch // ignore: cast_nullable_to_non_nullable
+ as bool,
+ isOpenTun: null == isOpenTun
+ ? _value.isOpenTun
+ : isOpenTun // ignore: cast_nullable_to_non_nullable
+ as bool,
+ ) as $Val);
+ }
+}
+
+/// @nodoc
+abstract class _$$AutoLaunchStateImplCopyWith<$Res>
+ implements $AutoLaunchStateCopyWith<$Res> {
+ factory _$$AutoLaunchStateImplCopyWith(_$AutoLaunchStateImpl value,
+ $Res Function(_$AutoLaunchStateImpl) then) =
+ __$$AutoLaunchStateImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({bool isAutoLaunch, bool isOpenTun});
+}
+
+/// @nodoc
+class __$$AutoLaunchStateImplCopyWithImpl<$Res>
+ extends _$AutoLaunchStateCopyWithImpl<$Res, _$AutoLaunchStateImpl>
+ implements _$$AutoLaunchStateImplCopyWith<$Res> {
+ __$$AutoLaunchStateImplCopyWithImpl(
+ _$AutoLaunchStateImpl _value, $Res Function(_$AutoLaunchStateImpl) _then)
+ : super(_value, _then);
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? isAutoLaunch = null,
+ Object? isOpenTun = null,
+ }) {
+ return _then(_$AutoLaunchStateImpl(
+ isAutoLaunch: null == isAutoLaunch
+ ? _value.isAutoLaunch
+ : isAutoLaunch // ignore: cast_nullable_to_non_nullable
+ as bool,
+ isOpenTun: null == isOpenTun
+ ? _value.isOpenTun
+ : isOpenTun // ignore: cast_nullable_to_non_nullable
+ as bool,
+ ));
+ }
+}
+
+/// @nodoc
+
+class _$AutoLaunchStateImpl implements _AutoLaunchState {
+ const _$AutoLaunchStateImpl(
+ {required this.isAutoLaunch, required this.isOpenTun});
+
+ @override
+ final bool isAutoLaunch;
+ @override
+ final bool isOpenTun;
+
+ @override
+ String toString() {
+ return 'AutoLaunchState(isAutoLaunch: $isAutoLaunch, isOpenTun: $isOpenTun)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$AutoLaunchStateImpl &&
+ (identical(other.isAutoLaunch, isAutoLaunch) ||
+ other.isAutoLaunch == isAutoLaunch) &&
+ (identical(other.isOpenTun, isOpenTun) ||
+ other.isOpenTun == isOpenTun));
+ }
+
+ @override
+ int get hashCode => Object.hash(runtimeType, isAutoLaunch, isOpenTun);
+
+ @JsonKey(ignore: true)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$AutoLaunchStateImplCopyWith<_$AutoLaunchStateImpl> get copyWith =>
+ __$$AutoLaunchStateImplCopyWithImpl<_$AutoLaunchStateImpl>(
+ this, _$identity);
+}
+
+abstract class _AutoLaunchState implements AutoLaunchState {
+ const factory _AutoLaunchState(
+ {required final bool isAutoLaunch,
+ required final bool isOpenTun}) = _$AutoLaunchStateImpl;
+
+ @override
+ bool get isAutoLaunch;
+ @override
+ bool get isOpenTun;
+ @override
+ @JsonKey(ignore: true)
+ _$$AutoLaunchStateImplCopyWith<_$AutoLaunchStateImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+mixin _$ProxyState {
+ bool get isStart => throw _privateConstructorUsedError;
+ bool get systemProxy => throw _privateConstructorUsedError;
+ int get port => throw _privateConstructorUsedError;
+
+ @JsonKey(ignore: true)
+ $ProxyStateCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $ProxyStateCopyWith<$Res> {
+ factory $ProxyStateCopyWith(
+ ProxyState value, $Res Function(ProxyState) then) =
+ _$ProxyStateCopyWithImpl<$Res, ProxyState>;
+ @useResult
+ $Res call({bool isStart, bool systemProxy, int port});
+}
+
+/// @nodoc
+class _$ProxyStateCopyWithImpl<$Res, $Val extends ProxyState>
+ implements $ProxyStateCopyWith<$Res> {
+ _$ProxyStateCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? isStart = null,
+ Object? systemProxy = null,
+ Object? port = null,
+ }) {
+ return _then(_value.copyWith(
+ isStart: null == isStart
+ ? _value.isStart
+ : isStart // ignore: cast_nullable_to_non_nullable
+ as bool,
+ systemProxy: null == systemProxy
+ ? _value.systemProxy
+ : systemProxy // ignore: cast_nullable_to_non_nullable
+ as bool,
+ port: null == port
+ ? _value.port
+ : port // ignore: cast_nullable_to_non_nullable
+ as int,
+ ) as $Val);
+ }
+}
+
+/// @nodoc
+abstract class _$$ProxyStateImplCopyWith<$Res>
+ implements $ProxyStateCopyWith<$Res> {
+ factory _$$ProxyStateImplCopyWith(
+ _$ProxyStateImpl value, $Res Function(_$ProxyStateImpl) then) =
+ __$$ProxyStateImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({bool isStart, bool systemProxy, int port});
+}
+
+/// @nodoc
+class __$$ProxyStateImplCopyWithImpl<$Res>
+ extends _$ProxyStateCopyWithImpl<$Res, _$ProxyStateImpl>
+ implements _$$ProxyStateImplCopyWith<$Res> {
+ __$$ProxyStateImplCopyWithImpl(
+ _$ProxyStateImpl _value, $Res Function(_$ProxyStateImpl) _then)
+ : super(_value, _then);
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? isStart = null,
+ Object? systemProxy = null,
+ Object? port = null,
+ }) {
+ return _then(_$ProxyStateImpl(
+ isStart: null == isStart
+ ? _value.isStart
+ : isStart // ignore: cast_nullable_to_non_nullable
+ as bool,
+ systemProxy: null == systemProxy
+ ? _value.systemProxy
+ : systemProxy // ignore: cast_nullable_to_non_nullable
+ as bool,
+ port: null == port
+ ? _value.port
+ : port // ignore: cast_nullable_to_non_nullable
+ as int,
+ ));
+ }
+}
+
+/// @nodoc
+
+class _$ProxyStateImpl implements _ProxyState {
+ const _$ProxyStateImpl(
+ {required this.isStart, required this.systemProxy, required this.port});
+
+ @override
+ final bool isStart;
+ @override
+ final bool systemProxy;
+ @override
+ final int port;
+
+ @override
+ String toString() {
+ return 'ProxyState(isStart: $isStart, systemProxy: $systemProxy, port: $port)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$ProxyStateImpl &&
+ (identical(other.isStart, isStart) || other.isStart == isStart) &&
+ (identical(other.systemProxy, systemProxy) ||
+ other.systemProxy == systemProxy) &&
+ (identical(other.port, port) || other.port == port));
+ }
+
+ @override
+ int get hashCode => Object.hash(runtimeType, isStart, systemProxy, port);
+
+ @JsonKey(ignore: true)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$ProxyStateImplCopyWith<_$ProxyStateImpl> get copyWith =>
+ __$$ProxyStateImplCopyWithImpl<_$ProxyStateImpl>(this, _$identity);
+}
+
+abstract class _ProxyState implements ProxyState {
+ const factory _ProxyState(
+ {required final bool isStart,
+ required final bool systemProxy,
+ required final int port}) = _$ProxyStateImpl;
+
+ @override
+ bool get isStart;
+ @override
+ bool get systemProxy;
+ @override
+ int get port;
+ @override
+ @JsonKey(ignore: true)
+ _$$ProxyStateImplCopyWith<_$ProxyStateImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+mixin _$ClashConfigState {
+ int get mixedPort => throw _privateConstructorUsedError;
+ bool get allowLan => throw _privateConstructorUsedError;
+ bool get ipv6 => throw _privateConstructorUsedError;
+ String get geodataLoader => throw _privateConstructorUsedError;
+ LogLevel get logLevel => throw _privateConstructorUsedError;
+ String get externalController => throw _privateConstructorUsedError;
+ Mode get mode => throw _privateConstructorUsedError;
+ FindProcessMode get findProcessMode => throw _privateConstructorUsedError;
+ int get keepAliveInterval => throw _privateConstructorUsedError;
+ bool get unifiedDelay => throw _privateConstructorUsedError;
+ bool get tcpConcurrent => throw _privateConstructorUsedError;
+ Tun get tun => throw _privateConstructorUsedError;
+ Dns get dns => throw _privateConstructorUsedError;
+ Map get geoXUrl => throw _privateConstructorUsedError;
+ List get rules => throw _privateConstructorUsedError;
+ String? get globalRealUa => throw _privateConstructorUsedError;
+
+ @JsonKey(ignore: true)
+ $ClashConfigStateCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $ClashConfigStateCopyWith<$Res> {
+ factory $ClashConfigStateCopyWith(
+ ClashConfigState value, $Res Function(ClashConfigState) then) =
+ _$ClashConfigStateCopyWithImpl<$Res, ClashConfigState>;
+ @useResult
+ $Res call(
+ {int mixedPort,
+ bool allowLan,
+ bool ipv6,
+ String geodataLoader,
+ LogLevel logLevel,
+ String externalController,
+ Mode mode,
+ FindProcessMode findProcessMode,
+ int keepAliveInterval,
+ bool unifiedDelay,
+ bool tcpConcurrent,
+ Tun tun,
+ Dns dns,
+ Map geoXUrl,
+ List rules,
+ String? globalRealUa});
+
+ $TunCopyWith<$Res> get tun;
+}
+
+/// @nodoc
+class _$ClashConfigStateCopyWithImpl<$Res, $Val extends ClashConfigState>
+ implements $ClashConfigStateCopyWith<$Res> {
+ _$ClashConfigStateCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? mixedPort = null,
+ Object? allowLan = null,
+ Object? ipv6 = null,
+ Object? geodataLoader = null,
+ Object? logLevel = null,
+ Object? externalController = null,
+ Object? mode = null,
+ Object? findProcessMode = null,
+ Object? keepAliveInterval = null,
+ Object? unifiedDelay = null,
+ Object? tcpConcurrent = null,
+ Object? tun = null,
+ Object? dns = null,
+ Object? geoXUrl = null,
+ Object? rules = null,
+ Object? globalRealUa = freezed,
+ }) {
+ return _then(_value.copyWith(
+ mixedPort: null == mixedPort
+ ? _value.mixedPort
+ : mixedPort // ignore: cast_nullable_to_non_nullable
+ as int,
+ allowLan: null == allowLan
+ ? _value.allowLan
+ : allowLan // ignore: cast_nullable_to_non_nullable
+ as bool,
+ ipv6: null == ipv6
+ ? _value.ipv6
+ : ipv6 // ignore: cast_nullable_to_non_nullable
+ as bool,
+ geodataLoader: null == geodataLoader
+ ? _value.geodataLoader
+ : geodataLoader // ignore: cast_nullable_to_non_nullable
+ as String,
+ logLevel: null == logLevel
+ ? _value.logLevel
+ : logLevel // ignore: cast_nullable_to_non_nullable
+ as LogLevel,
+ externalController: null == externalController
+ ? _value.externalController
+ : externalController // ignore: cast_nullable_to_non_nullable
+ as String,
+ mode: null == mode
+ ? _value.mode
+ : mode // ignore: cast_nullable_to_non_nullable
+ as Mode,
+ findProcessMode: null == findProcessMode
+ ? _value.findProcessMode
+ : findProcessMode // ignore: cast_nullable_to_non_nullable
+ as FindProcessMode,
+ keepAliveInterval: null == keepAliveInterval
+ ? _value.keepAliveInterval
+ : keepAliveInterval // ignore: cast_nullable_to_non_nullable
+ as int,
+ unifiedDelay: null == unifiedDelay
+ ? _value.unifiedDelay
+ : unifiedDelay // ignore: cast_nullable_to_non_nullable
+ as bool,
+ tcpConcurrent: null == tcpConcurrent
+ ? _value.tcpConcurrent
+ : tcpConcurrent // ignore: cast_nullable_to_non_nullable
+ as bool,
+ tun: null == tun
+ ? _value.tun
+ : tun // ignore: cast_nullable_to_non_nullable
+ as Tun,
+ dns: null == dns
+ ? _value.dns
+ : dns // ignore: cast_nullable_to_non_nullable
+ as Dns,
+ geoXUrl: null == geoXUrl
+ ? _value.geoXUrl
+ : geoXUrl // ignore: cast_nullable_to_non_nullable
+ as Map,
+ rules: null == rules
+ ? _value.rules
+ : rules // ignore: cast_nullable_to_non_nullable
+ as List,
+ globalRealUa: freezed == globalRealUa
+ ? _value.globalRealUa
+ : globalRealUa // ignore: cast_nullable_to_non_nullable
+ as String?,
+ ) as $Val);
+ }
+
+ @override
+ @pragma('vm:prefer-inline')
+ $TunCopyWith<$Res> get tun {
+ return $TunCopyWith<$Res>(_value.tun, (value) {
+ return _then(_value.copyWith(tun: value) as $Val);
+ });
+ }
+}
+
+/// @nodoc
+abstract class _$$ClashConfigStateImplCopyWith<$Res>
+ implements $ClashConfigStateCopyWith<$Res> {
+ factory _$$ClashConfigStateImplCopyWith(_$ClashConfigStateImpl value,
+ $Res Function(_$ClashConfigStateImpl) then) =
+ __$$ClashConfigStateImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call(
+ {int mixedPort,
+ bool allowLan,
+ bool ipv6,
+ String geodataLoader,
+ LogLevel logLevel,
+ String externalController,
+ Mode mode,
+ FindProcessMode findProcessMode,
+ int keepAliveInterval,
+ bool unifiedDelay,
+ bool tcpConcurrent,
+ Tun tun,
+ Dns dns,
+ Map geoXUrl,
+ List rules,
+ String? globalRealUa});
+
+ @override
+ $TunCopyWith<$Res> get tun;
+}
+
+/// @nodoc
+class __$$ClashConfigStateImplCopyWithImpl<$Res>
+ extends _$ClashConfigStateCopyWithImpl<$Res, _$ClashConfigStateImpl>
+ implements _$$ClashConfigStateImplCopyWith<$Res> {
+ __$$ClashConfigStateImplCopyWithImpl(_$ClashConfigStateImpl _value,
+ $Res Function(_$ClashConfigStateImpl) _then)
+ : super(_value, _then);
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? mixedPort = null,
+ Object? allowLan = null,
+ Object? ipv6 = null,
+ Object? geodataLoader = null,
+ Object? logLevel = null,
+ Object? externalController = null,
+ Object? mode = null,
+ Object? findProcessMode = null,
+ Object? keepAliveInterval = null,
+ Object? unifiedDelay = null,
+ Object? tcpConcurrent = null,
+ Object? tun = null,
+ Object? dns = null,
+ Object? geoXUrl = null,
+ Object? rules = null,
+ Object? globalRealUa = freezed,
+ }) {
+ return _then(_$ClashConfigStateImpl(
+ mixedPort: null == mixedPort
+ ? _value.mixedPort
+ : mixedPort // ignore: cast_nullable_to_non_nullable
+ as int,
+ allowLan: null == allowLan
+ ? _value.allowLan
+ : allowLan // ignore: cast_nullable_to_non_nullable
+ as bool,
+ ipv6: null == ipv6
+ ? _value.ipv6
+ : ipv6 // ignore: cast_nullable_to_non_nullable
+ as bool,
+ geodataLoader: null == geodataLoader
+ ? _value.geodataLoader
+ : geodataLoader // ignore: cast_nullable_to_non_nullable
+ as String,
+ logLevel: null == logLevel
+ ? _value.logLevel
+ : logLevel // ignore: cast_nullable_to_non_nullable
+ as LogLevel,
+ externalController: null == externalController
+ ? _value.externalController
+ : externalController // ignore: cast_nullable_to_non_nullable
+ as String,
+ mode: null == mode
+ ? _value.mode
+ : mode // ignore: cast_nullable_to_non_nullable
+ as Mode,
+ findProcessMode: null == findProcessMode
+ ? _value.findProcessMode
+ : findProcessMode // ignore: cast_nullable_to_non_nullable
+ as FindProcessMode,
+ keepAliveInterval: null == keepAliveInterval
+ ? _value.keepAliveInterval
+ : keepAliveInterval // ignore: cast_nullable_to_non_nullable
+ as int,
+ unifiedDelay: null == unifiedDelay
+ ? _value.unifiedDelay
+ : unifiedDelay // ignore: cast_nullable_to_non_nullable
+ as bool,
+ tcpConcurrent: null == tcpConcurrent
+ ? _value.tcpConcurrent
+ : tcpConcurrent // ignore: cast_nullable_to_non_nullable
+ as bool,
+ tun: null == tun
+ ? _value.tun
+ : tun // ignore: cast_nullable_to_non_nullable
+ as Tun,
+ dns: null == dns
+ ? _value.dns
+ : dns // ignore: cast_nullable_to_non_nullable
+ as Dns,
+ geoXUrl: null == geoXUrl
+ ? _value._geoXUrl
+ : geoXUrl // ignore: cast_nullable_to_non_nullable
+ as Map,
+ rules: null == rules
+ ? _value._rules
+ : rules // ignore: cast_nullable_to_non_nullable
+ as List,
+ globalRealUa: freezed == globalRealUa
+ ? _value.globalRealUa
+ : globalRealUa // ignore: cast_nullable_to_non_nullable
+ as String?,
+ ));
+ }
+}
+
+/// @nodoc
+
+class _$ClashConfigStateImpl implements _ClashConfigState {
+ const _$ClashConfigStateImpl(
+ {required this.mixedPort,
+ required this.allowLan,
+ required this.ipv6,
+ required this.geodataLoader,
+ required this.logLevel,
+ required this.externalController,
+ required this.mode,
+ required this.findProcessMode,
+ required this.keepAliveInterval,
+ required this.unifiedDelay,
+ required this.tcpConcurrent,
+ required this.tun,
+ required this.dns,
+ required final Map geoXUrl,
+ required final List rules,
+ required this.globalRealUa})
+ : _geoXUrl = geoXUrl,
+ _rules = rules;
+
+ @override
+ final int mixedPort;
+ @override
+ final bool allowLan;
+ @override
+ final bool ipv6;
+ @override
+ final String geodataLoader;
+ @override
+ final LogLevel logLevel;
+ @override
+ final String externalController;
+ @override
+ final Mode mode;
+ @override
+ final FindProcessMode findProcessMode;
+ @override
+ final int keepAliveInterval;
+ @override
+ final bool unifiedDelay;
+ @override
+ final bool tcpConcurrent;
+ @override
+ final Tun tun;
+ @override
+ final Dns dns;
+ final Map _geoXUrl;
+ @override
+ Map get geoXUrl {
+ if (_geoXUrl is EqualUnmodifiableMapView) return _geoXUrl;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableMapView(_geoXUrl);
+ }
+
+ final List _rules;
+ @override
+ List get rules {
+ if (_rules is EqualUnmodifiableListView) return _rules;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(_rules);
+ }
+
+ @override
+ final String? globalRealUa;
+
+ @override
+ String toString() {
+ return 'ClashConfigState(mixedPort: $mixedPort, allowLan: $allowLan, ipv6: $ipv6, geodataLoader: $geodataLoader, logLevel: $logLevel, externalController: $externalController, mode: $mode, findProcessMode: $findProcessMode, keepAliveInterval: $keepAliveInterval, unifiedDelay: $unifiedDelay, tcpConcurrent: $tcpConcurrent, tun: $tun, dns: $dns, geoXUrl: $geoXUrl, rules: $rules, globalRealUa: $globalRealUa)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$ClashConfigStateImpl &&
+ (identical(other.mixedPort, mixedPort) ||
+ other.mixedPort == mixedPort) &&
+ (identical(other.allowLan, allowLan) ||
+ other.allowLan == allowLan) &&
+ (identical(other.ipv6, ipv6) || other.ipv6 == ipv6) &&
+ (identical(other.geodataLoader, geodataLoader) ||
+ other.geodataLoader == geodataLoader) &&
+ (identical(other.logLevel, logLevel) ||
+ other.logLevel == logLevel) &&
+ (identical(other.externalController, externalController) ||
+ other.externalController == externalController) &&
+ (identical(other.mode, mode) || other.mode == mode) &&
+ (identical(other.findProcessMode, findProcessMode) ||
+ other.findProcessMode == findProcessMode) &&
+ (identical(other.keepAliveInterval, keepAliveInterval) ||
+ other.keepAliveInterval == keepAliveInterval) &&
+ (identical(other.unifiedDelay, unifiedDelay) ||
+ other.unifiedDelay == unifiedDelay) &&
+ (identical(other.tcpConcurrent, tcpConcurrent) ||
+ other.tcpConcurrent == tcpConcurrent) &&
+ (identical(other.tun, tun) || other.tun == tun) &&
+ (identical(other.dns, dns) || other.dns == dns) &&
+ const DeepCollectionEquality().equals(other._geoXUrl, _geoXUrl) &&
+ const DeepCollectionEquality().equals(other._rules, _rules) &&
+ (identical(other.globalRealUa, globalRealUa) ||
+ other.globalRealUa == globalRealUa));
+ }
+
+ @override
+ int get hashCode => Object.hash(
+ runtimeType,
+ mixedPort,
+ allowLan,
+ ipv6,
+ geodataLoader,
+ logLevel,
+ externalController,
+ mode,
+ findProcessMode,
+ keepAliveInterval,
+ unifiedDelay,
+ tcpConcurrent,
+ tun,
+ dns,
+ const DeepCollectionEquality().hash(_geoXUrl),
+ const DeepCollectionEquality().hash(_rules),
+ globalRealUa);
+
+ @JsonKey(ignore: true)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$ClashConfigStateImplCopyWith<_$ClashConfigStateImpl> get copyWith =>
+ __$$ClashConfigStateImplCopyWithImpl<_$ClashConfigStateImpl>(
+ this, _$identity);
+}
+
+abstract class _ClashConfigState implements ClashConfigState {
+ const factory _ClashConfigState(
+ {required final int mixedPort,
+ required final bool allowLan,
+ required final bool ipv6,
+ required final String geodataLoader,
+ required final LogLevel logLevel,
+ required final String externalController,
+ required final Mode mode,
+ required final FindProcessMode findProcessMode,
+ required final int keepAliveInterval,
+ required final bool unifiedDelay,
+ required final bool tcpConcurrent,
+ required final Tun tun,
+ required final Dns dns,
+ required final Map geoXUrl,
+ required final List rules,
+ required final String? globalRealUa}) = _$ClashConfigStateImpl;
+
+ @override
+ int get mixedPort;
+ @override
+ bool get allowLan;
+ @override
+ bool get ipv6;
+ @override
+ String get geodataLoader;
+ @override
+ LogLevel get logLevel;
+ @override
+ String get externalController;
+ @override
+ Mode get mode;
+ @override
+ FindProcessMode get findProcessMode;
+ @override
+ int get keepAliveInterval;
+ @override
+ bool get unifiedDelay;
+ @override
+ bool get tcpConcurrent;
+ @override
+ Tun get tun;
+ @override
+ Dns get dns;
+ @override
+ Map get geoXUrl;
+ @override
+ List get rules;
+ @override
+ String? get globalRealUa;
+ @override
+ @JsonKey(ignore: true)
+ _$$ClashConfigStateImplCopyWith<_$ClashConfigStateImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/lib/models/selector.dart b/lib/models/selector.dart
index d8868d4c..b73d8620 100644
--- a/lib/models/selector.dart
+++ b/lib/models/selector.dart
@@ -41,6 +41,14 @@ class ProfilesSelectorState with _$ProfilesSelectorState {
}) = _ProfilesSelectorState;
}
+@freezed
+class NetworkDetectionState with _$NetworkDetectionState {
+ const factory NetworkDetectionState({
+ required bool isTesting,
+ required IpInfo? ipInfo,
+ }) = _NetworkDetectionState;
+}
+
@freezed
class ApplicationSelectorState with _$ApplicationSelectorState {
const factory ApplicationSelectorState({
@@ -148,19 +156,19 @@ extension PackageListSelectorStateExt on PackageListSelectorState {
return packages
.where((item) => isFilterSystemApp ? item.isSystem == false : true)
.sorted(
- (a, b) {
+ (a, b) {
return switch (sort) {
AccessSortType.none => 0,
- AccessSortType.name =>
- other.sortByChar(
- PinyinHelper.getPinyin(a.label),
- PinyinHelper.getPinyin(b.label),
- ),
- AccessSortType.time => a.firstInstallTime.compareTo(b.firstInstallTime),
+ AccessSortType.name => other.sortByChar(
+ PinyinHelper.getPinyin(a.label),
+ PinyinHelper.getPinyin(b.label),
+ ),
+ AccessSortType.time =>
+ a.firstInstallTime.compareTo(b.firstInstallTime),
};
},
).sorted(
- (a, b) {
+ (a, b) {
final isSelectA = selectedList.contains(a.packageName);
final isSelectB = selectedList.contains(b.packageName);
if (isSelectA && isSelectB) return 0;
@@ -187,3 +195,42 @@ class ProxiesActionsState with _$ProxiesActionsState {
required bool hasProvider,
}) = _ProxiesActionsState;
}
+
+@freezed
+class AutoLaunchState with _$AutoLaunchState {
+ const factory AutoLaunchState({
+ required bool isAutoLaunch,
+ required bool isOpenTun,
+ }) = _AutoLaunchState;
+}
+
+@freezed
+class ProxyState with _$ProxyState {
+ const factory ProxyState({
+ required bool isStart,
+ required bool systemProxy,
+ required int port,
+ }) = _ProxyState;
+}
+
+@freezed
+class ClashConfigState with _$ClashConfigState {
+ const factory ClashConfigState({
+ required int mixedPort,
+ required bool allowLan,
+ required bool ipv6,
+ required String geodataLoader,
+ required LogLevel logLevel,
+ required String externalController,
+ required Mode mode,
+ required FindProcessMode findProcessMode,
+ required int keepAliveInterval,
+ required bool unifiedDelay,
+ required bool tcpConcurrent,
+ required Tun tun,
+ required Dns dns,
+ required GeoXMap geoXUrl,
+ required List rules,
+ required String? globalRealUa,
+ }) = _ClashConfigState;
+}
diff --git a/lib/pages/home.dart b/lib/pages/home.dart
index 45c202be..e1024559 100644
--- a/lib/pages/home.dart
+++ b/lib/pages/home.dart
@@ -13,20 +13,6 @@ typedef OnSelected = void Function(int index);
class HomePage extends StatelessWidget {
const HomePage({super.key});
- _navigationBarContainer({
- required BuildContext context,
- required Widget child,
- }) {
- // if (!system.isDesktop) return child;
- return Container(
- padding: const EdgeInsets.all(16).copyWith(
- right: 0,
- ),
- color: context.colorScheme.surface,
- child: child,
- );
- }
-
_getNavigationBar({
required BuildContext context,
required ViewMode viewMode,
@@ -47,61 +33,78 @@ class HomePage extends StatelessWidget {
selectedIndex: currentIndex,
);
}
- final extended = viewMode == ViewMode.desktop;
- return _navigationBarContainer(
- context: context,
- child: NavigationRail(
- groupAlignment: -0.8,
- selectedIconTheme: IconThemeData(
- color: context.colorScheme.onSurfaceVariant,
- ),
- unselectedIconTheme: IconThemeData(
- color: context.colorScheme.onSurfaceVariant,
- ),
- selectedLabelTextStyle: context.textTheme.labelLarge!.copyWith(
- color: context.colorScheme.onSurface,
- ),
- unselectedLabelTextStyle: context.textTheme.labelLarge!.copyWith(
- color: context.colorScheme.onSurface,
- ),
- destinations: navigationItems
- .map(
- (e) => NavigationRailDestination(
- icon: e.icon,
- label: Text(
- Intl.message(e.label),
+ return LayoutBuilder(
+ builder: (_, container) {
+ return Material(
+ color: context.colorScheme.surfaceContainer,
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ vertical: 16,
+ ),
+ height: container.maxHeight,
+ child: Column(
+ children: [
+ Expanded(
+ child: SingleChildScrollView(
+ child: IntrinsicHeight(
+ child: Selector(
+ selector: (_, config) => config.showLabel,
+ builder: (_, showLabel, __) {
+ return NavigationRail(
+ backgroundColor:
+ context.colorScheme.surfaceContainer,
+ selectedIconTheme: IconThemeData(
+ color: context.colorScheme.onSurfaceVariant,
+ ),
+ unselectedIconTheme: IconThemeData(
+ color: context.colorScheme.onSurfaceVariant,
+ ),
+ selectedLabelTextStyle:
+ context.textTheme.labelLarge!.copyWith(
+ color: context.colorScheme.onSurface,
+ ),
+ unselectedLabelTextStyle:
+ context.textTheme.labelLarge!.copyWith(
+ color: context.colorScheme.onSurface,
+ ),
+ destinations: navigationItems
+ .map(
+ (e) => NavigationRailDestination(
+ icon: e.icon,
+ label: Text(
+ Intl.message(e.label),
+ ),
+ ),
+ )
+ .toList(),
+ onDestinationSelected:
+ globalState.appController.toPage,
+ extended: false,
+ selectedIndex: currentIndex,
+ labelType: showLabel
+ ? NavigationRailLabelType.all
+ : NavigationRailLabelType.none,
+ );
+ },
+ ),
+ ),
+ ),
),
- ),
- )
- .toList(),
- onDestinationSelected: globalState.appController.toPage,
- extended: extended,
- minExtendedWidth: 200,
- selectedIndex: currentIndex,
- labelType: extended
- ? NavigationRailLabelType.none
- : NavigationRailLabelType.selected,
- ),
- );
- return NavigationRail(
- groupAlignment: -0.95,
- destinations: navigationItems
- .map(
- (e) => NavigationRailDestination(
- icon: e.icon,
- label: Text(
- Intl.message(e.label),
- ),
+ const SizedBox(
+ height: 16,
+ ),
+ IconButton(
+ onPressed: () {
+ final config = globalState.appController.config;
+ config.showLabel = !config.showLabel;
+ },
+ icon: const Icon(Icons.menu),
+ )
+ ],
),
- )
- .toList(),
- onDestinationSelected: globalState.appController.toPage,
- extended: extended,
- minExtendedWidth: 172,
- selectedIndex: currentIndex,
- labelType: extended
- ? NavigationRailLabelType.none
- : NavigationRailLabelType.selected,
+ ),
+ );
+ },
);
}
diff --git a/lib/plugins/service.dart b/lib/plugins/service.dart
new file mode 100644
index 00000000..0c854ba1
--- /dev/null
+++ b/lib/plugins/service.dart
@@ -0,0 +1,29 @@
+import 'dart:async';
+import 'dart:io';
+import 'dart:isolate';
+import 'package:flutter/services.dart';
+
+class Service {
+ static Service? _instance;
+ late MethodChannel methodChannel;
+ ReceivePort? receiver;
+
+ Service._internal() {
+ methodChannel = const MethodChannel("service");
+ }
+
+ factory Service() {
+ _instance ??= Service._internal();
+ return _instance!;
+ }
+
+ Future init() async {
+ return await methodChannel.invokeMethod("init");
+ }
+
+ Future destroy() async {
+ return await methodChannel.invokeMethod("destroy");
+ }
+}
+
+final service = Platform.isAndroid ? Service() : null;
diff --git a/lib/plugins/proxy.dart b/lib/plugins/vpn.dart
similarity index 63%
rename from lib/plugins/proxy.dart
rename to lib/plugins/vpn.dart
index 88d0cff9..d2bb609a 100644
--- a/lib/plugins/proxy.dart
+++ b/lib/plugins/vpn.dart
@@ -4,22 +4,18 @@ import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'package:fl_clash/clash/clash.dart';
-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/state.dart';
-import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
-import 'package:proxy/proxy_platform_interface.dart';
-class Proxy extends ProxyPlatform {
- static Proxy? _instance;
+class Vpn {
+ static Vpn? _instance;
late MethodChannel methodChannel;
ReceivePort? receiver;
ServiceMessageListener? _serviceMessageHandler;
- Proxy._internal() {
- methodChannel = const MethodChannel("proxy");
+ Vpn._internal() {
+ methodChannel = const MethodChannel("vpn");
methodChannel.setMethodCallHandler((call) async {
switch (call.method) {
case "started":
@@ -32,36 +28,21 @@ class Proxy extends ProxyPlatform {
});
}
- factory Proxy() {
- _instance ??= Proxy._internal();
+ factory Vpn() {
+ _instance ??= Vpn._internal();
return _instance!;
}
- Future initService() async {
- return await methodChannel.invokeMethod("initService");
- }
-
- handleStop() {
- globalState.stopSystemProxy();
- }
-
- @override
- Future startProxy(port) async {
+ Future startVpn(port) async {
final state = clashCore.getState();
- return await methodChannel.invokeMethod("startProxy", {
+ return await methodChannel.invokeMethod("start", {
'port': state.mixedPort,
'args': json.encode(state),
});
}
- @override
- Future stopProxy() async {
- clashCore.stopTun();
- final isStop = await methodChannel.invokeMethod("stopProxy");
- if (isStop == true) {
- startTime = null;
- }
- return isStop;
+ Future stopVpn() async {
+ return await methodChannel.invokeMethod("stop");
}
Future setProtect(int fd) async {
@@ -78,10 +59,7 @@ class Proxy extends ProxyPlatform {
});
}
- bool get isStart => startTime != null && startTime!.isBeforeNow;
-
onStarted(int? fd) {
- if (fd == null) return;
if (receiver != null) {
receiver!.close();
receiver == null;
@@ -90,11 +68,7 @@ class Proxy extends ProxyPlatform {
receiver!.listen((message) {
_handleServiceMessage(message);
});
- clashCore.startTun(fd, receiver!.sendPort.nativePort);
- }
-
- updateStartTime() {
- startTime = clashCore.getRunTime();
+ clashCore.startTun(fd ?? 0, receiver!.sendPort.nativePort);
}
setServiceMessageHandler(ServiceMessageListener serviceMessageListener) {
@@ -103,7 +77,6 @@ class Proxy extends ProxyPlatform {
_handleServiceMessage(String message) {
final m = ServiceMessage.fromJson(json.decode(message));
- debugPrint(m.toString());
switch (m.type) {
case ServiceMessageType.protect:
_serviceMessageHandler?.onProtect(Fd.fromJson(m.data));
@@ -117,4 +90,4 @@ class Proxy extends ProxyPlatform {
}
}
-final proxy = Platform.isAndroid ? Proxy() : null;
+final vpn = Platform.isAndroid ? Vpn() : null;
diff --git a/lib/state.dart b/lib/state.dart
index 351fc9f7..7999ac60 100644
--- a/lib/state.dart
+++ b/lib/state.dart
@@ -3,7 +3,8 @@ import 'dart:io';
import 'package:animations/animations.dart';
import 'package:fl_clash/clash/clash.dart';
-import 'package:fl_clash/plugins/proxy.dart';
+import 'package:fl_clash/plugins/service.dart';
+import 'package:fl_clash/plugins/vpn.dart';
import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
@@ -21,11 +22,14 @@ class GlobalState {
late PackageInfo packageInfo;
Function? updateCurrentDelayDebounce;
PageController? pageController;
+ DateTime? startTime;
final navigatorKey = GlobalKey();
late AppController appController;
GlobalKey homeScaffoldKey = GlobalKey();
List updateFunctionLists = [];
+ bool get isStart => startTime != null && startTime!.isBeforeNow;
+
startListenUpdate() {
if (timer != null && timer!.isActive == true) return;
timer = Timer.periodic(const Duration(seconds: 1), (Timer t) {
@@ -65,23 +69,32 @@ class GlobalState {
appState.versionInfo = clashCore.getVersionInfo();
}
- Future startSystemProxy({
- required AppState appState,
+ handleStart({
required Config config,
required ClashConfig clashConfig,
}) async {
- if (!globalState.isVpnService && Platform.isAndroid) {
- await proxy?.initService();
- } else {
- await proxyManager.startProxy(
- port: clashConfig.mixedPort,
- );
+ clashCore.start();
+ if (globalState.isVpnService) {
+ await vpn?.startVpn(clashConfig.mixedPort);
+ startListenUpdate();
+ return;
}
+ startTime ??= DateTime.now();
+ await service?.init();
startListenUpdate();
}
- Future stopSystemProxy() async {
- await proxyManager.stopProxy();
+ updateStartTime() {
+ startTime = clashCore.getRunTime();
+ }
+
+ handleStop() async {
+ clashCore.stop();
+ if (Platform.isAndroid) {
+ clashCore.stopTun();
+ }
+ await service?.destroy();
+ startTime = null;
stopListenUpdate();
}
@@ -116,12 +129,14 @@ class GlobalState {
);
clashCore.setState(
CoreState(
+ enable: config.vpnProps.enable,
accessControl: config.isAccessControl ? config.accessControl : null,
- allowBypass: config.allowBypass,
- systemProxy: config.systemProxy,
+ allowBypass: config.vpnProps.allowBypass,
+ systemProxy: config.vpnProps.systemProxy,
mixedPort: clashConfig.mixedPort,
onlyProxy: config.onlyProxy,
- currentProfileName: config.currentProfile?.label ?? config.currentProfileId ?? "",
+ currentProfileName:
+ config.currentProfile?.label ?? config.currentProfileId ?? "",
),
);
}
@@ -207,7 +222,7 @@ class GlobalState {
}) {
final traffic = clashCore.getTraffic();
if (Platform.isAndroid && isVpnService == true) {
- proxy?.startForeground(
+ vpn?.startForeground(
title: clashCore.getState().currentProfileName,
content: "$traffic",
);
diff --git a/lib/widgets/card.dart b/lib/widgets/card.dart
index 19920643..f957a22b 100644
--- a/lib/widgets/card.dart
+++ b/lib/widgets/card.dart
@@ -1,5 +1,6 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
+import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'text.dart';
@@ -29,12 +30,13 @@ class InfoHeader extends StatelessWidget {
return Container(
padding: const EdgeInsets.all(16),
child: Row(
- mainAxisSize: MainAxisSize.min,
+ mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Expanded(
+ Flexible(
+ flex: 1,
child: Row(
- mainAxisSize: MainAxisSize.min,
+ mainAxisSize: MainAxisSize.max,
children: [
if (info.iconData != null) ...[
Icon(
@@ -46,6 +48,7 @@ class InfoHeader extends StatelessWidget {
),
],
Flexible(
+ flex: 1,
child: TooltipText(
text: Text(
info.label,
@@ -58,6 +61,9 @@ class InfoHeader extends StatelessWidget {
],
),
),
+ const SizedBox(
+ width: 8,
+ ),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
@@ -155,6 +161,18 @@ class CommonCard extends StatelessWidget {
],
);
}
+ if (selectWidget != null && isSelected) {
+ final List children = [];
+ children.add(childWidget);
+ children.add(
+ Positioned.fill(
+ child: selectWidget!,
+ ),
+ );
+ childWidget = Stack(
+ children: children,
+ );
+ }
return OutlinedButton(
clipBehavior: Clip.antiAlias,
style: ButtonStyle(
@@ -172,25 +190,7 @@ class CommonCard extends StatelessWidget {
),
),
onPressed: onPressed,
- child: Builder(
- builder: (_) {
- if (selectWidget == null) {
- return childWidget;
- }
- List children = [];
- children.add(childWidget);
- if (isSelected) {
- children.add(
- Positioned.fill(
- child: selectWidget!,
- ),
- );
- }
- return Stack(
- children: children,
- );
- },
- ),
+ child: childWidget,
);
}
}
diff --git a/lib/widgets/clash_container.dart b/lib/widgets/clash_container.dart
index 96897d60..9c74179a 100644
--- a/lib/widgets/clash_container.dart
+++ b/lib/widgets/clash_container.dart
@@ -1,10 +1,11 @@
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/models/models.dart';
-import 'package:fl_clash/plugins/proxy.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
+import '../common/function.dart';
+
class ClashContainer extends StatefulWidget {
final Widget child;
@@ -19,12 +20,49 @@ class ClashContainer extends StatefulWidget {
class _ClashContainerState extends State
with AppMessageListener {
+ Function? updateClashConfigDebounce;
+
+ Widget _updateContainer(Widget child) {
+ return Selector(
+ selector: (_, clashConfig) => ClashConfigState(
+ mixedPort: clashConfig.mixedPort,
+ allowLan: clashConfig.allowLan,
+ ipv6: clashConfig.ipv6,
+ logLevel: clashConfig.logLevel,
+ geodataLoader: clashConfig.geodataLoader,
+ externalController: clashConfig.externalController,
+ mode: clashConfig.mode,
+ findProcessMode: clashConfig.findProcessMode,
+ keepAliveInterval: clashConfig.keepAliveInterval,
+ unifiedDelay: clashConfig.unifiedDelay,
+ tcpConcurrent: clashConfig.tcpConcurrent,
+ tun: clashConfig.tun,
+ dns: clashConfig.dns,
+ geoXUrl: clashConfig.geoXUrl,
+ rules: clashConfig.rules,
+ globalRealUa: clashConfig.globalRealUa,
+ ),
+ builder: (__, state, child) {
+ if (updateClashConfigDebounce == null) {
+ updateClashConfigDebounce = debounce(() async {
+ await globalState.appController.updateClashConfig();
+ });
+ } else {
+ updateClashConfigDebounce!();
+ }
+ return child!;
+ },
+ child: child,
+ );
+ }
+
Widget _updateCoreState(Widget child) {
return Selector2(
selector: (_, config, clashConfig) => CoreState(
accessControl: config.isAccessControl ? config.accessControl : null,
- allowBypass: config.allowBypass,
- systemProxy: config.systemProxy,
+ enable: config.vpnProps.enable,
+ allowBypass: config.vpnProps.allowBypass,
+ systemProxy: config.vpnProps.systemProxy,
mixedPort: clashConfig.mixedPort,
onlyProxy: config.onlyProxy,
currentProfileName:
@@ -61,7 +99,9 @@ class _ClashContainerState extends State
Widget build(BuildContext context) {
return _changeProfileContainer(
_updateCoreState(
- widget.child,
+ _updateContainer(
+ widget.child,
+ ),
),
);
}
@@ -89,6 +129,7 @@ class _ClashContainerState extends State
@override
void onLog(Log log) {
globalState.appController.appState.addLog(log);
+ debugPrint("$log");
super.onLog(log);
}
@@ -113,9 +154,7 @@ class _ClashContainerState extends State
@override
Future onStarted(String runTime) async {
super.onStarted(runTime);
- proxy?.updateStartTime();
final appController = globalState.appController;
await appController.applyProfile(isPrue: true);
- appController.addCheckIpNumDebounce();
}
}
diff --git a/lib/widgets/proxy_container.dart b/lib/widgets/proxy_container.dart
new file mode 100644
index 00000000..fefadac3
--- /dev/null
+++ b/lib/widgets/proxy_container.dart
@@ -0,0 +1,37 @@
+import 'package:fl_clash/common/proxy.dart';
+import 'package:fl_clash/models/models.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+class ProxyContainer extends StatelessWidget {
+ final Widget child;
+
+ const ProxyContainer({super.key, required this.child});
+
+ _updateProxy(ProxyState proxyState) {
+ final isStart = proxyState.isStart;
+ final systemProxy = proxyState.systemProxy;
+ final port = proxyState.port;
+ if (isStart && systemProxy) {
+ proxy?.startProxy(port);
+ }else{
+ proxy?.stopProxy();
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Selector3(
+ selector: (_, appState, config, clashConfig) => ProxyState(
+ isStart: appState.isStart,
+ systemProxy: config.desktopProps.systemProxy,
+ port: clashConfig.mixedPort,
+ ),
+ builder: (_, state, child) {
+ _updateProxy(state);
+ return child!;
+ },
+ child: child,
+ );
+ }
+}
diff --git a/lib/widgets/scaffold.dart b/lib/widgets/scaffold.dart
index 99f4c33e..86e0e657 100644
--- a/lib/widgets/scaffold.dart
+++ b/lib/widgets/scaffold.dart
@@ -109,7 +109,7 @@ class CommonScaffoldState extends State {
valueListenable: _actions,
builder: (_, actions, __) {
final realActions =
- actions.isNotEmpty ? actions : widget.actions;
+ actions.isNotEmpty ? actions : widget.actions;
return AppBar(
centerTitle: false,
automaticallyImplyLeading: widget.automaticallyImplyLeading,
diff --git a/lib/widgets/tile_container.dart b/lib/widgets/tile_container.dart
index 7494e3a6..d1ea68b7 100644
--- a/lib/widgets/tile_container.dart
+++ b/lib/widgets/tile_container.dart
@@ -24,13 +24,13 @@ class _TileContainerState extends State with TileListener {
@override
void onStart() {
- globalState.appController.updateSystemProxy(true);
+ globalState.appController.updateStatus(true);
super.onStart();
}
@override
void onStop() {
- globalState.appController.updateSystemProxy(false);
+ globalState.appController.updateStatus(false);
super.onStop();
}
diff --git a/lib/widgets/tray_container.dart b/lib/widgets/tray_container.dart
index 9d5a1482..1da87059 100644
--- a/lib/widgets/tray_container.dart
+++ b/lib/widgets/tray_container.dart
@@ -32,12 +32,12 @@ class _TrayContainerState extends State with TrayListener {
_updateOtherTray() async {
if (isTrayInit == false) {
- await trayManager.setIcon(
- other.getTrayIconPath(),
- );
await trayManager.setToolTip(
appName,
);
+ await trayManager.setIcon(
+ other.getTrayIconPath(),
+ );
isTrayInit = true;
}
}
@@ -110,7 +110,7 @@ class _TrayContainerState extends State with TrayListener {
final proxyMenuItem = MenuItem.checkbox(
label: appLocalizations.systemProxy,
onClick: (_) async {
- globalState.appController.updateSystemProxy(!state.isRun);
+ globalState.appController.updateStatus(!state.isRun);
},
checked: state.isRun,
);
diff --git a/lib/widgets/window_container.dart b/lib/widgets/window_container.dart
index 008b01d1..c9d71d9e 100644
--- a/lib/widgets/window_container.dart
+++ b/lib/widgets/window_container.dart
@@ -23,8 +23,8 @@ class _WindowContainerState extends State with WindowListener {
_autoLaunchContainer(Widget child) {
return Selector(
selector: (_, config) => config.autoLaunch,
- builder: (_, isAutoLaunch, child) {
- autoLaunch?.updateStatus(isAutoLaunch);
+ builder: (_, state, child) {
+ autoLaunch?.updateStatus(state);
return child!;
},
child: child,
@@ -33,22 +33,7 @@ class _WindowContainerState extends State with WindowListener {
@override
Widget build(BuildContext context) {
- return Stack(
- children: [
- Column(
- children: [
- SizedBox(
- height: kHeaderHeight,
- ),
- Expanded(
- flex: 1,
- child: _autoLaunchContainer(widget.child),
- ),
- ],
- ),
- const WindowHeader(),
- ],
- );
+ return _autoLaunchContainer(widget.child);
}
@override
@@ -98,6 +83,35 @@ class _WindowContainerState extends State with WindowListener {
}
}
+class WindowHeaderContainer extends StatelessWidget {
+ final Widget child;
+
+ const WindowHeaderContainer({
+ super.key,
+ required this.child,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Stack(
+ children: [
+ Column(
+ children: [
+ SizedBox(
+ height: kHeaderHeight,
+ ),
+ Expanded(
+ flex: 1,
+ child: child,
+ ),
+ ],
+ ),
+ const WindowHeader(),
+ ],
+ );
+ }
+}
+
class WindowHeader extends StatefulWidget {
const WindowHeader({super.key});
@@ -188,7 +202,7 @@ class _WindowHeaderState extends State {
),
IconButton(
onPressed: () {
- windowManager.close();
+ globalState.appController.handleBackOrExit();
},
icon: const Icon(Icons.close),
),
@@ -214,7 +228,7 @@ class _WindowHeaderState extends State {
_updateMaximized();
},
child: Container(
- color: context.colorScheme.surface,
+ color: context.colorScheme.secondary.toSoft(),
alignment: Alignment.centerLeft,
height: kHeaderHeight,
),
diff --git a/plugins/proxy/lib/proxy.dart b/plugins/proxy/lib/proxy.dart
index f0cad6e5..4096d67d 100644
--- a/plugins/proxy/lib/proxy.dart
+++ b/plugins/proxy/lib/proxy.dart
@@ -10,50 +10,22 @@ class Proxy extends ProxyPlatform {
@override
Future startProxy(int port) async {
- bool? isStart = false;
- switch (Platform.operatingSystem) {
- case "macos":
- isStart = await _startProxyWithMacos(port);
- break;
- case "linux":
- isStart = await _startProxyWithLinux(port);
- break;
- case "windows":
- isStart = await ProxyPlatform.instance.startProxy(port);
- break;
- }
- if (isStart == true) {
- startTime = DateTime.now();
- }
- return isStart;
+ return switch (Platform.operatingSystem) {
+ "macos" => await _startProxyWithMacos(port),
+ "linux" => await _startProxyWithLinux(port),
+ "windows" => await ProxyPlatform.instance.startProxy(port),
+ String() => false,
+ };
}
@override
Future stopProxy() async {
- bool? isStop = false;
- switch (Platform.operatingSystem) {
- case "macos":
- isStop = await _stopProxyWithMacos();
- break;
- case "linux":
- isStop = await _stopProxyWithLinux();
- break;
- case "windows":
- isStop = await ProxyPlatform.instance.stopProxy();
- break;
- }
- if (isStop == true) {
- startTime = null;
- }
- return isStop;
- }
-
- @override
- get startTime => ProxyPlatform.instance.startTime;
-
- @override
- set startTime(DateTime? dateTime) {
- ProxyPlatform.instance.startTime = dateTime;
+ return switch (Platform.operatingSystem) {
+ "macos" => await _stopProxyWithMacos(),
+ "linux" => await _stopProxyWithLinux(),
+ "windows" => await ProxyPlatform.instance.stopProxy(),
+ String() => false,
+ };
}
Future _startProxyWithLinux(int port) async {
@@ -205,4 +177,3 @@ class Proxy extends ProxyPlatform {
return lines;
}
}
-
diff --git a/plugins/proxy/lib/proxy_method_channel.dart b/plugins/proxy/lib/proxy_method_channel.dart
index 1ed6d18e..1bc0ffbc 100644
--- a/plugins/proxy/lib/proxy_method_channel.dart
+++ b/plugins/proxy/lib/proxy_method_channel.dart
@@ -18,10 +18,6 @@ class MethodChannelProxy extends ProxyPlatform {
@override
Future stopProxy() async {
- final isStop = await methodChannel.invokeMethod("StopProxy");
- if (isStop == true) {
- startTime = null;
- }
- return isStop;
+ return await methodChannel.invokeMethod("StopProxy");
}
}
diff --git a/plugins/proxy/lib/proxy_platform_interface.dart b/plugins/proxy/lib/proxy_platform_interface.dart
index 9d74ba75..e8e7279f 100644
--- a/plugins/proxy/lib/proxy_platform_interface.dart
+++ b/plugins/proxy/lib/proxy_platform_interface.dart
@@ -20,8 +20,6 @@ abstract class ProxyPlatform extends PlatformInterface {
_instance = instance;
}
- DateTime? startTime;
-
Future startProxy(int port) {
throw UnimplementedError('startProxy() has not been implemented.');
}
diff --git a/pubspec.yaml b/pubspec.yaml
index 0a7c0cd5..501282ab 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,9 +1,10 @@
name: fl_clash
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
publish_to: 'none'
-version: 0.8.53+202408151
+version: 0.8.54+202408221
environment:
sdk: '>=3.1.0 <4.0.0'
+ flutter: 3.22.3
dependencies:
flutter:
diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt
index 45d7b98c..8e9f68db 100644
--- a/windows/runner/CMakeLists.txt
+++ b/windows/runner/CMakeLists.txt
@@ -18,6 +18,8 @@ add_executable(${BINARY_NAME} WIN32
"runner.exe.manifest"
)
+SET_TARGET_PROPERTIES(${BINARY_NAME} PROPERTIES LINK_FLAGS "/MANIFESTUAC:\"level='requireAdministrator' uiAccess='false'\" /SUBSYSTEM:WINDOWS")
+
# add_executable(service
# "service.cpp"
# )