Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
043648f998 | ||
|
|
3eb26e8061 | ||
|
|
5d6bd6466f | ||
|
|
4e766d9407 | ||
|
|
80f8aa22ee | ||
|
|
97714e8b25 | ||
|
|
50bf4170d9 | ||
|
|
79efa67df3 | ||
|
|
ac397393a0 | ||
|
|
b685165230 | ||
|
|
402221aaa2 | ||
|
|
f6d9ed11d9 | ||
|
|
c38a671d57 | ||
|
|
75af47aead | ||
|
|
8dafe3b0ec | ||
|
|
813198a21d | ||
|
|
68dd262fef | ||
|
|
5ef020db73 | ||
|
|
e3c9035903 | ||
|
|
7fc54c5295 | ||
|
|
00a78b5fb4 |
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -1,7 +1,7 @@
|
||||
[submodule "core/Clash.Meta"]
|
||||
path = core/Clash.Meta
|
||||
url = git@github.com:chen08209/Clash.Meta.git
|
||||
branch = FlClash
|
||||
branch = FlClash-Alpha
|
||||
[submodule "plugins/flutter_distributor"]
|
||||
path = plugins/flutter_distributor
|
||||
url = git@github.com:chen08209/flutter_distributor.git
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package com.follow.clash
|
||||
|
||||
import com.follow.clash.models.Props
|
||||
import com.follow.clash.models.TunProps
|
||||
|
||||
interface BaseServiceInterface {
|
||||
fun start(port: Int, props: Props?): Int?
|
||||
fun start(port: Int, props: Props?): TunProps?
|
||||
fun stop()
|
||||
fun startForeground(title: String, content: String)
|
||||
}
|
||||
@@ -39,8 +39,7 @@ object GlobalState {
|
||||
}
|
||||
|
||||
fun getCurrentVPNPlugin(): VpnPlugin? {
|
||||
val currentEngine = if (serviceEngine != null) serviceEngine else flutterEngine
|
||||
return currentEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
|
||||
return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
|
||||
}
|
||||
|
||||
fun destroyServiceEngine() {
|
||||
|
||||
@@ -1,28 +1,20 @@
|
||||
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.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.system.OsConstants.IPPROTO_TCP
|
||||
import android.system.OsConstants.IPPROTO_UDP
|
||||
import android.util.Base64
|
||||
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.Inet4Address
|
||||
import java.net.Inet6Address
|
||||
import java.net.InetAddress
|
||||
|
||||
|
||||
suspend fun Drawable.getBase64(): String {
|
||||
@@ -41,6 +33,37 @@ fun Metadata.getProtocol(): Int? {
|
||||
return null
|
||||
}
|
||||
|
||||
private val CHANNEL = "FlClash"
|
||||
|
||||
private val notificationId: Int = 1
|
||||
fun ConnectivityManager.resolvePrimaryDns(network: Network?): String? {
|
||||
val properties = getLinkProperties(network) ?: return null
|
||||
return properties.dnsServers.firstOrNull()?.asSocketAddressText(53)
|
||||
}
|
||||
|
||||
fun InetAddress.asSocketAddressText(port: Int): String {
|
||||
return when (this) {
|
||||
is Inet6Address ->
|
||||
"[${numericToTextFormat(this.address)}]:$port"
|
||||
|
||||
is Inet4Address ->
|
||||
"${this.hostAddress}:$port"
|
||||
|
||||
else -> throw IllegalArgumentException("Unsupported Inet type ${this.javaClass}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun numericToTextFormat(src: ByteArray): String {
|
||||
val sb = StringBuilder(39)
|
||||
for (i in 0 until 8) {
|
||||
sb.append(
|
||||
Integer.toHexString(
|
||||
src[i shl 1].toInt() shl 8 and 0xff00
|
||||
or (src[(i shl 1) + 1].toInt() and 0xff)
|
||||
)
|
||||
)
|
||||
if (i < 7) {
|
||||
sb.append(":")
|
||||
}
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
26
android/app/src/main/kotlin/com/follow/clash/models/Dns.kt
Normal file
26
android/app/src/main/kotlin/com/follow/clash/models/Dns.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.follow.clash.models
|
||||
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
|
||||
val TRANSPORT_PRIORITY = sequence {
|
||||
yield(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 27) {
|
||||
yield(NetworkCapabilities.TRANSPORT_LOWPAN)
|
||||
}
|
||||
|
||||
yield(NetworkCapabilities.TRANSPORT_BLUETOOTH)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
yield(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
|
||||
}
|
||||
|
||||
yield(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
yield(NetworkCapabilities.TRANSPORT_USB)
|
||||
}
|
||||
|
||||
yield(NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||
}.toList()
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.follow.clash.models
|
||||
|
||||
import java.util.Date
|
||||
|
||||
data class Package(
|
||||
val packageName: String,
|
||||
val label: String,
|
||||
|
||||
@@ -17,3 +17,13 @@ data class Props(
|
||||
val allowBypass: Boolean?,
|
||||
val systemProxy: Boolean?,
|
||||
)
|
||||
|
||||
data class TunProps(
|
||||
val fd: Int,
|
||||
val gateway: String,
|
||||
val gateway6: String,
|
||||
val portal: String,
|
||||
val portal6: String,
|
||||
val dns: String,
|
||||
val dns6: String
|
||||
)
|
||||
@@ -8,7 +8,6 @@ import android.content.Intent
|
||||
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
|
||||
@@ -17,12 +16,9 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.getSystemService
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.extensions.getBase64
|
||||
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
|
||||
@@ -37,7 +33,6 @@ import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
|
||||
@@ -52,11 +47,10 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
|
||||
private lateinit var scope: CoroutineScope
|
||||
|
||||
private var connectivity: ConnectivityManager? = null
|
||||
|
||||
private var vpnCallBack: (() -> Unit)? = null
|
||||
|
||||
private val iconMap = mutableMapOf<String, String?>()
|
||||
|
||||
private val packages = mutableListOf<Package>()
|
||||
|
||||
private val skipPrefixList = listOf(
|
||||
@@ -114,7 +108,6 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex()
|
||||
}
|
||||
|
||||
|
||||
val VPN_PERMISSION_REQUEST_CODE = 1001
|
||||
|
||||
val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
|
||||
@@ -191,48 +184,6 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
}
|
||||
|
||||
"resolverProcess" -> {
|
||||
val data = call.argument<String>("data")
|
||||
val process =
|
||||
if (data != null) Gson().fromJson(
|
||||
data,
|
||||
Process::class.java
|
||||
) else null
|
||||
val metadata = process?.metadata
|
||||
val protocol = metadata?.getProtocol()
|
||||
if (protocol == null) {
|
||||
result.success(null)
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
result.success(null)
|
||||
return@withContext
|
||||
}
|
||||
if (connectivity == null) {
|
||||
connectivity = context.getSystemService<ConnectivityManager>()
|
||||
}
|
||||
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
|
||||
val dst = InetSocketAddress(
|
||||
metadata.destinationIP.ifEmpty { metadata.host },
|
||||
metadata.destinationPort
|
||||
)
|
||||
val uid = try {
|
||||
connectivity?.getConnectionOwnerUid(protocol, src, dst)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
if (uid == null || uid == -1) {
|
||||
result.success(null)
|
||||
return@withContext
|
||||
}
|
||||
val packages = context.packageManager?.getPackagesForUid(uid)
|
||||
result.success(packages?.first())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"tip" -> {
|
||||
val message = call.argument<String>("message")
|
||||
tip(message)
|
||||
@@ -379,7 +330,6 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun isChinaPackage(packageName: String): Boolean {
|
||||
val packageManager = context.packageManager ?: return false
|
||||
skipPrefixList.forEach {
|
||||
@@ -447,10 +397,6 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
return false
|
||||
}
|
||||
|
||||
fun requestGc() {
|
||||
channel.invokeMethod("gc", null)
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity;
|
||||
binding.addActivityResultListener(::onActivityResult)
|
||||
@@ -490,4 +436,4 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import androidx.core.content.getSystemService
|
||||
import com.follow.clash.GlobalState
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
|
||||
@@ -5,19 +5,33 @@ import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.content.getSystemService
|
||||
import com.follow.clash.BaseServiceInterface
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.RunState
|
||||
import com.follow.clash.extensions.getProtocol
|
||||
import com.follow.clash.extensions.resolvePrimaryDns
|
||||
import com.follow.clash.models.Props
|
||||
import com.follow.clash.models.TunProps
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.InetSocketAddress
|
||||
import kotlin.concurrent.withLock
|
||||
import com.follow.clash.models.Process
|
||||
|
||||
|
||||
class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
@@ -26,6 +40,11 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
private var flClashService: BaseServiceInterface? = null
|
||||
private var port: Int = 7890
|
||||
private var props: Props? = null
|
||||
private lateinit var scope: CoroutineScope
|
||||
|
||||
private val connectivity by lazy {
|
||||
context.getSystemService<ConnectivityManager>()
|
||||
}
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
@@ -43,55 +62,104 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
scope = CoroutineScope(Dispatchers.Default)
|
||||
context = flutterPluginBinding.applicationContext
|
||||
scope.launch {
|
||||
registerNetworkCallback()
|
||||
}
|
||||
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "vpn")
|
||||
flutterMethodChannel.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
unRegisterNetworkCallback()
|
||||
flutterMethodChannel.setMethodCallHandler(null)
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
|
||||
|
||||
"start" -> {
|
||||
port = call.argument<Int>("port")!!
|
||||
val args = call.argument<String>("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<Int>("fd")
|
||||
if (fd != null) {
|
||||
if (flClashService is FlClashVpnService) {
|
||||
(flClashService as FlClashVpnService).protect(fd)
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"start" -> {
|
||||
port = call.argument<Int>("port")!!
|
||||
val args = call.argument<String>("args")
|
||||
props =
|
||||
if (args != null) Gson().fromJson(args, Props::class.java) else null
|
||||
when (props?.enable == true) {
|
||||
true -> handleStartVpn()
|
||||
false -> start()
|
||||
}
|
||||
result.success(true)
|
||||
} else {
|
||||
result.success(false)
|
||||
}
|
||||
}
|
||||
|
||||
"startForeground" -> {
|
||||
val title = call.argument<String>("title") as String
|
||||
val content = call.argument<String>("content") as String
|
||||
startForeground(title, content)
|
||||
result.success(true)
|
||||
}
|
||||
"stop" -> {
|
||||
stop()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
"setProtect" -> {
|
||||
val fd = call.argument<Int>("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<String>("title") as String
|
||||
val content = call.argument<String>("content") as String
|
||||
startForeground(title, content)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"resolverProcess" -> {
|
||||
val data = call.argument<String>("data")
|
||||
val process =
|
||||
if (data != null) Gson().fromJson(
|
||||
data,
|
||||
Process::class.java
|
||||
) else null
|
||||
val metadata = process?.metadata
|
||||
if (metadata == null) {
|
||||
result.success(null)
|
||||
return
|
||||
}
|
||||
val protocol = metadata.getProtocol()
|
||||
if (protocol == null) {
|
||||
result.success(null)
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
result.success(null)
|
||||
return@withContext
|
||||
}
|
||||
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
|
||||
val dst = InetSocketAddress(
|
||||
metadata.destinationIP.ifEmpty { metadata.host },
|
||||
metadata.destinationPort
|
||||
)
|
||||
val uid = try {
|
||||
connectivity?.getConnectionOwnerUid(protocol, src, dst)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
if (uid == null || uid == -1) {
|
||||
result.success(null)
|
||||
return@withContext
|
||||
}
|
||||
val packages = context.packageManager?.getPackagesForUid(uid)
|
||||
result.success(packages?.first())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +170,62 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
}
|
||||
|
||||
fun requestGc() {
|
||||
flutterMethodChannel.invokeMethod("gc", null)
|
||||
}
|
||||
|
||||
val networks = mutableSetOf<Network>()
|
||||
|
||||
fun onUpdateNetwork() {
|
||||
val dns = networks.mapNotNull {
|
||||
connectivity?.resolvePrimaryDns(it)
|
||||
}.joinToString(separator = ",")
|
||||
scope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
flutterMethodChannel.invokeMethod("dnsChanged", dns)
|
||||
}
|
||||
}
|
||||
// if (flClashService is FlClashVpnService) {
|
||||
// val network = networks.maxByOrNull { net ->
|
||||
// connectivity?.getNetworkCapabilities(net)?.let { cap ->
|
||||
// TRANSPORT_PRIORITY.indexOfFirst { cap.hasTransport(it) }
|
||||
// } ?: -1
|
||||
// }
|
||||
// network?.let {
|
||||
// (flClashService as FlClashVpnService).updateUnderlyingNetworks(arrayOf(network))
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private val callback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
networks.add(network)
|
||||
onUpdateNetwork()
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
networks.remove(network)
|
||||
onUpdateNetwork()
|
||||
}
|
||||
}
|
||||
|
||||
private val request = NetworkRequest.Builder().apply {
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||
}.build()
|
||||
|
||||
private fun registerNetworkCallback() {
|
||||
networks.clear()
|
||||
connectivity?.registerNetworkCallback(request, callback)
|
||||
}
|
||||
|
||||
private fun unRegisterNetworkCallback() {
|
||||
connectivity?.unregisterNetworkCallback(callback)
|
||||
networks.clear()
|
||||
onUpdateNetwork()
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType")
|
||||
private fun startForeground(title: String, content: String) {
|
||||
GlobalState.runLock.withLock {
|
||||
@@ -118,8 +242,11 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
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)
|
||||
val tunProps = flClashService?.start(port, props)
|
||||
flutterMethodChannel.invokeMethod(
|
||||
"started",
|
||||
Gson().toJson(tunProps, TunProps::class.java)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ class FlClashService : Service(), BaseServiceInterface {
|
||||
}
|
||||
}
|
||||
|
||||
override fun start(port: Int, props: Props?): Int? = null
|
||||
override fun start(port: Int, props: Props?) = null
|
||||
|
||||
override fun stop() {
|
||||
stopSelf()
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
import android.net.Network
|
||||
import android.net.ProxyInfo
|
||||
import android.net.VpnService
|
||||
import android.os.Binder
|
||||
@@ -22,6 +23,7 @@ import com.follow.clash.MainActivity
|
||||
import com.follow.clash.R
|
||||
import com.follow.clash.models.AccessControlMode
|
||||
import com.follow.clash.models.Props
|
||||
import com.follow.clash.models.TunProps
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -30,36 +32,53 @@ import kotlinx.coroutines.launch
|
||||
@SuppressLint("WrongConstant")
|
||||
class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
|
||||
private val passList = listOf(
|
||||
"*zhihu.com",
|
||||
"*zhimg.com",
|
||||
"*jd.com",
|
||||
"100ime-iat-api.xfyun.cn",
|
||||
"*360buyimg.com",
|
||||
"localhost",
|
||||
"*.local",
|
||||
"127.*",
|
||||
"10.*",
|
||||
"172.16.*",
|
||||
"172.17.*",
|
||||
"172.18.*",
|
||||
"172.19.*",
|
||||
"172.2*",
|
||||
"172.30.*",
|
||||
"172.31.*",
|
||||
"192.168.*"
|
||||
)
|
||||
companion object {
|
||||
private val passList = listOf(
|
||||
"*zhihu.com",
|
||||
"*zhimg.com",
|
||||
"*jd.com",
|
||||
"100ime-iat-api.xfyun.cn",
|
||||
"*360buyimg.com",
|
||||
"localhost",
|
||||
"*.local",
|
||||
"127.*",
|
||||
"10.*",
|
||||
"172.16.*",
|
||||
"172.17.*",
|
||||
"172.18.*",
|
||||
"172.19.*",
|
||||
"172.2*",
|
||||
"172.30.*",
|
||||
"172.31.*",
|
||||
"192.168.*"
|
||||
)
|
||||
private const val TUN_MTU = 9000
|
||||
private const val TUN_SUBNET_PREFIX = 30
|
||||
private const val TUN_GATEWAY = "172.19.0.1"
|
||||
private const val TUN_SUBNET_PREFIX6 = 126
|
||||
private const val TUN_GATEWAY6 = "fdfe:dcba:9876::1"
|
||||
private const val TUN_PORTAL = "172.19.0.2"
|
||||
private const val TUN_PORTAL6 = "fdfe:dcba:9876::2"
|
||||
private const val TUN_DNS = TUN_PORTAL
|
||||
private const val TUN_DNS6 = TUN_PORTAL6
|
||||
private const val NET_ANY = "0.0.0.0"
|
||||
private const val NET_ANY6 = "::"
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
GlobalState.initServiceEngine(applicationContext)
|
||||
}
|
||||
|
||||
override fun start(port: Int, props: Props?): Int? {
|
||||
override fun start(port: Int, props: Props?): TunProps {
|
||||
return with(Builder()) {
|
||||
addAddress("172.16.0.1", 30)
|
||||
setMtu(9000)
|
||||
addRoute("0.0.0.0", 0)
|
||||
addAddress(TUN_GATEWAY, TUN_SUBNET_PREFIX)
|
||||
addAddress(TUN_GATEWAY6, TUN_SUBNET_PREFIX6)
|
||||
addRoute(NET_ANY, 0)
|
||||
addRoute(NET_ANY6, 0)
|
||||
addDnsServer(TUN_DNS)
|
||||
addDnsServer(TUN_DNS6)
|
||||
setMtu(TUN_MTU)
|
||||
props?.accessControl?.let { accessControl ->
|
||||
when (accessControl.mode) {
|
||||
AccessControlMode.acceptSelected -> {
|
||||
@@ -75,7 +94,6 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
}
|
||||
}
|
||||
}
|
||||
addDnsServer("172.16.0.2")
|
||||
setSession("FlClash")
|
||||
setBlocking(false)
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
@@ -93,10 +111,24 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
)
|
||||
)
|
||||
}
|
||||
establish()?.detachFd()
|
||||
TunProps(
|
||||
fd = establish()?.detachFd()
|
||||
?: throw NullPointerException("Establish VPN rejected by system"),
|
||||
gateway = "$TUN_GATEWAY/$TUN_SUBNET_PREFIX",
|
||||
gateway6 = "$TUN_GATEWAY6/$TUN_SUBNET_PREFIX6",
|
||||
portal = TUN_PORTAL,
|
||||
portal6 = TUN_PORTAL6,
|
||||
dns = TUN_DNS,
|
||||
dns6 = TUN_DNS6
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateUnderlyingNetworks( networks: Array<Network>){
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
this.setUnderlyingNetworks(networks)
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
stopSelf()
|
||||
@@ -165,7 +197,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
GlobalState.getCurrentAppPlugin()?.requestGc()
|
||||
GlobalState.getCurrentVPNPlugin()?.requestGc()
|
||||
}
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
BIN
assets/fonts/Icons.ttf
Normal file
BIN
assets/fonts/Icons.ttf
Normal file
Binary file not shown.
BIN
assets/images/icon_black.ico
Normal file
BIN
assets/images/icon_black.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
BIN
assets/images/icon_black.png
Normal file
BIN
assets/images/icon_black.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
assets/images/icon_white.ico
Normal file
BIN
assets/images/icon_white.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
Submodule core/Clash.Meta updated: 0125a90a77...e89569916a
@@ -4,6 +4,7 @@ import "C"
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
route "github.com/metacubex/mihomo/hub/route"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -26,7 +27,6 @@ import (
|
||||
cp "github.com/metacubex/mihomo/constant/provider"
|
||||
"github.com/metacubex/mihomo/hub"
|
||||
"github.com/metacubex/mihomo/hub/executor"
|
||||
"github.com/metacubex/mihomo/hub/route"
|
||||
"github.com/metacubex/mihomo/listener"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
rp "github.com/metacubex/mihomo/rules/provider"
|
||||
@@ -38,6 +38,7 @@ type ConfigExtendedParams struct {
|
||||
IsCompatible bool `json:"is-compatible"`
|
||||
SelectedMap map[string]string `json:"selected-map"`
|
||||
TestURL *string `json:"test-url"`
|
||||
OverrideDns bool `json:"override-dns"`
|
||||
}
|
||||
|
||||
type GenerateConfigParams struct {
|
||||
@@ -189,7 +190,7 @@ func toExternalProvider(p cp.Provider) (*ExternalProvider, error) {
|
||||
VehicleType: psp.VehicleType().String(),
|
||||
Count: psp.Count(),
|
||||
Path: psp.Vehicle().Path(),
|
||||
UpdateAt: psp.UpdatedAt,
|
||||
UpdateAt: psp.UpdatedAt(),
|
||||
}, nil
|
||||
case *rp.RuleSetProvider:
|
||||
rsp := p.(*rp.RuleSetProvider)
|
||||
@@ -199,7 +200,7 @@ func toExternalProvider(p cp.Provider) (*ExternalProvider, error) {
|
||||
VehicleType: rsp.VehicleType().String(),
|
||||
Count: rsp.Count(),
|
||||
Path: rsp.Vehicle().Path(),
|
||||
UpdateAt: rsp.UpdatedAt,
|
||||
UpdateAt: rsp.UpdatedAt(),
|
||||
}, nil
|
||||
default:
|
||||
return nil, errors.New("not external provider")
|
||||
@@ -379,6 +380,12 @@ func generateProxyGroupAndRule(proxyGroup *[]map[string]any, rule *[]string) {
|
||||
*rule = computedRule
|
||||
}
|
||||
|
||||
func genHosts(hosts, patchHosts map[string]any) {
|
||||
for k, v := range patchHosts {
|
||||
hosts[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) {
|
||||
targetConfig.ExternalController = patchConfig.ExternalController
|
||||
targetConfig.ExternalUI = ""
|
||||
@@ -386,7 +393,6 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
|
||||
targetConfig.ExternalUIURL = ""
|
||||
targetConfig.TCPConcurrent = patchConfig.TCPConcurrent
|
||||
targetConfig.UnifiedDelay = patchConfig.UnifiedDelay
|
||||
//targetConfig.GeodataMode = false
|
||||
targetConfig.IPv6 = patchConfig.IPv6
|
||||
targetConfig.LogLevel = patchConfig.LogLevel
|
||||
targetConfig.Port = 0
|
||||
@@ -404,24 +410,29 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
|
||||
targetConfig.Profile.StoreSelected = false
|
||||
targetConfig.GeoXUrl = patchConfig.GeoXUrl
|
||||
targetConfig.GlobalUA = patchConfig.GlobalUA
|
||||
if targetConfig.DNS.Enable == false {
|
||||
genHosts(targetConfig.Hosts, patchConfig.Hosts)
|
||||
if configParams.OverrideDns {
|
||||
targetConfig.DNS = patchConfig.DNS
|
||||
} else {
|
||||
if targetConfig.DNS.Enable == false {
|
||||
targetConfig.DNS.Enable = true
|
||||
}
|
||||
}
|
||||
//if runtime.GOOS == "android" {
|
||||
// targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, "dhcp://"+dns.SystemDNSPlaceholder)
|
||||
//} else if runtime.GOOS == "windows" {
|
||||
// targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, dns.SystemDNSPlaceholder)
|
||||
//}
|
||||
if configParams.IsCompatible == false {
|
||||
targetConfig.ProxyProvider = make(map[string]map[string]any)
|
||||
targetConfig.RuleProvider = make(map[string]map[string]any)
|
||||
generateProxyGroupAndRule(&targetConfig.ProxyGroup, &targetConfig.Rule)
|
||||
}
|
||||
//if configParams.IsCompatible == false {
|
||||
// targetConfig.ProxyProvider = make(map[string]map[string]any)
|
||||
// targetConfig.RuleProvider = make(map[string]map[string]any)
|
||||
// generateProxyGroupAndRule(&targetConfig.ProxyGroup, &targetConfig.Rule)
|
||||
//}
|
||||
}
|
||||
|
||||
func patchConfig(general *config.General) {
|
||||
func patchConfig(general *config.General, controller *config.Controller) {
|
||||
log.Infoln("[Apply] patch")
|
||||
route.ReStartServer(general.ExternalController)
|
||||
route.ReStartServer(controller.ExternalController)
|
||||
tunnel.SetSniffing(general.Sniffing)
|
||||
tunnel.SetFindProcessMode(general.FindProcessMode)
|
||||
dialer.SetTcpConcurrent(general.TCPConcurrent)
|
||||
@@ -437,6 +448,12 @@ var isRunning = false
|
||||
var runLock sync.Mutex
|
||||
|
||||
func updateListeners(general *config.General, listeners map[string]constant.InboundListener) {
|
||||
if !isRunning {
|
||||
return
|
||||
}
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
|
||||
listener.PatchInboundListeners(listeners, tunnel.Tunnel, true)
|
||||
listener.SetAllowLan(general.AllowLan)
|
||||
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
|
||||
@@ -446,14 +463,12 @@ func updateListeners(general *config.General, listeners map[string]constant.Inbo
|
||||
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.ReCreateMixed(general.MixedPort, tunnel.Tunnel)
|
||||
listener.ReCreateShadowSocks(general.ShadowSocksConfig, tunnel.Tunnel)
|
||||
listener.ReCreateVmess(general.VmessConfig, tunnel.Tunnel)
|
||||
listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel)
|
||||
listener.ReCreateTun(general.Tun, tunnel.Tunnel)
|
||||
listener.ReCreateRedirToTun(general.EBpf.RedirectToTun)
|
||||
}
|
||||
|
||||
func stopListeners() {
|
||||
@@ -515,15 +530,15 @@ func applyConfig() error {
|
||||
constant.DefaultTestURL = *configParams.TestURL
|
||||
}
|
||||
if configParams.IsPatch {
|
||||
patchConfig(cfg.General)
|
||||
patchConfig(cfg.General, cfg.Controller)
|
||||
} else {
|
||||
closeConnections()
|
||||
runtime.GC()
|
||||
hub.UltraApplyConfig(cfg)
|
||||
patchSelectGroup()
|
||||
}
|
||||
updateListeners(cfg.General, cfg.Listeners)
|
||||
if isRunning {
|
||||
updateListeners(cfg.General, cfg.Listeners)
|
||||
hcCompatibleProvider(cfg.Providers)
|
||||
}
|
||||
externalProviders = getExternalProvidersRaw()
|
||||
|
||||
20
core/dns.go
Normal file
20
core/dns.go
Normal file
@@ -0,0 +1,20 @@
|
||||
//go:build android
|
||||
|
||||
package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"github.com/metacubex/mihomo/dns"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//export updateDns
|
||||
func updateDns(s *C.char) {
|
||||
dnsList := C.GoString(s)
|
||||
go func() {
|
||||
log.Infoln("[DNS] updateDns %s", dnsList)
|
||||
dns.UpdateSystemDNS(strings.Split(dnsList, ","))
|
||||
dns.FlushCacheWithDefaultResolver()
|
||||
}()
|
||||
}
|
||||
56
core/go.mod
56
core/go.mod
@@ -4,13 +4,9 @@ go 1.21.0
|
||||
|
||||
replace github.com/metacubex/mihomo => ./Clash.Meta
|
||||
|
||||
require (
|
||||
github.com/Kr328/tun2socket v0.0.0-20220414050025-d07c78d06d34
|
||||
github.com/metacubex/mihomo v1.17.1
|
||||
github.com/miekg/dns v1.1.61
|
||||
golang.org/x/net v0.26.0
|
||||
golang.org/x/sync v0.7.0
|
||||
)
|
||||
require github.com/metacubex/mihomo v1.17.1
|
||||
|
||||
replace github.com/sagernet/sing => github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297
|
||||
|
||||
require (
|
||||
github.com/3andne/restls-client-go v0.1.6 // indirect
|
||||
@@ -20,17 +16,16 @@ require (
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/cilium/ebpf v0.12.3 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/coreos/go-iptables v0.7.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
|
||||
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gaukas/godicttls v0.0.4 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.14 // indirect
|
||||
github.com/go-chi/chi/v5 v5.1.0 // indirect
|
||||
github.com/go-chi/cors v1.2.1 // indirect
|
||||
github.com/go-chi/render v1.0.3 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
@@ -38,12 +33,12 @@ require (
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.2.0 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.3.0 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6 // indirect
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240812123929-b105c29bd1b5 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
@@ -52,19 +47,21 @@ require (
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
|
||||
github.com/metacubex/chacha v0.1.0 // indirect
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
|
||||
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec // indirect
|
||||
github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e // indirect
|
||||
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4 // indirect
|
||||
github.com/metacubex/randv2 v0.2.0 // indirect
|
||||
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72 // indirect
|
||||
github.com/metacubex/sing-shadowsocks v0.2.7 // indirect
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.1 // indirect
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240719141246-19c49ac9589d // indirect
|
||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 // indirect
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1 // indirect
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 // indirect
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a // indirect
|
||||
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 // indirect
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240826061955-1e4e67afe5cd // indirect
|
||||
github.com/metacubex/tfo-go v0.0.0-20240830120620-c5e019b67785 // indirect
|
||||
github.com/metacubex/utls v1.6.6 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
|
||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||
@@ -72,10 +69,9 @@ require (
|
||||
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.14 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.2.0 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect
|
||||
github.com/sagernet/fswatch v0.1.1 // indirect
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
|
||||
@@ -84,7 +80,7 @@ require (
|
||||
github.com/sagernet/sing-shadowtls v0.1.4 // indirect
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
|
||||
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e // indirect
|
||||
github.com/samber/lo v1.39.0 // indirect
|
||||
github.com/samber/lo v1.47.0 // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
|
||||
@@ -101,14 +97,16 @@ require (
|
||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/crypto v0.24.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/crypto v0.26.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/net v0.28.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect; indirect`
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
)
|
||||
|
||||
121
core/go.sum
121
core/go.sum
@@ -1,7 +1,5 @@
|
||||
github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08=
|
||||
github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY=
|
||||
github.com/Kr328/tun2socket v0.0.0-20220414050025-d07c78d06d34 h1:USCTqih5d1bUXUxWNS9ZD5Tx/lb0jXHEtRIIx/F9dMc=
|
||||
github.com/Kr328/tun2socket v0.0.0-20220414050025-d07c78d06d34/go.mod h1:YR9wK13TgI5ww8iKWm91MHiSoHC7Oz0U4beCCmtXqLw=
|
||||
github.com/RyuaNerin/elliptic2 v1.0.0/go.mod h1:wWB8fWrJI/6EPJkyV/r1Rj0hxUgrusmqSj8JN6yNf/A=
|
||||
github.com/RyuaNerin/go-krypto v1.2.4 h1:mXuNdK6M317aPV0llW6Xpjbo4moOlPF7Yxz4tb4b4Go=
|
||||
github.com/RyuaNerin/go-krypto v1.2.4/go.mod h1:QqCYkoutU3yInyD9INt2PGolVRsc3W4oraQadVGXJ/8=
|
||||
@@ -19,8 +17,6 @@ github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx2
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4=
|
||||
github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM=
|
||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||
github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
|
||||
@@ -28,8 +24,8 @@ github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFE
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
||||
@@ -40,14 +36,12 @@ github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBE
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4=
|
||||
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA=
|
||||
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok=
|
||||
github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
|
||||
github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
|
||||
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||
github.com/go-chi/chi/v5 v5.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0=
|
||||
github.com/go-chi/chi/v5 v5.0.14/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
@@ -65,8 +59,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/gofrs/uuid/v5 v5.2.0 h1:qw1GMx6/y8vhVsx626ImfKMuS5CvJmhIKKtuyvfajMM=
|
||||
github.com/gofrs/uuid/v5 v5.2.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk=
|
||||
github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
@@ -82,8 +76,8 @@ github.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7s
|
||||
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6 h1:dh8D8FksyMhD64mRMbUhZHWYJfNoNMCxfVq6eexleMw=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240812123929-b105c29bd1b5 h1:GkMacU5ftc+IEg1449N3UEy2XLDz58W4fkrRu2fibb8=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240812123929-b105c29bd1b5/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
@@ -92,10 +86,6 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
|
||||
@@ -106,34 +96,38 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/
|
||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
|
||||
github.com/metacubex/chacha v0.1.0 h1:tg9RSJ18NvL38cCWNyYH1eiG6qDCyyXIaTLQthon0sc=
|
||||
github.com/metacubex/chacha v0.1.0/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
|
||||
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec h1:HxreOiFTUrJXJautEo8rnE1uKTVGY8wtZepY1Tii/Nc=
|
||||
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec/go.mod h1:8BVmQ+3cxjqzWElafm24rb2Ae4jRI6vAXNXWqWjfrXw=
|
||||
github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e h1:bLYn3GuRvWDcBDAkIv5kUYIhzHwafDVq635BuybnKqI=
|
||||
github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e/go.mod h1:Yza2H7Ax1rxWPUcJx0vW+oAt9EsPuSiyQFhFabUPzwU=
|
||||
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4 h1:CgdUBRxmNlxEGkp35HwvgQ10jwOOUJKWdOxpi8yWi8o=
|
||||
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4/go.mod h1:Y7yRGqFE6UQL/3aKPYmiYdjfVkeujJaStP4+jiZMcN8=
|
||||
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
||||
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
||||
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72 h1:Wr4g1HCb5Z/QIFwFiVNjO2qL+dRu25+Mdn9xtAZZ+ew=
|
||||
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.7 h1:9f3Dt2+71TNp0e202llA2ug5h/rkWs2EZxQ5IMpf+9g=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.7/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.1 h1:XIZBXlazp8EEoPp1S0DViAhLkJakjQ2f+AOwwdKKNYg=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.1/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240719141246-19c49ac9589d h1:iYlepjRCYlPXtELupDL+pQjGqkCnQz4KQOfKImP9sog=
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240719141246-19c49ac9589d/go.mod h1:olbEx9yVcaw5tHTNlRamRoxmMKcvDvcVS1YLnQGzvWE=
|
||||
github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297 h1:YG/JkwGPbca5rUtEMHIu8ZuqzR7BSVm1iqY8hNoMeMA=
|
||||
github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 h1:HobpULaPK6OoxrHMmgcwLkwwIduXVmwdcznwUfH1GQM=
|
||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1 h1:ypfofGDZbP8p3Y4P/m74JYu7sQViesi3c8nbmT6cS0Y=
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1/go.mod h1:olbEx9yVcaw5tHTNlRamRoxmMKcvDvcVS1YLnQGzvWE=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a h1:NpSGclHJUYndUwBmyIpFBSoBVg8PoVX7QQKhYg0DjM0=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a/go.mod h1:uY+BYb0UEknLrqvbGcwi9i++KgrKxsurysgI6G1Pveo=
|
||||
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 h1:as/aO/fM8nv4W4pOr9EETP6kV/Oaujk3fUNyQSJK61c=
|
||||
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240826061955-1e4e67afe5cd h1:r7alry8u4qlUFLNMwGvG1A8ZcfPM6AMSmrm6E2yKdB4=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240826061955-1e4e67afe5cd/go.mod h1:uY+BYb0UEknLrqvbGcwi9i++KgrKxsurysgI6G1Pveo=
|
||||
github.com/metacubex/tfo-go v0.0.0-20240830120620-c5e019b67785 h1:NNmI+ZV0DzNuqaAInRQuZFLHlWVuyHeow8jYpdKjHjo=
|
||||
github.com/metacubex/tfo-go v0.0.0-20240830120620-c5e019b67785/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts=
|
||||
github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8=
|
||||
github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo=
|
||||
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
|
||||
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
|
||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
|
||||
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
|
||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
|
||||
@@ -156,25 +150,18 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.2.0 h1:9AzuUeF88YC5bK8u2vEG1Fpvu4wgpM1wfPIExfaaDxQ=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.2.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0=
|
||||
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=
|
||||
github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
|
||||
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
|
||||
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
|
||||
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
|
||||
github.com/sagernet/sing v0.5.0-alpha.13 h1:fpR4TFZfu/9V3LbHSAnnnwcaXGMF8ijmAAPoY2WHSKw=
|
||||
github.com/sagernet/sing v0.5.0-alpha.13/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 h1:5bCAkvDDzSMITiHFjolBwpdqYsvycdTu71FsMEFXQ14=
|
||||
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ=
|
||||
github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
|
||||
@@ -183,8 +170,8 @@ github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxe
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
|
||||
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e h1:iGH0RMv2FzELOFNFQtvsxH7NPmlo7X5JizEK51UCojo=
|
||||
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e/go.mod h1:YbL4TKHRR6APYQv3U2RGfwLDpPYSyWz6oUlpISBEzBE=
|
||||
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
|
||||
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
@@ -200,9 +187,15 @@ github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
@@ -230,21 +223,21 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -260,19 +253,19 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
|
||||
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
|
||||
23
core/hub.go
23
core/hub.go
@@ -5,9 +5,11 @@ package main
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
bridge "core/dart-bridge"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
@@ -18,7 +20,6 @@ import (
|
||||
"github.com/metacubex/mihomo/adapter"
|
||||
"github.com/metacubex/mihomo/adapter/outboundgroup"
|
||||
"github.com/metacubex/mihomo/adapter/provider"
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"github.com/metacubex/mihomo/component/updater"
|
||||
"github.com/metacubex/mihomo/config"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
@@ -27,7 +28,6 @@ import (
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"github.com/metacubex/mihomo/tunnel"
|
||||
"github.com/metacubex/mihomo/tunnel/statistic"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
var currentRawConfig = config.DefaultRawConfig()
|
||||
@@ -48,9 +48,11 @@ func start() {
|
||||
//export stop
|
||||
func stop() {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
isRunning = false
|
||||
stopListeners()
|
||||
go func() {
|
||||
defer runLock.Unlock()
|
||||
isRunning = false
|
||||
stopListeners()
|
||||
}()
|
||||
}
|
||||
|
||||
//export initClash
|
||||
@@ -223,11 +225,13 @@ func asyncTestDelay(s *C.char, port C.longlong) {
|
||||
var params = &TestDelayParams{}
|
||||
err := json.Unmarshal([]byte(paramsString), params)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, "")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
expectedStatus, err := utils.NewUnsignedRanges[uint16]("")
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, "")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -374,27 +378,28 @@ func updateGeoData(geoType *C.char, geoName *C.char, port C.longlong) {
|
||||
geoTypeString := C.GoString(geoType)
|
||||
geoNameString := C.GoString(geoName)
|
||||
go func() {
|
||||
path := constant.Path.Resolve(geoNameString)
|
||||
switch geoTypeString {
|
||||
case "MMDB":
|
||||
err := updater.UpdateMMDB(constant.Path.Resolve(geoNameString))
|
||||
err := updater.UpdateMMDBWithPath(path)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
case "ASN":
|
||||
err := updater.UpdateASN(constant.Path.Resolve(geoNameString))
|
||||
err := updater.UpdateASNWithPath(path)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
case "GeoIp":
|
||||
err := updater.UpdateGeoIp(constant.Path.Resolve(geoNameString))
|
||||
err := updater.UpdateGeoIpWithPath(path)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
case "GeoSite":
|
||||
err := updater.UpdateGeoSite(constant.Path.Resolve(geoNameString))
|
||||
err := updater.UpdateGeoSiteWithPath(path)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
|
||||
55
core/tun.go
55
core/tun.go
@@ -6,7 +6,9 @@ import "C"
|
||||
import (
|
||||
"core/platform"
|
||||
t "core/tun"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/metacubex/mihomo/listener/sing_tun"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -15,11 +17,9 @@ import (
|
||||
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"golang.org/x/sync/semaphore"
|
||||
)
|
||||
|
||||
var tunLock sync.Mutex
|
||||
var tun *t.Tun
|
||||
var runTime *time.Time
|
||||
|
||||
type FdMap struct {
|
||||
@@ -35,13 +35,18 @@ func (cm *FdMap) Load(key int64) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
var fdMap FdMap
|
||||
var (
|
||||
tunListener *sing_tun.Listener
|
||||
fdMap FdMap
|
||||
fdCounter int64 = 0
|
||||
)
|
||||
|
||||
//export startTUN
|
||||
func startTUN(fd C.int, port C.longlong) {
|
||||
func startTUN(s *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
ServicePort = i
|
||||
if fd == 0 {
|
||||
paramsString := C.GoString(s)
|
||||
if paramsString == "" {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
now := time.Now()
|
||||
@@ -57,28 +62,22 @@ func startTUN(fd C.int, port C.longlong) {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
|
||||
if tun != nil {
|
||||
tun.Close()
|
||||
tun = nil
|
||||
}
|
||||
|
||||
f := int(fd)
|
||||
gateway := "172.16.0.1/30"
|
||||
portal := "172.16.0.2"
|
||||
dns := "0.0.0.0"
|
||||
|
||||
tempTun := &t.Tun{Closed: false, Limit: semaphore.NewWeighted(4)}
|
||||
|
||||
closer, err := t.Start(f, gateway, portal, dns)
|
||||
|
||||
var tunProps = &t.Props{}
|
||||
err := json.Unmarshal([]byte(paramsString), tunProps)
|
||||
if err != nil {
|
||||
log.Errorln("startTUN error: %v", err)
|
||||
tempTun.Close()
|
||||
return
|
||||
}
|
||||
|
||||
tempTun.Closer = closer
|
||||
tunListener, err = t.Start(*tunProps)
|
||||
|
||||
tun = tempTun
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if tunListener != nil {
|
||||
log.Infoln("TUN address: %v", tunListener.Address())
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
@@ -108,10 +107,8 @@ func stopTun() {
|
||||
|
||||
runTime = nil
|
||||
|
||||
if tun != nil {
|
||||
log.Errorln("[Tun] stopTun")
|
||||
tun.Close()
|
||||
tun = nil
|
||||
if tunListener != nil {
|
||||
_ = tunListener.Close()
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -138,18 +135,12 @@ func markSocket(fd Fd) {
|
||||
})
|
||||
}
|
||||
|
||||
var fdCounter int64 = 0
|
||||
|
||||
func initSocketHook() {
|
||||
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
|
||||
if platform.ShouldBlockConnection() {
|
||||
return errBlocked
|
||||
}
|
||||
return conn.Control(func(fd uintptr) {
|
||||
if tun == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fdInt := int64(fd)
|
||||
timeout := time.After(100 * time.Millisecond)
|
||||
id := atomic.AddInt64(&fdCounter, 1)
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
//go:build android
|
||||
|
||||
package tun
|
||||
|
||||
import (
|
||||
"github.com/metacubex/mihomo/dns"
|
||||
D "github.com/miekg/dns"
|
||||
"net"
|
||||
)
|
||||
|
||||
func shouldHijackDns(dns net.IP, target net.IP, targetPort int) bool {
|
||||
if targetPort != 53 {
|
||||
return false
|
||||
}
|
||||
|
||||
return net.IPv4zero.Equal(dns) || target.Equal(dns)
|
||||
}
|
||||
|
||||
func relayDns(payload []byte) ([]byte, error) {
|
||||
msg := &D.Msg{}
|
||||
if err := msg.Unpack(payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r, err := dns.ServeDNSWithDefaultServer(msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.SetRcode(msg, r.Rcode)
|
||||
|
||||
return r.Pack()
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
//go:build android
|
||||
|
||||
package tun
|
||||
|
||||
import (
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
"net"
|
||||
)
|
||||
|
||||
func createMetadata(lAddr, rAddr *net.TCPAddr) *constant.Metadata {
|
||||
return &constant.Metadata{
|
||||
NetWork: constant.TCP,
|
||||
Type: constant.SOCKS5,
|
||||
SrcIP: lAddr.AddrPort().Addr(),
|
||||
DstIP: rAddr.AddrPort().Addr(),
|
||||
SrcPort: uint16(lAddr.Port),
|
||||
DstPort: uint16(rAddr.Port),
|
||||
Host: "",
|
||||
}
|
||||
}
|
||||
210
core/tun/tun.go
210
core/tun/tun.go
@@ -4,183 +4,65 @@ package tun
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"github.com/Kr328/tun2socket"
|
||||
"github.com/Kr328/tun2socket/nat"
|
||||
"github.com/metacubex/mihomo/adapter/inbound"
|
||||
"github.com/metacubex/mihomo/common/pool"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
LC "github.com/metacubex/mihomo/listener/config"
|
||||
"github.com/metacubex/mihomo/listener/sing_tun"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"github.com/metacubex/mihomo/transport/socks5"
|
||||
"github.com/metacubex/mihomo/tunnel"
|
||||
"golang.org/x/sync/semaphore"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type Tun struct {
|
||||
Closer io.Closer
|
||||
|
||||
Closed bool
|
||||
Limit *semaphore.Weighted
|
||||
type Props struct {
|
||||
Fd int `json:"fd"`
|
||||
Gateway string `json:"gateway"`
|
||||
Gateway6 string `json:"gateway6"`
|
||||
Portal string `json:"portal"`
|
||||
Portal6 string `json:"portal6"`
|
||||
Dns string `json:"dns"`
|
||||
Dns6 string `json:"dns6"`
|
||||
}
|
||||
|
||||
func (t *Tun) Close() {
|
||||
_ = t.Limit.Acquire(context.TODO(), 4)
|
||||
defer t.Limit.Release(4)
|
||||
|
||||
t.Closed = true
|
||||
|
||||
if t.Closer != nil {
|
||||
_ = t.Closer.Close()
|
||||
}
|
||||
}
|
||||
|
||||
var _, ipv4LoopBack, _ = net.ParseCIDR("127.0.0.0/8")
|
||||
|
||||
func Start(fd int, gateway, portal, dns string) (io.Closer, error) {
|
||||
device := os.NewFile(uintptr(fd), "/dev/tun")
|
||||
ip, network, err := net.ParseCIDR(gateway)
|
||||
func Start(tunProps Props) (*sing_tun.Listener, error) {
|
||||
var prefix4 []netip.Prefix
|
||||
tempPrefix4, err := netip.ParsePrefix(tunProps.Gateway)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
} else {
|
||||
network.IP = ip
|
||||
log.Errorln("startTUN error:", err)
|
||||
return nil, err
|
||||
}
|
||||
stack, err := tun2socket.StartTun2Socket(device, network, net.ParseIP(portal))
|
||||
prefix4 = append(prefix4, tempPrefix4)
|
||||
|
||||
var prefix6 []netip.Prefix
|
||||
tempPrefix6, err := netip.ParsePrefix(tunProps.Gateway6)
|
||||
if err != nil {
|
||||
log.Errorln("startTUN error:", err)
|
||||
return nil, err
|
||||
}
|
||||
prefix6 = append(prefix6, tempPrefix6)
|
||||
|
||||
var dnsHijack []string
|
||||
dnsHijack = append(dnsHijack, net.JoinHostPort(tunProps.Dns, "53"))
|
||||
dnsHijack = append(dnsHijack, net.JoinHostPort(tunProps.Dns6, "53"))
|
||||
|
||||
options := LC.Tun{
|
||||
Enable: true,
|
||||
Device: sing_tun.InterfaceName,
|
||||
Stack: constant.TunSystem,
|
||||
DNSHijack: dnsHijack,
|
||||
AutoRoute: false,
|
||||
AutoDetectInterface: false,
|
||||
Inet4Address: prefix4,
|
||||
Inet6Address: prefix6,
|
||||
MTU: 9000,
|
||||
FileDescriptor: tunProps.Fd,
|
||||
}
|
||||
|
||||
listener, err := sing_tun.New(options, tunnel.Tunnel)
|
||||
|
||||
if err != nil {
|
||||
_ = device.Close()
|
||||
log.Errorln("startTUN error:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dnsAddr := net.ParseIP(dns)
|
||||
|
||||
tcp := func() {
|
||||
defer func(tcp *nat.TCP) {
|
||||
_ = tcp.Close()
|
||||
}(stack.TCP())
|
||||
defer log.Debugln("TCP: closed")
|
||||
|
||||
for stack.TCP().SetDeadline(time.Time{}) == nil {
|
||||
conn, err := stack.TCP().Accept()
|
||||
if err != nil {
|
||||
log.Errorln("Accept connection: %v", err)
|
||||
continue
|
||||
}
|
||||
lAddr := conn.LocalAddr().(*net.TCPAddr)
|
||||
rAddr := conn.RemoteAddr().(*net.TCPAddr)
|
||||
|
||||
if ipv4LoopBack.Contains(rAddr.IP) {
|
||||
_ = conn.Close()
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if shouldHijackDns(dnsAddr, rAddr.IP, rAddr.Port) {
|
||||
go func() {
|
||||
defer func(conn net.Conn) {
|
||||
_ = conn.Close()
|
||||
}(conn)
|
||||
|
||||
buf := pool.Get(pool.UDPBufferSize)
|
||||
defer func(buf []byte) {
|
||||
_ = pool.Put(buf)
|
||||
}(buf)
|
||||
|
||||
for {
|
||||
_ = conn.SetReadDeadline(time.Now().Add(constant.DefaultTCPTimeout))
|
||||
|
||||
length := uint16(0)
|
||||
if err := binary.Read(conn, binary.BigEndian, &length); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if int(length) > len(buf) {
|
||||
return
|
||||
}
|
||||
|
||||
n, err := conn.Read(buf[:length])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
msg, err := relayDns(buf[:n])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = conn.Write(msg)
|
||||
}
|
||||
}()
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
go tunnel.Tunnel.HandleTCPConn(conn, createMetadata(lAddr, rAddr))
|
||||
}
|
||||
}
|
||||
|
||||
udp := func() {
|
||||
defer func(udp *nat.UDP) {
|
||||
_ = udp.Close()
|
||||
}(stack.UDP())
|
||||
defer log.Debugln("UDP: closed")
|
||||
|
||||
for {
|
||||
buf := pool.Get(pool.UDPBufferSize)
|
||||
|
||||
n, lRAddr, rRAddr, err := stack.UDP().ReadFrom(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
raw := buf[:n]
|
||||
lAddr := lRAddr.(*net.UDPAddr)
|
||||
rAddr := rRAddr.(*net.UDPAddr)
|
||||
|
||||
if ipv4LoopBack.Contains(rAddr.IP) {
|
||||
_ = pool.Put(buf)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if shouldHijackDns(dnsAddr, rAddr.IP, rAddr.Port) {
|
||||
go func() {
|
||||
defer func(buf []byte) {
|
||||
_ = pool.Put(buf)
|
||||
}(buf)
|
||||
|
||||
msg, err := relayDns(raw)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = stack.UDP().WriteTo(msg, rAddr, lAddr)
|
||||
}()
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
pkt := &packet{
|
||||
local: lAddr,
|
||||
data: raw,
|
||||
writeBack: func(b []byte, addr net.Addr) (int, error) {
|
||||
return stack.UDP().WriteTo(b, addr, lAddr)
|
||||
},
|
||||
drop: func() {
|
||||
_ = pool.Put(buf)
|
||||
},
|
||||
}
|
||||
|
||||
tunnel.Tunnel.HandleUDPPacket(inbound.NewPacket(socks5.ParseAddrToSocksAddr(rAddr), pkt, constant.SOCKS5))
|
||||
}
|
||||
}
|
||||
|
||||
go tcp()
|
||||
go udp()
|
||||
|
||||
return stack, nil
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
//go:build android
|
||||
|
||||
package tun
|
||||
|
||||
import "net"
|
||||
|
||||
type packet struct {
|
||||
local *net.UDPAddr
|
||||
data []byte
|
||||
writeBack func(b []byte, addr net.Addr) (int, error)
|
||||
drop func()
|
||||
}
|
||||
|
||||
func (pkt *packet) Data() []byte {
|
||||
return pkt.data
|
||||
}
|
||||
|
||||
func (pkt *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) {
|
||||
return pkt.writeBack(b, addr)
|
||||
}
|
||||
|
||||
func (pkt *packet) Drop() {
|
||||
pkt.drop()
|
||||
}
|
||||
|
||||
func (pkt *packet) LocalAddr() net.Addr {
|
||||
return pkt.local
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
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/manager/hotkey_manager.dart';
|
||||
import 'package:fl_clash/manager/manager.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';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -53,6 +54,7 @@ class Application extends StatefulWidget {
|
||||
|
||||
class ApplicationState extends State<Application> {
|
||||
late SystemColorSchemes systemColorSchemes;
|
||||
Timer? timer;
|
||||
|
||||
final _pageTransitionsTheme = const PageTransitionsTheme(
|
||||
builders: <TargetPlatform, PageTransitionsBuilder>{
|
||||
@@ -81,6 +83,7 @@ class ApplicationState extends State<Application> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initTimer();
|
||||
globalState.appController = AppController(context);
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
final currentContext = globalState.navigatorKey.currentContext;
|
||||
@@ -92,23 +95,52 @@ class ApplicationState extends State<Application> {
|
||||
});
|
||||
}
|
||||
|
||||
_initTimer() {
|
||||
_cancelTimer();
|
||||
timer = Timer.periodic(const Duration(milliseconds: 20000), (_) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
globalState.appController.updateGroupDebounce();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_cancelTimer() {
|
||||
if (timer != null) {
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
_buildApp(Widget app) {
|
||||
if (system.isDesktop) {
|
||||
return WindowContainer(
|
||||
child: TrayContainer(
|
||||
child: ProxyContainer(
|
||||
child: app,
|
||||
return WindowManager(
|
||||
child: TrayManager(
|
||||
child: HotKeyManager(
|
||||
child: ProxyManager(
|
||||
child: app,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return AndroidContainer(
|
||||
child: TileContainer(
|
||||
return AndroidManager(
|
||||
child: TileManager(
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_buildPage(Widget page) {
|
||||
if (system.isDesktop) {
|
||||
return WindowHeaderContainer(
|
||||
child: page,
|
||||
);
|
||||
}
|
||||
return VpnManager(
|
||||
child: page,
|
||||
);
|
||||
}
|
||||
|
||||
_updateSystemColorSchemes(
|
||||
ColorScheme? lightDynamic,
|
||||
ColorScheme? darkDynamic,
|
||||
@@ -125,8 +157,8 @@ class ApplicationState extends State<Application> {
|
||||
@override
|
||||
Widget build(context) {
|
||||
return _buildApp(
|
||||
AppStateContainer(
|
||||
child: ClashContainer(
|
||||
AppStateManager(
|
||||
child: ClashManager(
|
||||
child: Selector2<AppState, Config, ApplicationSelectorState>(
|
||||
selector: (_, appState, config) => ApplicationSelectorState(
|
||||
locale: config.locale,
|
||||
@@ -147,10 +179,9 @@ class ApplicationState extends State<Application> {
|
||||
GlobalWidgetsLocalizations.delegate
|
||||
],
|
||||
builder: (_, child) {
|
||||
if (system.isDesktop) {
|
||||
return WindowHeaderContainer(child: child!);
|
||||
}
|
||||
return child!;
|
||||
return MediaManager(
|
||||
child: _buildPage(child!),
|
||||
);
|
||||
},
|
||||
scrollBehavior: BaseScrollBehavior(),
|
||||
title: appName,
|
||||
@@ -193,5 +224,6 @@ class ApplicationState extends State<Application> {
|
||||
linkManager.destroy();
|
||||
await globalState.appController.savePreferences();
|
||||
super.dispose();
|
||||
_cancelTimer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ class ClashCore {
|
||||
final externalProviderRawString =
|
||||
externalProviderRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(externalProviderRaw);
|
||||
if(externalProviderRawString.isEmpty) return null;
|
||||
if (externalProviderRawString.isEmpty) return null;
|
||||
return ExternalProvider.fromJson(json.decode(externalProviderRawString));
|
||||
}
|
||||
|
||||
@@ -322,9 +322,18 @@ class ClashCore {
|
||||
clashFFI.stopLog();
|
||||
}
|
||||
|
||||
startTun(int fd, int port) {
|
||||
startTun(TunProps? tunProps, int port) {
|
||||
if (!Platform.isAndroid) return;
|
||||
clashFFI.startTUN(fd, port);
|
||||
final tunPropsChar = json.encode(tunProps).toNativeUtf8().cast<Char>();
|
||||
clashFFI.startTUN(tunPropsChar, port);
|
||||
malloc.free(tunPropsChar);
|
||||
}
|
||||
|
||||
updateDns(String dns) {
|
||||
if (!Platform.isAndroid) return;
|
||||
final dnsChar = dns.toNativeUtf8().cast<Char>();
|
||||
clashFFI.updateDns(dnsChar);
|
||||
malloc.free(dnsChar);
|
||||
}
|
||||
|
||||
requestGc() {
|
||||
|
||||
@@ -5264,6 +5264,20 @@ class ClashFFI {
|
||||
late final _getProxies =
|
||||
_getProxiesPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
void updateDns(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
) {
|
||||
return _updateDns(
|
||||
s,
|
||||
);
|
||||
}
|
||||
|
||||
late final _updateDnsPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'updateDns');
|
||||
late final _updateDns =
|
||||
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
void changeProxy(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
) {
|
||||
@@ -5567,19 +5581,20 @@ class ClashFFI {
|
||||
_setStatePtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
void startTUN(
|
||||
int fd,
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
int port,
|
||||
) {
|
||||
return _startTUN(
|
||||
fd,
|
||||
s,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _startTUNPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int, ffi.LongLong)>>(
|
||||
'startTUN');
|
||||
late final _startTUN = _startTUNPtr.asFunction<void Function(int, int)>();
|
||||
late final _startTUNPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.LongLong)>>('startTUN');
|
||||
late final _startTUN =
|
||||
_startTUNPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getRunTime() {
|
||||
return _getRunTime();
|
||||
|
||||
@@ -25,4 +25,7 @@ export 'package.dart';
|
||||
export 'measure.dart';
|
||||
export 'windows.dart';
|
||||
export 'iterable.dart';
|
||||
export 'scroll.dart';
|
||||
export 'scroll.dart';
|
||||
export 'icons.dart';
|
||||
export 'http.dart';
|
||||
export 'keyboard.dart';
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
@@ -16,7 +17,11 @@ const mmdbFileName = "geoip.metadb";
|
||||
const asnFileName = "ASN.mmdb";
|
||||
const geoIpFileName = "GeoIP.dat";
|
||||
const geoSiteFileName = "GeoSite.dat";
|
||||
final double kHeaderHeight = system.isDesktop ? 40 : 0;
|
||||
final double kHeaderHeight = system.isDesktop
|
||||
? !Platform.isMacOS
|
||||
? 40
|
||||
: 26
|
||||
: 0;
|
||||
const GeoXMap defaultGeoXMap = {
|
||||
"mmdb":
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:webdav_client/webdav_client.dart';
|
||||
class DAVClient {
|
||||
late Client client;
|
||||
Completer<bool> pingCompleter = Completer();
|
||||
late String fileName;
|
||||
|
||||
DAVClient(DAV dav) {
|
||||
client = newClient(
|
||||
@@ -15,6 +16,7 @@ class DAVClient {
|
||||
user: dav.user,
|
||||
password: dav.password,
|
||||
);
|
||||
fileName = dav.fileName;
|
||||
client.setHeaders(
|
||||
{
|
||||
'accept-charset': 'utf-8',
|
||||
@@ -38,7 +40,7 @@ class DAVClient {
|
||||
|
||||
get root => "/$appName";
|
||||
|
||||
get backupFile => "$root/backup.zip";
|
||||
get backupFile => "$root/$fileName";
|
||||
|
||||
backup(Uint8List data) async {
|
||||
await client.mkdir("$root");
|
||||
|
||||
21
lib/common/http.dart
Normal file
21
lib/common/http.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../state.dart';
|
||||
|
||||
class FlClashHttpOverrides extends HttpOverrides {
|
||||
@override
|
||||
HttpClient createHttpClient(SecurityContext? context) {
|
||||
final client = super.createHttpClient(context);
|
||||
client.badCertificateCallback = (_, __, ___) => true;
|
||||
client.findProxy = (url) {
|
||||
debugPrint("find $url");
|
||||
final port = globalState.appController.clashConfig.mixedPort;
|
||||
final isStart = globalState.appController.appState.isStart;
|
||||
if (!isStart) return "DIRECT";
|
||||
return "PROXY localhost:$port";
|
||||
};
|
||||
return client;
|
||||
}
|
||||
}
|
||||
6
lib/common/icons.dart
Normal file
6
lib/common/icons.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class IconsExt{
|
||||
static const IconData target =
|
||||
IconData(0xe900, fontFamily: "Icons");
|
||||
}
|
||||
106
lib/common/keyboard.dart
Normal file
106
lib/common/keyboard.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:uni_platform/uni_platform.dart';
|
||||
|
||||
final Map<PhysicalKeyboardKey, String> _knownKeyLabels =
|
||||
<PhysicalKeyboardKey, String>{
|
||||
PhysicalKeyboardKey.keyA: 'A',
|
||||
PhysicalKeyboardKey.keyB: 'B',
|
||||
PhysicalKeyboardKey.keyC: 'C',
|
||||
PhysicalKeyboardKey.keyD: 'D',
|
||||
PhysicalKeyboardKey.keyE: 'E',
|
||||
PhysicalKeyboardKey.keyF: 'F',
|
||||
PhysicalKeyboardKey.keyG: 'G',
|
||||
PhysicalKeyboardKey.keyH: 'H',
|
||||
PhysicalKeyboardKey.keyI: 'I',
|
||||
PhysicalKeyboardKey.keyJ: 'J',
|
||||
PhysicalKeyboardKey.keyK: 'K',
|
||||
PhysicalKeyboardKey.keyL: 'L',
|
||||
PhysicalKeyboardKey.keyM: 'M',
|
||||
PhysicalKeyboardKey.keyN: 'N',
|
||||
PhysicalKeyboardKey.keyO: 'O',
|
||||
PhysicalKeyboardKey.keyP: 'P',
|
||||
PhysicalKeyboardKey.keyQ: 'Q',
|
||||
PhysicalKeyboardKey.keyR: 'R',
|
||||
PhysicalKeyboardKey.keyS: 'S',
|
||||
PhysicalKeyboardKey.keyT: 'T',
|
||||
PhysicalKeyboardKey.keyU: 'U',
|
||||
PhysicalKeyboardKey.keyV: 'V',
|
||||
PhysicalKeyboardKey.keyW: 'W',
|
||||
PhysicalKeyboardKey.keyX: 'X',
|
||||
PhysicalKeyboardKey.keyY: 'Y',
|
||||
PhysicalKeyboardKey.keyZ: 'Z',
|
||||
PhysicalKeyboardKey.digit1: '1',
|
||||
PhysicalKeyboardKey.digit2: '2',
|
||||
PhysicalKeyboardKey.digit3: '3',
|
||||
PhysicalKeyboardKey.digit4: '4',
|
||||
PhysicalKeyboardKey.digit5: '5',
|
||||
PhysicalKeyboardKey.digit6: '6',
|
||||
PhysicalKeyboardKey.digit7: '7',
|
||||
PhysicalKeyboardKey.digit8: '8',
|
||||
PhysicalKeyboardKey.digit9: '9',
|
||||
PhysicalKeyboardKey.digit0: '0',
|
||||
PhysicalKeyboardKey.enter: 'ENTER',
|
||||
PhysicalKeyboardKey.escape: 'ESCAPE',
|
||||
PhysicalKeyboardKey.backspace: 'BACKSPACE',
|
||||
PhysicalKeyboardKey.tab: 'TAB',
|
||||
PhysicalKeyboardKey.space: 'SPACE',
|
||||
PhysicalKeyboardKey.minus: '-',
|
||||
PhysicalKeyboardKey.equal: '=',
|
||||
PhysicalKeyboardKey.bracketLeft: '[',
|
||||
PhysicalKeyboardKey.bracketRight: ']',
|
||||
PhysicalKeyboardKey.backslash: '\\',
|
||||
PhysicalKeyboardKey.semicolon: ';',
|
||||
PhysicalKeyboardKey.quote: '"',
|
||||
PhysicalKeyboardKey.backquote: '`',
|
||||
PhysicalKeyboardKey.comma: ',',
|
||||
PhysicalKeyboardKey.period: '.',
|
||||
PhysicalKeyboardKey.slash: '/',
|
||||
PhysicalKeyboardKey.capsLock: 'CAPSLOCK',
|
||||
PhysicalKeyboardKey.f1: 'F1',
|
||||
PhysicalKeyboardKey.f2: 'F2',
|
||||
PhysicalKeyboardKey.f3: 'F3',
|
||||
PhysicalKeyboardKey.f4: 'F4',
|
||||
PhysicalKeyboardKey.f5: 'F5',
|
||||
PhysicalKeyboardKey.f6: 'F6',
|
||||
PhysicalKeyboardKey.f7: 'F7',
|
||||
PhysicalKeyboardKey.f8: 'F8',
|
||||
PhysicalKeyboardKey.f9: 'F9',
|
||||
PhysicalKeyboardKey.f10: 'F10',
|
||||
PhysicalKeyboardKey.f11: 'F11',
|
||||
PhysicalKeyboardKey.f12: 'F12',
|
||||
PhysicalKeyboardKey.home: 'HOME',
|
||||
PhysicalKeyboardKey.pageUp: 'PAGEUP',
|
||||
PhysicalKeyboardKey.delete: 'DELETE',
|
||||
PhysicalKeyboardKey.end: 'END',
|
||||
PhysicalKeyboardKey.pageDown: 'PAGEDOWN',
|
||||
PhysicalKeyboardKey.arrowRight: '→',
|
||||
PhysicalKeyboardKey.arrowLeft: '←',
|
||||
PhysicalKeyboardKey.arrowDown: '↓',
|
||||
PhysicalKeyboardKey.arrowUp: '↑',
|
||||
PhysicalKeyboardKey.controlLeft: "CTRL",
|
||||
PhysicalKeyboardKey.shiftLeft: 'SHIFT',
|
||||
PhysicalKeyboardKey.altLeft: "ALT",
|
||||
PhysicalKeyboardKey.metaLeft: Platform.isMacOS ? '⌘' : 'WIN',
|
||||
PhysicalKeyboardKey.controlRight: "CTRL",
|
||||
PhysicalKeyboardKey.shiftRight: 'SHIFT',
|
||||
PhysicalKeyboardKey.altRight: "ALT",
|
||||
PhysicalKeyboardKey.metaRight: Platform.isMacOS ? '⌘' : 'WIN',
|
||||
PhysicalKeyboardKey.fn: 'FN',
|
||||
};
|
||||
|
||||
extension KeyboardKeyExt on KeyboardKey {
|
||||
String get label {
|
||||
PhysicalKeyboardKey? physicalKey;
|
||||
if (this is LogicalKeyboardKey) {
|
||||
physicalKey = (this as LogicalKeyboardKey).physicalKey;
|
||||
} else if (this is PhysicalKeyboardKey) {
|
||||
physicalKey = this as PhysicalKeyboardKey;
|
||||
}
|
||||
return _knownKeyLabels[physicalKey] ?? physicalKey?.debugName ?? 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:fl_clash/models/models.dart' hide Process;
|
||||
import 'package:launch_at_startup/launch_at_startup.dart';
|
||||
|
||||
import 'constant.dart';
|
||||
import 'system.dart';
|
||||
import 'windows.dart';
|
||||
|
||||
class AutoLaunch {
|
||||
static AutoLaunch? _instance;
|
||||
@@ -34,6 +36,9 @@ class AutoLaunch {
|
||||
}
|
||||
|
||||
Future<bool> enable() async {
|
||||
if (Platform.isWindows) {
|
||||
await windowsDisable();
|
||||
}
|
||||
return await launchAtStartup.enable();
|
||||
}
|
||||
|
||||
@@ -51,45 +56,47 @@ class AutoLaunch {
|
||||
return res.exitCode == 0;
|
||||
}
|
||||
|
||||
windowsEnable() async {
|
||||
await Process.run(
|
||||
'schtasks',
|
||||
[
|
||||
'/Create',
|
||||
'/SC',
|
||||
'ONLOGON',
|
||||
'/TN',
|
||||
appName,
|
||||
'/TR',
|
||||
Platform.resolvedExecutable,
|
||||
'/RL',
|
||||
'HIGHEST',
|
||||
'/F'
|
||||
],
|
||||
runInShell: true,
|
||||
);
|
||||
Future<bool> windowsEnable() async {
|
||||
await disable();
|
||||
return windows?.runas(
|
||||
'schtasks',
|
||||
[
|
||||
'/Create',
|
||||
'/SC',
|
||||
'ONLOGON',
|
||||
'/TN',
|
||||
appName,
|
||||
'/TR',
|
||||
'"${Platform.resolvedExecutable}"',
|
||||
'/RL',
|
||||
'HIGHEST',
|
||||
'/F'
|
||||
].join(" "),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
Future<bool> disable() async {
|
||||
return await launchAtStartup.disable();
|
||||
}
|
||||
|
||||
updateStatus(bool value) async {
|
||||
final currentEnable =
|
||||
Platform.isWindows ? await windowsIsEnable : await isEnable;
|
||||
if (value == currentEnable) {
|
||||
return;
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
if (value) {
|
||||
enable();
|
||||
windowsEnable();
|
||||
updateStatus(AutoLaunchState state) async {
|
||||
final isOpenTun = state.isOpenTun;
|
||||
final isAutoLaunch = state.isAutoLaunch;
|
||||
if (Platform.isWindows && isOpenTun) {
|
||||
if (await windowsIsEnable == isAutoLaunch) return;
|
||||
if (isAutoLaunch) {
|
||||
final isEnable = await windowsEnable();
|
||||
if (!isEnable) {
|
||||
enable();
|
||||
}
|
||||
} else {
|
||||
windowsDisable();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (value == true) {
|
||||
if (await isEnable == isAutoLaunch) return;
|
||||
if (isAutoLaunch == true) {
|
||||
enable();
|
||||
} else {
|
||||
disable();
|
||||
|
||||
@@ -2,4 +2,17 @@ extension ListExtension<T> on List<T> {
|
||||
List<T> intersection(List<T> list) {
|
||||
return where((item) => list.contains(item)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
List<List<T>> batch(int maxConcurrent) {
|
||||
final batches = (length / maxConcurrent).ceil();
|
||||
final List<List<T>> res = [];
|
||||
for (int i = 0; i < batches; i++) {
|
||||
if (i != batches - 1) {
|
||||
res.add(sublist(i * maxConcurrent, maxConcurrent * (i + 1)));
|
||||
} else {
|
||||
res.add(sublist(i * maxConcurrent, length));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,23 +3,21 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Measure {
|
||||
Measure.of(this.context);
|
||||
final TextScaler _textScale;
|
||||
late BuildContext context;
|
||||
|
||||
final _textScaleFactor =
|
||||
WidgetsBinding.instance.platformDispatcher.textScaleFactor;
|
||||
Measure.of(this.context) : _textScale = MediaQuery.of(context).textScaler;
|
||||
|
||||
Size computeTextSize(Text text) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(text: text.data, style: text.style),
|
||||
maxLines: text.maxLines,
|
||||
textScaler: TextScaler.linear(_textScaleFactor),
|
||||
textScaler: _textScale,
|
||||
textDirection: text.textDirection ?? TextDirection.ltr,
|
||||
)..layout();
|
||||
return textPainter.size;
|
||||
}
|
||||
|
||||
late BuildContext context;
|
||||
|
||||
double? _bodyMediumHeight;
|
||||
double? _bodySmallHeight;
|
||||
double? _labelSmallHeight;
|
||||
|
||||
@@ -6,6 +6,7 @@ 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;
|
||||
|
||||
@@ -99,12 +100,18 @@ class Other {
|
||||
}
|
||||
}
|
||||
|
||||
String getTrayIconPath() {
|
||||
if (Platform.isWindows) {
|
||||
return "assets/images/icon.ico";
|
||||
} else {
|
||||
return "assets/images/icon_monochrome.png";
|
||||
String getTrayIconPath({
|
||||
required bool isStart,
|
||||
required Brightness brightness,
|
||||
}) {
|
||||
final suffix = Platform.isWindows ? "ico" : "png";
|
||||
if (!isStart && Platform.isWindows) {
|
||||
return switch (brightness) {
|
||||
Brightness.dark => "assets/images/icon_white.$suffix",
|
||||
Brightness.light => "assets/images/icon_black.$suffix",
|
||||
};
|
||||
}
|
||||
return "assets/images/icon.$suffix";
|
||||
}
|
||||
|
||||
int compareVersions(String version1, String version2) {
|
||||
@@ -130,6 +137,12 @@ class Other {
|
||||
return build1.compareTo(build2);
|
||||
}
|
||||
|
||||
String getPinyin(String value) {
|
||||
return value.isNotEmpty
|
||||
? PinyinHelper.getFirstWordPinyin(value.substring(0, 1))
|
||||
: "";
|
||||
}
|
||||
|
||||
Future<String?> parseQRCode(Uint8List? bytes) {
|
||||
return Isolate.run<String?>(() {
|
||||
if (bytes == null) return null;
|
||||
@@ -194,9 +207,9 @@ class Other {
|
||||
int getProxiesColumns(double viewWidth, ProxiesLayout proxiesLayout) {
|
||||
final columns = max((viewWidth / 300).ceil(), 2);
|
||||
return switch (proxiesLayout) {
|
||||
ProxiesLayout.tight => columns - 1,
|
||||
ProxiesLayout.tight => columns + 1,
|
||||
ProxiesLayout.standard => columns,
|
||||
ProxiesLayout.loose => columns + 1,
|
||||
ProxiesLayout.loose => columns - 1,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
@@ -14,12 +15,16 @@ class Picker {
|
||||
return filePickerResult?.files.first;
|
||||
}
|
||||
|
||||
Future<String?> saveFile(String fileName,Uint8List bytes) async {
|
||||
Future<String?> saveFile(String fileName, Uint8List bytes) async {
|
||||
final path = await FilePicker.platform.saveFile(
|
||||
fileName: fileName,
|
||||
initialDirectory: await appPath.getDownloadDirPath(),
|
||||
bytes: bytes,
|
||||
bytes: Platform.isAndroid ? bytes : null,
|
||||
);
|
||||
if (!Platform.isAndroid && path != null) {
|
||||
final file = await File(path).create(recursive: true);
|
||||
await file.writeAsBytes(bytes);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,50 +1,27 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/ip.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class Request {
|
||||
late final Dio _dio;
|
||||
int? _port;
|
||||
bool _isStart = false;
|
||||
String? userAgent;
|
||||
|
||||
Request() {
|
||||
_dio = Dio();
|
||||
_dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
_updateAdapter();
|
||||
return handler.next(options); // 继续请求
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_updateAdapter() {
|
||||
final port = globalState.appController.clashConfig.mixedPort;
|
||||
final isStart = globalState.appController.appState.isStart;
|
||||
if (_port != port || isStart != _isStart) {
|
||||
_port = port;
|
||||
_isStart = isStart;
|
||||
_dio.httpClientAdapter = IOHttpClientAdapter(
|
||||
createHttpClient: () {
|
||||
final client = HttpClient();
|
||||
if (!_isStart) return client;
|
||||
client.userAgent = globalState.appController.clashConfig.globalUa;
|
||||
client.findProxy = (url) {
|
||||
return "PROXY localhost:$_port;DIRECT";
|
||||
};
|
||||
return client;
|
||||
},
|
||||
validateCertificate: (_, __, ___) => true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Response> getFileResponseForUrl(String url) async {
|
||||
final response = await _dio
|
||||
.get(
|
||||
@@ -62,6 +39,19 @@ class Request {
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<MemoryImage?> getImage(String url) async {
|
||||
if (url.isEmpty) return null;
|
||||
final response = await _dio.get<Uint8List>(
|
||||
url,
|
||||
options: Options(
|
||||
responseType: ResponseType.bytes,
|
||||
),
|
||||
);
|
||||
final data = response.data;
|
||||
if (data == null) return null;
|
||||
return MemoryImage(data);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> checkForUpdate() async {
|
||||
final response = await _dio.get(
|
||||
"https://api.github.com/repos/$repository/releases/latest",
|
||||
@@ -101,7 +91,7 @@ class Request {
|
||||
return source.value(response.data!);
|
||||
}
|
||||
} catch (e) {
|
||||
if(cancelToken?.isCancelled == true){
|
||||
if (cancelToken?.isCancelled == true) {
|
||||
throw "cancelled";
|
||||
}
|
||||
continue;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import 'window.dart';
|
||||
|
||||
@@ -18,6 +20,22 @@ class System {
|
||||
bool get isDesktop =>
|
||||
Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
||||
|
||||
get isAdmin async {
|
||||
if (!Platform.isWindows) return false;
|
||||
final result = await Process.run('net', ['session'], runInShell: true);
|
||||
return result.exitCode == 0;
|
||||
}
|
||||
|
||||
Future<int> get version async {
|
||||
final deviceInfo = await DeviceInfoPlugin().deviceInfo;
|
||||
return switch (Platform.operatingSystem) {
|
||||
"macos" => (deviceInfo as MacOsDeviceInfo).majorVersion,
|
||||
"android" => (deviceInfo as AndroidDeviceInfo).version.sdkInt,
|
||||
"windows" => (deviceInfo as WindowsDeviceInfo).majorVersion,
|
||||
String() => 0
|
||||
};
|
||||
}
|
||||
|
||||
back() async {
|
||||
await app?.moveTaskToBack();
|
||||
await window?.hide();
|
||||
|
||||
18
lib/common/window.dart
Normal file → Executable file
18
lib/common/window.dart
Normal file → Executable file
@@ -9,7 +9,7 @@ import 'protocol.dart';
|
||||
import 'system.dart';
|
||||
|
||||
class Window {
|
||||
init(WindowProps props) async {
|
||||
init(WindowProps props, int version) async {
|
||||
if (Platform.isWindows) {
|
||||
await WindowsSingleInstance.ensureSingleInstance([], "FlClash");
|
||||
protocol.register("clash");
|
||||
@@ -20,8 +20,6 @@ class Window {
|
||||
WindowOptions windowOptions = WindowOptions(
|
||||
size: Size(props.width, props.height),
|
||||
minimumSize: const Size(380, 500),
|
||||
windowButtonVisibility: false,
|
||||
titleBarStyle: TitleBarStyle.hidden,
|
||||
);
|
||||
if (props.left != null || props.top != null) {
|
||||
await windowManager.setPosition(
|
||||
@@ -30,17 +28,22 @@ class Window {
|
||||
} else {
|
||||
await windowManager.setAlignment(Alignment.center);
|
||||
}
|
||||
// if(Platform.isWindows){
|
||||
// await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
|
||||
// }
|
||||
if(!Platform.isMacOS || version > 10){
|
||||
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
|
||||
}
|
||||
await windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
// await windowManager.setPreventClose(true);
|
||||
await windowManager.setPreventClose(true);
|
||||
});
|
||||
}
|
||||
|
||||
show() async {
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
await windowManager.setSkipTaskbar(false);
|
||||
}
|
||||
|
||||
Future<bool> isVisible() async {
|
||||
return await windowManager.isVisible();
|
||||
}
|
||||
|
||||
close() async {
|
||||
@@ -49,6 +52,7 @@ class Window {
|
||||
|
||||
hide() async {
|
||||
await windowManager.hide();
|
||||
await windowManager.setSkipTaskbar(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class Windows {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
void runAsAdministrator(String command, String arguments) async {
|
||||
bool runas(String command, String arguments) {
|
||||
final commandPtr = command.toNativeUtf16();
|
||||
final argumentsPtr = arguments.toNativeUtf16();
|
||||
final operationPtr = 'runas'.toNativeUtf16();
|
||||
@@ -50,8 +50,9 @@ class Windows {
|
||||
calloc.free(operationPtr);
|
||||
|
||||
if (result <= 32) {
|
||||
throw Exception('Failed to launch $command with UAC');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import 'package:fl_clash/common/archive.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lpinyin/lpinyin.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@@ -22,10 +21,10 @@ class AppController {
|
||||
late AppState appState;
|
||||
late Config config;
|
||||
late ClashConfig clashConfig;
|
||||
late Measure measure;
|
||||
late Function updateClashConfigDebounce;
|
||||
late Function updateGroupDebounce;
|
||||
late Function addCheckIpNumDebounce;
|
||||
late Function applyProfileDebounce;
|
||||
|
||||
AppController(this.context) {
|
||||
appState = context.read<AppState>();
|
||||
@@ -34,13 +33,15 @@ class AppController {
|
||||
updateClashConfigDebounce = debounce<Function()>(() async {
|
||||
await updateClashConfig();
|
||||
});
|
||||
applyProfileDebounce = debounce<Function()>(() async {
|
||||
await applyProfile(isPrue: true);
|
||||
});
|
||||
addCheckIpNumDebounce = debounce(() {
|
||||
appState.checkIpNum++;
|
||||
});
|
||||
updateGroupDebounce = debounce(() async {
|
||||
await updateGroups();
|
||||
});
|
||||
measure = Measure.of(context);
|
||||
}
|
||||
|
||||
updateStatus(bool isStart) async {
|
||||
@@ -55,8 +56,7 @@ class AppController {
|
||||
updateRunTime,
|
||||
updateTraffic,
|
||||
];
|
||||
if (Platform.isAndroid) return;
|
||||
await applyProfile(isPrue: true);
|
||||
applyProfileDebounce();
|
||||
} else {
|
||||
await globalState.handleStop();
|
||||
clashCore.resetTraffic();
|
||||
@@ -102,6 +102,7 @@ class AppController {
|
||||
final updateId = config.profiles.first.id;
|
||||
changeProfile(updateId);
|
||||
} else {
|
||||
changeProfile(null);
|
||||
updateStatus(false);
|
||||
}
|
||||
}
|
||||
@@ -229,6 +230,7 @@ class AppController {
|
||||
|
||||
handleExit() async {
|
||||
await updateStatus(false);
|
||||
await proxy?.stopProxy();
|
||||
await savePreferences();
|
||||
clashCore.shutdown();
|
||||
system.exit();
|
||||
@@ -293,6 +295,10 @@ class AppController {
|
||||
}
|
||||
|
||||
init() async {
|
||||
final isDisclaimerAccepted = await handlerDisclaimer();
|
||||
if (!isDisclaimerAccepted) {
|
||||
system.exit();
|
||||
}
|
||||
updateLogStatus();
|
||||
if (!config.silentLaunch) {
|
||||
window?.show();
|
||||
@@ -367,6 +373,51 @@ class AppController {
|
||||
);
|
||||
}
|
||||
|
||||
showSnackBar(String message) {
|
||||
globalState.showSnackBar(context, message: message);
|
||||
}
|
||||
|
||||
Future<bool> showDisclaimer() async {
|
||||
return await globalState.showCommonDialog<bool>(
|
||||
dismissible: false,
|
||||
child: AlertDialog(
|
||||
title: Text(appLocalizations.disclaimer),
|
||||
content: Container(
|
||||
width: dialogCommonWidth,
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
child: SingleChildScrollView(
|
||||
child: SelectableText(
|
||||
appLocalizations.disclaimerDesc,
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop<bool>(false);
|
||||
},
|
||||
child: Text(appLocalizations.exit),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
config.isDisclaimerAccepted = true;
|
||||
Navigator.of(context).pop<bool>(true);
|
||||
},
|
||||
child: Text(appLocalizations.agree),
|
||||
)
|
||||
],
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
Future<bool> handlerDisclaimer() async {
|
||||
if (config.isDisclaimerAccepted) {
|
||||
return true;
|
||||
}
|
||||
return showDisclaimer();
|
||||
}
|
||||
|
||||
addProfileFormURL(String url) async {
|
||||
if (globalState.navigatorKey.currentState?.canPop() ?? false) {
|
||||
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
|
||||
@@ -426,8 +477,8 @@ class AppController {
|
||||
return List.of(proxies)
|
||||
..sort(
|
||||
(a, b) => other.sortByChar(
|
||||
PinyinHelper.getPinyin(a.name),
|
||||
PinyinHelper.getPinyin(b.name),
|
||||
other.getPinyin(a.name),
|
||||
other.getPinyin(b.name),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -482,6 +533,44 @@ class AppController {
|
||||
});
|
||||
}
|
||||
|
||||
updateTun() {
|
||||
clashConfig.tun = clashConfig.tun.copyWith(
|
||||
enable: !clashConfig.tun.enable,
|
||||
);
|
||||
}
|
||||
|
||||
updateSystemProxy() {
|
||||
config.desktopProps = config.desktopProps.copyWith(
|
||||
systemProxy: !config.desktopProps.systemProxy,
|
||||
);
|
||||
}
|
||||
|
||||
updateStart() {
|
||||
updateStatus(!appState.isStart);
|
||||
}
|
||||
|
||||
updateAutoLaunch() {
|
||||
config.autoLaunch = !config.autoLaunch;
|
||||
}
|
||||
|
||||
updateVisible() async {
|
||||
final visible = await window?.isVisible();
|
||||
if (visible != null && !visible) {
|
||||
window?.show();
|
||||
} else {
|
||||
window?.hide();
|
||||
}
|
||||
}
|
||||
|
||||
updateMode() {
|
||||
final index = Mode.values.indexWhere((item) => item == clashConfig.mode);
|
||||
if (index == -1) {
|
||||
return;
|
||||
}
|
||||
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1;
|
||||
clashConfig.mode = Mode.values[nextIndex];
|
||||
}
|
||||
|
||||
recoveryData(
|
||||
List<int> data,
|
||||
RecoveryOption recoveryOption,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
// ignore_for_file: constant_identifier_names
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
|
||||
enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay }
|
||||
|
||||
enum GroupName { GLOBAL, Proxy, Auto, Fallback }
|
||||
@@ -86,6 +90,65 @@ enum CommonCardType { plain, filled }
|
||||
|
||||
enum ProxiesType { tab, list }
|
||||
|
||||
enum ProxiesLayout{ loose, standard, tight }
|
||||
enum ProxiesLayout { loose, standard, tight }
|
||||
|
||||
enum ProxyCardType { expand, shrink, min }
|
||||
|
||||
enum DnsMode {
|
||||
normal,
|
||||
@JsonValue("fake-ip")
|
||||
fakeIp,
|
||||
@JsonValue("redir-host")
|
||||
redirHost,
|
||||
hosts
|
||||
}
|
||||
|
||||
enum KeyboardModifier {
|
||||
alt([
|
||||
PhysicalKeyboardKey.altLeft,
|
||||
PhysicalKeyboardKey.altRight,
|
||||
]),
|
||||
capsLock([
|
||||
PhysicalKeyboardKey.capsLock,
|
||||
]),
|
||||
control([
|
||||
PhysicalKeyboardKey.controlLeft,
|
||||
PhysicalKeyboardKey.controlRight,
|
||||
]),
|
||||
fn([
|
||||
PhysicalKeyboardKey.fn,
|
||||
]),
|
||||
meta([
|
||||
PhysicalKeyboardKey.metaLeft,
|
||||
PhysicalKeyboardKey.metaRight,
|
||||
]),
|
||||
shift([
|
||||
PhysicalKeyboardKey.shiftLeft,
|
||||
PhysicalKeyboardKey.shiftRight,
|
||||
]);
|
||||
|
||||
final List<PhysicalKeyboardKey> physicalKeys;
|
||||
|
||||
const KeyboardModifier(this.physicalKeys);
|
||||
}
|
||||
|
||||
extension KeyboardModifierExt on KeyboardModifier {
|
||||
HotKeyModifier toHotKeyModifier() {
|
||||
return switch (this) {
|
||||
KeyboardModifier.alt => HotKeyModifier.alt,
|
||||
KeyboardModifier.capsLock => HotKeyModifier.capsLock,
|
||||
KeyboardModifier.control => HotKeyModifier.control,
|
||||
KeyboardModifier.fn => HotKeyModifier.fn,
|
||||
KeyboardModifier.meta => HotKeyModifier.meta,
|
||||
KeyboardModifier.shift => HotKeyModifier.shift,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum HotAction {
|
||||
start,
|
||||
view,
|
||||
mode,
|
||||
proxy,
|
||||
tun,
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class AboutFragment extends StatelessWidget {
|
||||
title: const Text("Telegram"),
|
||||
onTap: () {
|
||||
globalState.openUrl(
|
||||
"https://t.me/+G-veVtwBOl4wODc1",
|
||||
"https://t.me/FlClash",
|
||||
);
|
||||
},
|
||||
trailing: const Icon(Icons.launch),
|
||||
|
||||
@@ -3,8 +3,7 @@ import 'dart:typed_data';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/common/dav_client.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/config.dart';
|
||||
import 'package:fl_clash/models/dav.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/fade_box.dart';
|
||||
import 'package:fl_clash/widgets/list.dart';
|
||||
@@ -73,11 +72,11 @@ class BackupAndRecovery extends StatelessWidget {
|
||||
final res = await commonScaffoldState?.loadingRun<bool>(
|
||||
() async {
|
||||
final backupData = await globalState.appController.backupData();
|
||||
final value = await picker.saveFile(
|
||||
final value = await picker.saveFile(
|
||||
other.getBackupFileName(),
|
||||
Uint8List.fromList(backupData),
|
||||
);
|
||||
if(value == null) return false;
|
||||
if (value == null) return false;
|
||||
return true;
|
||||
},
|
||||
title: appLocalizations.backup,
|
||||
@@ -204,6 +203,24 @@ class BackupAndRecovery extends StatelessWidget {
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
ListItem.input(
|
||||
title: Text(appLocalizations.file),
|
||||
subtitle: Text(dav.fileName),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.file,
|
||||
value: dav.fileName,
|
||||
resetValue: defaultDavFileName,
|
||||
onChanged: (String? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
globalState.appController.config.dav =
|
||||
globalState.appController.config.dav?.copyWith(
|
||||
fileName: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_backupOnWebDAV(context, client);
|
||||
|
||||
@@ -1,689 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
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:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ConfigFragment extends StatefulWidget {
|
||||
const ConfigFragment({super.key});
|
||||
|
||||
@override
|
||||
State<ConfigFragment> createState() => _ConfigFragmentState();
|
||||
}
|
||||
|
||||
class _ConfigFragmentState extends State<ConfigFragment> {
|
||||
_modifyMixedPort(num mixedPort) async {
|
||||
final port = await globalState.showCommonDialog(
|
||||
child: MixedPortFormDialog(
|
||||
mixedPort: mixedPort,
|
||||
),
|
||||
);
|
||||
if (port != null && port != mixedPort && mounted) {
|
||||
try {
|
||||
final mixedPort = int.parse(port);
|
||||
if (mixedPort < 1024 || mixedPort > 49151) throw "Invalid port";
|
||||
globalState.appController.clashConfig.mixedPort = mixedPort;
|
||||
} catch (e) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.proxyPort,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_showLogLevelDialog(LogLevel value) {
|
||||
globalState.showCommonDialog(
|
||||
child: AlertDialog(
|
||||
title: Text(appLocalizations.logLevel),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 16,
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 250,
|
||||
child: Wrap(
|
||||
children: [
|
||||
for (final logLevel in LogLevel.values)
|
||||
ListItem.radio(
|
||||
delegate: RadioDelegate<LogLevel>(
|
||||
value: logLevel,
|
||||
groupValue: value,
|
||||
onChanged: (LogLevel? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.logLevel = value;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
title: Text(logLevel.name),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_showUaDialog(String? value) {
|
||||
const uas = [
|
||||
null,
|
||||
"clash-verge/v1.6.6",
|
||||
"ClashforWindows/0.19.23",
|
||||
];
|
||||
globalState.showCommonDialog(
|
||||
child: AlertDialog(
|
||||
title: const Text("UA"),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 16,
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 250,
|
||||
child: Wrap(
|
||||
children: [
|
||||
for (final ua in uas)
|
||||
ListItem.radio(
|
||||
delegate: RadioDelegate<String?>(
|
||||
value: ua,
|
||||
groupValue: value,
|
||||
onChanged: (String? value) {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.globalRealUa = value;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
title: Text(ua ?? appLocalizations.defaultText),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_modifyTestUrl(String testUrl) async {
|
||||
final newTestUrl = await globalState.showCommonDialog<String>(
|
||||
child: TestUrlFormDialog(
|
||||
testUrl: testUrl,
|
||||
),
|
||||
);
|
||||
if (newTestUrl != null && newTestUrl != testUrl && mounted) {
|
||||
try {
|
||||
if (!newTestUrl.isUrl) {
|
||||
throw "Invalid url";
|
||||
}
|
||||
globalState.appController.config.testUrl = newTestUrl;
|
||||
} catch (e) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.testUrl,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_updateKeepAliveInterval(int keepAliveInterval) async {
|
||||
final newKeepAliveIntervalString =
|
||||
await globalState.showCommonDialog<String>(
|
||||
child: KeepAliveIntervalFormDialog(
|
||||
keepAliveInterval: keepAliveInterval,
|
||||
),
|
||||
);
|
||||
if (newKeepAliveIntervalString != null &&
|
||||
newKeepAliveIntervalString != "$keepAliveInterval" &&
|
||||
mounted) {
|
||||
try {
|
||||
final newKeepAliveInterval = int.parse(newKeepAliveIntervalString);
|
||||
if (newKeepAliveInterval <= 0) {
|
||||
throw "Invalid keepAliveInterval";
|
||||
}
|
||||
globalState.appController.clashConfig.keepAliveInterval =
|
||||
newKeepAliveInterval;
|
||||
globalState.appController.updateClashConfigDebounce();
|
||||
} catch (e) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.testUrl,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _buildAppSection() {
|
||||
return generateSection(
|
||||
title: appLocalizations.app,
|
||||
items: [
|
||||
if (Platform.isAndroid) ...[
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.vpnProps.allowBypass,
|
||||
builder: (_, allowBypass, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.arrow_forward_outlined),
|
||||
title: Text(appLocalizations.allowBypass),
|
||||
subtitle: Text(appLocalizations.allowBypassDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: allowBypass,
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
final vpnProps = config.vpnProps;
|
||||
config.vpnProps = vpnProps.copyWith(
|
||||
allowBypass: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.vpnProps.systemProxy,
|
||||
builder: (_, systemProxy, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.settings_ethernet),
|
||||
title: Text(appLocalizations.systemProxy),
|
||||
subtitle: Text(appLocalizations.systemProxyDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: systemProxy,
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
final vpnProps = config.vpnProps;
|
||||
config.vpnProps = vpnProps.copyWith(
|
||||
systemProxy: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.isCloseConnections,
|
||||
builder: (_, isCloseConnections, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.auto_delete_outlined),
|
||||
title: Text(appLocalizations.autoCloseConnections),
|
||||
subtitle: Text(appLocalizations.autoCloseConnectionsDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: isCloseConnections,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.config.isCloseConnections = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.onlyProxy,
|
||||
builder: (_, onlyProxy, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.data_usage_outlined),
|
||||
title: Text(appLocalizations.onlyStatisticsProxy),
|
||||
subtitle: Text(appLocalizations.onlyStatisticsProxyDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: onlyProxy,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.config.onlyProxy = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
// Selector<Config, bool>(
|
||||
// selector: (_, config) => config.isCompatible,
|
||||
// builder: (_, isCompatible, __) {
|
||||
// return ListItem.switchItem(
|
||||
// leading: const Icon(Icons.expand_outlined),
|
||||
// title: Text(appLocalizations.compatible),
|
||||
// subtitle: Text(appLocalizations.compatibleDesc),
|
||||
// delegate: SwitchDelegate(
|
||||
// value: isCompatible,
|
||||
// onChanged: (bool value) async {
|
||||
// final appController = globalState.appController;
|
||||
// appController.config.isCompatible = value;
|
||||
// await appController.applyProfile();
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildGeneralSection() {
|
||||
return generateSection(
|
||||
title: appLocalizations.general,
|
||||
items: [
|
||||
Selector<ClashConfig, LogLevel>(
|
||||
selector: (_, clashConfig) => clashConfig.logLevel,
|
||||
builder: (_, value, __) {
|
||||
return ListItem(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: Text(appLocalizations.logLevel),
|
||||
subtitle: Text(value.name),
|
||||
onTap: () {
|
||||
_showLogLevelDialog(value);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<ClashConfig, String?>(
|
||||
selector: (_, clashConfig) => clashConfig.globalRealUa,
|
||||
builder: (_, value, __) {
|
||||
return ListItem(
|
||||
leading: const Icon(Icons.computer_outlined),
|
||||
title: const Text("UA"),
|
||||
subtitle: Text(value ?? appLocalizations.defaultText),
|
||||
onTap: () {
|
||||
_showUaDialog(value);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<ClashConfig, int>(
|
||||
selector: (_, config) => config.keepAliveInterval,
|
||||
builder: (_, value, __) {
|
||||
return ListItem(
|
||||
leading: const Icon(Icons.timer_outlined),
|
||||
title: Text(appLocalizations.keepAliveIntervalDesc),
|
||||
subtitle: Text("$value ${appLocalizations.seconds}"),
|
||||
onTap: () {
|
||||
_updateKeepAliveInterval(value);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<Config, String>(
|
||||
selector: (_, config) => config.testUrl,
|
||||
builder: (_, value, __) {
|
||||
return ListItem(
|
||||
leading: const Icon(Icons.timeline),
|
||||
title: Text(appLocalizations.testUrl),
|
||||
subtitle: Text(value),
|
||||
onTap: () {
|
||||
_modifyTestUrl(value);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<ClashConfig, int>(
|
||||
selector: (_, clashConfig) => clashConfig.mixedPort,
|
||||
builder: (_, mixedPort, __) {
|
||||
return ListItem(
|
||||
onTap: () {
|
||||
_modifyMixedPort(mixedPort);
|
||||
},
|
||||
leading: const Icon(Icons.adjust_outlined),
|
||||
title: Text(appLocalizations.proxyPort),
|
||||
subtitle: Text(appLocalizations.proxyPortDesc),
|
||||
trailing: FilledButton.tonal(
|
||||
onPressed: () {
|
||||
_modifyMixedPort(mixedPort);
|
||||
},
|
||||
child: Text(
|
||||
"$mixedPort",
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.ipv6,
|
||||
builder: (_, ipv6, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.water_outlined),
|
||||
title: const Text("IPv6"),
|
||||
subtitle: Text(appLocalizations.ipv6Desc),
|
||||
delegate: SwitchDelegate(
|
||||
value: ipv6,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.ipv6 = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.allowLan,
|
||||
builder: (_, allowLan, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.device_hub),
|
||||
title: Text(appLocalizations.allowLan),
|
||||
subtitle: Text(appLocalizations.allowLanDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: allowLan,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = context.read<ClashConfig>();
|
||||
clashConfig.allowLan = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.unifiedDelay,
|
||||
builder: (_, unifiedDelay, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.compress_outlined),
|
||||
title: Text(appLocalizations.unifiedDelay),
|
||||
subtitle: Text(appLocalizations.unifiedDelayDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: unifiedDelay,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.unifiedDelay = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) =>
|
||||
clashConfig.findProcessMode == FindProcessMode.always,
|
||||
builder: (_, findProcess, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.polymer_outlined),
|
||||
title: Text(appLocalizations.findProcessMode),
|
||||
subtitle: Text(appLocalizations.findProcessModeDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: findProcess,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.findProcessMode =
|
||||
value ? FindProcessMode.always : FindProcessMode.off;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.tcpConcurrent,
|
||||
builder: (_, tcpConcurrent, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.double_arrow_outlined),
|
||||
title: Text(appLocalizations.tcpConcurrent),
|
||||
subtitle: Text(appLocalizations.tcpConcurrentDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: tcpConcurrent,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.tcpConcurrent = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) =>
|
||||
clashConfig.geodataLoader == geodataLoaderMemconservative,
|
||||
builder: (_, memconservative, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.memory),
|
||||
title: Text(appLocalizations.geodataLoader),
|
||||
subtitle: Text(appLocalizations.geodataLoaderDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: memconservative,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.geodataLoader = value
|
||||
? geodataLoaderMemconservative
|
||||
: geodataLoaderStandard;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) =>
|
||||
clashConfig.externalController.isNotEmpty,
|
||||
builder: (_, hasExternalController, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.api_outlined),
|
||||
title: Text(appLocalizations.externalController),
|
||||
subtitle: Text(appLocalizations.externalControllerDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: hasExternalController,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.externalController =
|
||||
value ? defaultExternalController : '';
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildMoreSection() {
|
||||
return generateSection(
|
||||
title: appLocalizations.more,
|
||||
items: [
|
||||
if (system.isDesktop)
|
||||
Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.tun.enable,
|
||||
builder: (_, tunEnable, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.important_devices_outlined),
|
||||
title: Text(appLocalizations.tun),
|
||||
subtitle: Text(appLocalizations.tunDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: tunEnable,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = context.read<ClashConfig>();
|
||||
clashConfig.tun = Tun(enable: value);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> items = [
|
||||
..._buildAppSection(),
|
||||
..._buildGeneralSection(),
|
||||
..._buildMoreSection(),
|
||||
];
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.only(bottom: 32),
|
||||
itemBuilder: (_, index) {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
child: items[index],
|
||||
);
|
||||
},
|
||||
itemCount: items.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MixedPortFormDialog extends StatefulWidget {
|
||||
final num mixedPort;
|
||||
|
||||
const MixedPortFormDialog({super.key, required this.mixedPort});
|
||||
|
||||
@override
|
||||
State<MixedPortFormDialog> createState() => _MixedPortFormDialogState();
|
||||
}
|
||||
|
||||
class _MixedPortFormDialogState extends State<MixedPortFormDialog> {
|
||||
late TextEditingController portController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
portController = TextEditingController(text: "${widget.mixedPort}");
|
||||
}
|
||||
|
||||
_handleUpdate() async {
|
||||
final port = portController.value.text;
|
||||
if (port.isEmpty) return;
|
||||
Navigator.of(context).pop<String>(port);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(appLocalizations.proxyPort),
|
||||
content: SizedBox(
|
||||
width: 300,
|
||||
child: Wrap(
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
TextField(
|
||||
controller: portController,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _handleUpdate,
|
||||
child: Text(appLocalizations.submit),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TestUrlFormDialog extends StatefulWidget {
|
||||
final String testUrl;
|
||||
|
||||
const TestUrlFormDialog({
|
||||
super.key,
|
||||
required this.testUrl,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TestUrlFormDialog> createState() => _TestUrlFormDialogState();
|
||||
}
|
||||
|
||||
class _TestUrlFormDialogState extends State<TestUrlFormDialog> {
|
||||
late TextEditingController testUrlController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
testUrlController = TextEditingController(text: widget.testUrl);
|
||||
}
|
||||
|
||||
_handleUpdate() async {
|
||||
final testUrl = testUrlController.value.text;
|
||||
if (testUrl.isEmpty) return;
|
||||
Navigator.of(context).pop<String>(testUrl);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(appLocalizations.testUrl),
|
||||
content: SizedBox(
|
||||
width: 300,
|
||||
child: Wrap(
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
TextField(
|
||||
maxLines: 5,
|
||||
minLines: 1,
|
||||
controller: testUrlController,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _handleUpdate,
|
||||
child: Text(appLocalizations.submit),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class KeepAliveIntervalFormDialog extends StatefulWidget {
|
||||
final int keepAliveInterval;
|
||||
|
||||
const KeepAliveIntervalFormDialog({
|
||||
super.key,
|
||||
required this.keepAliveInterval,
|
||||
});
|
||||
|
||||
@override
|
||||
State<KeepAliveIntervalFormDialog> createState() =>
|
||||
_KeepAliveIntervalFormDialogState();
|
||||
}
|
||||
|
||||
class _KeepAliveIntervalFormDialogState
|
||||
extends State<KeepAliveIntervalFormDialog> {
|
||||
late TextEditingController keepAliveIntervalController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
keepAliveIntervalController = TextEditingController(
|
||||
text: "${widget.keepAliveInterval}",
|
||||
);
|
||||
}
|
||||
|
||||
_handleUpdate() async {
|
||||
final keepAliveInterval = keepAliveIntervalController.value.text;
|
||||
if (keepAliveInterval.isEmpty) return;
|
||||
Navigator.of(context).pop<String>(keepAliveInterval);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(appLocalizations.keepAliveIntervalDesc),
|
||||
content: SizedBox(
|
||||
width: 300,
|
||||
child: Wrap(
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
TextField(
|
||||
maxLines: 1,
|
||||
minLines: 1,
|
||||
controller: keepAliveIntervalController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
suffixText: appLocalizations.seconds,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _handleUpdate,
|
||||
child: Text(appLocalizations.submit),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
63
lib/fragments/config/app.dart
Normal file
63
lib/fragments/config/app.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/config.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 CloseConnectionsSwitch extends StatelessWidget {
|
||||
const CloseConnectionsSwitch({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.isCloseConnections,
|
||||
builder: (_, isCloseConnections, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.auto_delete_outlined),
|
||||
title: Text(appLocalizations.autoCloseConnections),
|
||||
subtitle: Text(appLocalizations.autoCloseConnectionsDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: isCloseConnections,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.config.isCloseConnections = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UsageSwitch extends StatelessWidget {
|
||||
const UsageSwitch({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.onlyProxy,
|
||||
builder: (_, onlyProxy, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.data_usage_outlined),
|
||||
title: Text(appLocalizations.onlyStatisticsProxy),
|
||||
subtitle: Text(appLocalizations.onlyStatisticsProxyDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: onlyProxy,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.config.onlyProxy = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final appItems = [
|
||||
const CloseConnectionsSwitch(),
|
||||
const UsageSwitch(),
|
||||
];
|
||||
91
lib/fragments/config/config.dart
Normal file
91
lib/fragments/config/config.dart
Normal file
@@ -0,0 +1,91 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/fragments/config/app.dart';
|
||||
import 'package:fl_clash/fragments/config/dns.dart';
|
||||
import 'package:fl_clash/fragments/config/general.dart';
|
||||
import 'package:fl_clash/fragments/config/vpn.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ConfigFragment extends StatefulWidget {
|
||||
const ConfigFragment({super.key});
|
||||
|
||||
@override
|
||||
State<ConfigFragment> createState() => _ConfigFragmentState();
|
||||
}
|
||||
|
||||
class _ConfigFragmentState extends State<ConfigFragment> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> items = [
|
||||
ListItem.open(
|
||||
title: Text(appLocalizations.app),
|
||||
subtitle: Text(appLocalizations.appDesc),
|
||||
leading: const Icon(Icons.settings_applications),
|
||||
delegate: OpenDelegate(
|
||||
title: appLocalizations.app,
|
||||
isBlur: false,
|
||||
widget: generateListView(
|
||||
appItems
|
||||
.separated(
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (Platform.isAndroid)
|
||||
ListItem.open(
|
||||
title: const Text("VPN"),
|
||||
subtitle: Text(appLocalizations.vpnDesc),
|
||||
leading: const Icon(Icons.vpn_key),
|
||||
delegate: OpenDelegate(
|
||||
title: "VPN",
|
||||
isBlur: false,
|
||||
widget: generateListView(
|
||||
vpnItems,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListItem.open(
|
||||
title: Text(appLocalizations.general),
|
||||
subtitle: Text(appLocalizations.generalDesc),
|
||||
leading: const Icon(Icons.build),
|
||||
delegate: OpenDelegate(
|
||||
title: appLocalizations.general,
|
||||
widget: generateListView(
|
||||
generalItems,
|
||||
),
|
||||
isBlur: false,
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
),
|
||||
ListItem.open(
|
||||
title: const Text("DNS"),
|
||||
subtitle: Text(appLocalizations.dnsDesc),
|
||||
leading: const Icon(Icons.dns),
|
||||
delegate: OpenDelegate(
|
||||
title: "DNS",
|
||||
widget: generateListView(
|
||||
dnsItems,
|
||||
),
|
||||
isScaffold: true,
|
||||
isBlur: false,
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
)
|
||||
];
|
||||
return generateListView(
|
||||
items
|
||||
.separated(
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
843
lib/fragments/config/dns.dart
Normal file
843
lib/fragments/config/dns.dart
Normal file
@@ -0,0 +1,843 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/common/app_localizations.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:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class OverrideItem extends StatelessWidget {
|
||||
const OverrideItem({super.key});
|
||||
|
||||
_initActions(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final commonScaffoldState =
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
globalState.appController.clashConfig.dns = const Dns();
|
||||
},
|
||||
tooltip: appLocalizations.resetDns,
|
||||
icon: const Icon(
|
||||
Icons.replay,
|
||||
),
|
||||
)
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_initActions(context);
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.overrideDns,
|
||||
builder: (_, override, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.overrideDns),
|
||||
subtitle: Text(appLocalizations.overrideDnsDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: override,
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
config.overrideDns = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DnsDisabledContainer extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const DnsDisabledContainer(
|
||||
this.child, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.overrideDns,
|
||||
builder: (_, enable, child) {
|
||||
return AbsorbPointer(
|
||||
absorbing: !enable,
|
||||
child: DisabledMask(
|
||||
status: !enable,
|
||||
child: Container(
|
||||
color: context.colorScheme.surface,
|
||||
child: child!,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StatusItem extends StatelessWidget {
|
||||
const StatusItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.enable,
|
||||
builder: (_, enable, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.status),
|
||||
subtitle: Text(appLocalizations.statusDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: enable,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
enable: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PreferH3Item extends StatelessWidget {
|
||||
const PreferH3Item({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.preferH3,
|
||||
builder: (_, preferH3, __) {
|
||||
return ListItem.switchItem(
|
||||
title: const Text("PreferH3"),
|
||||
subtitle: Text(appLocalizations.preferH3Desc),
|
||||
delegate: SwitchDelegate(
|
||||
value: preferH3,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
preferH3: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class IPv6Item extends StatelessWidget {
|
||||
const IPv6Item({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.ipv6,
|
||||
builder: (_, ipv6, __) {
|
||||
return ListItem.switchItem(
|
||||
title: const Text("IPv6"),
|
||||
delegate: SwitchDelegate(
|
||||
value: ipv6,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
ipv6: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RespectRulesItem extends StatelessWidget {
|
||||
const RespectRulesItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.respectRules,
|
||||
builder: (_, respectRules, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.respectRules),
|
||||
subtitle: Text(appLocalizations.respectRulesDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: respectRules,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
respectRules: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DnsModeItem extends StatelessWidget {
|
||||
const DnsModeItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, DnsMode>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.enhancedMode,
|
||||
builder: (_, enhancedMode, __) {
|
||||
return ListItem<DnsMode>.options(
|
||||
title: Text(appLocalizations.dnsMode),
|
||||
subtitle: Text(enhancedMode.name),
|
||||
delegate: OptionsDelegate(
|
||||
title: appLocalizations.dnsMode,
|
||||
options: DnsMode.values,
|
||||
onChanged: (value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(enhancedMode: value);
|
||||
},
|
||||
textBuilder: (dnsMode) => dnsMode.name,
|
||||
value: enhancedMode,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FakeIpRangeItem extends StatelessWidget {
|
||||
const FakeIpRangeItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, String>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fakeIpRange,
|
||||
builder: (_, fakeIpRange, __) {
|
||||
return ListItem.input(
|
||||
title: Text(appLocalizations.fakeipRange),
|
||||
subtitle: Text(fakeIpRange),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.fakeipRange,
|
||||
value: fakeIpRange,
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
try {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
clashConfig.dns = clashConfig.dns.copyWith(
|
||||
fakeIpRange: value,
|
||||
);
|
||||
} catch (e) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.fakeipRange,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FakeIpFilterItem extends StatelessWidget {
|
||||
const FakeIpFilterItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.fakeipFilter),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
title: appLocalizations.fakeipFilter,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fakeIpFilter,
|
||||
shouldRebuild: (prev, next) =>
|
||||
!const ListEquality<String>().equals(prev, next),
|
||||
builder: (_, fakeIpFilter, __) {
|
||||
return UpdatePage(
|
||||
title: appLocalizations.fakeipFilter,
|
||||
items: fakeIpFilter,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onRemove: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fakeIpFilter: List.from(dns.fakeIpFilter)..remove(value),
|
||||
);
|
||||
},
|
||||
onAdd: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
if (fakeIpFilter.contains(value)) return;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fakeIpFilter: List.from(dns.fakeIpFilter)..add(value),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultNameserverItem extends StatelessWidget {
|
||||
const DefaultNameserverItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.defaultNameserver),
|
||||
subtitle: Text(appLocalizations.defaultNameserverDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
title: appLocalizations.defaultNameserver,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.defaultNameserver,
|
||||
shouldRebuild: (prev, next) =>
|
||||
!const ListEquality<String>().equals(prev, next),
|
||||
builder: (_, defaultNameserver, __) {
|
||||
return UpdatePage(
|
||||
title: appLocalizations.defaultNameserver,
|
||||
items: defaultNameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onRemove: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
defaultNameserver: List.from(dns.defaultNameserver)
|
||||
..remove(value),
|
||||
);
|
||||
},
|
||||
onAdd: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
if (defaultNameserver.contains(value)) return;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
defaultNameserver: List.from(dns.defaultNameserver)
|
||||
..add(value),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NameserverItem extends StatelessWidget {
|
||||
const NameserverItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.nameserver),
|
||||
subtitle: Text(appLocalizations.nameserverDesc),
|
||||
delegate: OpenDelegate(
|
||||
title: appLocalizations.nameserver,
|
||||
isBlur: false,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.nameserver,
|
||||
shouldRebuild: (prev, next) =>
|
||||
!const ListEquality<String>().equals(prev, next),
|
||||
builder: (_, nameserver, __) {
|
||||
return UpdatePage(
|
||||
title: "域名服务器",
|
||||
items: nameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onRemove: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
nameserver: List.from(dns.nameserver)..remove(value),
|
||||
);
|
||||
},
|
||||
onAdd: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
if (nameserver.contains(value)) return;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
nameserver: List.from(dns.nameserver)..add(value),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UseHostsItem extends StatelessWidget {
|
||||
const UseHostsItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.useHosts,
|
||||
builder: (_, useHosts, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.useHosts),
|
||||
delegate: SwitchDelegate(
|
||||
value: useHosts,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
useHosts: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UseSystemHostsItem extends StatelessWidget {
|
||||
const UseSystemHostsItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.useSystemHosts,
|
||||
builder: (_, useSystemHosts, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.useSystemHosts),
|
||||
delegate: SwitchDelegate(
|
||||
value: useSystemHosts,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
useSystemHosts: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NameserverPolicyItem extends StatelessWidget {
|
||||
const NameserverPolicyItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.nameserverPolicy),
|
||||
subtitle: Text(appLocalizations.nameserverPolicyDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
title: appLocalizations.nameserverPolicy,
|
||||
widget: Selector<ClashConfig, Map<String, String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.nameserverPolicy,
|
||||
shouldRebuild: (prev, next) =>
|
||||
!const MapEquality<String, String>().equals(prev, next),
|
||||
builder: (_, nameserverPolicy, __) {
|
||||
return UpdatePage(
|
||||
title: appLocalizations.nameserverPolicy,
|
||||
items: nameserverPolicy.entries,
|
||||
titleBuilder: (item) => Text(item.key),
|
||||
subtitleBuilder: (item) => Text(item.value),
|
||||
isMap: true,
|
||||
onRemove: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
nameserverPolicy: Map.from(dns.nameserverPolicy)
|
||||
..remove(value.key),
|
||||
);
|
||||
},
|
||||
onAdd: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
nameserverPolicy: Map.from(dns.nameserverPolicy)
|
||||
..addEntries([value]),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProxyServerNameserverItem extends StatelessWidget {
|
||||
const ProxyServerNameserverItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.proxyNameserver),
|
||||
subtitle: Text(appLocalizations.proxyNameserverDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
title: appLocalizations.proxyNameserver,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.proxyServerNameserver,
|
||||
shouldRebuild: (prev, next) =>
|
||||
!const ListEquality<String>().equals(prev, next),
|
||||
builder: (_, proxyServerNameserver, __) {
|
||||
return UpdatePage(
|
||||
title: appLocalizations.proxyNameserver,
|
||||
items: proxyServerNameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onRemove: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
proxyServerNameserver: List.from(dns.proxyServerNameserver)
|
||||
..remove(value),
|
||||
);
|
||||
},
|
||||
onAdd: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
if (proxyServerNameserver.contains(value)) return;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
proxyServerNameserver: List.from(dns.proxyServerNameserver)
|
||||
..add(value),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FallbackItem extends StatelessWidget {
|
||||
const FallbackItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.fallback),
|
||||
subtitle: Text(appLocalizations.fallbackDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
title: appLocalizations.fallback,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fallback,
|
||||
shouldRebuild: (prev, next) =>
|
||||
!const ListEquality<String>().equals(prev, next),
|
||||
builder: (_, fallback, __) {
|
||||
return UpdatePage(
|
||||
title: appLocalizations.fallback,
|
||||
items: fallback,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onRemove: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallback: List.from(dns.fallback)..remove(value),
|
||||
);
|
||||
},
|
||||
onAdd: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
if (fallback.contains(value)) return;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallback: List.from(dns.fallback)..add(value),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GeoipItem extends StatelessWidget {
|
||||
const GeoipItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoip,
|
||||
builder: (_, geoip, __) {
|
||||
return ListItem.switchItem(
|
||||
title: const Text("Geoip"),
|
||||
delegate: SwitchDelegate(
|
||||
value: geoip,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallbackFilter: dns.fallbackFilter.copyWith(geoip: value),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GeoipCodeItem extends StatelessWidget {
|
||||
const GeoipCodeItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, String>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoipCode,
|
||||
builder: (_, geoipCode, __) {
|
||||
return ListItem.input(
|
||||
title: Text(appLocalizations.geoipCode),
|
||||
subtitle: Text(geoipCode),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.geoipCode,
|
||||
value: geoipCode,
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
try {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
||||
geoipCode: value,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.geoipCode,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GeositeItem extends StatelessWidget {
|
||||
const GeositeItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListItem.open(
|
||||
title: const Text("Geosite"),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
title: "Geosite",
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geosite,
|
||||
shouldRebuild: (prev, next) =>
|
||||
!const ListEquality<String>().equals(prev, next),
|
||||
builder: (_, geosite, __) {
|
||||
return UpdatePage(
|
||||
title: "Geosite",
|
||||
items: geosite,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onRemove: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
||||
geosite: List.from(geosite)..remove(value),
|
||||
),
|
||||
);
|
||||
},
|
||||
onAdd: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
||||
geosite: List.from(geosite)..add(value),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class IpcidrItem extends StatelessWidget {
|
||||
const IpcidrItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.ipcidr),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
title: appLocalizations.ipcidr,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.ipcidr,
|
||||
shouldRebuild: (prev, next) =>
|
||||
!const ListEquality<String>().equals(prev, next),
|
||||
builder: (_, ipcidr, __) {
|
||||
return UpdatePage(
|
||||
title: appLocalizations.ipcidr,
|
||||
items: ipcidr,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onRemove: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
||||
ipcidr: List.from(ipcidr)..remove(value),
|
||||
),
|
||||
);
|
||||
},
|
||||
onAdd: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
||||
ipcidr: List.from(ipcidr)..add(value),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DomainItem extends StatelessWidget {
|
||||
const DomainItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.domain),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
title: appLocalizations.domain,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.domain,
|
||||
shouldRebuild: (prev, next) =>
|
||||
!const ListEquality<String>().equals(prev, next),
|
||||
builder: (_, domain, __) {
|
||||
return UpdatePage(
|
||||
title: appLocalizations.domain,
|
||||
items: domain,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onRemove: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
||||
domain: List.from(domain)..remove(value),
|
||||
),
|
||||
);
|
||||
},
|
||||
onAdd: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
||||
domain: List.from(domain)..add(value),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DnsOptions extends StatelessWidget {
|
||||
const DnsOptions({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DnsDisabledContainer(
|
||||
Column(
|
||||
children: generateSection(
|
||||
title: appLocalizations.options,
|
||||
items: [
|
||||
const StatusItem(),
|
||||
const UseHostsItem(),
|
||||
const UseSystemHostsItem(),
|
||||
const IPv6Item(),
|
||||
const RespectRulesItem(),
|
||||
const PreferH3Item(),
|
||||
const DnsModeItem(),
|
||||
const FakeIpRangeItem(),
|
||||
const FakeIpFilterItem(),
|
||||
const DefaultNameserverItem(),
|
||||
const NameserverPolicyItem(),
|
||||
const NameserverItem(),
|
||||
const FallbackItem(),
|
||||
const ProxyServerNameserverItem(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FallbackFilterOptions extends StatelessWidget {
|
||||
const FallbackFilterOptions({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DnsDisabledContainer(
|
||||
Column(
|
||||
children: generateSection(
|
||||
title: appLocalizations.fallbackFilter,
|
||||
items: [
|
||||
const GeoipItem(),
|
||||
const GeoipCodeItem(),
|
||||
const GeositeItem(),
|
||||
const IpcidrItem(),
|
||||
const DomainItem(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const dnsItems = <Widget>[
|
||||
OverrideItem(),
|
||||
DnsOptions(),
|
||||
FallbackFilterOptions(),
|
||||
];
|
||||
439
lib/fragments/config/general.dart
Normal file
439
lib/fragments/config/general.dart
Normal file
@@ -0,0 +1,439 @@
|
||||
import 'package:collection/collection.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:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class LogLevelItem extends StatelessWidget {
|
||||
const LogLevelItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, LogLevel>(
|
||||
selector: (_, clashConfig) => clashConfig.logLevel,
|
||||
builder: (_, value, __) {
|
||||
return ListItem<LogLevel>.options(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: Text(appLocalizations.logLevel),
|
||||
subtitle: Text(value.name),
|
||||
delegate: OptionsDelegate<LogLevel>(
|
||||
title: appLocalizations.logLevel,
|
||||
options: LogLevel.values,
|
||||
onChanged: (LogLevel? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.logLevel = value;
|
||||
},
|
||||
textBuilder: (logLevel) => logLevel.name,
|
||||
value: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UaItem extends StatelessWidget {
|
||||
const UaItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, String?>(
|
||||
selector: (_, clashConfig) => clashConfig.globalRealUa,
|
||||
builder: (_, value, __) {
|
||||
return ListItem<String?>.options(
|
||||
leading: const Icon(Icons.computer_outlined),
|
||||
title: const Text("UA"),
|
||||
subtitle: Text(value ?? appLocalizations.defaultText),
|
||||
delegate: OptionsDelegate<String?>(
|
||||
title: "UA",
|
||||
options: [
|
||||
null,
|
||||
"clash-verge/v1.6.6",
|
||||
"ClashforWindows/0.19.23",
|
||||
],
|
||||
value: value,
|
||||
onChanged: (ua) {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.globalRealUa = ua;
|
||||
},
|
||||
textBuilder: (ua) => ua ?? appLocalizations.defaultText,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class KeepAliveIntervalItem extends StatelessWidget {
|
||||
const KeepAliveIntervalItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, int>(
|
||||
selector: (_, config) => config.keepAliveInterval,
|
||||
builder: (_, value, __) {
|
||||
return ListItem.input(
|
||||
leading: const Icon(Icons.timer_outlined),
|
||||
title: Text(appLocalizations.keepAliveIntervalDesc),
|
||||
subtitle: Text("$value ${appLocalizations.seconds}"),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.keepAliveIntervalDesc,
|
||||
suffixText: appLocalizations.seconds,
|
||||
resetValue: "$defaultKeepAliveInterval",
|
||||
value: "$value",
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
try {
|
||||
final intValue = int.parse(value);
|
||||
if (intValue <= 0) {
|
||||
throw "Invalid keepAliveInterval";
|
||||
}
|
||||
globalState.appController.clashConfig.keepAliveInterval =
|
||||
intValue;
|
||||
} catch (e) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.keepAliveIntervalDesc,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TestUrlItem extends StatelessWidget {
|
||||
const TestUrlItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, String>(
|
||||
selector: (_, config) => config.testUrl,
|
||||
builder: (_, value, __) {
|
||||
return ListItem.input(
|
||||
leading: const Icon(Icons.timeline),
|
||||
title: Text(appLocalizations.testUrl),
|
||||
subtitle: Text(value),
|
||||
delegate: InputDelegate(
|
||||
resetValue: defaultTestUrl,
|
||||
title: appLocalizations.testUrl,
|
||||
value: value,
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
try {
|
||||
if (!value.isUrl) {
|
||||
throw "Invalid url";
|
||||
}
|
||||
globalState.appController.config.testUrl = value;
|
||||
} catch (e) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.testUrl,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MixedPortItem extends StatelessWidget {
|
||||
const MixedPortItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, int>(
|
||||
selector: (_, clashConfig) => clashConfig.mixedPort,
|
||||
builder: (_, value, __) {
|
||||
return ListItem.input(
|
||||
leading: const Icon(Icons.adjust_outlined),
|
||||
title: Text(appLocalizations.proxyPort),
|
||||
subtitle: Text("$value"),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.proxyPort,
|
||||
value: "$value",
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
try {
|
||||
final mixedPort = int.parse(value);
|
||||
if (mixedPort < 1024 || mixedPort > 49151) {
|
||||
throw "Invalid port";
|
||||
}
|
||||
globalState.appController.clashConfig.mixedPort = mixedPort;
|
||||
} catch (e) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.proxyPort,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
resetValue: "$defaultMixedPort",
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HostsItem extends StatelessWidget {
|
||||
const HostsItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListItem.open(
|
||||
leading: const Icon(Icons.view_list_outlined),
|
||||
title: const Text("Hosts"),
|
||||
subtitle: Text(appLocalizations.hostsDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
title: "Hosts",
|
||||
widget: Selector<ClashConfig, HostsMap>(
|
||||
selector: (_, clashConfig) => clashConfig.hosts,
|
||||
shouldRebuild: (prev, next) =>
|
||||
!const MapEquality<String, String>().equals(prev, next),
|
||||
builder: (_, hosts, ___) {
|
||||
final entries = hosts.entries;
|
||||
return UpdatePage(
|
||||
title: "Hosts",
|
||||
items: entries,
|
||||
titleBuilder: (item) => Text(item.key),
|
||||
subtitleBuilder: (item) => Text(item.value),
|
||||
onRemove: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
clashConfig.hosts = Map.from(hosts)..remove(value.key);
|
||||
},
|
||||
onAdd: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
clashConfig.hosts = Map.from(clashConfig.hosts)
|
||||
..addEntries([value]);
|
||||
},
|
||||
isMap: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Ipv6Item extends StatelessWidget {
|
||||
const Ipv6Item({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.ipv6,
|
||||
builder: (_, ipv6, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.water_outlined),
|
||||
title: const Text("IPv6"),
|
||||
subtitle: Text(appLocalizations.ipv6Desc),
|
||||
delegate: SwitchDelegate(
|
||||
value: ipv6,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.ipv6 = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AllowLanItem extends StatelessWidget {
|
||||
const AllowLanItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.allowLan,
|
||||
builder: (_, allowLan, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.device_hub),
|
||||
title: Text(appLocalizations.allowLan),
|
||||
subtitle: Text(appLocalizations.allowLanDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: allowLan,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = context.read<ClashConfig>();
|
||||
clashConfig.allowLan = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UnifiedDelayItem extends StatelessWidget {
|
||||
const UnifiedDelayItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.unifiedDelay,
|
||||
builder: (_, unifiedDelay, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.compress_outlined),
|
||||
title: Text(appLocalizations.unifiedDelay),
|
||||
subtitle: Text(appLocalizations.unifiedDelayDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: unifiedDelay,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.unifiedDelay = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FindProcessItem extends StatelessWidget {
|
||||
const FindProcessItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) =>
|
||||
clashConfig.findProcessMode == FindProcessMode.always,
|
||||
builder: (_, findProcess, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.polymer_outlined),
|
||||
title: Text(appLocalizations.findProcessMode),
|
||||
subtitle: Text(appLocalizations.findProcessModeDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: findProcess,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.findProcessMode =
|
||||
value ? FindProcessMode.always : FindProcessMode.off;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TcpConcurrentItem extends StatelessWidget {
|
||||
const TcpConcurrentItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.tcpConcurrent,
|
||||
builder: (_, tcpConcurrent, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.double_arrow_outlined),
|
||||
title: Text(appLocalizations.tcpConcurrent),
|
||||
subtitle: Text(appLocalizations.tcpConcurrentDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: tcpConcurrent,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.tcpConcurrent = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GeodataLoaderItem extends StatelessWidget {
|
||||
const GeodataLoaderItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) =>
|
||||
clashConfig.geodataLoader == geodataLoaderMemconservative,
|
||||
builder: (_, memconservative, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.memory),
|
||||
title: Text(appLocalizations.geodataLoader),
|
||||
subtitle: Text(appLocalizations.geodataLoaderDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: memconservative,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.geodataLoader =
|
||||
value ? geodataLoaderMemconservative : geodataLoaderStandard;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ExternalControllerItem extends StatelessWidget {
|
||||
const ExternalControllerItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.externalController.isNotEmpty,
|
||||
builder: (_, hasExternalController, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.api_outlined),
|
||||
title: Text(appLocalizations.externalController),
|
||||
subtitle: Text(appLocalizations.externalControllerDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: hasExternalController,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.externalController =
|
||||
value ? defaultExternalController : '';
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final generalItems = const [
|
||||
LogLevelItem(),
|
||||
UaItem(),
|
||||
KeepAliveIntervalItem(),
|
||||
TestUrlItem(),
|
||||
MixedPortItem(),
|
||||
HostsItem(),
|
||||
Ipv6Item(),
|
||||
AllowLanItem(),
|
||||
UnifiedDelayItem(),
|
||||
FindProcessItem(),
|
||||
TcpConcurrentItem(),
|
||||
GeodataLoaderItem(),
|
||||
ExternalControllerItem(),
|
||||
]
|
||||
.separated(
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
140
lib/fragments/config/vpn.dart
Normal file
140
lib/fragments/config/vpn.dart
Normal file
@@ -0,0 +1,140 @@
|
||||
import 'package:fl_clash/common/common.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 Selector<Config, bool>(
|
||||
selector: (_, config) => config.vpnProps.enable,
|
||||
builder: (_, enable, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.stacked_line_chart),
|
||||
title: const Text("VPN"),
|
||||
subtitle: Text(appLocalizations.vpnEnableDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: enable,
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
final vpnProps = config.vpnProps;
|
||||
config.vpnProps = vpnProps.copyWith(
|
||||
enable: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VPNDisabledContainer extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const VPNDisabledContainer(
|
||||
this.child, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.vpnProps.enable,
|
||||
builder: (_, enable, child) {
|
||||
return AbsorbPointer(
|
||||
absorbing: !enable,
|
||||
child: DisabledMask(
|
||||
status: !enable,
|
||||
child: child!,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AllowBypassSwitch extends StatelessWidget {
|
||||
const AllowBypassSwitch({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.vpnProps.allowBypass,
|
||||
builder: (_, allowBypass, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.arrow_forward_outlined),
|
||||
title: Text(appLocalizations.allowBypass),
|
||||
subtitle: Text(appLocalizations.allowBypassDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: allowBypass,
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
final vpnProps = config.vpnProps;
|
||||
config.vpnProps = vpnProps.copyWith(
|
||||
allowBypass: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SystemProxySwitch extends StatelessWidget {
|
||||
const SystemProxySwitch({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.vpnProps.systemProxy,
|
||||
builder: (_, systemProxy, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.settings_ethernet),
|
||||
title: Text(appLocalizations.systemProxy),
|
||||
subtitle: Text(appLocalizations.systemProxyDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: systemProxy,
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
final vpnProps = config.vpnProps;
|
||||
config.vpnProps = vpnProps.copyWith(
|
||||
systemProxy: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VpnOptions extends StatelessWidget {
|
||||
const VpnOptions({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return VPNDisabledContainer(
|
||||
Column(
|
||||
children: generateSection(
|
||||
title: appLocalizations.options,
|
||||
items: [
|
||||
const SystemProxySwitch(),
|
||||
const AllowBypassSwitch(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final vpnItems = [
|
||||
const VPNSwitch(),
|
||||
const VpnOptions(),
|
||||
];
|
||||
@@ -48,11 +48,11 @@ class _DashboardFragmentState extends State<DashboardFragment> {
|
||||
crossAxisCellCount: 8,
|
||||
child: NetworkSpeed(),
|
||||
),
|
||||
if (Platform.isAndroid)
|
||||
GridItem(
|
||||
crossAxisCellCount: switchCount,
|
||||
child: const VPNSwitch(),
|
||||
),
|
||||
// if (Platform.isAndroid)
|
||||
// GridItem(
|
||||
// crossAxisCellCount: switchCount,
|
||||
// child: const VPNSwitch(),
|
||||
// ),
|
||||
if (system.isDesktop) ...[
|
||||
GridItem(
|
||||
crossAxisCellCount: switchCount,
|
||||
|
||||
@@ -53,7 +53,7 @@ class _IntranetIPState extends State<IntranetIP> {
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16).copyWith(top: 0),
|
||||
height: globalState.appController.measure.titleLargeHeight + 24 - 2,
|
||||
height: globalState.measure.titleLargeHeight + 24 - 2,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: ipNotifier,
|
||||
builder: (_, value, __) {
|
||||
|
||||
@@ -46,9 +46,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
isTesting: false,
|
||||
ipInfo: ipInfo,
|
||||
);
|
||||
} catch (_) {
|
||||
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
_checkIpContainer(Widget child) {
|
||||
@@ -122,7 +120,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
: ipInfo != null
|
||||
? Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
height: globalState.appController
|
||||
height: globalState
|
||||
.measure.titleMediumHeight,
|
||||
child: Text(
|
||||
countryCodeToEmoji(
|
||||
@@ -150,9 +148,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: globalState.appController.measure.titleLargeHeight +
|
||||
24 -
|
||||
2,
|
||||
height: globalState.measure.titleLargeHeight + 24 - 2,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.all(16).copyWith(top: 0),
|
||||
child: FadeBox(
|
||||
|
||||
@@ -59,7 +59,7 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
|
||||
style: bodyMedium,
|
||||
maxLines: 1,
|
||||
);
|
||||
final size = globalState.appController.measure.computeTextSize(valueText);
|
||||
final size = globalState.measure.computeTextSize(valueText);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
|
||||
@@ -74,7 +74,7 @@ class _StartButtonState extends State<StartButton>
|
||||
if (!state.isInit || !state.hasProfile) {
|
||||
return Container();
|
||||
}
|
||||
final textWidth = globalState.appController.measure
|
||||
final textWidth = globalState.measure
|
||||
.computeTextSize(
|
||||
Text(
|
||||
other.getTimeDifference(
|
||||
|
||||
@@ -5,34 +5,34 @@ 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<Config, bool>(
|
||||
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 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<Config, bool>(
|
||||
// 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});
|
||||
@@ -47,14 +47,16 @@ class TUNSwitch extends StatelessWidget {
|
||||
child: Selector<ClashConfig, bool>(
|
||||
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,
|
||||
);
|
||||
},
|
||||
return LocaleBuilder(
|
||||
builder: (_) => Switch(
|
||||
value: enable,
|
||||
onChanged: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
clashConfig.tun = clashConfig.tun.copyWith(
|
||||
enable: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -75,14 +77,16 @@ class ProxySwitch extends StatelessWidget {
|
||||
child: Selector<Config, bool>(
|
||||
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);
|
||||
},
|
||||
return LocaleBuilder(
|
||||
builder: (_) => Switch(
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
value: systemProxy,
|
||||
onChanged: (value) {
|
||||
final config = globalState.appController.config;
|
||||
config.desktopProps =
|
||||
config.desktopProps.copyWith(systemProxy: value);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -5,9 +5,9 @@ export 'profiles/profiles.dart';
|
||||
export 'logs.dart';
|
||||
export 'connections.dart';
|
||||
export 'access.dart';
|
||||
export 'config.dart';
|
||||
export 'config/config.dart';
|
||||
export 'application_setting.dart';
|
||||
export 'about.dart';
|
||||
export 'backup_and_recovery.dart';
|
||||
export 'resources.dart';
|
||||
export 'requests.dart';
|
||||
export 'requests.dart';
|
||||
|
||||
250
lib/fragments/hotkey.dart
Normal file
250
lib/fragments/hotkey.dart
Normal file
@@ -0,0 +1,250 @@
|
||||
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:fl_clash/widgets/card.dart';
|
||||
import 'package:fl_clash/widgets/list.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
extension IntlExt on Intl {
|
||||
static actionMessage(String messageText) =>
|
||||
Intl.message("action_$messageText");
|
||||
}
|
||||
|
||||
class HotKeyFragment extends StatelessWidget {
|
||||
const HotKeyFragment({super.key});
|
||||
|
||||
String getSubtitle(HotKeyAction hotKeyAction) {
|
||||
final key = hotKeyAction.key;
|
||||
if (key == null) {
|
||||
return appLocalizations.noHotKey;
|
||||
}
|
||||
final modifierLabels =
|
||||
hotKeyAction.modifiers.map((item) => item.physicalKeys.first.label);
|
||||
var text = "";
|
||||
if (modifierLabels.isNotEmpty) {
|
||||
text += "${modifierLabels.join(" ")}+";
|
||||
}
|
||||
text += PhysicalKeyboardKey(key).label;
|
||||
return text;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
itemCount: HotAction.values.length,
|
||||
itemBuilder: (_, index) {
|
||||
final hotAction = HotAction.values[index];
|
||||
return Selector<Config, HotKeyAction>(
|
||||
selector: (_, config) {
|
||||
final index = config.hotKeyActions.indexWhere(
|
||||
(item) => item.action == hotAction,
|
||||
);
|
||||
return index != -1
|
||||
? config.hotKeyActions[index]
|
||||
: HotKeyAction(
|
||||
action: hotAction,
|
||||
);
|
||||
},
|
||||
builder: (_, value, __) {
|
||||
return ListItem(
|
||||
title: Text(IntlExt.actionMessage(hotAction.name)),
|
||||
subtitle: Text(
|
||||
getSubtitle(value),
|
||||
style: context.textTheme.bodyMedium
|
||||
?.copyWith(color: context.colorScheme.primary),
|
||||
),
|
||||
onTap: () {
|
||||
globalState.showCommonDialog(
|
||||
child: HotKeyRecorder(
|
||||
hotKeyAction: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HotKeyRecorder extends StatefulWidget {
|
||||
final HotKeyAction hotKeyAction;
|
||||
|
||||
const HotKeyRecorder({
|
||||
super.key,
|
||||
required this.hotKeyAction,
|
||||
});
|
||||
|
||||
@override
|
||||
State<HotKeyRecorder> createState() => _HotKeyRecorderState();
|
||||
}
|
||||
|
||||
class _HotKeyRecorderState extends State<HotKeyRecorder> {
|
||||
late ValueNotifier<HotKeyAction> hotKeyActionNotifier;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
hotKeyActionNotifier = ValueNotifier<HotKeyAction>(
|
||||
widget.hotKeyAction.copyWith(),
|
||||
);
|
||||
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
||||
}
|
||||
|
||||
bool _handleKeyEvent(KeyEvent keyEvent) {
|
||||
if (keyEvent is KeyUpEvent) return false;
|
||||
final keys = HardwareKeyboard.instance.physicalKeysPressed;
|
||||
|
||||
final key = keyEvent.physicalKey;
|
||||
|
||||
final modifiers = KeyboardModifier.values
|
||||
.where((e) =>
|
||||
e.physicalKeys.any(keys.contains) && !e.physicalKeys.contains(key))
|
||||
.toSet();
|
||||
hotKeyActionNotifier.value = hotKeyActionNotifier.value.copyWith(
|
||||
modifiers: modifiers,
|
||||
key: key.usbHidUsage,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_handleRemove() {
|
||||
Navigator.of(context).pop();
|
||||
final config = globalState.appController.config;
|
||||
config.updateOrAddHotKeyAction(
|
||||
hotKeyActionNotifier.value.copyWith(
|
||||
modifiers: {},
|
||||
key: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_handleConfirm() {
|
||||
Navigator.of(context).pop();
|
||||
final config = globalState.appController.config;
|
||||
final currentHotkeyAction = hotKeyActionNotifier.value;
|
||||
if (currentHotkeyAction.key == null ||
|
||||
currentHotkeyAction.modifiers.isEmpty) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(text: appLocalizations.inputCorrectHotkey),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final hotKeyActions = config.hotKeyActions;
|
||||
final index = hotKeyActions.indexWhere(
|
||||
(item) =>
|
||||
item.key == currentHotkeyAction.key &&
|
||||
keyboardModifiersEquality.equals(
|
||||
item.modifiers,
|
||||
currentHotkeyAction.modifiers,
|
||||
),
|
||||
);
|
||||
if (index != -1) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(text: appLocalizations.hotkeyConflict),
|
||||
);
|
||||
return;
|
||||
}
|
||||
config.updateOrAddHotKeyAction(
|
||||
currentHotkeyAction,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(IntlExt.actionMessage((widget.hotKeyAction.action.name))),
|
||||
content: ValueListenableBuilder(
|
||||
valueListenable: hotKeyActionNotifier,
|
||||
builder: (_, hotKeyAction, ___) {
|
||||
final key = hotKeyAction.key;
|
||||
final modifiers = hotKeyAction.modifiers;
|
||||
return SizedBox(
|
||||
width: dialogCommonWidth,
|
||||
child: key != null
|
||||
? Wrap(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
for (final modifier in modifiers)
|
||||
KeyboardKeyBox(
|
||||
keyboardKey: modifier.physicalKeys.first,
|
||||
),
|
||||
if (modifiers.isNotEmpty)
|
||||
Text(
|
||||
"+",
|
||||
style: context.textTheme.titleMedium,
|
||||
),
|
||||
KeyboardKeyBox(
|
||||
keyboardKey: PhysicalKeyboardKey(key),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
appLocalizations.pressKeyboard,
|
||||
style: context.textTheme.titleMedium,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_handleRemove();
|
||||
},
|
||||
child: Text(appLocalizations.remove),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_handleConfirm();
|
||||
},
|
||||
child: Text(
|
||||
appLocalizations.confirm,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class KeyboardKeyBox extends StatelessWidget {
|
||||
final KeyboardKey keyboardKey;
|
||||
|
||||
const KeyboardKeyBox({
|
||||
super.key,
|
||||
required this.keyboardKey,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonCard(
|
||||
type: CommonCardType.filled,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(
|
||||
keyboardKey.label,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: () {},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -46,13 +46,14 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
|
||||
final messages = [];
|
||||
final updateProfiles = profiles.map<Future>(
|
||||
(profile) async {
|
||||
if (profile.type == ProfileType.file) return;
|
||||
config.setProfile(
|
||||
profile.copyWith(isUpdating: true),
|
||||
);
|
||||
try {
|
||||
await appController.updateProfile(profile);
|
||||
if (profile.id == appController.config.currentProfile?.id) {
|
||||
appController.applyProfile(isPrue: true);
|
||||
appController.applyProfileDebounce();
|
||||
}
|
||||
} catch (e) {
|
||||
messages.add("${profile.label ?? profile.id}: $e \n");
|
||||
@@ -225,7 +226,7 @@ class ProfileItem extends StatelessWidget {
|
||||
);
|
||||
await appController.updateProfile(profile);
|
||||
if (profile.id == appController.config.currentProfile?.id) {
|
||||
appController.applyProfile(isPrue: true);
|
||||
appController.applyProfileDebounce();
|
||||
}
|
||||
} catch (e) {
|
||||
config.setProfile(
|
||||
@@ -494,26 +495,29 @@ class _ReorderableProfilesState extends State<ReorderableProfiles> {
|
||||
itemCount: profiles.length,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 12,
|
||||
vertical: 16,
|
||||
horizontal: 24,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
globalState.appController.config.profiles = profiles;
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.check,
|
||||
),
|
||||
iconSize: 32,
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: FilledButton.tonal(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
globalState.appController.config.profiles = profiles;
|
||||
},
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
appLocalizations.confirm,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -23,7 +23,11 @@ class ProxyCard extends StatelessWidget {
|
||||
required this.type,
|
||||
});
|
||||
|
||||
Measure get measure => globalState.appController.measure;
|
||||
Measure get measure => globalState.measure;
|
||||
|
||||
_handleTestCurrentDelay() {
|
||||
proxyDelayTest(proxy);
|
||||
}
|
||||
|
||||
Widget _buildDelayText() {
|
||||
return SizedBox(
|
||||
@@ -36,24 +40,31 @@ class ProxyCard extends StatelessWidget {
|
||||
return FadeBox(
|
||||
child: Builder(
|
||||
builder: (_) {
|
||||
if (delay == null) {
|
||||
return Container();
|
||||
}
|
||||
if (delay == 0) {
|
||||
if (delay == 0 || delay == null) {
|
||||
return SizedBox(
|
||||
height: measure.labelSmallHeight,
|
||||
width: measure.labelSmallHeight,
|
||||
child: const CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
child: delay == 0
|
||||
? const CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
)
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.bolt),
|
||||
iconSize: globalState.measure.labelSmallHeight,
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: _handleTestCurrentDelay,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Text(
|
||||
delay > 0 ? '$delay ms' : "Timeout",
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: other.getDelayColor(
|
||||
delay,
|
||||
return GestureDetector(
|
||||
onTap: _handleTestCurrentDelay,
|
||||
child: Text(
|
||||
delay > 0 ? '$delay ms' : "Timeout",
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: other.getDelayColor(
|
||||
delay,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -119,7 +130,7 @@ class ProxyCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final measure = globalState.appController.measure;
|
||||
final measure = globalState.measure;
|
||||
final delayText = _buildDelayText();
|
||||
final proxyNameText = _buildProxyNameText(context);
|
||||
return currentGroupProxyNameBuilder(
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/other.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';
|
||||
@@ -25,12 +23,12 @@ Widget currentGroupProxyNameBuilder({
|
||||
}
|
||||
|
||||
double get listHeaderHeight {
|
||||
final measure = globalState.appController.measure;
|
||||
final measure = globalState.measure;
|
||||
return 24 + measure.titleMediumHeight + 4 + measure.bodyMediumHeight;
|
||||
}
|
||||
|
||||
double getItemHeight(ProxyCardType proxyCardType) {
|
||||
final measure = globalState.appController.measure;
|
||||
final measure = globalState.measure;
|
||||
final baseHeight =
|
||||
12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8;
|
||||
return switch (proxyCardType) {
|
||||
@@ -40,10 +38,26 @@ double getItemHeight(ProxyCardType proxyCardType) {
|
||||
};
|
||||
}
|
||||
|
||||
proxyDelayTest(Proxy proxy) async {
|
||||
final appController = globalState.appController;
|
||||
final proxyName = appController.appState.getRealProxyName(proxy.name);
|
||||
globalState.appController.setDelay(
|
||||
Delay(
|
||||
name: proxyName,
|
||||
value: 0,
|
||||
),
|
||||
);
|
||||
globalState.appController.setDelay(await clashCore.getDelay(proxyName));
|
||||
}
|
||||
|
||||
delayTest(List<Proxy> proxies) async {
|
||||
final appController = globalState.appController;
|
||||
final delayProxies = proxies.map<Future>((proxy) async {
|
||||
final proxyName = appController.appState.getRealProxyName(proxy.name);
|
||||
final proxyNames = proxies
|
||||
.map((proxy) => appController.appState.getRealProxyName(proxy.name))
|
||||
.toSet()
|
||||
.toList();
|
||||
|
||||
final delayProxies = proxyNames.map<Future>((proxyName) async {
|
||||
globalState.appController.setDelay(
|
||||
Delay(
|
||||
name: proxyName,
|
||||
@@ -51,8 +65,12 @@ delayTest(List<Proxy> proxies) async {
|
||||
),
|
||||
);
|
||||
globalState.appController.setDelay(await clashCore.getDelay(proxyName));
|
||||
});
|
||||
await Future.wait(delayProxies);
|
||||
}).toList();
|
||||
|
||||
final batchesDelayProxies = delayProxies.batch(100);
|
||||
for (final batchDelayProxies in batchesDelayProxies) {
|
||||
await Future.wait(batchDelayProxies);
|
||||
}
|
||||
appController.appState.sortNum++;
|
||||
}
|
||||
|
||||
@@ -72,5 +90,5 @@ double getScrollToSelectedOffset({
|
||||
);
|
||||
final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0;
|
||||
final rows = (selectedIndex / columns).floor();
|
||||
return max(rows * (getItemHeight(proxyCardType) + 8) - 8, 0);
|
||||
return rows * getItemHeight(proxyCardType) + (rows - 1) * 8;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:collection/collection.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:fl_clash/widgets/builder.dart';
|
||||
import 'package:fl_clash/widgets/card.dart';
|
||||
import 'package:fl_clash/widgets/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -217,11 +221,15 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
final currentInitOffset = _headerOffset[index];
|
||||
final proxies = _lastGroupNameProxiesMap[groupName];
|
||||
_controller.animateTo(
|
||||
currentInitOffset +
|
||||
getScrollToSelectedOffset(
|
||||
groupName: groupName,
|
||||
proxies: proxies ?? [],
|
||||
),
|
||||
min(
|
||||
currentInitOffset +
|
||||
8 +
|
||||
getScrollToSelectedOffset(
|
||||
groupName: groupName,
|
||||
proxies: proxies ?? [],
|
||||
),
|
||||
_controller.position.maxScrollExtent,
|
||||
),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeIn,
|
||||
);
|
||||
@@ -256,73 +264,75 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
return prev != next;
|
||||
},
|
||||
builder: (_, state, __) {
|
||||
final items = _buildItems(
|
||||
groupNames: state.groupNames,
|
||||
currentUnfoldSet: state.currentUnfoldSet,
|
||||
columns: state.columns,
|
||||
type: state.proxyCardType,
|
||||
);
|
||||
final itemsOffset = _getItemHeightList(items, state.proxyCardType);
|
||||
return Scrollbar(
|
||||
controller: _controller,
|
||||
thumbVisibility: true,
|
||||
trackVisibility: true,
|
||||
thickness: 8,
|
||||
radius: const Radius.circular(8),
|
||||
interactive: true,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: ScrollConfiguration(
|
||||
behavior: HiddenBarScrollBehavior(),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
controller: _controller,
|
||||
itemExtentBuilder: (index, __) {
|
||||
return itemsOffset[index];
|
||||
},
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
return ScaleBuilder(builder: (_) {
|
||||
final items = _buildItems(
|
||||
groupNames: state.groupNames,
|
||||
currentUnfoldSet: state.currentUnfoldSet,
|
||||
columns: state.columns,
|
||||
type: state.proxyCardType,
|
||||
);
|
||||
final itemsOffset = _getItemHeightList(items, state.proxyCardType);
|
||||
return Scrollbar(
|
||||
controller: _controller,
|
||||
thumbVisibility: true,
|
||||
trackVisibility: true,
|
||||
thickness: 8,
|
||||
radius: const Radius.circular(8),
|
||||
interactive: true,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: ScrollConfiguration(
|
||||
behavior: HiddenBarScrollBehavior(),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
controller: _controller,
|
||||
itemExtentBuilder: (index, __) {
|
||||
return itemsOffset[index];
|
||||
},
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
LayoutBuilder(builder: (_, container) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _headerStateNotifier,
|
||||
builder: (_, headerState, ___) {
|
||||
final index =
|
||||
headerState.currentIndex > state.groupNames.length - 1
|
||||
? 0
|
||||
: headerState.currentIndex;
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: -headerState.offset,
|
||||
child: Container(
|
||||
width: container.maxWidth,
|
||||
color: context.colorScheme.surface,
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 8,
|
||||
),
|
||||
child: _buildHeader(
|
||||
groupName: state.groupNames[index],
|
||||
currentUnfoldSet: state.currentUnfoldSet,
|
||||
LayoutBuilder(builder: (_, container) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _headerStateNotifier,
|
||||
builder: (_, headerState, ___) {
|
||||
final index =
|
||||
headerState.currentIndex > state.groupNames.length - 1
|
||||
? 0
|
||||
: headerState.currentIndex;
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: -headerState.offset,
|
||||
child: Container(
|
||||
width: container.maxWidth,
|
||||
color: context.colorScheme.surface,
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 8,
|
||||
),
|
||||
child: _buildHeader(
|
||||
groupName: state.groupNames[index],
|
||||
currentUnfoldSet: state.currentUnfoldSet,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -353,6 +363,8 @@ class _ListHeaderState extends State<ListHeader>
|
||||
late Animation<double> _iconTurns;
|
||||
var isLock = false;
|
||||
|
||||
String get icon => widget.group.icon;
|
||||
|
||||
String get groupName => widget.group.name;
|
||||
|
||||
String get groupType => widget.group.type.name;
|
||||
@@ -412,6 +424,7 @@ class _ListHeaderState extends State<ListHeader>
|
||||
Widget build(BuildContext context) {
|
||||
return CommonCard(
|
||||
key: widget.key,
|
||||
radius: 24,
|
||||
type: CommonCardType.filled,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
@@ -419,57 +432,96 @@ class _ListHeaderState extends State<ListHeader>
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
groupName,
|
||||
style: context.textTheme.titleMedium,
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
Container(
|
||||
height: 48,
|
||||
width: 48,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: icon.isNotEmpty
|
||||
? CachedNetworkImage(
|
||||
imageUrl: icon,
|
||||
errorWidget: (_, __, ___) => const Icon(
|
||||
IconsExt.target,
|
||||
size: 32,
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
IconsExt.target,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
width: 16,
|
||||
),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: Row(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
groupType,
|
||||
style: context.textTheme.labelMedium?.toLight,
|
||||
groupName,
|
||||
style: context.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: currentGroupProxyNameBuilder(
|
||||
groupName: groupName,
|
||||
builder: (currentGroupName) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (currentGroupName.isNotEmpty) ...[
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: EmojiText(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
" · $currentGroupName",
|
||||
style: context
|
||||
.textTheme.labelMedium?.toLight,
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
groupType,
|
||||
style: context.textTheme.labelMedium?.toLight,
|
||||
),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: currentGroupProxyNameBuilder(
|
||||
groupName: groupName,
|
||||
builder: (currentGroupName) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.start,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (currentGroupName.isNotEmpty) ...[
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: EmojiText(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
" · $currentGroupName",
|
||||
style: context.textTheme
|
||||
.labelMedium?.toLight,
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -68,19 +68,30 @@ class _ProvidersState extends State<Providers> {
|
||||
return Selector<AppState, List<ExternalProvider>>(
|
||||
selector: (_, appState) => appState.providers,
|
||||
builder: (_, providers, ___) {
|
||||
return ListView.separated(
|
||||
itemBuilder: (_, index) {
|
||||
return ProviderItem(
|
||||
provider: providers[index],
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, index) {
|
||||
return const Divider(
|
||||
height: 0,
|
||||
);
|
||||
},
|
||||
itemCount: providers.length,
|
||||
final proxyProviders =
|
||||
providers.where((item) => item.type == "Proxy").map(
|
||||
(item) => ProviderItem(
|
||||
provider: item,
|
||||
),
|
||||
);
|
||||
final ruleProviders =
|
||||
providers.where((item) => item.type == "Rule").map(
|
||||
(item) => ProviderItem(
|
||||
provider: item,
|
||||
),
|
||||
);
|
||||
final proxySection = generateSection(
|
||||
title: appLocalizations.proxyProviders,
|
||||
items: proxyProviders,
|
||||
);
|
||||
final ruleSection = generateSection(
|
||||
title: appLocalizations.ruleProviders,
|
||||
items: ruleProviders,
|
||||
);
|
||||
return generateListView([
|
||||
...proxySection,
|
||||
...ruleSection,
|
||||
]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,11 +29,11 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showExtendPage(
|
||||
forceNotSide: true,
|
||||
isScaffold: true,
|
||||
extendPageWidth: 360,
|
||||
context,
|
||||
body: const Providers(),
|
||||
title: appLocalizations.externalResources,
|
||||
title: appLocalizations.providers,
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
@@ -138,7 +140,7 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
||||
GroupNameKeyMap keyMap = {};
|
||||
final children = state.groupNames.map((groupName) {
|
||||
keyMap[groupName] = GlobalObjectKey(groupName);
|
||||
return KeepContainer(
|
||||
return KeepScope(
|
||||
child: ProxyGroupView(
|
||||
key: keyMap[groupName],
|
||||
groupName: groupName,
|
||||
@@ -266,11 +268,14 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
|
||||
return;
|
||||
}
|
||||
_controller.animateTo(
|
||||
16 +
|
||||
getScrollToSelectedOffset(
|
||||
groupName: groupName,
|
||||
proxies: _lastProxies,
|
||||
),
|
||||
min(
|
||||
16 +
|
||||
getScrollToSelectedOffset(
|
||||
groupName: groupName,
|
||||
proxies: _lastProxies,
|
||||
),
|
||||
_controller.position.maxScrollExtent,
|
||||
),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeIn,
|
||||
);
|
||||
@@ -309,26 +314,33 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
|
||||
},
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: GridView.builder(
|
||||
controller: _controller,
|
||||
padding: const EdgeInsets.all(16),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: columns,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisExtent: getItemHeight(proxyCardType),
|
||||
child: ScaleBuilder(
|
||||
builder: (_) => GridView.builder(
|
||||
controller: _controller,
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 80,
|
||||
),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: columns,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisExtent: getItemHeight(proxyCardType),
|
||||
),
|
||||
itemCount: sortedProxies.length,
|
||||
itemBuilder: (_, index) {
|
||||
final proxy = sortedProxies[index];
|
||||
return ProxyCard(
|
||||
groupType: state.groupType,
|
||||
type: proxyCardType,
|
||||
key: ValueKey('$groupName.${proxy.name}'),
|
||||
proxy: proxy,
|
||||
groupName: groupName,
|
||||
);
|
||||
},
|
||||
),
|
||||
itemCount: sortedProxies.length,
|
||||
itemBuilder: (_, index) {
|
||||
final proxy = sortedProxies[index];
|
||||
return ProxyCard(
|
||||
groupType: state.groupType,
|
||||
type: proxyCardType,
|
||||
key: ValueKey('$groupName.${proxy.name}'),
|
||||
proxy: proxy,
|
||||
groupName: groupName,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -33,13 +33,21 @@ class Resources extends StatelessWidget {
|
||||
fileName: geoIpFileName,
|
||||
key: "geoip",
|
||||
),
|
||||
GeoItem(label: "GeoSite", fileName: geoSiteFileName, key: "geosite"),
|
||||
GeoItem(
|
||||
label: "GeoSite",
|
||||
fileName: geoSiteFileName,
|
||||
key: "geosite",
|
||||
),
|
||||
GeoItem(
|
||||
label: "MMDB",
|
||||
fileName: mmdbFileName,
|
||||
key: "mmdb",
|
||||
),
|
||||
GeoItem(label: "ASN", fileName: asnFileName, key: "asn"),
|
||||
GeoItem(
|
||||
label: "ASN",
|
||||
fileName: asnFileName,
|
||||
key: "asn",
|
||||
),
|
||||
];
|
||||
|
||||
return ListView.separated(
|
||||
@@ -81,6 +89,7 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
||||
child: UpdateGeoUrlFormDialog(
|
||||
title: geoItem.label,
|
||||
url: url,
|
||||
defaultValue: defaultGeoXMap[geoItem.key],
|
||||
),
|
||||
);
|
||||
if (newUrl != null && newUrl != url && mounted) {
|
||||
@@ -238,11 +247,13 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
||||
class UpdateGeoUrlFormDialog extends StatefulWidget {
|
||||
final String title;
|
||||
final String url;
|
||||
final String? defaultValue;
|
||||
|
||||
const UpdateGeoUrlFormDialog({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.url,
|
||||
this.defaultValue
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -258,6 +269,13 @@ class _UpdateGeoUrlFormDialogState extends State<UpdateGeoUrlFormDialog> {
|
||||
urlController = TextEditingController(text: widget.url);
|
||||
}
|
||||
|
||||
_handleReset() async {
|
||||
if (widget.defaultValue == null) {
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pop<String>(widget.defaultValue);
|
||||
}
|
||||
|
||||
_handleUpdate() async {
|
||||
final url = urlController.value.text;
|
||||
if (url.isEmpty) return;
|
||||
@@ -285,6 +303,16 @@ class _UpdateGeoUrlFormDialogState extends State<UpdateGeoUrlFormDialog> {
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (widget.defaultValue != null &&
|
||||
urlController.value.text != widget.defaultValue) ...[
|
||||
TextButton(
|
||||
onPressed: _handleReset,
|
||||
child: Text(appLocalizations.reset),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
],
|
||||
TextButton(
|
||||
onPressed: _handleUpdate,
|
||||
child: Text(appLocalizations.submit),
|
||||
|
||||
@@ -238,15 +238,66 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
|
||||
),
|
||||
title: Text(appLocalizations.prueBlackMode),
|
||||
delegate: SwitchDelegate(
|
||||
value: value,
|
||||
onChanged: (value){
|
||||
globalState.appController.config.prueBlack = value;
|
||||
}
|
||||
),
|
||||
value: value,
|
||||
onChanged: (value) {
|
||||
globalState.appController.config.prueBlack = value;
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
// child: Selector<Config, bool>(
|
||||
// selector: (_, config) => config.scaleProps.custom,
|
||||
// builder: (_, value, ___) {
|
||||
// return ListItem.switchItem(
|
||||
// leading: Icon(
|
||||
// Icons.format_size_sharp,
|
||||
// color: context.colorScheme.primary,
|
||||
// ),
|
||||
// title: const Text("自定义字体大小"),
|
||||
// delegate: SwitchDelegate(
|
||||
// value: value,
|
||||
// onChanged: (value) {
|
||||
// globalState.appController.config.scaleProps =
|
||||
// globalState.appController.config.scaleProps.copyWith(
|
||||
// custom: value,
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// SizedBox(
|
||||
// height: 20,
|
||||
// child: Selector<Config, ScaleProps>(
|
||||
// selector: (_, config) => config.scaleProps,
|
||||
// builder: (_, props, ___) {
|
||||
// return AbsorbPointer(
|
||||
// absorbing: !props.custom,
|
||||
// child: DisabledMask(
|
||||
// status: !props.custom,
|
||||
// child: Slider(
|
||||
// value: props.scale,
|
||||
// min: 0.8,
|
||||
// max: 1.2,
|
||||
// onChanged: (value) {
|
||||
// globalState.appController.config.scaleProps =
|
||||
// globalState.appController.config.scaleProps.copyWith(
|
||||
// scale: value,
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
const SizedBox(
|
||||
height: 64,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,16 +4,18 @@ import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/fragments/about.dart';
|
||||
import 'package:fl_clash/fragments/access.dart';
|
||||
import 'package:fl_clash/fragments/application_setting.dart';
|
||||
import 'package:fl_clash/fragments/config.dart';
|
||||
import 'package:fl_clash/fragments/config/config.dart';
|
||||
import 'package:fl_clash/fragments/hotkey.dart';
|
||||
import 'package:fl_clash/l10n/l10n.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:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../widgets/widgets.dart';
|
||||
import 'backup_and_recovery.dart';
|
||||
import 'theme.dart';
|
||||
import 'package:path/path.dart' show dirname, join;
|
||||
|
||||
class ToolsFragment extends StatefulWidget {
|
||||
const ToolsFragment({super.key});
|
||||
@@ -61,6 +63,17 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
|
||||
return generateSection(
|
||||
title: appLocalizations.other,
|
||||
items: [
|
||||
ListItem(
|
||||
leading: const Icon(Icons.gavel),
|
||||
title: Text(appLocalizations.disclaimer),
|
||||
onTap: () async {
|
||||
final isDisclaimerAccepted =
|
||||
await globalState.appController.showDisclaimer();
|
||||
if (!isDisclaimerAccepted) {
|
||||
system.exit();
|
||||
}
|
||||
},
|
||||
),
|
||||
ListItem.open(
|
||||
leading: const Icon(Icons.info),
|
||||
title: Text(appLocalizations.about),
|
||||
@@ -82,44 +95,20 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
|
||||
builder: (_, localeString, __) {
|
||||
final subTitle = localeString ?? appLocalizations.defaultText;
|
||||
final currentLocale = other.getLocaleForString(localeString);
|
||||
return ListTile(
|
||||
return ListItem<Locale?>.options(
|
||||
leading: const Icon(Icons.language_outlined),
|
||||
title: Text(appLocalizations.language),
|
||||
subtitle: Text(Intl.message(subTitle)),
|
||||
onTap: () {
|
||||
globalState.showCommonDialog(
|
||||
child: AlertDialog(
|
||||
title: Text(appLocalizations.language),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 16,
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 250,
|
||||
child: Wrap(
|
||||
children: [
|
||||
for (final locale in [
|
||||
null,
|
||||
...AppLocalizations.delegate.supportedLocales
|
||||
])
|
||||
ListItem.radio(
|
||||
delegate: RadioDelegate<Locale?>(
|
||||
value: locale,
|
||||
groupValue: currentLocale,
|
||||
onChanged: (Locale? value) {
|
||||
final config = context.read<Config>();
|
||||
config.locale = value?.toString();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
title: Text(_getLocaleString(locale)),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
delegate: OptionsDelegate(
|
||||
title: appLocalizations.language,
|
||||
options: [null, ...AppLocalizations.delegate.supportedLocales],
|
||||
onChanged: (Locale? value) {
|
||||
final config = context.read<Config>();
|
||||
config.locale = value?.toString();
|
||||
},
|
||||
textBuilder: (locale) => _getLocaleString(locale),
|
||||
value: currentLocale,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -142,6 +131,28 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
|
||||
widget: const BackupAndRecovery(),
|
||||
),
|
||||
),
|
||||
if (system.isDesktop)
|
||||
ListItem.open(
|
||||
leading: const Icon(Icons.keyboard),
|
||||
title: Text(appLocalizations.hotkeyManagement),
|
||||
subtitle: Text(appLocalizations.hotkeyManagementDesc),
|
||||
delegate: OpenDelegate(
|
||||
title: appLocalizations.hotkeyManagement,
|
||||
widget: const HotKeyFragment(),
|
||||
),
|
||||
),
|
||||
if (Platform.isWindows)
|
||||
ListItem(
|
||||
leading: const Icon(Icons.lock),
|
||||
title: Text(appLocalizations.loopback),
|
||||
subtitle: Text(appLocalizations.loopbackDesc),
|
||||
onTap: () {
|
||||
windows?.runas(
|
||||
'"${join(dirname(Platform.resolvedExecutable), "EnableLoopback.exe")}"',
|
||||
"",
|
||||
);
|
||||
},
|
||||
),
|
||||
if (Platform.isAndroid)
|
||||
ListItem.open(
|
||||
leading: const Icon(Icons.view_list),
|
||||
@@ -159,11 +170,10 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
|
||||
delegate: OpenDelegate(
|
||||
title: appLocalizations.override,
|
||||
widget: const ConfigFragment(),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
),
|
||||
ListItem.open(
|
||||
leading: const Icon(Icons.settings_applications),
|
||||
leading: const Icon(Icons.settings),
|
||||
title: Text(appLocalizations.application),
|
||||
subtitle: Text(appLocalizations.applicationDesc),
|
||||
delegate: OpenDelegate(
|
||||
@@ -177,9 +187,8 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, String?>(
|
||||
selector: (_, config) => config.locale,
|
||||
builder: (_, __, ___) {
|
||||
return LocaleBuilder(
|
||||
builder: (_) {
|
||||
final items = [
|
||||
Selector<AppState, MoreToolsSelectorState>(
|
||||
selector: (_, appState) {
|
||||
@@ -212,6 +221,7 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
|
||||
return ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, index) => items[index],
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -241,5 +241,67 @@
|
||||
"tight": "Tight",
|
||||
"standard": "Standard",
|
||||
"loose": "Loose",
|
||||
"profilesSort": "Profiles sort"
|
||||
"profilesSort": "Profiles sort",
|
||||
"start": "Start",
|
||||
"stop": "Stop",
|
||||
"appDesc": "Processing app related settings",
|
||||
"vpnDesc": "Modify VPN related settings",
|
||||
"generalDesc": "Overwrite general settings",
|
||||
"dnsDesc": "Update DNS related settings",
|
||||
"key": "Key",
|
||||
"value": "Value",
|
||||
"keyNotEmpty": "The key cannot be empty",
|
||||
"valueNotEmpty": "The value cannot be empty",
|
||||
"hostsDesc": "Add Hosts",
|
||||
"vpnTip": "Changes take effect after restarting the VPN",
|
||||
"vpnEnableDesc": "Auto routes all system traffic through VpnService",
|
||||
"options": "Options",
|
||||
"loopback": "Loopback unlock tool",
|
||||
"loopbackDesc": "Used for UWP loopback unlocking",
|
||||
"providers": "Providers",
|
||||
"proxyProviders": "Proxy providers",
|
||||
"ruleProviders": "Rule providers",
|
||||
"overrideDns": "Override Dns",
|
||||
"overrideDnsDesc": "Turning it on will override the DNS options in the profile",
|
||||
"status": "Status",
|
||||
"statusDesc": "System DNS will be used when turned off",
|
||||
"preferH3Desc": "Prioritize the use of DOH's http/3",
|
||||
"respectRules": "Respect rules",
|
||||
"respectRulesDesc": "DNS connection following rules, need to configure proxy-server-nameserver",
|
||||
"dnsMode": "DNS mode",
|
||||
"fakeipRange": "Fakeip range",
|
||||
"fakeipFilter": "Fakeip filter",
|
||||
"defaultNameserver": "Default nameserver",
|
||||
"defaultNameserverDesc": "For resolving DNS server",
|
||||
"nameserver": "Nameserver",
|
||||
"nameserverDesc": "For resolving domain",
|
||||
"useHosts": "Use hosts",
|
||||
"useSystemHosts": "Use system hosts",
|
||||
"nameserverPolicy": "Nameserver policy",
|
||||
"nameserverPolicyDesc": "Specify the corresponding nameserver policy",
|
||||
"proxyNameserver": "Proxy nameserver",
|
||||
"proxyNameserverDesc": "Domain for resolving proxy nodes",
|
||||
"fallback": "Fallback",
|
||||
"fallbackDesc": "Generally use offshore DNS",
|
||||
"fallbackFilter": "Fallback filter",
|
||||
"geoipCode": "Geoip code",
|
||||
"ipcidr": "Ipcidr",
|
||||
"domain": "Domain",
|
||||
"resetDns": "Reset Dns",
|
||||
"reset": "Reset",
|
||||
"action_view": "Show/Hide",
|
||||
"action_start": "Start/Stop",
|
||||
"action_mode": "Switch mode",
|
||||
"action_proxy": "System proxy",
|
||||
"action_tun": "TUN",
|
||||
"disclaimer": "Disclaimer",
|
||||
"disclaimerDesc": "This software is only used for non-commercial purposes such as learning exchanges and scientific research. It is strictly prohibited to use this software for commercial purposes. Any commercial activity, if any, has nothing to do with this software.",
|
||||
"agree": "Agree",
|
||||
"hotkeyManagement": "Hotkey Management",
|
||||
"hotkeyManagementDesc": "Use keyboard to control applications",
|
||||
"pressKeyboard": "Please press the keyboard.",
|
||||
"inputCorrectHotkey": "Please enter the correct hotkey",
|
||||
"hotkeyConflict": "Hotkey conflict",
|
||||
"remove": "Remove",
|
||||
"noHotKey": "No HotKey"
|
||||
}
|
||||
@@ -238,8 +238,70 @@
|
||||
"clipboardImport": "剪贴板导入",
|
||||
"clipboardExport": "导出剪贴板",
|
||||
"layout": "布局",
|
||||
"tight": "宽松",
|
||||
"tight": "紧凑",
|
||||
"standard": "标准",
|
||||
"loose": "紧凑",
|
||||
"profilesSort": "配置排序"
|
||||
"loose": "宽松",
|
||||
"profilesSort": "配置排序",
|
||||
"start": "启动",
|
||||
"stop": "暂停",
|
||||
"appDesc": "处理应用相关设置",
|
||||
"vpnDesc": "修改VPN相关设置",
|
||||
"generalDesc": "覆写基础设置",
|
||||
"dnsDesc": "更新DNS相关设置",
|
||||
"key": "键",
|
||||
"value": "值",
|
||||
"keyNotEmpty": "键不能为空",
|
||||
"valueNotEmpty": "值不能为空",
|
||||
"hostsDesc": "追加Hosts",
|
||||
"vpnTip": "重启VPN后改变生效",
|
||||
"vpnEnableDesc": "通过VpnService自动路由系统所有流量",
|
||||
"options": "选项",
|
||||
"loopback": "回环解锁工具",
|
||||
"loopbackDesc": "用于UWP回环解锁",
|
||||
"providers": "提供者",
|
||||
"proxyProviders": "代理提供者",
|
||||
"ruleProviders": "规则提供者",
|
||||
"overrideDns": "覆写DNS",
|
||||
"overrideDnsDesc": "开启后将覆盖配置中的DNS选项",
|
||||
"status": "状态",
|
||||
"statusDesc": "关闭后将使用系统DNS",
|
||||
"preferH3Desc": "优先使用DOH的http/3",
|
||||
"respectRules": "遵守规则",
|
||||
"respectRulesDesc": "DNS连接跟随rules,需配置proxy-server-nameserver",
|
||||
"dnsMode": "DNS模式",
|
||||
"fakeipRange": "Fakeip范围",
|
||||
"fakeipFilter": "Fakeip过滤",
|
||||
"defaultNameserver": "默认域名服务器",
|
||||
"defaultNameserverDesc": "用于解析DNS服务器",
|
||||
"nameserver": "域名服务器",
|
||||
"nameserverDesc": "用于解析域名",
|
||||
"useHosts": "使用Hosts",
|
||||
"useSystemHosts": "使用系统Hosts",
|
||||
"nameserverPolicy": "域名服务器策略",
|
||||
"nameserverPolicyDesc": "指定对应域名服务器策略",
|
||||
"proxyNameserver": "代理域名服务器",
|
||||
"proxyNameserverDesc": "用于解析代理节点的域名",
|
||||
"fallback": "Fallback",
|
||||
"fallbackDesc": "一般情况下使用境外DNS",
|
||||
"fallbackFilter": "Fallback过滤",
|
||||
"geoipCode": "Geoip代码",
|
||||
"ipcidr": "IP/掩码",
|
||||
"domain": "域名",
|
||||
"resetDns": "重置DNS",
|
||||
"reset": "重置",
|
||||
"action_view": "显示/隐藏",
|
||||
"action_start": "启动/停止",
|
||||
"action_mode": "切换模式",
|
||||
"action_proxy": "系统代理",
|
||||
"action_tun": "虚拟网卡",
|
||||
"disclaimer": "免责声明",
|
||||
"disclaimerDesc": "本软件仅供学习交流、科研等非商业性质的用途,严禁将本软件用于商业目的。如有任何商业行为,均与本软件无关。",
|
||||
"agree": "同意",
|
||||
"hotkeyManagement": "快捷键管理",
|
||||
"hotkeyManagementDesc": "使用键盘控制应用程序",
|
||||
"pressKeyboard": "请按下按键",
|
||||
"inputCorrectHotkey": "请输入正确的快捷键",
|
||||
"hotkeyConflict": "快捷键冲突",
|
||||
"remove": "移除",
|
||||
"noHotKey": "暂无快捷键"
|
||||
}
|
||||
@@ -34,6 +34,11 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"accountTip":
|
||||
MessageLookupByLibrary.simpleMessage("Account cannot be empty"),
|
||||
"action": MessageLookupByLibrary.simpleMessage("Action"),
|
||||
"action_mode": MessageLookupByLibrary.simpleMessage("Switch mode"),
|
||||
"action_proxy": MessageLookupByLibrary.simpleMessage("System proxy"),
|
||||
"action_start": MessageLookupByLibrary.simpleMessage("Start/Stop"),
|
||||
"action_tun": MessageLookupByLibrary.simpleMessage("TUN"),
|
||||
"action_view": MessageLookupByLibrary.simpleMessage("Show/Hide"),
|
||||
"add": MessageLookupByLibrary.simpleMessage("Add"),
|
||||
"address": MessageLookupByLibrary.simpleMessage("Address"),
|
||||
"addressHelp":
|
||||
@@ -41,6 +46,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"addressTip": MessageLookupByLibrary.simpleMessage(
|
||||
"Please enter a valid WebDAV address"),
|
||||
"ago": MessageLookupByLibrary.simpleMessage(" Ago"),
|
||||
"agree": MessageLookupByLibrary.simpleMessage("Agree"),
|
||||
"allApps": MessageLookupByLibrary.simpleMessage("All apps"),
|
||||
"allowBypass": MessageLookupByLibrary.simpleMessage(
|
||||
"Allow applications to bypass VPN"),
|
||||
@@ -52,6 +58,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"app": MessageLookupByLibrary.simpleMessage("App"),
|
||||
"appAccessControl":
|
||||
MessageLookupByLibrary.simpleMessage("App access control"),
|
||||
"appDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Processing app related settings"),
|
||||
"application": MessageLookupByLibrary.simpleMessage("Application"),
|
||||
"applicationDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Modify application related settings"),
|
||||
@@ -114,6 +122,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"dark": MessageLookupByLibrary.simpleMessage("Dark"),
|
||||
"dashboard": MessageLookupByLibrary.simpleMessage("Dashboard"),
|
||||
"days": MessageLookupByLibrary.simpleMessage("Days"),
|
||||
"defaultNameserver":
|
||||
MessageLookupByLibrary.simpleMessage("Default nameserver"),
|
||||
"defaultNameserverDesc":
|
||||
MessageLookupByLibrary.simpleMessage("For resolving DNS server"),
|
||||
"defaultSort": MessageLookupByLibrary.simpleMessage("Sort by default"),
|
||||
"defaultText": MessageLookupByLibrary.simpleMessage("Default"),
|
||||
"delay": MessageLookupByLibrary.simpleMessage("Delay"),
|
||||
@@ -124,12 +136,19 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"desc": MessageLookupByLibrary.simpleMessage(
|
||||
"A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free."),
|
||||
"direct": MessageLookupByLibrary.simpleMessage("Direct"),
|
||||
"disclaimer": MessageLookupByLibrary.simpleMessage("Disclaimer"),
|
||||
"disclaimerDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"This software is only used for non-commercial purposes such as learning exchanges and scientific research. It is strictly prohibited to use this software for commercial purposes. Any commercial activity, if any, has nothing to do with this software."),
|
||||
"discoverNewVersion":
|
||||
MessageLookupByLibrary.simpleMessage("Discover the new version"),
|
||||
"discovery":
|
||||
MessageLookupByLibrary.simpleMessage("Discovery a new version"),
|
||||
"dnsDesc":
|
||||
MessageLookupByLibrary.simpleMessage("Update DNS related settings"),
|
||||
"dnsMode": MessageLookupByLibrary.simpleMessage("DNS mode"),
|
||||
"doYouWantToPass":
|
||||
MessageLookupByLibrary.simpleMessage("Do you want to pass"),
|
||||
"domain": MessageLookupByLibrary.simpleMessage("Domain"),
|
||||
"download": MessageLookupByLibrary.simpleMessage("Download"),
|
||||
"edit": MessageLookupByLibrary.simpleMessage("Edit"),
|
||||
"en": MessageLookupByLibrary.simpleMessage("English"),
|
||||
@@ -149,6 +168,13 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"externalLink": MessageLookupByLibrary.simpleMessage("External link"),
|
||||
"externalResources":
|
||||
MessageLookupByLibrary.simpleMessage("External resources"),
|
||||
"fakeipFilter": MessageLookupByLibrary.simpleMessage("Fakeip filter"),
|
||||
"fakeipRange": MessageLookupByLibrary.simpleMessage("Fakeip range"),
|
||||
"fallback": MessageLookupByLibrary.simpleMessage("Fallback"),
|
||||
"fallbackDesc":
|
||||
MessageLookupByLibrary.simpleMessage("Generally use offshore DNS"),
|
||||
"fallbackFilter":
|
||||
MessageLookupByLibrary.simpleMessage("Fallback filter"),
|
||||
"file": MessageLookupByLibrary.simpleMessage("File"),
|
||||
"fileDesc":
|
||||
MessageLookupByLibrary.simpleMessage("Directly upload profile"),
|
||||
@@ -159,28 +185,44 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"There is a risk of flashback after opening"),
|
||||
"fourColumns": MessageLookupByLibrary.simpleMessage("Four columns"),
|
||||
"general": MessageLookupByLibrary.simpleMessage("General"),
|
||||
"generalDesc":
|
||||
MessageLookupByLibrary.simpleMessage("Overwrite general settings"),
|
||||
"geoData": MessageLookupByLibrary.simpleMessage("GeoData"),
|
||||
"geodataLoader":
|
||||
MessageLookupByLibrary.simpleMessage("Geo Low Memory Mode"),
|
||||
"geodataLoaderDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Enabling will use the Geo low memory loader"),
|
||||
"geoipCode": MessageLookupByLibrary.simpleMessage("Geoip code"),
|
||||
"global": MessageLookupByLibrary.simpleMessage("Global"),
|
||||
"go": MessageLookupByLibrary.simpleMessage("Go"),
|
||||
"goDownload": MessageLookupByLibrary.simpleMessage("Go to download"),
|
||||
"hostsDesc": MessageLookupByLibrary.simpleMessage("Add Hosts"),
|
||||
"hotkeyConflict":
|
||||
MessageLookupByLibrary.simpleMessage("Hotkey conflict"),
|
||||
"hotkeyManagement":
|
||||
MessageLookupByLibrary.simpleMessage("Hotkey Management"),
|
||||
"hotkeyManagementDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Use keyboard to control applications"),
|
||||
"hours": MessageLookupByLibrary.simpleMessage("Hours"),
|
||||
"importFromURL":
|
||||
MessageLookupByLibrary.simpleMessage("Import from URL"),
|
||||
"infiniteTime":
|
||||
MessageLookupByLibrary.simpleMessage("Long term effective"),
|
||||
"init": MessageLookupByLibrary.simpleMessage("Init"),
|
||||
"inputCorrectHotkey": MessageLookupByLibrary.simpleMessage(
|
||||
"Please enter the correct hotkey"),
|
||||
"intelligentSelected":
|
||||
MessageLookupByLibrary.simpleMessage("Intelligent selection"),
|
||||
"intranetIP": MessageLookupByLibrary.simpleMessage("Intranet IP"),
|
||||
"ipcidr": MessageLookupByLibrary.simpleMessage("Ipcidr"),
|
||||
"ipv6Desc": MessageLookupByLibrary.simpleMessage(
|
||||
"When turned on it will be able to receive IPv6 traffic"),
|
||||
"just": MessageLookupByLibrary.simpleMessage("Just"),
|
||||
"keepAliveIntervalDesc":
|
||||
MessageLookupByLibrary.simpleMessage("Tcp keep alive interval"),
|
||||
"key": MessageLookupByLibrary.simpleMessage("Key"),
|
||||
"keyNotEmpty":
|
||||
MessageLookupByLibrary.simpleMessage("The key cannot be empty"),
|
||||
"language": MessageLookupByLibrary.simpleMessage("Language"),
|
||||
"layout": MessageLookupByLibrary.simpleMessage("Layout"),
|
||||
"light": MessageLookupByLibrary.simpleMessage("Light"),
|
||||
@@ -196,6 +238,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Disabling will hide the log entry"),
|
||||
"logs": MessageLookupByLibrary.simpleMessage("Logs"),
|
||||
"logsDesc": MessageLookupByLibrary.simpleMessage("Log capture records"),
|
||||
"loopback":
|
||||
MessageLookupByLibrary.simpleMessage("Loopback unlock tool"),
|
||||
"loopbackDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Used for UWP loopback unlocking"),
|
||||
"loose": MessageLookupByLibrary.simpleMessage("Loose"),
|
||||
"min": MessageLookupByLibrary.simpleMessage("Min"),
|
||||
"minimizeOnExit":
|
||||
@@ -208,9 +254,17 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"more": MessageLookupByLibrary.simpleMessage("More"),
|
||||
"name": MessageLookupByLibrary.simpleMessage("Name"),
|
||||
"nameSort": MessageLookupByLibrary.simpleMessage("Sort by name"),
|
||||
"nameserver": MessageLookupByLibrary.simpleMessage("Nameserver"),
|
||||
"nameserverDesc":
|
||||
MessageLookupByLibrary.simpleMessage("For resolving domain"),
|
||||
"nameserverPolicy":
|
||||
MessageLookupByLibrary.simpleMessage("Nameserver policy"),
|
||||
"nameserverPolicyDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Specify the corresponding nameserver policy"),
|
||||
"networkDetection":
|
||||
MessageLookupByLibrary.simpleMessage("Network detection"),
|
||||
"networkSpeed": MessageLookupByLibrary.simpleMessage("Network speed"),
|
||||
"noHotKey": MessageLookupByLibrary.simpleMessage("No HotKey"),
|
||||
"noInfo": MessageLookupByLibrary.simpleMessage("No info"),
|
||||
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("No more info"),
|
||||
"noProxy": MessageLookupByLibrary.simpleMessage("No proxy"),
|
||||
@@ -233,6 +287,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("Only statistics proxy"),
|
||||
"onlyStatisticsProxyDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"When turned on, only statistics proxy traffic"),
|
||||
"options": MessageLookupByLibrary.simpleMessage("Options"),
|
||||
"other": MessageLookupByLibrary.simpleMessage("Other"),
|
||||
"otherContributors":
|
||||
MessageLookupByLibrary.simpleMessage("Other contributors"),
|
||||
@@ -240,6 +295,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"override": MessageLookupByLibrary.simpleMessage("Override"),
|
||||
"overrideDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Override Proxy related config"),
|
||||
"overrideDns": MessageLookupByLibrary.simpleMessage("Override Dns"),
|
||||
"overrideDnsDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Turning it on will override the DNS options in the profile"),
|
||||
"password": MessageLookupByLibrary.simpleMessage("Password"),
|
||||
"passwordTip":
|
||||
MessageLookupByLibrary.simpleMessage("Password cannot be empty"),
|
||||
@@ -251,6 +309,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage(
|
||||
"Please upload a valid QR code"),
|
||||
"port": MessageLookupByLibrary.simpleMessage("Port"),
|
||||
"preferH3Desc": MessageLookupByLibrary.simpleMessage(
|
||||
"Prioritize the use of DOH\'s http/3"),
|
||||
"pressKeyboard":
|
||||
MessageLookupByLibrary.simpleMessage("Please press the keyboard."),
|
||||
"preview": MessageLookupByLibrary.simpleMessage("Preview"),
|
||||
"profile": MessageLookupByLibrary.simpleMessage("Profile"),
|
||||
"profileAutoUpdateIntervalInvalidValidationDesc":
|
||||
@@ -270,13 +332,20 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"profiles": MessageLookupByLibrary.simpleMessage("Profiles"),
|
||||
"profilesSort": MessageLookupByLibrary.simpleMessage("Profiles sort"),
|
||||
"project": MessageLookupByLibrary.simpleMessage("Project"),
|
||||
"providers": MessageLookupByLibrary.simpleMessage("Providers"),
|
||||
"proxies": MessageLookupByLibrary.simpleMessage("Proxies"),
|
||||
"proxiesSetting":
|
||||
MessageLookupByLibrary.simpleMessage("Proxies setting"),
|
||||
"proxyGroup": MessageLookupByLibrary.simpleMessage("Proxy group"),
|
||||
"proxyNameserver":
|
||||
MessageLookupByLibrary.simpleMessage("Proxy nameserver"),
|
||||
"proxyNameserverDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Domain for resolving proxy nodes"),
|
||||
"proxyPort": MessageLookupByLibrary.simpleMessage("ProxyPort"),
|
||||
"proxyPortDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Set the Clash listening port"),
|
||||
"proxyProviders":
|
||||
MessageLookupByLibrary.simpleMessage("Proxy providers"),
|
||||
"prueBlackMode":
|
||||
MessageLookupByLibrary.simpleMessage("Prue black mode"),
|
||||
"qrcode": MessageLookupByLibrary.simpleMessage("QR code"),
|
||||
@@ -294,13 +363,20 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("Backup local data to WebDAV"),
|
||||
"remoteRecoveryDesc":
|
||||
MessageLookupByLibrary.simpleMessage("Recovery data from WebDAV"),
|
||||
"remove": MessageLookupByLibrary.simpleMessage("Remove"),
|
||||
"requests": MessageLookupByLibrary.simpleMessage("Requests"),
|
||||
"requestsDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"View recently request records"),
|
||||
"reset": MessageLookupByLibrary.simpleMessage("Reset"),
|
||||
"resetDns": MessageLookupByLibrary.simpleMessage("Reset Dns"),
|
||||
"resources": MessageLookupByLibrary.simpleMessage("Resources"),
|
||||
"resourcesDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"External resource related info"),
|
||||
"respectRules": MessageLookupByLibrary.simpleMessage("Respect rules"),
|
||||
"respectRulesDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"DNS connection following rules, need to configure proxy-server-nameserver"),
|
||||
"rule": MessageLookupByLibrary.simpleMessage("Rule"),
|
||||
"ruleProviders": MessageLookupByLibrary.simpleMessage("Rule providers"),
|
||||
"save": MessageLookupByLibrary.simpleMessage("Save"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"seconds": MessageLookupByLibrary.simpleMessage("Seconds"),
|
||||
@@ -316,7 +392,12 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"sort": MessageLookupByLibrary.simpleMessage("Sort"),
|
||||
"source": MessageLookupByLibrary.simpleMessage("Source"),
|
||||
"standard": MessageLookupByLibrary.simpleMessage("Standard"),
|
||||
"start": MessageLookupByLibrary.simpleMessage("Start"),
|
||||
"startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."),
|
||||
"status": MessageLookupByLibrary.simpleMessage("Status"),
|
||||
"statusDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"System DNS will be used when turned off"),
|
||||
"stop": MessageLookupByLibrary.simpleMessage("Stop"),
|
||||
"stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."),
|
||||
"style": MessageLookupByLibrary.simpleMessage("Style"),
|
||||
"submit": MessageLookupByLibrary.simpleMessage("Submit"),
|
||||
@@ -359,7 +440,19 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"url": MessageLookupByLibrary.simpleMessage("URL"),
|
||||
"urlDesc":
|
||||
MessageLookupByLibrary.simpleMessage("Obtain profile through URL"),
|
||||
"useHosts": MessageLookupByLibrary.simpleMessage("Use hosts"),
|
||||
"useSystemHosts":
|
||||
MessageLookupByLibrary.simpleMessage("Use system hosts"),
|
||||
"value": MessageLookupByLibrary.simpleMessage("Value"),
|
||||
"valueNotEmpty":
|
||||
MessageLookupByLibrary.simpleMessage("The value cannot be empty"),
|
||||
"view": MessageLookupByLibrary.simpleMessage("View"),
|
||||
"vpnDesc":
|
||||
MessageLookupByLibrary.simpleMessage("Modify VPN related settings"),
|
||||
"vpnEnableDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Auto routes all system traffic through VpnService"),
|
||||
"vpnTip": MessageLookupByLibrary.simpleMessage(
|
||||
"Changes take effect after restarting the VPN"),
|
||||
"webDAVConfiguration":
|
||||
MessageLookupByLibrary.simpleMessage("WebDAV configuration"),
|
||||
"whitelistMode": MessageLookupByLibrary.simpleMessage("Whitelist mode"),
|
||||
|
||||
@@ -32,11 +32,17 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"account": MessageLookupByLibrary.simpleMessage("账号"),
|
||||
"accountTip": MessageLookupByLibrary.simpleMessage("账号不能为空"),
|
||||
"action": MessageLookupByLibrary.simpleMessage("操作"),
|
||||
"action_mode": MessageLookupByLibrary.simpleMessage("切换模式"),
|
||||
"action_proxy": MessageLookupByLibrary.simpleMessage("系统代理"),
|
||||
"action_start": MessageLookupByLibrary.simpleMessage("启动/停止"),
|
||||
"action_tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),
|
||||
"action_view": MessageLookupByLibrary.simpleMessage("显示/隐藏"),
|
||||
"add": MessageLookupByLibrary.simpleMessage("添加"),
|
||||
"address": MessageLookupByLibrary.simpleMessage("地址"),
|
||||
"addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"),
|
||||
"addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"),
|
||||
"ago": MessageLookupByLibrary.simpleMessage("前"),
|
||||
"agree": MessageLookupByLibrary.simpleMessage("同意"),
|
||||
"allApps": MessageLookupByLibrary.simpleMessage("所有应用"),
|
||||
"allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"),
|
||||
"allowBypassDesc":
|
||||
@@ -45,6 +51,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"allowLanDesc": MessageLookupByLibrary.simpleMessage("允许通过局域网访问代理"),
|
||||
"app": MessageLookupByLibrary.simpleMessage("应用"),
|
||||
"appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"),
|
||||
"appDesc": MessageLookupByLibrary.simpleMessage("处理应用相关设置"),
|
||||
"application": MessageLookupByLibrary.simpleMessage("应用程序"),
|
||||
"applicationDesc": MessageLookupByLibrary.simpleMessage("修改应用程序相关设置"),
|
||||
"auto": MessageLookupByLibrary.simpleMessage("自动"),
|
||||
@@ -94,6 +101,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"dark": MessageLookupByLibrary.simpleMessage("深色"),
|
||||
"dashboard": MessageLookupByLibrary.simpleMessage("仪表盘"),
|
||||
"days": MessageLookupByLibrary.simpleMessage("天"),
|
||||
"defaultNameserver": MessageLookupByLibrary.simpleMessage("默认域名服务器"),
|
||||
"defaultNameserverDesc":
|
||||
MessageLookupByLibrary.simpleMessage("用于解析DNS服务器"),
|
||||
"defaultSort": MessageLookupByLibrary.simpleMessage("按默认排序"),
|
||||
"defaultText": MessageLookupByLibrary.simpleMessage("默认"),
|
||||
"delay": MessageLookupByLibrary.simpleMessage("延迟"),
|
||||
@@ -103,9 +113,15 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"desc": MessageLookupByLibrary.simpleMessage(
|
||||
"基于ClashMeta的多平台代理客户端,简单易用,开源无广告。"),
|
||||
"direct": MessageLookupByLibrary.simpleMessage("直连"),
|
||||
"disclaimer": MessageLookupByLibrary.simpleMessage("免责声明"),
|
||||
"disclaimerDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"本软件仅供学习交流、科研等非商业性质的用途,严禁将本软件用于商业目的。如有任何商业行为,均与本软件无关。"),
|
||||
"discoverNewVersion": MessageLookupByLibrary.simpleMessage("发现新版本"),
|
||||
"discovery": MessageLookupByLibrary.simpleMessage("发现新版本"),
|
||||
"dnsDesc": MessageLookupByLibrary.simpleMessage("更新DNS相关设置"),
|
||||
"dnsMode": MessageLookupByLibrary.simpleMessage("DNS模式"),
|
||||
"doYouWantToPass": MessageLookupByLibrary.simpleMessage("是否要通过"),
|
||||
"domain": MessageLookupByLibrary.simpleMessage("域名"),
|
||||
"download": MessageLookupByLibrary.simpleMessage("下载"),
|
||||
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
|
||||
"en": MessageLookupByLibrary.simpleMessage("英语"),
|
||||
@@ -121,6 +137,11 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制Clash内核"),
|
||||
"externalLink": MessageLookupByLibrary.simpleMessage("外部链接"),
|
||||
"externalResources": MessageLookupByLibrary.simpleMessage("外部资源"),
|
||||
"fakeipFilter": MessageLookupByLibrary.simpleMessage("Fakeip过滤"),
|
||||
"fakeipRange": MessageLookupByLibrary.simpleMessage("Fakeip范围"),
|
||||
"fallback": MessageLookupByLibrary.simpleMessage("Fallback"),
|
||||
"fallbackDesc": MessageLookupByLibrary.simpleMessage("一般情况下使用境外DNS"),
|
||||
"fallbackFilter": MessageLookupByLibrary.simpleMessage("Fallback过滤"),
|
||||
"file": MessageLookupByLibrary.simpleMessage("文件"),
|
||||
"fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
|
||||
"filterSystemApp": MessageLookupByLibrary.simpleMessage("过滤系统应用"),
|
||||
@@ -129,23 +150,34 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("开启后存在闪退风险"),
|
||||
"fourColumns": MessageLookupByLibrary.simpleMessage("四列"),
|
||||
"general": MessageLookupByLibrary.simpleMessage("基础"),
|
||||
"generalDesc": MessageLookupByLibrary.simpleMessage("覆写基础设置"),
|
||||
"geoData": MessageLookupByLibrary.simpleMessage("地理数据"),
|
||||
"geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"),
|
||||
"geodataLoaderDesc":
|
||||
MessageLookupByLibrary.simpleMessage("开启将使用Geo低内存加载器"),
|
||||
"geoipCode": MessageLookupByLibrary.simpleMessage("Geoip代码"),
|
||||
"global": MessageLookupByLibrary.simpleMessage("全局"),
|
||||
"go": MessageLookupByLibrary.simpleMessage("前往"),
|
||||
"goDownload": MessageLookupByLibrary.simpleMessage("前往下载"),
|
||||
"hostsDesc": MessageLookupByLibrary.simpleMessage("追加Hosts"),
|
||||
"hotkeyConflict": MessageLookupByLibrary.simpleMessage("快捷键冲突"),
|
||||
"hotkeyManagement": MessageLookupByLibrary.simpleMessage("快捷键管理"),
|
||||
"hotkeyManagementDesc":
|
||||
MessageLookupByLibrary.simpleMessage("使用键盘控制应用程序"),
|
||||
"hours": MessageLookupByLibrary.simpleMessage("小时"),
|
||||
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
|
||||
"infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"),
|
||||
"init": MessageLookupByLibrary.simpleMessage("初始化"),
|
||||
"inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("请输入正确的快捷键"),
|
||||
"intelligentSelected": MessageLookupByLibrary.simpleMessage("智能选择"),
|
||||
"intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"),
|
||||
"ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"),
|
||||
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"),
|
||||
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
|
||||
"keepAliveIntervalDesc":
|
||||
MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"),
|
||||
"key": MessageLookupByLibrary.simpleMessage("键"),
|
||||
"keyNotEmpty": MessageLookupByLibrary.simpleMessage("键不能为空"),
|
||||
"language": MessageLookupByLibrary.simpleMessage("语言"),
|
||||
"layout": MessageLookupByLibrary.simpleMessage("布局"),
|
||||
"light": MessageLookupByLibrary.simpleMessage("浅色"),
|
||||
@@ -158,7 +190,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"),
|
||||
"logs": MessageLookupByLibrary.simpleMessage("日志"),
|
||||
"logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"),
|
||||
"loose": MessageLookupByLibrary.simpleMessage("紧凑"),
|
||||
"loopback": MessageLookupByLibrary.simpleMessage("回环解锁工具"),
|
||||
"loopbackDesc": MessageLookupByLibrary.simpleMessage("用于UWP回环解锁"),
|
||||
"loose": MessageLookupByLibrary.simpleMessage("宽松"),
|
||||
"min": MessageLookupByLibrary.simpleMessage("最小"),
|
||||
"minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"),
|
||||
"minimizeOnExitDesc":
|
||||
@@ -169,8 +203,14 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"more": MessageLookupByLibrary.simpleMessage("更多"),
|
||||
"name": MessageLookupByLibrary.simpleMessage("名称"),
|
||||
"nameSort": MessageLookupByLibrary.simpleMessage("按名称排序"),
|
||||
"nameserver": MessageLookupByLibrary.simpleMessage("域名服务器"),
|
||||
"nameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析域名"),
|
||||
"nameserverPolicy": MessageLookupByLibrary.simpleMessage("域名服务器策略"),
|
||||
"nameserverPolicyDesc":
|
||||
MessageLookupByLibrary.simpleMessage("指定对应域名服务器策略"),
|
||||
"networkDetection": MessageLookupByLibrary.simpleMessage("网络检测"),
|
||||
"networkSpeed": MessageLookupByLibrary.simpleMessage("网络速度"),
|
||||
"noHotKey": MessageLookupByLibrary.simpleMessage("暂无快捷键"),
|
||||
"noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"),
|
||||
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暂无更多信息"),
|
||||
"noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"),
|
||||
@@ -188,11 +228,15 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("仅统计代理"),
|
||||
"onlyStatisticsProxyDesc":
|
||||
MessageLookupByLibrary.simpleMessage("开启后,将只统计代理流量"),
|
||||
"options": MessageLookupByLibrary.simpleMessage("选项"),
|
||||
"other": MessageLookupByLibrary.simpleMessage("其他"),
|
||||
"otherContributors": MessageLookupByLibrary.simpleMessage("其他贡献者"),
|
||||
"outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"),
|
||||
"override": MessageLookupByLibrary.simpleMessage("覆写"),
|
||||
"overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"),
|
||||
"overrideDns": MessageLookupByLibrary.simpleMessage("覆写DNS"),
|
||||
"overrideDnsDesc":
|
||||
MessageLookupByLibrary.simpleMessage("开启后将覆盖配置中的DNS选项"),
|
||||
"password": MessageLookupByLibrary.simpleMessage("密码"),
|
||||
"passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"),
|
||||
"paste": MessageLookupByLibrary.simpleMessage("粘贴"),
|
||||
@@ -201,6 +245,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"pleaseUploadValidQrcode":
|
||||
MessageLookupByLibrary.simpleMessage("请上传有效的二维码"),
|
||||
"port": MessageLookupByLibrary.simpleMessage("端口"),
|
||||
"preferH3Desc": MessageLookupByLibrary.simpleMessage("优先使用DOH的http/3"),
|
||||
"pressKeyboard": MessageLookupByLibrary.simpleMessage("请按下按键"),
|
||||
"preview": MessageLookupByLibrary.simpleMessage("预览"),
|
||||
"profile": MessageLookupByLibrary.simpleMessage("配置"),
|
||||
"profileAutoUpdateIntervalInvalidValidationDesc":
|
||||
@@ -218,11 +264,16 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"profiles": MessageLookupByLibrary.simpleMessage("配置"),
|
||||
"profilesSort": MessageLookupByLibrary.simpleMessage("配置排序"),
|
||||
"project": MessageLookupByLibrary.simpleMessage("项目"),
|
||||
"providers": MessageLookupByLibrary.simpleMessage("提供者"),
|
||||
"proxies": MessageLookupByLibrary.simpleMessage("代理"),
|
||||
"proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"),
|
||||
"proxyGroup": MessageLookupByLibrary.simpleMessage("代理组"),
|
||||
"proxyNameserver": MessageLookupByLibrary.simpleMessage("代理域名服务器"),
|
||||
"proxyNameserverDesc":
|
||||
MessageLookupByLibrary.simpleMessage("用于解析代理节点的域名"),
|
||||
"proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"),
|
||||
"proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"),
|
||||
"proxyProviders": MessageLookupByLibrary.simpleMessage("代理提供者"),
|
||||
"prueBlackMode": MessageLookupByLibrary.simpleMessage("纯黑模式"),
|
||||
"qrcode": MessageLookupByLibrary.simpleMessage("二维码"),
|
||||
"qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"),
|
||||
@@ -234,11 +285,18 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"remoteBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"),
|
||||
"remoteRecoveryDesc":
|
||||
MessageLookupByLibrary.simpleMessage("通过WebDAV恢复数据"),
|
||||
"remove": MessageLookupByLibrary.simpleMessage("移除"),
|
||||
"requests": MessageLookupByLibrary.simpleMessage("请求"),
|
||||
"requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求记录"),
|
||||
"reset": MessageLookupByLibrary.simpleMessage("重置"),
|
||||
"resetDns": MessageLookupByLibrary.simpleMessage("重置DNS"),
|
||||
"resources": MessageLookupByLibrary.simpleMessage("资源"),
|
||||
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"),
|
||||
"respectRules": MessageLookupByLibrary.simpleMessage("遵守规则"),
|
||||
"respectRulesDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"DNS连接跟随rules,需配置proxy-server-nameserver"),
|
||||
"rule": MessageLookupByLibrary.simpleMessage("规则"),
|
||||
"ruleProviders": MessageLookupByLibrary.simpleMessage("规则提供者"),
|
||||
"save": MessageLookupByLibrary.simpleMessage("保存"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("搜索"),
|
||||
"seconds": MessageLookupByLibrary.simpleMessage("秒"),
|
||||
@@ -253,7 +311,11 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"sort": MessageLookupByLibrary.simpleMessage("排序"),
|
||||
"source": MessageLookupByLibrary.simpleMessage("来源"),
|
||||
"standard": MessageLookupByLibrary.simpleMessage("标准"),
|
||||
"start": MessageLookupByLibrary.simpleMessage("启动"),
|
||||
"startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."),
|
||||
"status": MessageLookupByLibrary.simpleMessage("状态"),
|
||||
"statusDesc": MessageLookupByLibrary.simpleMessage("关闭后将使用系统DNS"),
|
||||
"stop": MessageLookupByLibrary.simpleMessage("暂停"),
|
||||
"stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."),
|
||||
"style": MessageLookupByLibrary.simpleMessage("风格"),
|
||||
"submit": MessageLookupByLibrary.simpleMessage("提交"),
|
||||
@@ -273,7 +335,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),
|
||||
"themeMode": MessageLookupByLibrary.simpleMessage("主题模式"),
|
||||
"threeColumns": MessageLookupByLibrary.simpleMessage("三列"),
|
||||
"tight": MessageLookupByLibrary.simpleMessage("宽松"),
|
||||
"tight": MessageLookupByLibrary.simpleMessage("紧凑"),
|
||||
"time": MessageLookupByLibrary.simpleMessage("时间"),
|
||||
"tip": MessageLookupByLibrary.simpleMessage("提示"),
|
||||
"tools": MessageLookupByLibrary.simpleMessage("工具"),
|
||||
@@ -290,7 +352,15 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"upload": MessageLookupByLibrary.simpleMessage("上传"),
|
||||
"url": MessageLookupByLibrary.simpleMessage("URL"),
|
||||
"urlDesc": MessageLookupByLibrary.simpleMessage("通过URL获取配置文件"),
|
||||
"useHosts": MessageLookupByLibrary.simpleMessage("使用Hosts"),
|
||||
"useSystemHosts": MessageLookupByLibrary.simpleMessage("使用系统Hosts"),
|
||||
"value": MessageLookupByLibrary.simpleMessage("值"),
|
||||
"valueNotEmpty": MessageLookupByLibrary.simpleMessage("值不能为空"),
|
||||
"view": MessageLookupByLibrary.simpleMessage("查看"),
|
||||
"vpnDesc": MessageLookupByLibrary.simpleMessage("修改VPN相关设置"),
|
||||
"vpnEnableDesc":
|
||||
MessageLookupByLibrary.simpleMessage("通过VpnService自动路由系统所有流量"),
|
||||
"vpnTip": MessageLookupByLibrary.simpleMessage("重启VPN后改变生效"),
|
||||
"webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"),
|
||||
"whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"),
|
||||
"years": MessageLookupByLibrary.simpleMessage("年"),
|
||||
|
||||
@@ -2479,6 +2479,626 @@ class AppLocalizations {
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Start`
|
||||
String get start {
|
||||
return Intl.message(
|
||||
'Start',
|
||||
name: 'start',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Stop`
|
||||
String get stop {
|
||||
return Intl.message(
|
||||
'Stop',
|
||||
name: 'stop',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Processing app related settings`
|
||||
String get appDesc {
|
||||
return Intl.message(
|
||||
'Processing app related settings',
|
||||
name: 'appDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Modify VPN related settings`
|
||||
String get vpnDesc {
|
||||
return Intl.message(
|
||||
'Modify VPN related settings',
|
||||
name: 'vpnDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Overwrite general settings`
|
||||
String get generalDesc {
|
||||
return Intl.message(
|
||||
'Overwrite general settings',
|
||||
name: 'generalDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Update DNS related settings`
|
||||
String get dnsDesc {
|
||||
return Intl.message(
|
||||
'Update DNS related settings',
|
||||
name: 'dnsDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Key`
|
||||
String get key {
|
||||
return Intl.message(
|
||||
'Key',
|
||||
name: 'key',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Value`
|
||||
String get value {
|
||||
return Intl.message(
|
||||
'Value',
|
||||
name: 'value',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `The key cannot be empty`
|
||||
String get keyNotEmpty {
|
||||
return Intl.message(
|
||||
'The key cannot be empty',
|
||||
name: 'keyNotEmpty',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `The value cannot be empty`
|
||||
String get valueNotEmpty {
|
||||
return Intl.message(
|
||||
'The value cannot be empty',
|
||||
name: 'valueNotEmpty',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Add Hosts`
|
||||
String get hostsDesc {
|
||||
return Intl.message(
|
||||
'Add Hosts',
|
||||
name: 'hostsDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Changes take effect after restarting the VPN`
|
||||
String get vpnTip {
|
||||
return Intl.message(
|
||||
'Changes take effect after restarting the VPN',
|
||||
name: 'vpnTip',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Auto routes all system traffic through VpnService`
|
||||
String get vpnEnableDesc {
|
||||
return Intl.message(
|
||||
'Auto routes all system traffic through VpnService',
|
||||
name: 'vpnEnableDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Options`
|
||||
String get options {
|
||||
return Intl.message(
|
||||
'Options',
|
||||
name: 'options',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Loopback unlock tool`
|
||||
String get loopback {
|
||||
return Intl.message(
|
||||
'Loopback unlock tool',
|
||||
name: 'loopback',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Used for UWP loopback unlocking`
|
||||
String get loopbackDesc {
|
||||
return Intl.message(
|
||||
'Used for UWP loopback unlocking',
|
||||
name: 'loopbackDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Providers`
|
||||
String get providers {
|
||||
return Intl.message(
|
||||
'Providers',
|
||||
name: 'providers',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Proxy providers`
|
||||
String get proxyProviders {
|
||||
return Intl.message(
|
||||
'Proxy providers',
|
||||
name: 'proxyProviders',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Rule providers`
|
||||
String get ruleProviders {
|
||||
return Intl.message(
|
||||
'Rule providers',
|
||||
name: 'ruleProviders',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Override Dns`
|
||||
String get overrideDns {
|
||||
return Intl.message(
|
||||
'Override Dns',
|
||||
name: 'overrideDns',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Turning it on will override the DNS options in the profile`
|
||||
String get overrideDnsDesc {
|
||||
return Intl.message(
|
||||
'Turning it on will override the DNS options in the profile',
|
||||
name: 'overrideDnsDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Status`
|
||||
String get status {
|
||||
return Intl.message(
|
||||
'Status',
|
||||
name: 'status',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `System DNS will be used when turned off`
|
||||
String get statusDesc {
|
||||
return Intl.message(
|
||||
'System DNS will be used when turned off',
|
||||
name: 'statusDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Prioritize the use of DOH's http/3`
|
||||
String get preferH3Desc {
|
||||
return Intl.message(
|
||||
'Prioritize the use of DOH\'s http/3',
|
||||
name: 'preferH3Desc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Respect rules`
|
||||
String get respectRules {
|
||||
return Intl.message(
|
||||
'Respect rules',
|
||||
name: 'respectRules',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `DNS connection following rules, need to configure proxy-server-nameserver`
|
||||
String get respectRulesDesc {
|
||||
return Intl.message(
|
||||
'DNS connection following rules, need to configure proxy-server-nameserver',
|
||||
name: 'respectRulesDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `DNS mode`
|
||||
String get dnsMode {
|
||||
return Intl.message(
|
||||
'DNS mode',
|
||||
name: 'dnsMode',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Fakeip range`
|
||||
String get fakeipRange {
|
||||
return Intl.message(
|
||||
'Fakeip range',
|
||||
name: 'fakeipRange',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Fakeip filter`
|
||||
String get fakeipFilter {
|
||||
return Intl.message(
|
||||
'Fakeip filter',
|
||||
name: 'fakeipFilter',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Default nameserver`
|
||||
String get defaultNameserver {
|
||||
return Intl.message(
|
||||
'Default nameserver',
|
||||
name: 'defaultNameserver',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `For resolving DNS server`
|
||||
String get defaultNameserverDesc {
|
||||
return Intl.message(
|
||||
'For resolving DNS server',
|
||||
name: 'defaultNameserverDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Nameserver`
|
||||
String get nameserver {
|
||||
return Intl.message(
|
||||
'Nameserver',
|
||||
name: 'nameserver',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `For resolving domain`
|
||||
String get nameserverDesc {
|
||||
return Intl.message(
|
||||
'For resolving domain',
|
||||
name: 'nameserverDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Use hosts`
|
||||
String get useHosts {
|
||||
return Intl.message(
|
||||
'Use hosts',
|
||||
name: 'useHosts',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Use system hosts`
|
||||
String get useSystemHosts {
|
||||
return Intl.message(
|
||||
'Use system hosts',
|
||||
name: 'useSystemHosts',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Nameserver policy`
|
||||
String get nameserverPolicy {
|
||||
return Intl.message(
|
||||
'Nameserver policy',
|
||||
name: 'nameserverPolicy',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Specify the corresponding nameserver policy`
|
||||
String get nameserverPolicyDesc {
|
||||
return Intl.message(
|
||||
'Specify the corresponding nameserver policy',
|
||||
name: 'nameserverPolicyDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Proxy nameserver`
|
||||
String get proxyNameserver {
|
||||
return Intl.message(
|
||||
'Proxy nameserver',
|
||||
name: 'proxyNameserver',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Domain for resolving proxy nodes`
|
||||
String get proxyNameserverDesc {
|
||||
return Intl.message(
|
||||
'Domain for resolving proxy nodes',
|
||||
name: 'proxyNameserverDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Fallback`
|
||||
String get fallback {
|
||||
return Intl.message(
|
||||
'Fallback',
|
||||
name: 'fallback',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Generally use offshore DNS`
|
||||
String get fallbackDesc {
|
||||
return Intl.message(
|
||||
'Generally use offshore DNS',
|
||||
name: 'fallbackDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Fallback filter`
|
||||
String get fallbackFilter {
|
||||
return Intl.message(
|
||||
'Fallback filter',
|
||||
name: 'fallbackFilter',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Geoip code`
|
||||
String get geoipCode {
|
||||
return Intl.message(
|
||||
'Geoip code',
|
||||
name: 'geoipCode',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Ipcidr`
|
||||
String get ipcidr {
|
||||
return Intl.message(
|
||||
'Ipcidr',
|
||||
name: 'ipcidr',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Domain`
|
||||
String get domain {
|
||||
return Intl.message(
|
||||
'Domain',
|
||||
name: 'domain',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Reset Dns`
|
||||
String get resetDns {
|
||||
return Intl.message(
|
||||
'Reset Dns',
|
||||
name: 'resetDns',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Reset`
|
||||
String get reset {
|
||||
return Intl.message(
|
||||
'Reset',
|
||||
name: 'reset',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Show/Hide`
|
||||
String get action_view {
|
||||
return Intl.message(
|
||||
'Show/Hide',
|
||||
name: 'action_view',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Start/Stop`
|
||||
String get action_start {
|
||||
return Intl.message(
|
||||
'Start/Stop',
|
||||
name: 'action_start',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Switch mode`
|
||||
String get action_mode {
|
||||
return Intl.message(
|
||||
'Switch mode',
|
||||
name: 'action_mode',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `System proxy`
|
||||
String get action_proxy {
|
||||
return Intl.message(
|
||||
'System proxy',
|
||||
name: 'action_proxy',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `TUN`
|
||||
String get action_tun {
|
||||
return Intl.message(
|
||||
'TUN',
|
||||
name: 'action_tun',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Disclaimer`
|
||||
String get disclaimer {
|
||||
return Intl.message(
|
||||
'Disclaimer',
|
||||
name: 'disclaimer',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `This software is only used for non-commercial purposes such as learning exchanges and scientific research. It is strictly prohibited to use this software for commercial purposes. Any commercial activity, if any, has nothing to do with this software.`
|
||||
String get disclaimerDesc {
|
||||
return Intl.message(
|
||||
'This software is only used for non-commercial purposes such as learning exchanges and scientific research. It is strictly prohibited to use this software for commercial purposes. Any commercial activity, if any, has nothing to do with this software.',
|
||||
name: 'disclaimerDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Agree`
|
||||
String get agree {
|
||||
return Intl.message(
|
||||
'Agree',
|
||||
name: 'agree',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Hotkey Management`
|
||||
String get hotkeyManagement {
|
||||
return Intl.message(
|
||||
'Hotkey Management',
|
||||
name: 'hotkeyManagement',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Use keyboard to control applications`
|
||||
String get hotkeyManagementDesc {
|
||||
return Intl.message(
|
||||
'Use keyboard to control applications',
|
||||
name: 'hotkeyManagementDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Please press the keyboard.`
|
||||
String get pressKeyboard {
|
||||
return Intl.message(
|
||||
'Please press the keyboard.',
|
||||
name: 'pressKeyboard',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Please enter the correct hotkey`
|
||||
String get inputCorrectHotkey {
|
||||
return Intl.message(
|
||||
'Please enter the correct hotkey',
|
||||
name: 'inputCorrectHotkey',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Hotkey conflict`
|
||||
String get hotkeyConflict {
|
||||
return Intl.message(
|
||||
'Hotkey conflict',
|
||||
name: 'hotkeyConflict',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Remove`
|
||||
String get remove {
|
||||
return Intl.message(
|
||||
'Remove',
|
||||
name: 'remove',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `No HotKey`
|
||||
String get noHotKey {
|
||||
return Intl.message(
|
||||
'No HotKey',
|
||||
name: 'noHotKey',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/http.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/plugins/tile.dart';
|
||||
import 'package:fl_clash/plugins/vpn.dart';
|
||||
@@ -17,12 +18,15 @@ Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
clashCore.initMessage();
|
||||
globalState.packageInfo = await PackageInfo.fromPlatform();
|
||||
final version = await system.version;
|
||||
final config = await preferences.getConfig() ?? Config();
|
||||
globalState.autoRun = config.autoRun;
|
||||
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
|
||||
await android?.init();
|
||||
await window?.init(config.windowProps);
|
||||
await window?.init(config.windowProps, version);
|
||||
final appState = AppState(
|
||||
mode: clashConfig.mode,
|
||||
version: version,
|
||||
isCompatible: config.isCompatible,
|
||||
selectedMap: config.currentSelectedMap,
|
||||
);
|
||||
@@ -35,6 +39,7 @@ Future<void> main() async {
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
);
|
||||
HttpOverrides.global = FlClashHttpOverrides();
|
||||
runAppWithPreferences(
|
||||
const Application(),
|
||||
appState: appState,
|
||||
@@ -48,12 +53,14 @@ Future<void> vpnService() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
globalState.isVpnService = true;
|
||||
globalState.packageInfo = await PackageInfo.fromPlatform();
|
||||
final version = await system.version;
|
||||
final config = await preferences.getConfig() ?? Config();
|
||||
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
|
||||
final appState = AppState(
|
||||
mode: clashConfig.mode,
|
||||
isCompatible: config.isCompatible,
|
||||
selectedMap: config.currentSelectedMap,
|
||||
version: version,
|
||||
);
|
||||
await globalState.init(
|
||||
appState: appState,
|
||||
@@ -68,7 +75,7 @@ Future<void> vpnService() async {
|
||||
clashCore.setFdMap(fd.id);
|
||||
},
|
||||
onProcess: (Process process) async {
|
||||
final packageName = await app?.resolverProcess(process);
|
||||
final packageName = await vpn?.resolverProcess(process);
|
||||
clashCore.setProcessMap(
|
||||
ProcessMapItem(
|
||||
id: process.id,
|
||||
|
||||
@@ -5,20 +5,25 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class AndroidContainer extends StatefulWidget {
|
||||
class AndroidManager extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const AndroidContainer({
|
||||
const AndroidManager({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AndroidContainer> createState() => _AndroidContainerState();
|
||||
State<AndroidManager> createState() => _AndroidContainerState();
|
||||
}
|
||||
|
||||
class _AndroidContainerState extends State<AndroidContainer>
|
||||
with WidgetsBindingObserver {
|
||||
class _AndroidContainerState extends State<AndroidManager> {
|
||||
@override
|
||||
void initState() {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Widget _excludeContainer(Widget child) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.isExclude,
|
||||
@@ -44,31 +49,10 @@ class _AndroidContainerState extends State<AndroidContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
|
||||
final isPaused = state == AppLifecycleState.paused;
|
||||
if (isPaused) {
|
||||
await globalState.appController.savePreferences();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _systemUiOverlayContainer(
|
||||
_excludeContainer(widget.child),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,20 @@ import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class AppStateContainer extends StatelessWidget {
|
||||
class AppStateManager extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const AppStateContainer({
|
||||
const AppStateManager({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AppStateManager> createState() => _AppStateManagerState();
|
||||
}
|
||||
|
||||
class _AppStateManagerState extends State<AppStateManager>
|
||||
with WidgetsBindingObserver {
|
||||
_updateNavigationsContainer(Widget child) {
|
||||
return Selector2<AppState, Config, UpdateNavigationsSelector>(
|
||||
selector: (_, appState, config) {
|
||||
@@ -38,10 +44,36 @@ class AppStateContainer extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
|
||||
final isPaused = state == AppLifecycleState.paused;
|
||||
if (isPaused) {
|
||||
await globalState.appController.savePreferences();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangePlatformBrightness() {
|
||||
globalState.appController.appState.brightness =
|
||||
WidgetsBinding.instance.platformDispatcher.platformBrightness;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _updateNavigationsContainer(
|
||||
child,
|
||||
widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,32 @@
|
||||
import 'package:fl_clash/clash/clash.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:provider/provider.dart';
|
||||
|
||||
import '../common/function.dart';
|
||||
import '../../common/function.dart';
|
||||
|
||||
class ClashContainer extends StatefulWidget {
|
||||
class ClashManager extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const ClashContainer({
|
||||
const ClashManager({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ClashContainer> createState() => _ClashContainerState();
|
||||
State<ClashManager> createState() => _ClashContainerState();
|
||||
}
|
||||
|
||||
class _ClashContainerState extends State<ClashContainer>
|
||||
class _ClashContainerState extends State<ClashManager>
|
||||
with AppMessageListener {
|
||||
Function? updateClashConfigDebounce;
|
||||
|
||||
Widget _updateContainer(Widget child) {
|
||||
return Selector<ClashConfig, ClashConfigState>(
|
||||
selector: (_, clashConfig) => ClashConfigState(
|
||||
return Selector2<Config, ClashConfig, ClashConfigState>(
|
||||
selector: (_, config, clashConfig) => ClashConfigState(
|
||||
overrideDns: config.overrideDns,
|
||||
mixedPort: clashConfig.mixedPort,
|
||||
allowLan: clashConfig.allowLan,
|
||||
ipv6: clashConfig.ipv6,
|
||||
@@ -38,18 +40,21 @@ class _ClashContainerState extends State<ClashContainer>
|
||||
tcpConcurrent: clashConfig.tcpConcurrent,
|
||||
tun: clashConfig.tun,
|
||||
dns: clashConfig.dns,
|
||||
hosts: clashConfig.hosts,
|
||||
geoXUrl: clashConfig.geoXUrl,
|
||||
rules: clashConfig.rules,
|
||||
globalRealUa: clashConfig.globalRealUa,
|
||||
),
|
||||
builder: (__, state, child) {
|
||||
if (updateClashConfigDebounce == null) {
|
||||
updateClashConfigDebounce = debounce<Function()>(() async {
|
||||
shouldRebuild: (prev, next) {
|
||||
if (prev != next) {
|
||||
updateClashConfigDebounce ??= debounce<Function()>(() async {
|
||||
await globalState.appController.updateClashConfig();
|
||||
});
|
||||
} else {
|
||||
updateClashConfigDebounce!();
|
||||
}
|
||||
return prev != next;
|
||||
},
|
||||
builder: (__, state, child) {
|
||||
return child!;
|
||||
},
|
||||
child: child,
|
||||
@@ -76,8 +81,12 @@ class _ClashContainerState extends State<ClashContainer>
|
||||
);
|
||||
}
|
||||
|
||||
_changeProfileHandle() {
|
||||
_changeProfile() async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (globalState.autoRun) {
|
||||
globalState.autoRun = false;
|
||||
return;
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
appController.appState.delayMap = {};
|
||||
await appController.applyProfile();
|
||||
@@ -88,7 +97,7 @@ class _ClashContainerState extends State<ClashContainer>
|
||||
return Selector<Config, String?>(
|
||||
selector: (_, config) => config.currentProfileId,
|
||||
builder: (__, state, child) {
|
||||
_changeProfileHandle();
|
||||
_changeProfile();
|
||||
return child!;
|
||||
},
|
||||
child: child,
|
||||
@@ -129,6 +138,9 @@ class _ClashContainerState extends State<ClashContainer>
|
||||
@override
|
||||
void onLog(Log log) {
|
||||
globalState.appController.appState.addLog(log);
|
||||
if (log.logLevel == LogLevel.error) {
|
||||
globalState.appController.showSnackBar(log.payload ?? '');
|
||||
}
|
||||
debugPrint("$log");
|
||||
super.onLog(log);
|
||||
}
|
||||
@@ -150,11 +162,4 @@ class _ClashContainerState extends State<ClashContainer>
|
||||
appController.addCheckIpNumDebounce();
|
||||
super.onLoaded(providerName);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onStarted(String runTime) async {
|
||||
super.onStarted(runTime);
|
||||
final appController = globalState.appController;
|
||||
await appController.applyProfile(isPrue: true);
|
||||
}
|
||||
}
|
||||
75
lib/manager/hotkey_manager.dart
Normal file
75
lib/manager/hotkey_manager.dart
Normal file
@@ -0,0 +1,75 @@
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/common.dart';
|
||||
import 'package:fl_clash/models/config.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class HotKeyManager extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const HotKeyManager({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
_handleHotKeyAction(HotAction action) async {
|
||||
switch (action) {
|
||||
case HotAction.mode:
|
||||
globalState.appController.updateMode();
|
||||
case HotAction.start:
|
||||
globalState.appController.updateStart();
|
||||
case HotAction.view:
|
||||
globalState.appController.updateVisible();
|
||||
case HotAction.proxy:
|
||||
globalState.appController.updateSystemProxy();
|
||||
case HotAction.tun:
|
||||
globalState.appController.updateTun();
|
||||
}
|
||||
}
|
||||
|
||||
_updateHotKeys({
|
||||
required List<HotKeyAction> hotKeyActions,
|
||||
}) async {
|
||||
await hotKeyManager.unregisterAll();
|
||||
final hotkeyActionHandles = hotKeyActions.where(
|
||||
(hotKeyAction) {
|
||||
return hotKeyAction.key != null && hotKeyAction.modifiers.isNotEmpty;
|
||||
},
|
||||
).map<Future>(
|
||||
(hotKeyAction) async {
|
||||
final modifiers = hotKeyAction.modifiers
|
||||
.map((item) => item.toHotKeyModifier())
|
||||
.toList();
|
||||
final hotKey = HotKey(
|
||||
key: PhysicalKeyboardKey(hotKeyAction.key!),
|
||||
modifiers: modifiers,
|
||||
);
|
||||
return await hotKeyManager.register(
|
||||
hotKey,
|
||||
keyDownHandler: (_) {
|
||||
_handleHotKeyAction(hotKeyAction.action);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
await Future.wait(hotkeyActionHandles);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, List<HotKeyAction>>(
|
||||
selector: (_, config) => config.hotKeyActions,
|
||||
shouldRebuild: (prev, next) {
|
||||
return !hotKeyActionsEquality.equals(prev, next);
|
||||
},
|
||||
builder: (_, hotKeyActions, __) {
|
||||
_updateHotKeys(hotKeyActions: hotKeyActions);
|
||||
return child;
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
9
lib/manager/manager.dart
Normal file
9
lib/manager/manager.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
export 'tray_manager.dart';
|
||||
export 'window_manager.dart';
|
||||
export 'android_manager.dart';
|
||||
export 'clash_manager.dart';
|
||||
export 'tile_manager.dart';
|
||||
export 'app_state_manager.dart';
|
||||
export 'vpn_manager.dart';
|
||||
export 'media_manager.dart';
|
||||
export 'proxy_manager.dart';
|
||||
41
lib/manager/media_manager.dart
Normal file
41
lib/manager/media_manager.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/config.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class MediaManager extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const MediaManager({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, ScaleProps>(
|
||||
selector: (_, config) => config.scaleProps,
|
||||
builder: (_, props, child) {
|
||||
globalState.measure = Measure.of(context);
|
||||
return child!;
|
||||
// final textScaleFactor =
|
||||
// WidgetsBinding.instance.platformDispatcher.textScaleFactor;
|
||||
// return MediaQuery(
|
||||
// data: MediaQuery.of(context).copyWith(
|
||||
// textScaler: props.custom
|
||||
// ? TextScaler.linear(props.scale * textScaleFactor)
|
||||
// : null,
|
||||
// ),
|
||||
// child: Builder(
|
||||
// builder: (context) {
|
||||
// globalState.measure = Measure.of(context);
|
||||
// return child!;
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,10 @@ import 'package:fl_clash/models/models.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ProxyContainer extends StatelessWidget {
|
||||
class ProxyManager extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const ProxyContainer({super.key, required this.child});
|
||||
const ProxyManager({super.key, required this.child});
|
||||
|
||||
_updateProxy(ProxyState proxyState) {
|
||||
final isStart = proxyState.isStart;
|
||||
@@ -2,19 +2,19 @@ import 'package:fl_clash/plugins/tile.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TileContainer extends StatefulWidget {
|
||||
class TileManager extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const TileContainer({
|
||||
const TileManager({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TileContainer> createState() => _TileContainerState();
|
||||
State<TileManager> createState() => _TileContainerState();
|
||||
}
|
||||
|
||||
class _TileContainerState extends State<TileContainer> with TileListener {
|
||||
class _TileContainerState extends State<TileManager> with TileListener {
|
||||
|
||||
|
||||
@override
|
||||
177
lib/manager/tray_manager.dart
Executable file
177
lib/manager/tray_manager.dart
Executable file
@@ -0,0 +1,177 @@
|
||||
import 'dart:io';
|
||||
|
||||
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:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
|
||||
class TrayManager extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const TrayManager({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TrayManager> createState() => _TrayContainerState();
|
||||
}
|
||||
|
||||
class _TrayContainerState extends State<TrayManager> with TrayListener {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
trayManager.addListener(this);
|
||||
}
|
||||
|
||||
_updateSystemTray({
|
||||
required bool isStart,
|
||||
required Brightness? brightness,
|
||||
}) async {
|
||||
if (Platform.isLinux) {
|
||||
await trayManager.destroy();
|
||||
}
|
||||
await trayManager.setIcon(
|
||||
other.getTrayIconPath(
|
||||
isStart: isStart,
|
||||
brightness: brightness ??
|
||||
WidgetsBinding.instance.platformDispatcher.platformBrightness,
|
||||
),
|
||||
);
|
||||
if(!Platform.isLinux){
|
||||
await trayManager.setToolTip(
|
||||
appName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_updateTray(TrayState trayState) async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!Platform.isLinux) {
|
||||
_updateSystemTray(
|
||||
isStart: trayState.isStart,
|
||||
brightness: trayState.brightness,
|
||||
);
|
||||
}
|
||||
List<MenuItem> menuItems = [];
|
||||
final showMenuItem = MenuItem(
|
||||
label: appLocalizations.show,
|
||||
onClick: (_) {
|
||||
window?.show();
|
||||
},
|
||||
);
|
||||
menuItems.add(showMenuItem);
|
||||
final startMenuItem = MenuItem.checkbox(
|
||||
label:
|
||||
trayState.isStart ? appLocalizations.stop : appLocalizations.start,
|
||||
onClick: (_) async {
|
||||
globalState.appController.updateStart();
|
||||
},
|
||||
checked: false,
|
||||
);
|
||||
menuItems.add(startMenuItem);
|
||||
menuItems.add(MenuItem.separator());
|
||||
for (final mode in Mode.values) {
|
||||
menuItems.add(
|
||||
MenuItem.checkbox(
|
||||
label: Intl.message(mode.name),
|
||||
onClick: (_) {
|
||||
globalState.appController.clashConfig.mode = mode;
|
||||
},
|
||||
checked: mode == trayState.mode,
|
||||
),
|
||||
);
|
||||
}
|
||||
menuItems.add(MenuItem.separator());
|
||||
if (trayState.isStart) {
|
||||
menuItems.add(
|
||||
MenuItem.checkbox(
|
||||
label: appLocalizations.tun,
|
||||
onClick: (_) {
|
||||
globalState.appController.updateTun();
|
||||
},
|
||||
checked: trayState.tunEnable,
|
||||
),
|
||||
);
|
||||
menuItems.add(
|
||||
MenuItem.checkbox(
|
||||
label: appLocalizations.systemProxy,
|
||||
onClick: (_) {
|
||||
globalState.appController.updateSystemProxy();
|
||||
},
|
||||
checked: trayState.systemProxy,
|
||||
),
|
||||
);
|
||||
menuItems.add(MenuItem.separator());
|
||||
}
|
||||
final autoStartMenuItem = MenuItem.checkbox(
|
||||
label: appLocalizations.autoLaunch,
|
||||
onClick: (_) async {
|
||||
globalState.appController.updateAutoLaunch();
|
||||
},
|
||||
checked: trayState.autoLaunch,
|
||||
);
|
||||
menuItems.add(autoStartMenuItem);
|
||||
menuItems.add(MenuItem.separator());
|
||||
final exitMenuItem = MenuItem(
|
||||
label: appLocalizations.exit,
|
||||
onClick: (_) async {
|
||||
await globalState.appController.handleExit();
|
||||
},
|
||||
);
|
||||
menuItems.add(exitMenuItem);
|
||||
final menu = Menu();
|
||||
menu.items = menuItems;
|
||||
trayManager.setContextMenu(menu);
|
||||
if (Platform.isLinux) {
|
||||
_updateSystemTray(
|
||||
isStart: trayState.isStart,
|
||||
brightness: trayState.brightness,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector3<AppState, Config, ClashConfig, TrayState>(
|
||||
selector: (_, appState, config, clashConfig) => TrayState(
|
||||
mode: clashConfig.mode,
|
||||
autoLaunch: config.autoLaunch,
|
||||
isStart: appState.isStart,
|
||||
locale: config.locale,
|
||||
systemProxy: config.desktopProps.systemProxy,
|
||||
tunEnable: clashConfig.tun.enable,
|
||||
brightness: appState.brightness,
|
||||
),
|
||||
shouldRebuild: (prev, next) {
|
||||
return prev != next;
|
||||
},
|
||||
builder: (_, state, child) {
|
||||
_updateTray(state);
|
||||
return child!;
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTrayIconRightMouseDown() {
|
||||
trayManager.popUpContextMenu();
|
||||
}
|
||||
|
||||
@override
|
||||
onTrayIconMouseDown() {
|
||||
window?.show();
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
trayManager.removeListener(this);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
58
lib/manager/vpn_manager.dart
Normal file
58
lib/manager/vpn_manager.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:fl_clash/common/app_localizations.dart';
|
||||
import 'package:fl_clash/models/config.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../common/function.dart';
|
||||
|
||||
class VpnManager extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const VpnManager({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
State<VpnManager> createState() => _VpnContainerState();
|
||||
}
|
||||
|
||||
class _VpnContainerState extends State<VpnManager> {
|
||||
Function? vpnTipDebounce;
|
||||
|
||||
showTip() {
|
||||
vpnTipDebounce ??= debounce<Function()>(() async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final appState = globalState.appController.appState;
|
||||
if (appState.isStart) {
|
||||
globalState.showSnackBar(
|
||||
context,
|
||||
message: appLocalizations.vpnTip,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
vpnTipDebounce!();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, VPNState>(
|
||||
selector: (_, config) => VPNState(
|
||||
accessControl: config.accessControl,
|
||||
vpnProps: config.vpnProps,
|
||||
),
|
||||
shouldRebuild: (prev,next){
|
||||
if(prev != next){
|
||||
showTip();
|
||||
}
|
||||
return prev != next;
|
||||
},
|
||||
builder: (_, __, child) {
|
||||
return child!;
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,24 +7,30 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class WindowContainer extends StatefulWidget {
|
||||
class WindowManager extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const WindowContainer({
|
||||
const WindowManager({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
State<WindowContainer> createState() => _WindowContainerState();
|
||||
State<WindowManager> createState() => _WindowContainerState();
|
||||
}
|
||||
|
||||
class _WindowContainerState extends State<WindowContainer> with WindowListener {
|
||||
class _WindowContainerState extends State<WindowManager> with WindowListener {
|
||||
Function? updateLaunchDebounce;
|
||||
|
||||
_autoLaunchContainer(Widget child) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.autoLaunch,
|
||||
return Selector2<Config, ClashConfig, AutoLaunchState>(
|
||||
selector: (_, config, clashConfig) => AutoLaunchState(
|
||||
isAutoLaunch: config.autoLaunch, isOpenTun: clashConfig.tun.enable),
|
||||
builder: (_, state, child) {
|
||||
autoLaunch?.updateStatus(state);
|
||||
updateLaunchDebounce ??= debounce((AutoLaunchState state) {
|
||||
autoLaunch?.updateStatus(state);
|
||||
});
|
||||
updateLaunchDebounce!([state]);
|
||||
return child!;
|
||||
},
|
||||
child: child,
|
||||
@@ -93,21 +99,30 @@ class WindowHeaderContainer extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Column(
|
||||
return Selector<AppState, int>(
|
||||
selector: (_, appState) => appState.version,
|
||||
builder: (_, version, child) {
|
||||
if (version <= 10 && Platform.isMacOS) {
|
||||
return child!;
|
||||
}
|
||||
return Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: kHeaderHeight,
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: child,
|
||||
Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: kHeaderHeight,
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: child!,
|
||||
),
|
||||
],
|
||||
),
|
||||
const WindowHeader(),
|
||||
],
|
||||
),
|
||||
const WindowHeader(),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -234,14 +249,20 @@ class _WindowHeaderState extends State<WindowHeader> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const Positioned(
|
||||
left: 0,
|
||||
child: AppIcon(),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
child: _buildActions(),
|
||||
),
|
||||
if (Platform.isMacOS)
|
||||
const Text(
|
||||
appName,
|
||||
)
|
||||
else ...[
|
||||
const Positioned(
|
||||
left: 0,
|
||||
child: AppIcon(),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
child: _buildActions(),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -4,16 +4,9 @@ import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'connection.dart';
|
||||
import 'common.dart';
|
||||
import 'ffi.dart';
|
||||
import 'log.dart';
|
||||
import 'navigation.dart';
|
||||
import 'package.dart';
|
||||
import 'profile.dart';
|
||||
import 'proxy.dart';
|
||||
import 'system_color_scheme.dart';
|
||||
import 'traffic.dart';
|
||||
import 'version.dart';
|
||||
|
||||
typedef DelayMap = Map<String, int?>;
|
||||
|
||||
@@ -38,11 +31,14 @@ class AppState with ChangeNotifier {
|
||||
num _checkIpNum;
|
||||
List<ExternalProvider> _providers;
|
||||
List<Package> _packages;
|
||||
Brightness? _brightness;
|
||||
int _version;
|
||||
|
||||
AppState({
|
||||
required Mode mode,
|
||||
required bool isCompatible,
|
||||
required SelectedMap selectedMap,
|
||||
required int version,
|
||||
}) : _navigationItems = [],
|
||||
_isInit = false,
|
||||
_currentLabel = "dashboard",
|
||||
@@ -54,13 +50,15 @@ class AppState with ChangeNotifier {
|
||||
_checkIpNum = 0,
|
||||
_requests = [],
|
||||
_mode = mode,
|
||||
_brightness = null,
|
||||
_totalTraffic = Traffic(),
|
||||
_delayMap = {},
|
||||
_groups = [],
|
||||
_providers = [],
|
||||
_packages = [],
|
||||
_isCompatible = isCompatible,
|
||||
_systemColorSchemes = const SystemColorSchemes();
|
||||
_systemColorSchemes = const SystemColorSchemes(),
|
||||
_version = version;
|
||||
|
||||
String get currentLabel => _currentLabel;
|
||||
|
||||
@@ -354,7 +352,7 @@ class AppState with ChangeNotifier {
|
||||
}
|
||||
|
||||
setProvider(ExternalProvider? provider) {
|
||||
if(provider == null) return;
|
||||
if (provider == null) return;
|
||||
final index = _providers.indexWhere((item) => item.name == provider.name);
|
||||
if (index == -1) return;
|
||||
_providers = List.from(_providers)..[index] = provider;
|
||||
@@ -366,4 +364,22 @@ class AppState with ChangeNotifier {
|
||||
currentGroups.indexWhere((element) => element.name == groupName);
|
||||
return index != -1 ? currentGroups[index] : null;
|
||||
}
|
||||
|
||||
Brightness? get brightness => _brightness;
|
||||
|
||||
set brightness(Brightness? value) {
|
||||
if (_brightness != value) {
|
||||
_brightness = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
int get version => _version;
|
||||
|
||||
set version(int value) {
|
||||
if (_version != value) {
|
||||
_version = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,89 +26,94 @@ class Tun with _$Tun {
|
||||
factory Tun.fromJson(Map<String, Object?> json) => _$TunFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Dns {
|
||||
bool enable;
|
||||
bool ipv6;
|
||||
@JsonKey(name: "default-nameserver")
|
||||
List<String> defaultNameserver;
|
||||
@JsonKey(name: "enhanced-mode")
|
||||
String enhancedMode;
|
||||
@JsonKey(name: "fake-ip-range")
|
||||
String fakeIpRange;
|
||||
@JsonKey(name: "use-hosts")
|
||||
bool useHosts;
|
||||
List<String> nameserver;
|
||||
List<String> fallback;
|
||||
@JsonKey(name: "fake-ip-filter")
|
||||
List<String> fakeIpFilter;
|
||||
@freezed
|
||||
class FallbackFilter with _$FallbackFilter {
|
||||
const factory FallbackFilter({
|
||||
@Default(true) bool geoip,
|
||||
@Default("CN") @JsonKey(name: "geoip-code") String geoipCode,
|
||||
@Default(["gfw"]) List<String> geosite,
|
||||
@Default(["240.0.0.0/4"]) List<String> ipcidr,
|
||||
@Default([
|
||||
"+.google.com",
|
||||
"+.facebook.com",
|
||||
"+.youtube.com",
|
||||
])
|
||||
List<String> domain,
|
||||
}) = _FallbackFilter;
|
||||
|
||||
Dns()
|
||||
: enable = true,
|
||||
ipv6 = false,
|
||||
defaultNameserver = [
|
||||
"223.5.5.5",
|
||||
"119.29.29.29",
|
||||
"8.8.4.4",
|
||||
"1.0.0.1",
|
||||
],
|
||||
enhancedMode = "fake-ip",
|
||||
fakeIpRange = "198.18.0.1/16",
|
||||
useHosts = true,
|
||||
nameserver = [
|
||||
"8.8.8.8",
|
||||
"114.114.114.114",
|
||||
"https://doh.pub/dns-query",
|
||||
"https://dns.alidns.com/dns-query",
|
||||
],
|
||||
fallback = [
|
||||
'https://doh.dns.sb/dns-query',
|
||||
'https://dns.cloudflare.com/dns-query',
|
||||
'https://dns.twnic.tw/dns-query',
|
||||
'tls://8.8.4.4:853',
|
||||
],
|
||||
fakeIpFilter = [
|
||||
// Stun Services
|
||||
"+.stun.*.*",
|
||||
"+.stun.*.*.*",
|
||||
"+.stun.*.*.*.*",
|
||||
"+.stun.*.*.*.*.*",
|
||||
factory FallbackFilter.fromJson(Map<String, Object?> json) =>
|
||||
_$FallbackFilterFromJson(json);
|
||||
}
|
||||
|
||||
// Google Voices
|
||||
"lens.l.google.com",
|
||||
@freezed
|
||||
class Dns with _$Dns {
|
||||
const factory Dns({
|
||||
@Default(true) bool enable,
|
||||
@Default(false) @JsonKey(name: "prefer-h3") bool preferH3,
|
||||
@Default(true) @JsonKey(name: "use-hosts") bool useHosts,
|
||||
@Default(true) @JsonKey(name: "use-system-hosts") bool useSystemHosts,
|
||||
@Default(true) @JsonKey(name: "respect-rules") bool respectRules,
|
||||
@Default(false) bool ipv6,
|
||||
@Default(["223.5.5.5"])
|
||||
@JsonKey(name: "default-nameserver")
|
||||
List<String> defaultNameserver,
|
||||
@Default(DnsMode.fakeIp)
|
||||
@JsonKey(name: "enhanced-mode")
|
||||
DnsMode enhancedMode,
|
||||
@Default("198.18.0.1/16")
|
||||
@JsonKey(name: "fake-ip-range")
|
||||
String fakeIpRange,
|
||||
@Default([
|
||||
"*.lan",
|
||||
"localhost.ptlogin2.qq.com",
|
||||
])
|
||||
@JsonKey(name: "fake-ip-filter")
|
||||
List<String> fakeIpFilter,
|
||||
@Default({
|
||||
"www.baidu.com": "114.114.114.114",
|
||||
"+.internal.crop.com": "10.0.0.1",
|
||||
"geosite:cn": "https://doh.pub/dns-query"
|
||||
})
|
||||
@JsonKey(name: "nameserver-policy")
|
||||
Map<String, String> nameserverPolicy,
|
||||
@Default([
|
||||
"https://doh.pub/dns-query",
|
||||
"https://dns.alidns.com/dns-query",
|
||||
])
|
||||
List<String> nameserver,
|
||||
@Default([
|
||||
"tls://8.8.4.4",
|
||||
"tls://1.1.1.1",
|
||||
])
|
||||
List<String> fallback,
|
||||
@Default([
|
||||
"https://doh.pub/dns-query",
|
||||
])
|
||||
@JsonKey(name: "proxy-server-nameserver")
|
||||
List<String> proxyServerNameserver,
|
||||
@Default(FallbackFilter())
|
||||
@JsonKey(name: "fallback-filter")
|
||||
FallbackFilter fallbackFilter,
|
||||
}) = _Dns;
|
||||
|
||||
// Nintendo Switch STUN
|
||||
"*.n.n.srv.nintendo.net",
|
||||
factory Dns.fromJson(Map<String, Object?> json) => _$DnsFromJson(json);
|
||||
|
||||
// PlayStation STUN
|
||||
"+.stun.playstation.net",
|
||||
|
||||
// XBox
|
||||
"xbox.*.*.microsoft.com",
|
||||
"*.*.xboxlive.com",
|
||||
|
||||
// Microsoft Captive Portal
|
||||
"*.msftncsi.com",
|
||||
"*.msftconnecttest.com",
|
||||
|
||||
// Bilibili CDN
|
||||
"*.mcdn.bilivideo.cn",
|
||||
|
||||
// Windows Default LAN WorkGroup
|
||||
"WORKGROUP",
|
||||
];
|
||||
|
||||
factory Dns.fromJson(Map<String, dynamic> json) {
|
||||
return _$DnsFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$DnsToJson(this);
|
||||
factory Dns.safeDnsFromJson(Map<String, Object?> json) {
|
||||
try {
|
||||
return Dns.fromJson(json);
|
||||
} catch (_) {
|
||||
return const Dns();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typedef GeoXMap = Map<String, String>;
|
||||
|
||||
typedef HostsMap = Map<String, String>;
|
||||
|
||||
const defaultMixedPort = 7890;
|
||||
const defaultKeepAliveInterval = 30;
|
||||
|
||||
@JsonSerializable()
|
||||
class ClashConfig extends ChangeNotifier {
|
||||
int _mixedPort;
|
||||
@@ -127,9 +132,10 @@ class ClashConfig extends ChangeNotifier {
|
||||
GeoXMap _geoXUrl;
|
||||
List<String> _rules;
|
||||
String? _globalRealUa;
|
||||
HostsMap _hosts;
|
||||
|
||||
ClashConfig()
|
||||
: _mixedPort = 7890,
|
||||
: _mixedPort = defaultMixedPort,
|
||||
_mode = Mode.rule,
|
||||
_ipv6 = false,
|
||||
_findProcessMode = FindProcessMode.off,
|
||||
@@ -140,12 +146,13 @@ class ClashConfig extends ChangeNotifier {
|
||||
_unifiedDelay = false,
|
||||
_geodataLoader = geodataLoaderMemconservative,
|
||||
_externalController = '',
|
||||
_keepAliveInterval = 30,
|
||||
_dns = Dns(),
|
||||
_keepAliveInterval = defaultKeepAliveInterval,
|
||||
_dns = const Dns(),
|
||||
_geoXUrl = defaultGeoXMap,
|
||||
_rules = [];
|
||||
_rules = [],
|
||||
_hosts = {};
|
||||
|
||||
@JsonKey(name: "mixed-port", defaultValue: 7890)
|
||||
@JsonKey(name: "mixed-port", defaultValue: defaultMixedPort)
|
||||
int get mixedPort => _mixedPort;
|
||||
|
||||
set mixedPort(int value) {
|
||||
@@ -205,7 +212,7 @@ class ClashConfig extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
@JsonKey(name: "keep-alive-interval", defaultValue: 30)
|
||||
@JsonKey(name: "keep-alive-interval", defaultValue: defaultKeepAliveInterval)
|
||||
int get keepAliveInterval => _keepAliveInterval;
|
||||
|
||||
set keepAliveInterval(int value) {
|
||||
@@ -269,6 +276,7 @@ class ClashConfig extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
@JsonKey(fromJson: Dns.safeDnsFromJson)
|
||||
Dns get dns => _dns;
|
||||
|
||||
set dns(Dns value) {
|
||||
@@ -316,10 +324,21 @@ class ClashConfig extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
@JsonKey(defaultValue: {})
|
||||
HostsMap get hosts => _hosts;
|
||||
|
||||
set hosts(HostsMap value) {
|
||||
if (!const MapEquality<String, String>().equals(value, _hosts)) {
|
||||
_hosts = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
update([ClashConfig? clashConfig]) {
|
||||
if (clashConfig != null) {
|
||||
_mixedPort = clashConfig._mixedPort;
|
||||
_allowLan = clashConfig._allowLan;
|
||||
_hosts = clashConfig._hosts;
|
||||
_mode = clashConfig._mode;
|
||||
_logLevel = clashConfig._logLevel;
|
||||
_tun = clashConfig._tun;
|
||||
|
||||
438
lib/models/common.dart
Normal file
438
lib/models/common.dart
Normal file
@@ -0,0 +1,438 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'generated/common.freezed.dart';
|
||||
|
||||
part 'generated/common.g.dart';
|
||||
|
||||
@freezed
|
||||
class NavigationItem with _$NavigationItem {
|
||||
const factory NavigationItem({
|
||||
required Icon icon,
|
||||
required String label,
|
||||
final String? description,
|
||||
required Widget fragment,
|
||||
@Default(true) bool keep,
|
||||
String? path,
|
||||
@Default([NavigationItemMode.mobile, NavigationItemMode.desktop])
|
||||
List<NavigationItemMode> modes,
|
||||
}) = _NavigationItem;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Package with _$Package {
|
||||
const factory Package({
|
||||
required String packageName,
|
||||
required String label,
|
||||
required bool isSystem,
|
||||
required int firstInstallTime,
|
||||
}) = _Package;
|
||||
|
||||
factory Package.fromJson(Map<String, Object?> json) =>
|
||||
_$PackageFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Metadata with _$Metadata {
|
||||
const factory Metadata({
|
||||
required int uid,
|
||||
required String network,
|
||||
required String sourceIP,
|
||||
required String sourcePort,
|
||||
required String destinationIP,
|
||||
required String destinationPort,
|
||||
required String host,
|
||||
required String process,
|
||||
required String remoteDestination,
|
||||
}) = _Metadata;
|
||||
|
||||
factory Metadata.fromJson(Map<String, Object?> json) =>
|
||||
_$MetadataFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Connection with _$Connection {
|
||||
const factory Connection({
|
||||
required String id,
|
||||
num? upload,
|
||||
num? download,
|
||||
required DateTime start,
|
||||
required Metadata metadata,
|
||||
required List<String> chains,
|
||||
}) = _Connection;
|
||||
|
||||
factory Connection.fromJson(Map<String, Object?> json) =>
|
||||
_$ConnectionFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Log {
|
||||
@JsonKey(name: "LogLevel")
|
||||
LogLevel logLevel;
|
||||
@JsonKey(name: "Payload")
|
||||
String? payload;
|
||||
DateTime _dateTime;
|
||||
|
||||
Log({
|
||||
required this.logLevel,
|
||||
this.payload,
|
||||
}) : _dateTime = DateTime.now();
|
||||
|
||||
DateTime get dateTime => _dateTime;
|
||||
|
||||
factory Log.fromJson(Map<String, dynamic> json) {
|
||||
return _$LogFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$LogToJson(this);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Log{logLevel: $logLevel, payload: $payload, dateTime: $dateTime}';
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class LogsAndKeywords with _$LogsAndKeywords {
|
||||
const factory LogsAndKeywords({
|
||||
@Default([]) List<Log> logs,
|
||||
@Default([]) List<String> keywords,
|
||||
}) = _LogsAndKeywords;
|
||||
|
||||
factory LogsAndKeywords.fromJson(Map<String, Object?> json) =>
|
||||
_$LogsAndKeywordsFromJson(json);
|
||||
}
|
||||
|
||||
extension LogsAndKeywordsExt on LogsAndKeywords {
|
||||
List<Log> get filteredLogs => logs
|
||||
.where(
|
||||
(log) => {log.logLevel.name}.containsAll(keywords),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ConnectionsAndKeywords with _$ConnectionsAndKeywords {
|
||||
const factory ConnectionsAndKeywords({
|
||||
@Default([]) List<Connection> connections,
|
||||
@Default([]) List<String> keywords,
|
||||
}) = _ConnectionsAndKeywords;
|
||||
|
||||
factory ConnectionsAndKeywords.fromJson(Map<String, Object?> json) =>
|
||||
_$ConnectionsAndKeywordsFromJson(json);
|
||||
}
|
||||
|
||||
extension ConnectionsAndKeywordsExt on ConnectionsAndKeywords {
|
||||
List<Connection> get filteredConnections => connections
|
||||
.where((connection) => {
|
||||
...connection.chains,
|
||||
connection.metadata.process,
|
||||
}.containsAll(keywords))
|
||||
.toList();
|
||||
}
|
||||
|
||||
const defaultDavFileName = "backup.zip";
|
||||
|
||||
@freezed
|
||||
class DAV with _$DAV {
|
||||
const factory DAV({
|
||||
required String uri,
|
||||
required String user,
|
||||
required String password,
|
||||
@Default(defaultDavFileName) String fileName,
|
||||
}) = _DAV;
|
||||
|
||||
factory DAV.fromJson(Map<String, Object?> json) => _$DAVFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class FileInfo with _$FileInfo {
|
||||
const factory FileInfo({
|
||||
required int size,
|
||||
required DateTime lastModified,
|
||||
}) = _FileInfo;
|
||||
}
|
||||
|
||||
extension FileInfoExt on FileInfo {
|
||||
String get desc =>
|
||||
"${TrafficValue(value: size).show} · ${lastModified.lastUpdateTimeDesc}";
|
||||
}
|
||||
|
||||
@freezed
|
||||
class VersionInfo with _$VersionInfo {
|
||||
const factory VersionInfo({
|
||||
@Default("") String clashName,
|
||||
@Default("") String version,
|
||||
}) = _VersionInfo;
|
||||
|
||||
factory VersionInfo.fromJson(Map<String, Object?> json) =>
|
||||
_$VersionInfoFromJson(json);
|
||||
}
|
||||
|
||||
class Traffic {
|
||||
int id;
|
||||
TrafficValue up;
|
||||
TrafficValue down;
|
||||
|
||||
Traffic({num? up, num? down})
|
||||
: id = DateTime.now().millisecondsSinceEpoch,
|
||||
up = TrafficValue(value: up),
|
||||
down = TrafficValue(value: down);
|
||||
|
||||
num get speed => up.value + down.value;
|
||||
|
||||
factory Traffic.fromMap(Map<String, dynamic> map) {
|
||||
return Traffic(
|
||||
up: map['up'],
|
||||
down: map['down'],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$up↑ $down↓';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Traffic &&
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id &&
|
||||
up == other.up &&
|
||||
down == other.down;
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode ^ up.hashCode ^ down.hashCode;
|
||||
}
|
||||
|
||||
@immutable
|
||||
class TrafficValueShow {
|
||||
final String value;
|
||||
final TrafficUnit unit;
|
||||
|
||||
const TrafficValueShow({
|
||||
required this.value,
|
||||
required this.unit,
|
||||
});
|
||||
}
|
||||
|
||||
@immutable
|
||||
class TrafficValue {
|
||||
final num _value;
|
||||
|
||||
const TrafficValue({num? value}) : _value = value ?? 0;
|
||||
|
||||
num get value => _value;
|
||||
|
||||
String get show => "$showValue $showUnit";
|
||||
|
||||
String get showValue => trafficValueShow.value;
|
||||
|
||||
String get showUnit => trafficValueShow.unit.name;
|
||||
|
||||
TrafficValueShow get trafficValueShow {
|
||||
if (_value > pow(1024, 4)) {
|
||||
return TrafficValueShow(
|
||||
value: (_value / pow(1024, 4)).fixed(),
|
||||
unit: TrafficUnit.TB,
|
||||
);
|
||||
}
|
||||
if (_value > pow(1024, 3)) {
|
||||
return TrafficValueShow(
|
||||
value: (_value / pow(1024, 3)).fixed(),
|
||||
unit: TrafficUnit.GB,
|
||||
);
|
||||
}
|
||||
if (_value > pow(1024, 2)) {
|
||||
return TrafficValueShow(
|
||||
value: (_value / pow(1024, 2)).fixed(), unit: TrafficUnit.MB);
|
||||
}
|
||||
if (_value > pow(1024, 1)) {
|
||||
return TrafficValueShow(
|
||||
value: (_value / pow(1024, 1)).fixed(),
|
||||
unit: TrafficUnit.KB,
|
||||
);
|
||||
}
|
||||
return TrafficValueShow(
|
||||
value: _value.fixed(),
|
||||
unit: TrafficUnit.B,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "$showValue$showUnit";
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is TrafficValue &&
|
||||
runtimeType == other.runtimeType &&
|
||||
_value == other._value;
|
||||
|
||||
@override
|
||||
int get hashCode => _value.hashCode;
|
||||
}
|
||||
|
||||
typedef ProxyMap = Map<String, Proxy>;
|
||||
|
||||
@freezed
|
||||
class Group with _$Group {
|
||||
const factory Group({
|
||||
required GroupType type,
|
||||
@Default([]) List<Proxy> all,
|
||||
String? now,
|
||||
bool? hidden,
|
||||
@Default("") String icon,
|
||||
required String name,
|
||||
}) = _Group;
|
||||
|
||||
factory Group.fromJson(Map<String, Object?> json) => _$GroupFromJson(json);
|
||||
}
|
||||
|
||||
extension GroupExt on Group {
|
||||
String get realNow => now ?? "";
|
||||
|
||||
String getCurrentSelectedName(String proxyName) {
|
||||
if (type == GroupType.URLTest) {
|
||||
return realNow.isNotEmpty ? realNow : proxyName;
|
||||
}
|
||||
return proxyName.isNotEmpty ? proxyName : realNow;
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Proxy with _$Proxy {
|
||||
const factory Proxy({
|
||||
required String name,
|
||||
required String type,
|
||||
String? now,
|
||||
}) = _Proxy;
|
||||
|
||||
factory Proxy.fromJson(Map<String, Object?> json) => _$ProxyFromJson(json);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class SystemColorSchemes {
|
||||
final ColorScheme? lightColorScheme;
|
||||
final ColorScheme? darkColorScheme;
|
||||
|
||||
const SystemColorSchemes({
|
||||
this.lightColorScheme,
|
||||
this.darkColorScheme,
|
||||
});
|
||||
|
||||
getSystemColorSchemeForBrightness(Brightness? brightness) {
|
||||
if (brightness == Brightness.dark) {
|
||||
return darkColorScheme != null
|
||||
? ColorScheme.fromSeed(
|
||||
seedColor: darkColorScheme!.primary,
|
||||
brightness: Brightness.dark,
|
||||
)
|
||||
: ColorScheme.fromSeed(
|
||||
seedColor: defaultPrimaryColor,
|
||||
brightness: Brightness.dark,
|
||||
);
|
||||
}
|
||||
return lightColorScheme != null
|
||||
? ColorScheme.fromSeed(seedColor: darkColorScheme!.primary)
|
||||
: ColorScheme.fromSeed(seedColor: defaultPrimaryColor);
|
||||
}
|
||||
}
|
||||
|
||||
class IpInfo {
|
||||
final String ip;
|
||||
final String countryCode;
|
||||
|
||||
const IpInfo({
|
||||
required this.ip,
|
||||
required this.countryCode,
|
||||
});
|
||||
|
||||
static IpInfo fromIpInfoIoJson(Map<String, dynamic> json) {
|
||||
return switch (json) {
|
||||
{
|
||||
"ip": final String ip,
|
||||
"country": final String country,
|
||||
} =>
|
||||
IpInfo(
|
||||
ip: ip,
|
||||
countryCode: country,
|
||||
),
|
||||
_ => throw const FormatException("invalid json"),
|
||||
};
|
||||
}
|
||||
|
||||
static IpInfo fromIpApiCoJson(Map<String, dynamic> json) {
|
||||
return switch (json) {
|
||||
{
|
||||
"ip": final String ip,
|
||||
"country_code": final String countryCode,
|
||||
} =>
|
||||
IpInfo(
|
||||
ip: ip,
|
||||
countryCode: countryCode,
|
||||
),
|
||||
_ => throw const FormatException("invalid json"),
|
||||
};
|
||||
}
|
||||
|
||||
static IpInfo fromIpSbJson(Map<String, dynamic> json) {
|
||||
return switch (json) {
|
||||
{
|
||||
"ip": final String ip,
|
||||
"country_code": final String countryCode,
|
||||
} =>
|
||||
IpInfo(
|
||||
ip: ip,
|
||||
countryCode: countryCode,
|
||||
),
|
||||
_ => throw const FormatException("invalid json"),
|
||||
};
|
||||
}
|
||||
|
||||
static IpInfo fromIpwhoIsJson(Map<String, dynamic> json) {
|
||||
return switch (json) {
|
||||
{
|
||||
"ip": final String ip,
|
||||
"country_code": final String countryCode,
|
||||
} =>
|
||||
IpInfo(
|
||||
ip: ip,
|
||||
countryCode: countryCode,
|
||||
),
|
||||
_ => throw const FormatException("invalid json"),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'IpInfo{ip: $ip, countryCode: $countryCode}';
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class HotKeyAction with _$HotKeyAction {
|
||||
const factory HotKeyAction({
|
||||
required HotAction action,
|
||||
int? key,
|
||||
@Default({}) Set<KeyboardModifier> modifiers,
|
||||
}) = _HotKeyAction;
|
||||
|
||||
factory HotKeyAction.fromJson(Map<String, Object?> json) =>
|
||||
_$HotKeyActionFromJson(json);
|
||||
}
|
||||
|
||||
const keyboardModifiersEquality = SetEquality<KeyboardModifier>();
|
||||
const hotKeyActionsEquality = ListEquality<HotKeyAction>();
|
||||
|
||||
@@ -49,6 +49,17 @@ class CoreState with _$CoreState {
|
||||
_$CoreStateFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class VPNState with _$VPNState {
|
||||
const factory VPNState({
|
||||
required AccessControl? accessControl,
|
||||
required VpnProps vpnProps,
|
||||
}) = _VPNState;
|
||||
|
||||
factory VPNState.fromJson(Map<String, Object?> json) =>
|
||||
_$VPNStateFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class WindowProps with _$WindowProps {
|
||||
const factory WindowProps({
|
||||
@@ -84,6 +95,21 @@ class DesktopProps with _$DesktopProps {
|
||||
json == null ? const DesktopProps() : _$DesktopPropsFromJson(json);
|
||||
}
|
||||
|
||||
const defaultCustomFontSizeScale = 1.0;
|
||||
|
||||
const defaultScaleProps = ScaleProps();
|
||||
|
||||
@freezed
|
||||
class ScaleProps with _$ScaleProps {
|
||||
const factory ScaleProps({
|
||||
@Default(false) bool custom,
|
||||
@Default(defaultCustomFontSizeScale) double scale,
|
||||
}) = _ScaleProps;
|
||||
|
||||
factory ScaleProps.fromJson(Map<String, Object?>? json) =>
|
||||
json == null ? defaultScaleProps : _$ScalePropsFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Config extends ChangeNotifier {
|
||||
List<Profile> _profiles;
|
||||
@@ -113,8 +139,12 @@ class Config extends ChangeNotifier {
|
||||
bool _onlyProxy;
|
||||
bool _prueBlack;
|
||||
VpnProps _vpnProps;
|
||||
ScaleProps _scaleProps;
|
||||
DesktopProps _desktopProps;
|
||||
bool _showLabel;
|
||||
bool _overrideDns;
|
||||
List<HotKeyAction> _hotKeyActions;
|
||||
bool _isDisclaimerAccepted;
|
||||
|
||||
Config()
|
||||
: _profiles = [],
|
||||
@@ -142,7 +172,11 @@ class Config extends ChangeNotifier {
|
||||
_proxiesLayout = ProxiesLayout.standard,
|
||||
_vpnProps = const VpnProps(),
|
||||
_desktopProps = const DesktopProps(),
|
||||
_showLabel = false;
|
||||
_showLabel = false,
|
||||
_overrideDns = false,
|
||||
_scaleProps = const ScaleProps(),
|
||||
_isDisclaimerAccepted = false,
|
||||
_hotKeyActions = [];
|
||||
|
||||
deleteProfileById(String id) {
|
||||
_profiles = profiles.where((element) => element.id != id).toList();
|
||||
@@ -538,6 +572,16 @@ class Config extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
ScaleProps get scaleProps => _scaleProps;
|
||||
|
||||
set scaleProps(ScaleProps value) {
|
||||
if (_scaleProps != value) {
|
||||
_scaleProps = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool get showLabel => _showLabel;
|
||||
|
||||
set showLabel(bool value) {
|
||||
@@ -547,6 +591,47 @@ class Config extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool get overrideDns => _overrideDns;
|
||||
|
||||
set overrideDns(bool value) {
|
||||
if (_overrideDns != value) {
|
||||
_overrideDns = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool get isDisclaimerAccepted => _isDisclaimerAccepted;
|
||||
|
||||
set isDisclaimerAccepted(bool value) {
|
||||
if (_isDisclaimerAccepted != value) {
|
||||
_isDisclaimerAccepted = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@JsonKey(defaultValue: [])
|
||||
List<HotKeyAction> get hotKeyActions => _hotKeyActions;
|
||||
|
||||
set hotKeyActions(List<HotKeyAction> value) {
|
||||
if (_hotKeyActions != value) {
|
||||
_hotKeyActions = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
updateOrAddHotKeyAction(HotKeyAction hotKeyAction) {
|
||||
final index =
|
||||
_hotKeyActions.indexWhere((item) => item.action == hotKeyAction.action);
|
||||
if (index == -1) {
|
||||
_hotKeyActions = List.from(_hotKeyActions)..add(hotKeyAction);
|
||||
} else {
|
||||
_hotKeyActions = List.from(_hotKeyActions)..[index] = hotKeyAction;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
update([
|
||||
Config? config,
|
||||
RecoveryOption recoveryOptions = RecoveryOption.all,
|
||||
@@ -565,6 +650,7 @@ class Config extends ChangeNotifier {
|
||||
_isCloseConnections = config._isCloseConnections;
|
||||
_isCompatible = config._isCompatible;
|
||||
_autoLaunch = config._autoLaunch;
|
||||
_dav = config._dav;
|
||||
_silentLaunch = config._silentLaunch;
|
||||
_autoRun = config._autoRun;
|
||||
_proxiesType = config._proxiesType;
|
||||
@@ -583,7 +669,9 @@ class Config extends ChangeNotifier {
|
||||
_isExclude = config._isExclude;
|
||||
_windowProps = config._windowProps;
|
||||
_vpnProps = config._vpnProps;
|
||||
_overrideDns = config._overrideDns;
|
||||
_desktopProps = config._desktopProps;
|
||||
_hotKeyActions = config._hotKeyActions;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'generated/connection.g.dart';
|
||||
|
||||
part 'generated/connection.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class Metadata with _$Metadata {
|
||||
const factory Metadata({
|
||||
required int uid,
|
||||
required String network,
|
||||
required String sourceIP,
|
||||
required String sourcePort,
|
||||
required String destinationIP,
|
||||
required String destinationPort,
|
||||
required String host,
|
||||
required String process,
|
||||
required String remoteDestination,
|
||||
}) = _Metadata;
|
||||
|
||||
factory Metadata.fromJson(Map<String, Object?> json) =>
|
||||
_$MetadataFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Connection with _$Connection {
|
||||
const factory Connection({
|
||||
required String id,
|
||||
num? upload,
|
||||
num? download,
|
||||
required DateTime start,
|
||||
required Metadata metadata,
|
||||
required List<String> chains,
|
||||
}) = _Connection;
|
||||
|
||||
factory Connection.fromJson(Map<String, Object?> json) =>
|
||||
_$ConnectionFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ConnectionsAndKeywords with _$ConnectionsAndKeywords {
|
||||
const factory ConnectionsAndKeywords({
|
||||
@Default([]) List<Connection> connections,
|
||||
@Default([]) List<String> keywords,
|
||||
}) = _ConnectionsAndKeywords;
|
||||
|
||||
factory ConnectionsAndKeywords.fromJson(Map<String, Object?> json) =>
|
||||
_$ConnectionsAndKeywordsFromJson(json);
|
||||
}
|
||||
|
||||
extension ConnectionsAndKeywordsExt on ConnectionsAndKeywords {
|
||||
List<Connection> get filteredConnections => connections
|
||||
.where((connection) => {
|
||||
...connection.chains,
|
||||
connection.metadata.process,
|
||||
}.containsAll(keywords))
|
||||
.toList();
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'generated/dav.g.dart';
|
||||
|
||||
part 'generated/dav.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class DAV with _$DAV{
|
||||
const factory DAV({
|
||||
required String uri,
|
||||
required String user,
|
||||
required String password,
|
||||
}) = _DAV;
|
||||
|
||||
factory DAV.fromJson(Map<String, Object?> json) =>
|
||||
_$DAVFromJson(json);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ class ConfigExtendedParams with _$ConfigExtendedParams {
|
||||
@JsonKey(name: "is-patch") required bool isPatch,
|
||||
@JsonKey(name: "is-compatible") required bool isCompatible,
|
||||
@JsonKey(name: "selected-map") required SelectedMap selectedMap,
|
||||
@JsonKey(name: "override-dns") required bool overrideDns,
|
||||
@JsonKey(name: "test-url") required String testUrl,
|
||||
}) = _ConfigExtendedParams;
|
||||
|
||||
@@ -134,6 +135,22 @@ class ExternalProvider with _$ExternalProvider {
|
||||
_$ExternalProviderFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TunProps with _$TunProps {
|
||||
const factory TunProps({
|
||||
required int fd,
|
||||
required String gateway,
|
||||
required String gateway6,
|
||||
required String portal,
|
||||
required String portal6,
|
||||
required String dns,
|
||||
required String dns6,
|
||||
}) = _TunProps;
|
||||
|
||||
factory TunProps.fromJson(Map<String, Object?> json) =>
|
||||
_$TunPropsFromJson(json);
|
||||
}
|
||||
|
||||
abstract mixin class AppMessageListener {
|
||||
void onLog(Log log) {}
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import 'package:fl_clash/common/datetime.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'traffic.dart';
|
||||
|
||||
part 'generated/file.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class FileInfo with _$FileInfo {
|
||||
const factory FileInfo({
|
||||
required int size,
|
||||
required DateTime lastModified,
|
||||
}) = _FileInfo;
|
||||
}
|
||||
|
||||
|
||||
extension FileInfoExt on FileInfo{
|
||||
String get desc => "${TrafficValue(value: size).show} · ${lastModified.lastUpdateTimeDesc}";
|
||||
}
|
||||
|
||||
|
||||
@@ -220,3 +220,818 @@ abstract class _Tun implements Tun {
|
||||
_$$TunImplCopyWith<_$TunImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
FallbackFilter _$FallbackFilterFromJson(Map<String, dynamic> json) {
|
||||
return _FallbackFilter.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$FallbackFilter {
|
||||
bool get geoip => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "geoip-code")
|
||||
String get geoipCode => throw _privateConstructorUsedError;
|
||||
List<String> get geosite => throw _privateConstructorUsedError;
|
||||
List<String> get ipcidr => throw _privateConstructorUsedError;
|
||||
List<String> get domain => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$FallbackFilterCopyWith<FallbackFilter> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $FallbackFilterCopyWith<$Res> {
|
||||
factory $FallbackFilterCopyWith(
|
||||
FallbackFilter value, $Res Function(FallbackFilter) then) =
|
||||
_$FallbackFilterCopyWithImpl<$Res, FallbackFilter>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{bool geoip,
|
||||
@JsonKey(name: "geoip-code") String geoipCode,
|
||||
List<String> geosite,
|
||||
List<String> ipcidr,
|
||||
List<String> domain});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$FallbackFilterCopyWithImpl<$Res, $Val extends FallbackFilter>
|
||||
implements $FallbackFilterCopyWith<$Res> {
|
||||
_$FallbackFilterCopyWithImpl(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? geoip = null,
|
||||
Object? geoipCode = null,
|
||||
Object? geosite = null,
|
||||
Object? ipcidr = null,
|
||||
Object? domain = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
geoip: null == geoip
|
||||
? _value.geoip
|
||||
: geoip // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
geoipCode: null == geoipCode
|
||||
? _value.geoipCode
|
||||
: geoipCode // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
geosite: null == geosite
|
||||
? _value.geosite
|
||||
: geosite // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
ipcidr: null == ipcidr
|
||||
? _value.ipcidr
|
||||
: ipcidr // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
domain: null == domain
|
||||
? _value.domain
|
||||
: domain // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$FallbackFilterImplCopyWith<$Res>
|
||||
implements $FallbackFilterCopyWith<$Res> {
|
||||
factory _$$FallbackFilterImplCopyWith(_$FallbackFilterImpl value,
|
||||
$Res Function(_$FallbackFilterImpl) then) =
|
||||
__$$FallbackFilterImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{bool geoip,
|
||||
@JsonKey(name: "geoip-code") String geoipCode,
|
||||
List<String> geosite,
|
||||
List<String> ipcidr,
|
||||
List<String> domain});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$FallbackFilterImplCopyWithImpl<$Res>
|
||||
extends _$FallbackFilterCopyWithImpl<$Res, _$FallbackFilterImpl>
|
||||
implements _$$FallbackFilterImplCopyWith<$Res> {
|
||||
__$$FallbackFilterImplCopyWithImpl(
|
||||
_$FallbackFilterImpl _value, $Res Function(_$FallbackFilterImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? geoip = null,
|
||||
Object? geoipCode = null,
|
||||
Object? geosite = null,
|
||||
Object? ipcidr = null,
|
||||
Object? domain = null,
|
||||
}) {
|
||||
return _then(_$FallbackFilterImpl(
|
||||
geoip: null == geoip
|
||||
? _value.geoip
|
||||
: geoip // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
geoipCode: null == geoipCode
|
||||
? _value.geoipCode
|
||||
: geoipCode // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
geosite: null == geosite
|
||||
? _value._geosite
|
||||
: geosite // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
ipcidr: null == ipcidr
|
||||
? _value._ipcidr
|
||||
: ipcidr // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
domain: null == domain
|
||||
? _value._domain
|
||||
: domain // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$FallbackFilterImpl implements _FallbackFilter {
|
||||
const _$FallbackFilterImpl(
|
||||
{this.geoip = true,
|
||||
@JsonKey(name: "geoip-code") this.geoipCode = "CN",
|
||||
final List<String> geosite = const ["gfw"],
|
||||
final List<String> ipcidr = const ["240.0.0.0/4"],
|
||||
final List<String> domain = const [
|
||||
"+.google.com",
|
||||
"+.facebook.com",
|
||||
"+.youtube.com"
|
||||
]})
|
||||
: _geosite = geosite,
|
||||
_ipcidr = ipcidr,
|
||||
_domain = domain;
|
||||
|
||||
factory _$FallbackFilterImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$FallbackFilterImplFromJson(json);
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool geoip;
|
||||
@override
|
||||
@JsonKey(name: "geoip-code")
|
||||
final String geoipCode;
|
||||
final List<String> _geosite;
|
||||
@override
|
||||
@JsonKey()
|
||||
List<String> get geosite {
|
||||
if (_geosite is EqualUnmodifiableListView) return _geosite;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_geosite);
|
||||
}
|
||||
|
||||
final List<String> _ipcidr;
|
||||
@override
|
||||
@JsonKey()
|
||||
List<String> get ipcidr {
|
||||
if (_ipcidr is EqualUnmodifiableListView) return _ipcidr;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_ipcidr);
|
||||
}
|
||||
|
||||
final List<String> _domain;
|
||||
@override
|
||||
@JsonKey()
|
||||
List<String> get domain {
|
||||
if (_domain is EqualUnmodifiableListView) return _domain;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_domain);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FallbackFilter(geoip: $geoip, geoipCode: $geoipCode, geosite: $geosite, ipcidr: $ipcidr, domain: $domain)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$FallbackFilterImpl &&
|
||||
(identical(other.geoip, geoip) || other.geoip == geoip) &&
|
||||
(identical(other.geoipCode, geoipCode) ||
|
||||
other.geoipCode == geoipCode) &&
|
||||
const DeepCollectionEquality().equals(other._geosite, _geosite) &&
|
||||
const DeepCollectionEquality().equals(other._ipcidr, _ipcidr) &&
|
||||
const DeepCollectionEquality().equals(other._domain, _domain));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
geoip,
|
||||
geoipCode,
|
||||
const DeepCollectionEquality().hash(_geosite),
|
||||
const DeepCollectionEquality().hash(_ipcidr),
|
||||
const DeepCollectionEquality().hash(_domain));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$FallbackFilterImplCopyWith<_$FallbackFilterImpl> get copyWith =>
|
||||
__$$FallbackFilterImplCopyWithImpl<_$FallbackFilterImpl>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$FallbackFilterImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _FallbackFilter implements FallbackFilter {
|
||||
const factory _FallbackFilter(
|
||||
{final bool geoip,
|
||||
@JsonKey(name: "geoip-code") final String geoipCode,
|
||||
final List<String> geosite,
|
||||
final List<String> ipcidr,
|
||||
final List<String> domain}) = _$FallbackFilterImpl;
|
||||
|
||||
factory _FallbackFilter.fromJson(Map<String, dynamic> json) =
|
||||
_$FallbackFilterImpl.fromJson;
|
||||
|
||||
@override
|
||||
bool get geoip;
|
||||
@override
|
||||
@JsonKey(name: "geoip-code")
|
||||
String get geoipCode;
|
||||
@override
|
||||
List<String> get geosite;
|
||||
@override
|
||||
List<String> get ipcidr;
|
||||
@override
|
||||
List<String> get domain;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$FallbackFilterImplCopyWith<_$FallbackFilterImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
Dns _$DnsFromJson(Map<String, dynamic> json) {
|
||||
return _Dns.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$Dns {
|
||||
bool get enable => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "prefer-h3")
|
||||
bool get preferH3 => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "use-hosts")
|
||||
bool get useHosts => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "use-system-hosts")
|
||||
bool get useSystemHosts => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "respect-rules")
|
||||
bool get respectRules => throw _privateConstructorUsedError;
|
||||
bool get ipv6 => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "default-nameserver")
|
||||
List<String> get defaultNameserver => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "enhanced-mode")
|
||||
DnsMode get enhancedMode => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "fake-ip-range")
|
||||
String get fakeIpRange => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "fake-ip-filter")
|
||||
List<String> get fakeIpFilter => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "nameserver-policy")
|
||||
Map<String, String> get nameserverPolicy =>
|
||||
throw _privateConstructorUsedError;
|
||||
List<String> get nameserver => throw _privateConstructorUsedError;
|
||||
List<String> get fallback => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "proxy-server-nameserver")
|
||||
List<String> get proxyServerNameserver => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "fallback-filter")
|
||||
FallbackFilter get fallbackFilter => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$DnsCopyWith<Dns> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $DnsCopyWith<$Res> {
|
||||
factory $DnsCopyWith(Dns value, $Res Function(Dns) then) =
|
||||
_$DnsCopyWithImpl<$Res, Dns>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{bool enable,
|
||||
@JsonKey(name: "prefer-h3") bool preferH3,
|
||||
@JsonKey(name: "use-hosts") bool useHosts,
|
||||
@JsonKey(name: "use-system-hosts") bool useSystemHosts,
|
||||
@JsonKey(name: "respect-rules") bool respectRules,
|
||||
bool ipv6,
|
||||
@JsonKey(name: "default-nameserver") List<String> defaultNameserver,
|
||||
@JsonKey(name: "enhanced-mode") DnsMode enhancedMode,
|
||||
@JsonKey(name: "fake-ip-range") String fakeIpRange,
|
||||
@JsonKey(name: "fake-ip-filter") List<String> fakeIpFilter,
|
||||
@JsonKey(name: "nameserver-policy") Map<String, String> nameserverPolicy,
|
||||
List<String> nameserver,
|
||||
List<String> fallback,
|
||||
@JsonKey(name: "proxy-server-nameserver")
|
||||
List<String> proxyServerNameserver,
|
||||
@JsonKey(name: "fallback-filter") FallbackFilter fallbackFilter});
|
||||
|
||||
$FallbackFilterCopyWith<$Res> get fallbackFilter;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$DnsCopyWithImpl<$Res, $Val extends Dns> implements $DnsCopyWith<$Res> {
|
||||
_$DnsCopyWithImpl(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? preferH3 = null,
|
||||
Object? useHosts = null,
|
||||
Object? useSystemHosts = null,
|
||||
Object? respectRules = null,
|
||||
Object? ipv6 = null,
|
||||
Object? defaultNameserver = null,
|
||||
Object? enhancedMode = null,
|
||||
Object? fakeIpRange = null,
|
||||
Object? fakeIpFilter = null,
|
||||
Object? nameserverPolicy = null,
|
||||
Object? nameserver = null,
|
||||
Object? fallback = null,
|
||||
Object? proxyServerNameserver = null,
|
||||
Object? fallbackFilter = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
enable: null == enable
|
||||
? _value.enable
|
||||
: enable // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
preferH3: null == preferH3
|
||||
? _value.preferH3
|
||||
: preferH3 // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
useHosts: null == useHosts
|
||||
? _value.useHosts
|
||||
: useHosts // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
useSystemHosts: null == useSystemHosts
|
||||
? _value.useSystemHosts
|
||||
: useSystemHosts // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
respectRules: null == respectRules
|
||||
? _value.respectRules
|
||||
: respectRules // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
ipv6: null == ipv6
|
||||
? _value.ipv6
|
||||
: ipv6 // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
defaultNameserver: null == defaultNameserver
|
||||
? _value.defaultNameserver
|
||||
: defaultNameserver // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
enhancedMode: null == enhancedMode
|
||||
? _value.enhancedMode
|
||||
: enhancedMode // ignore: cast_nullable_to_non_nullable
|
||||
as DnsMode,
|
||||
fakeIpRange: null == fakeIpRange
|
||||
? _value.fakeIpRange
|
||||
: fakeIpRange // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
fakeIpFilter: null == fakeIpFilter
|
||||
? _value.fakeIpFilter
|
||||
: fakeIpFilter // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
nameserverPolicy: null == nameserverPolicy
|
||||
? _value.nameserverPolicy
|
||||
: nameserverPolicy // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, String>,
|
||||
nameserver: null == nameserver
|
||||
? _value.nameserver
|
||||
: nameserver // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
fallback: null == fallback
|
||||
? _value.fallback
|
||||
: fallback // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
proxyServerNameserver: null == proxyServerNameserver
|
||||
? _value.proxyServerNameserver
|
||||
: proxyServerNameserver // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
fallbackFilter: null == fallbackFilter
|
||||
? _value.fallbackFilter
|
||||
: fallbackFilter // ignore: cast_nullable_to_non_nullable
|
||||
as FallbackFilter,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$FallbackFilterCopyWith<$Res> get fallbackFilter {
|
||||
return $FallbackFilterCopyWith<$Res>(_value.fallbackFilter, (value) {
|
||||
return _then(_value.copyWith(fallbackFilter: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$DnsImplCopyWith<$Res> implements $DnsCopyWith<$Res> {
|
||||
factory _$$DnsImplCopyWith(_$DnsImpl value, $Res Function(_$DnsImpl) then) =
|
||||
__$$DnsImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{bool enable,
|
||||
@JsonKey(name: "prefer-h3") bool preferH3,
|
||||
@JsonKey(name: "use-hosts") bool useHosts,
|
||||
@JsonKey(name: "use-system-hosts") bool useSystemHosts,
|
||||
@JsonKey(name: "respect-rules") bool respectRules,
|
||||
bool ipv6,
|
||||
@JsonKey(name: "default-nameserver") List<String> defaultNameserver,
|
||||
@JsonKey(name: "enhanced-mode") DnsMode enhancedMode,
|
||||
@JsonKey(name: "fake-ip-range") String fakeIpRange,
|
||||
@JsonKey(name: "fake-ip-filter") List<String> fakeIpFilter,
|
||||
@JsonKey(name: "nameserver-policy") Map<String, String> nameserverPolicy,
|
||||
List<String> nameserver,
|
||||
List<String> fallback,
|
||||
@JsonKey(name: "proxy-server-nameserver")
|
||||
List<String> proxyServerNameserver,
|
||||
@JsonKey(name: "fallback-filter") FallbackFilter fallbackFilter});
|
||||
|
||||
@override
|
||||
$FallbackFilterCopyWith<$Res> get fallbackFilter;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$DnsImplCopyWithImpl<$Res> extends _$DnsCopyWithImpl<$Res, _$DnsImpl>
|
||||
implements _$$DnsImplCopyWith<$Res> {
|
||||
__$$DnsImplCopyWithImpl(_$DnsImpl _value, $Res Function(_$DnsImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? enable = null,
|
||||
Object? preferH3 = null,
|
||||
Object? useHosts = null,
|
||||
Object? useSystemHosts = null,
|
||||
Object? respectRules = null,
|
||||
Object? ipv6 = null,
|
||||
Object? defaultNameserver = null,
|
||||
Object? enhancedMode = null,
|
||||
Object? fakeIpRange = null,
|
||||
Object? fakeIpFilter = null,
|
||||
Object? nameserverPolicy = null,
|
||||
Object? nameserver = null,
|
||||
Object? fallback = null,
|
||||
Object? proxyServerNameserver = null,
|
||||
Object? fallbackFilter = null,
|
||||
}) {
|
||||
return _then(_$DnsImpl(
|
||||
enable: null == enable
|
||||
? _value.enable
|
||||
: enable // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
preferH3: null == preferH3
|
||||
? _value.preferH3
|
||||
: preferH3 // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
useHosts: null == useHosts
|
||||
? _value.useHosts
|
||||
: useHosts // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
useSystemHosts: null == useSystemHosts
|
||||
? _value.useSystemHosts
|
||||
: useSystemHosts // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
respectRules: null == respectRules
|
||||
? _value.respectRules
|
||||
: respectRules // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
ipv6: null == ipv6
|
||||
? _value.ipv6
|
||||
: ipv6 // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
defaultNameserver: null == defaultNameserver
|
||||
? _value._defaultNameserver
|
||||
: defaultNameserver // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
enhancedMode: null == enhancedMode
|
||||
? _value.enhancedMode
|
||||
: enhancedMode // ignore: cast_nullable_to_non_nullable
|
||||
as DnsMode,
|
||||
fakeIpRange: null == fakeIpRange
|
||||
? _value.fakeIpRange
|
||||
: fakeIpRange // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
fakeIpFilter: null == fakeIpFilter
|
||||
? _value._fakeIpFilter
|
||||
: fakeIpFilter // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
nameserverPolicy: null == nameserverPolicy
|
||||
? _value._nameserverPolicy
|
||||
: nameserverPolicy // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, String>,
|
||||
nameserver: null == nameserver
|
||||
? _value._nameserver
|
||||
: nameserver // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
fallback: null == fallback
|
||||
? _value._fallback
|
||||
: fallback // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
proxyServerNameserver: null == proxyServerNameserver
|
||||
? _value._proxyServerNameserver
|
||||
: proxyServerNameserver // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
fallbackFilter: null == fallbackFilter
|
||||
? _value.fallbackFilter
|
||||
: fallbackFilter // ignore: cast_nullable_to_non_nullable
|
||||
as FallbackFilter,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$DnsImpl implements _Dns {
|
||||
const _$DnsImpl(
|
||||
{this.enable = true,
|
||||
@JsonKey(name: "prefer-h3") this.preferH3 = false,
|
||||
@JsonKey(name: "use-hosts") this.useHosts = true,
|
||||
@JsonKey(name: "use-system-hosts") this.useSystemHosts = true,
|
||||
@JsonKey(name: "respect-rules") this.respectRules = true,
|
||||
this.ipv6 = false,
|
||||
@JsonKey(name: "default-nameserver")
|
||||
final List<String> defaultNameserver = const ["223.5.5.5"],
|
||||
@JsonKey(name: "enhanced-mode") this.enhancedMode = DnsMode.fakeIp,
|
||||
@JsonKey(name: "fake-ip-range") this.fakeIpRange = "198.18.0.1/16",
|
||||
@JsonKey(name: "fake-ip-filter") final List<String> fakeIpFilter = const [
|
||||
"*.lan",
|
||||
"localhost.ptlogin2.qq.com"
|
||||
],
|
||||
@JsonKey(name: "nameserver-policy")
|
||||
final Map<String, String> nameserverPolicy = const {
|
||||
"www.baidu.com": "114.114.114.114",
|
||||
"+.internal.crop.com": "10.0.0.1",
|
||||
"geosite:cn": "https://doh.pub/dns-query"
|
||||
},
|
||||
final List<String> nameserver = const [
|
||||
"https://doh.pub/dns-query",
|
||||
"https://dns.alidns.com/dns-query"
|
||||
],
|
||||
final List<String> fallback = const ["tls://8.8.4.4", "tls://1.1.1.1"],
|
||||
@JsonKey(name: "proxy-server-nameserver")
|
||||
final List<String> proxyServerNameserver = const [
|
||||
"https://doh.pub/dns-query"
|
||||
],
|
||||
@JsonKey(name: "fallback-filter")
|
||||
this.fallbackFilter = const FallbackFilter()})
|
||||
: _defaultNameserver = defaultNameserver,
|
||||
_fakeIpFilter = fakeIpFilter,
|
||||
_nameserverPolicy = nameserverPolicy,
|
||||
_nameserver = nameserver,
|
||||
_fallback = fallback,
|
||||
_proxyServerNameserver = proxyServerNameserver;
|
||||
|
||||
factory _$DnsImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$DnsImplFromJson(json);
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool enable;
|
||||
@override
|
||||
@JsonKey(name: "prefer-h3")
|
||||
final bool preferH3;
|
||||
@override
|
||||
@JsonKey(name: "use-hosts")
|
||||
final bool useHosts;
|
||||
@override
|
||||
@JsonKey(name: "use-system-hosts")
|
||||
final bool useSystemHosts;
|
||||
@override
|
||||
@JsonKey(name: "respect-rules")
|
||||
final bool respectRules;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool ipv6;
|
||||
final List<String> _defaultNameserver;
|
||||
@override
|
||||
@JsonKey(name: "default-nameserver")
|
||||
List<String> get defaultNameserver {
|
||||
if (_defaultNameserver is EqualUnmodifiableListView)
|
||||
return _defaultNameserver;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_defaultNameserver);
|
||||
}
|
||||
|
||||
@override
|
||||
@JsonKey(name: "enhanced-mode")
|
||||
final DnsMode enhancedMode;
|
||||
@override
|
||||
@JsonKey(name: "fake-ip-range")
|
||||
final String fakeIpRange;
|
||||
final List<String> _fakeIpFilter;
|
||||
@override
|
||||
@JsonKey(name: "fake-ip-filter")
|
||||
List<String> get fakeIpFilter {
|
||||
if (_fakeIpFilter is EqualUnmodifiableListView) return _fakeIpFilter;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_fakeIpFilter);
|
||||
}
|
||||
|
||||
final Map<String, String> _nameserverPolicy;
|
||||
@override
|
||||
@JsonKey(name: "nameserver-policy")
|
||||
Map<String, String> get nameserverPolicy {
|
||||
if (_nameserverPolicy is EqualUnmodifiableMapView) return _nameserverPolicy;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_nameserverPolicy);
|
||||
}
|
||||
|
||||
final List<String> _nameserver;
|
||||
@override
|
||||
@JsonKey()
|
||||
List<String> get nameserver {
|
||||
if (_nameserver is EqualUnmodifiableListView) return _nameserver;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_nameserver);
|
||||
}
|
||||
|
||||
final List<String> _fallback;
|
||||
@override
|
||||
@JsonKey()
|
||||
List<String> get fallback {
|
||||
if (_fallback is EqualUnmodifiableListView) return _fallback;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_fallback);
|
||||
}
|
||||
|
||||
final List<String> _proxyServerNameserver;
|
||||
@override
|
||||
@JsonKey(name: "proxy-server-nameserver")
|
||||
List<String> get proxyServerNameserver {
|
||||
if (_proxyServerNameserver is EqualUnmodifiableListView)
|
||||
return _proxyServerNameserver;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_proxyServerNameserver);
|
||||
}
|
||||
|
||||
@override
|
||||
@JsonKey(name: "fallback-filter")
|
||||
final FallbackFilter fallbackFilter;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Dns(enable: $enable, preferH3: $preferH3, useHosts: $useHosts, useSystemHosts: $useSystemHosts, respectRules: $respectRules, ipv6: $ipv6, defaultNameserver: $defaultNameserver, enhancedMode: $enhancedMode, fakeIpRange: $fakeIpRange, fakeIpFilter: $fakeIpFilter, nameserverPolicy: $nameserverPolicy, nameserver: $nameserver, fallback: $fallback, proxyServerNameserver: $proxyServerNameserver, fallbackFilter: $fallbackFilter)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$DnsImpl &&
|
||||
(identical(other.enable, enable) || other.enable == enable) &&
|
||||
(identical(other.preferH3, preferH3) ||
|
||||
other.preferH3 == preferH3) &&
|
||||
(identical(other.useHosts, useHosts) ||
|
||||
other.useHosts == useHosts) &&
|
||||
(identical(other.useSystemHosts, useSystemHosts) ||
|
||||
other.useSystemHosts == useSystemHosts) &&
|
||||
(identical(other.respectRules, respectRules) ||
|
||||
other.respectRules == respectRules) &&
|
||||
(identical(other.ipv6, ipv6) || other.ipv6 == ipv6) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._defaultNameserver, _defaultNameserver) &&
|
||||
(identical(other.enhancedMode, enhancedMode) ||
|
||||
other.enhancedMode == enhancedMode) &&
|
||||
(identical(other.fakeIpRange, fakeIpRange) ||
|
||||
other.fakeIpRange == fakeIpRange) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._fakeIpFilter, _fakeIpFilter) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._nameserverPolicy, _nameserverPolicy) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._nameserver, _nameserver) &&
|
||||
const DeepCollectionEquality().equals(other._fallback, _fallback) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._proxyServerNameserver, _proxyServerNameserver) &&
|
||||
(identical(other.fallbackFilter, fallbackFilter) ||
|
||||
other.fallbackFilter == fallbackFilter));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
enable,
|
||||
preferH3,
|
||||
useHosts,
|
||||
useSystemHosts,
|
||||
respectRules,
|
||||
ipv6,
|
||||
const DeepCollectionEquality().hash(_defaultNameserver),
|
||||
enhancedMode,
|
||||
fakeIpRange,
|
||||
const DeepCollectionEquality().hash(_fakeIpFilter),
|
||||
const DeepCollectionEquality().hash(_nameserverPolicy),
|
||||
const DeepCollectionEquality().hash(_nameserver),
|
||||
const DeepCollectionEquality().hash(_fallback),
|
||||
const DeepCollectionEquality().hash(_proxyServerNameserver),
|
||||
fallbackFilter);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$DnsImplCopyWith<_$DnsImpl> get copyWith =>
|
||||
__$$DnsImplCopyWithImpl<_$DnsImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$DnsImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _Dns implements Dns {
|
||||
const factory _Dns(
|
||||
{final bool enable,
|
||||
@JsonKey(name: "prefer-h3") final bool preferH3,
|
||||
@JsonKey(name: "use-hosts") final bool useHosts,
|
||||
@JsonKey(name: "use-system-hosts") final bool useSystemHosts,
|
||||
@JsonKey(name: "respect-rules") final bool respectRules,
|
||||
final bool ipv6,
|
||||
@JsonKey(name: "default-nameserver") final List<String> defaultNameserver,
|
||||
@JsonKey(name: "enhanced-mode") final DnsMode enhancedMode,
|
||||
@JsonKey(name: "fake-ip-range") final String fakeIpRange,
|
||||
@JsonKey(name: "fake-ip-filter") final List<String> fakeIpFilter,
|
||||
@JsonKey(name: "nameserver-policy")
|
||||
final Map<String, String> nameserverPolicy,
|
||||
final List<String> nameserver,
|
||||
final List<String> fallback,
|
||||
@JsonKey(name: "proxy-server-nameserver")
|
||||
final List<String> proxyServerNameserver,
|
||||
@JsonKey(name: "fallback-filter")
|
||||
final FallbackFilter fallbackFilter}) = _$DnsImpl;
|
||||
|
||||
factory _Dns.fromJson(Map<String, dynamic> json) = _$DnsImpl.fromJson;
|
||||
|
||||
@override
|
||||
bool get enable;
|
||||
@override
|
||||
@JsonKey(name: "prefer-h3")
|
||||
bool get preferH3;
|
||||
@override
|
||||
@JsonKey(name: "use-hosts")
|
||||
bool get useHosts;
|
||||
@override
|
||||
@JsonKey(name: "use-system-hosts")
|
||||
bool get useSystemHosts;
|
||||
@override
|
||||
@JsonKey(name: "respect-rules")
|
||||
bool get respectRules;
|
||||
@override
|
||||
bool get ipv6;
|
||||
@override
|
||||
@JsonKey(name: "default-nameserver")
|
||||
List<String> get defaultNameserver;
|
||||
@override
|
||||
@JsonKey(name: "enhanced-mode")
|
||||
DnsMode get enhancedMode;
|
||||
@override
|
||||
@JsonKey(name: "fake-ip-range")
|
||||
String get fakeIpRange;
|
||||
@override
|
||||
@JsonKey(name: "fake-ip-filter")
|
||||
List<String> get fakeIpFilter;
|
||||
@override
|
||||
@JsonKey(name: "nameserver-policy")
|
||||
Map<String, String> get nameserverPolicy;
|
||||
@override
|
||||
List<String> get nameserver;
|
||||
@override
|
||||
List<String> get fallback;
|
||||
@override
|
||||
@JsonKey(name: "proxy-server-nameserver")
|
||||
List<String> get proxyServerNameserver;
|
||||
@override
|
||||
@JsonKey(name: "fallback-filter")
|
||||
FallbackFilter get fallbackFilter;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$DnsImplCopyWith<_$DnsImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user