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" # )