Compare commits

...

24 Commits

Author SHA1 Message Date
chen08209
30dc3f4d13 Fix android system dns issues
Optimize dns default option

Fix some issues
2024-09-20 11:20:31 +08:00
chen08209
2c5f8525a7 Update readme 2024-09-18 00:41:46 +08:00
chen08209
5566f2b54f Update README.md 2 2024-09-18 00:22:31 +08:00
chen08209
511b7c521a Update README.md 2 2024-09-18 00:20:00 +08:00
chen08209
fb01d87371 Update README.md 2024-09-18 00:17:12 +08:00
chen08209
043648f998 Fix build error2 2024-09-17 23:06:32 +08:00
chen08209
3eb26e8061 Fix build error 2024-09-17 22:42:10 +08:00
chen08209
5d6bd6466f Support desktop hotkey
Support android ipv6 inbound

Support android system dns

fix some bugs
2024-09-17 21:42:13 +08:00
chen08209
4e766d9407 Fix delete profile error 2024-09-09 09:49:22 +08:00
chen08209
80f8aa22ee Fix submit error 2 2024-09-08 21:38:58 +08:00
chen08209
97714e8b25 Fix submit error 2024-09-08 21:22:02 +08:00
chen08209
50bf4170d9 Optimize DNS strategy
Fix the problem that the tray is not displayed in some cases

Optimize tray

Update core

Fix some error
2024-09-08 20:58:02 +08:00
chen08209
79efa67df3 Fix tun update issues 2024-09-02 16:42:47 +08:00
chen08209
ac397393a0 Add DNS override
Fixed some bugs
Optimize more detail
2024-09-02 16:09:51 +08:00
chen08209
b685165230 Add Hosts override 2024-09-02 16:09:27 +08:00
chen08209
402221aaa2 fix android tip error
fix windows auto launch error
2024-08-26 20:35:38 +08:00
chen08209
f6d9ed11d9 Fix windows tray issues
Optimize windows logic
2024-08-25 23:40:13 +08:00
chen08209
c38a671d57 Optimize app logic
Support windows administrator auto launch

Support android close vpn
2024-08-22 19:56:19 +08:00
chen08209
75af47aead Change flutter version 2024-08-15 14:34:02 +08:00
chen08209
8dafe3b0ec Support profiles sort
Support windows country flags display

Optimize proxies page and profiles page columns
2024-08-15 14:18:33 +08:00
chen08209
813198a21d Update flutter version 2024-08-11 17:45:57 +08:00
chen08209
68dd262fef Update version 2024-08-11 17:09:31 +08:00
chen08209
5ef020db73 Update timeout time 2024-08-11 17:08:51 +08:00
chen08209
e3c9035903 Update access control page
Fix bug
2024-08-11 17:08:46 +08:00
194 changed files with 15761 additions and 7596 deletions

View File

@@ -87,7 +87,7 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.x'
flutter-version: 3.22.x
channel: 'stable'
cache: true
@@ -136,18 +136,25 @@ jobs:
gitchangelog "${pre}.." >> release.md 2>&1 || echo "Error in gitchangelog"
echo -e "\n\n</details>" >> release.md
fi
- name: Release
uses: softprops/action-gh-release@v2
with:
files: ./dist/*
body_path: './release.md'
- name: Create Fdroid Source Dir
run: |
mkdir -p ./tmp
cp ./dist/*android-arm64-v8a* ./tmp/ || true
echo "Files copied successfully"
- name: Push to fdroid repo
uses: cpina/github-action-push-to-another-repository@v1.7.2
env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
with:
source-directory: ./dist/
source-directory: ./tmp/
destination-github-username: chen08209
destination-repository-name: FlClash-fdroid-repo
user-name: 'github-actions[bot]'

2
.gitmodules vendored
View File

@@ -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

View File

@@ -6,13 +6,9 @@
## FlClash
<p style="text-align: left;">
<img alt="stars" src="https://img.shields.io/github/stars/chen08209/FlClash?style=flat-square&logo=github"/>
<img alt="downloads" src="https://img.shields.io/github/downloads/chen08209/FlClash/total"/>
<a href="LICENSE">
<img alt="license" src="https://img.shields.io/github/license/chen08209/FlClash"/>
</a>
</p>
[![Downloads](https://img.shields.io/github/downloads/chen08209/FlClash/total?style=flat-square&logo=github)](https://github.com/chen08209/FlClash/releases/)[![Last Version](https://img.shields.io/github/release/chen08209/FlClash/all.svg?style=flat-square)](https://github.com/chen08209/FlClash/releases/)[![License](https://img.shields.io/github/license/chen08209/FlClash?style=flat-square)](LICENSE)
[![Channel](https://img.shields.io/badge/Telegram-Channel-blue?style=flat-square&logo=telegram)](https://t.me/FlClash)
A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
@@ -42,10 +38,6 @@ on Mobile:
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
## Contact
[Telegram](https://t.me/+G-veVtwBOl4wODc1)
## Build
1. Update submodules
@@ -100,9 +92,6 @@ on Mobile:
```bash
dart .\setup.dart
```
## Star

View File

@@ -6,13 +6,10 @@
## FlClash
<p style="text-align: left;">
<img alt="stars" src="https://img.shields.io/github/stars/chen08209/FlClash?style=flat-square&logo=github"/>
<img alt="downloads" src="https://img.shields.io/github/downloads/chen08209/FlClash/total"/>
<a href="LICENSE">
<img alt="license" src="https://img.shields.io/github/license/chen08209/FlClash"/>
</a>
</p>
[![Downloads](https://img.shields.io/github/downloads/chen08209/FlClash/total?style=flat-square&logo=github)](https://github.com/chen08209/FlClash/releases/)[![Last Version](https://img.shields.io/github/release/chen08209/FlClash/all.svg?style=flat-square)](https://github.com/chen08209/FlClash/releases/)[![License](https://img.shields.io/github/license/chen08209/FlClash?style=flat-square)](LICENSE)
[![Channel](https://img.shields.io/badge/Telegram-Channel-blue?style=flat-square&logo=telegram)](https://t.me/FlClash)
基于ClashMeta的多平台代理客户端简单易用开源无广告。
@@ -42,11 +39,6 @@ on Mobile:
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
## Contact
[Telegram](https://t.me/+G-veVtwBOl4wODc1)
## Build
1. 更新 submodules

View File

@@ -102,6 +102,9 @@ flutter {
dependencies {
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation 'com.google.code.gson:gson:2.10'
implementation("com.android.tools.smali:smali-dexlib2:3.0.7") {
exclude group: "com.google.guava", module: "guava"
}
}

View File

@@ -2,11 +2,11 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@@ -14,18 +14,20 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
tools:ignore="SystemPermissionTypo" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:networkSecurityConfig="@xml/network_security_config"
android:extractNativeLibs="true"
android:enableOnBackInvokedCallback="true"
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher"
android:label="FlClash"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="tiramisu">
<activity
android:name="com.follow.clash.MainActivity"
@@ -56,17 +58,25 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="clash"/>
<data android:scheme="clashmeta"/>
<data android:scheme="flclash"/>
<data android:scheme="clash" />
<data android:scheme="clashmeta" />
<data android:scheme="flclash" />
<data android:host="install-config"/>
<data android:host="install-config" />
</intent-filter>
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="com.follow.clash.action.START" />
</intent-filter>
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="com.follow.clash.action.STOP" />
</intent-filter>
</activity>
<!-- <meta-data-->
<!-- android:name="io.flutter.embedding.android.EnableImpeller"-->
<!-- android:value="true" />-->
<!-- <meta-data-->
<!-- android:name="io.flutter.embedding.android.EnableImpeller"-->
<!-- android:value="true" />-->
<activity
android:name=".TempActivity"
@@ -75,11 +85,10 @@
<service
android:name=".services.FlClashTileService"
android:exported="true"
android:icon="@drawable/ic_stat_name"
android:foregroundServiceType="specialUse"
android:icon="@drawable/ic_stat_name"
android:label="FlClash"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
>
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
@@ -114,13 +123,17 @@
android:name=".services.FlClashVpnService"
android:exported="false"
android:foregroundServiceType="specialUse"
android:permission="android.permission.BIND_VPN_SERVICE"
>
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
<service
android:name=".services.FlClashService"
android:exported="false"
android:foregroundServiceType="specialUse" />
<meta-data
android:name="flutterEmbedding"
android:value="2" />

View File

@@ -0,0 +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?): TunProps?
fun stop()
fun startForeground(title: String, content: String)
}

View File

@@ -1,10 +1,10 @@
package com.follow.clash
import android.content.Context
import android.util.Log
import androidx.lifecycle.MutableLiveData
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ProxyPlugin
import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.VpnPlugin
import com.follow.clash.plugins.TilePlugin
import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine
@@ -22,6 +22,7 @@ enum class RunState {
object GlobalState {
private val lock = ReentrantLock()
val runLock = ReentrantLock()
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
var flutterEngine: FlutterEngine? = null
@@ -32,11 +33,15 @@ object GlobalState {
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
}
fun getCurrentTitlePlugin(): TilePlugin? {
fun getCurrentTilePlugin(): TilePlugin? {
val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine
return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
}
fun getCurrentVPNPlugin(): VpnPlugin? {
return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
}
fun destroyServiceEngine() {
serviceEngine?.destroy()
serviceEngine = null
@@ -47,9 +52,10 @@ object GlobalState {
lock.withLock {
destroyServiceEngine()
serviceEngine = FlutterEngine(context)
serviceEngine?.plugins?.add(ProxyPlugin())
serviceEngine?.plugins?.add(VpnPlugin())
serviceEngine?.plugins?.add(AppPlugin())
serviceEngine?.plugins?.add(TilePlugin())
serviceEngine?.plugins?.add(ServicePlugin())
val vpnService = DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"vpnService"

View File

@@ -1,18 +1,34 @@
package com.follow.clash
import android.content.Intent
import android.os.Bundle
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ProxyPlugin
import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.VpnPlugin
import com.follow.clash.plugins.TilePlugin
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
class MainActivity : FlutterActivity() {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
when (intent.action) {
"com.follow.clash.action.START" -> {
GlobalState.getCurrentTilePlugin()?.handleStart()
}
"com.follow.clash.action.STOP" -> {
GlobalState.getCurrentTilePlugin()?.handleStop()
}
}
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
flutterEngine.plugins.add(AppPlugin())
flutterEngine.plugins.add(ProxyPlugin())
flutterEngine.plugins.add(VpnPlugin())
flutterEngine.plugins.add(ServicePlugin())
flutterEngine.plugins.add(TilePlugin())
GlobalState.flutterEngine = flutterEngine
}

View File

@@ -1,37 +0,0 @@
package com.follow.clash.extensions
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.system.OsConstants.IPPROTO_TCP
import android.system.OsConstants.IPPROTO_UDP
import android.util.Base64
import java.net.URL
import androidx.core.graphics.drawable.toBitmap
import com.follow.clash.models.Metadata
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
import java.net.InetAddress
import java.net.InetSocketAddress
suspend fun Drawable.getBase64(): String {
val drawable = this
return withContext(Dispatchers.IO) {
val bitmap = drawable.toBitmap()
val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)
}
}
fun Metadata.getProtocol(): Int? {
if (network.startsWith("tcp")) return IPPROTO_TCP
if (network.startsWith("udp")) return IPPROTO_UDP
return null
}
fun String.getInetSocketAddress(): InetSocketAddress {
val url = URL("https://$this")
return InetSocketAddress(InetAddress.getByName(url.host), url.port)
}

View File

@@ -0,0 +1,69 @@
package com.follow.clash.extensions
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
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.graphics.drawable.toBitmap
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 {
val drawable = this
return withContext(Dispatchers.IO) {
val bitmap = drawable.toBitmap()
val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)
}
}
fun Metadata.getProtocol(): Int? {
if (network.startsWith("tcp")) return IPPROTO_TCP
if (network.startsWith("udp")) return IPPROTO_UDP
return null
}
fun ConnectivityManager.resolveDns(network: Network?): List<String> {
val properties = getLinkProperties(network) ?: return listOf()
return properties.dnsServers.map { it.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()
}

View 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()

View File

@@ -3,5 +3,6 @@ package com.follow.clash.models
data class Package(
val packageName: String,
val label: String,
val isSystem:Boolean
val isSystem: Boolean,
val firstInstallTime: Long,
)

View File

@@ -12,7 +12,18 @@ data class AccessControl(
)
data class Props(
val enable: Boolean?,
val accessControl: AccessControl?,
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
)

View File

@@ -6,19 +6,21 @@ import android.app.ActivityManager
import android.content.Context
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
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
import androidx.core.content.FileProvider
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
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
@@ -31,8 +33,7 @@ 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 {
@@ -40,22 +41,84 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private var toast: Toast? = null
private var context: Context? = null
private lateinit var context: Context
private lateinit var channel: MethodChannel
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(
"com.google",
"com.android.chrome",
"com.android.vending",
"com.microsoft",
"com.apple",
"com.zhiliaoapp.musically", // Banned by China
)
private val chinaAppPrefixList = listOf(
"com.tencent",
"com.alibaba",
"com.umeng",
"com.qihoo",
"com.ali",
"com.alipay",
"com.amap",
"com.sina",
"com.weibo",
"com.vivo",
"com.xiaomi",
"com.huawei",
"com.taobao",
"com.secneo",
"s.h.e.l.l",
"com.stub",
"com.kiwisec",
"com.secshell",
"com.wrapper",
"cn.securitystack",
"com.mogosec",
"com.secoen",
"com.netease",
"com.mx",
"com.qq.e",
"com.baidu",
"com.bytedance",
"com.bugly",
"com.miui",
"com.oppo",
"com.coloros",
"com.iqoo",
"com.meizu",
"com.gionee",
"cn.nubia",
"com.oplus",
"andes.oplus",
"com.unionpay",
"cn.wps"
)
private val chinaAppRegex by lazy {
("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex()
}
val VPN_PERMISSION_REQUEST_CODE = 1001
val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
private var isBlockNotification: Boolean = false
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
scope = CoroutineScope(Dispatchers.Default)
context = flutterPluginBinding.applicationContext;
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
channel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
@@ -88,7 +151,13 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
"getPackages" -> {
scope.launch {
result.success(getPackages())
result.success(getPackagesToJson())
}
}
"getChinaPackageNames" -> {
scope.launch {
result.success(getChinaPackageNames())
}
}
@@ -107,7 +176,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
if (iconMap["default"] == null) {
iconMap["default"] =
context?.packageManager?.defaultActivityIcon?.getBase64()
context.packageManager?.defaultActivityIcon?.getBase64()
}
result.success(iconMap["default"])
return@launch
@@ -115,52 +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 (context == null) {
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)
@@ -180,46 +203,43 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
private fun openFile(path: String) {
context?.let {
val file = File(path)
val uri = FileProvider.getUriForFile(
it,
"${it.packageName}.fileProvider",
file
)
val file = File(path)
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileProvider",
file
)
val intent = Intent(Intent.ACTION_VIEW).setDataAndType(
val intent = Intent(Intent.ACTION_VIEW).setDataAndType(
uri,
"text/plain"
)
val flags =
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
val resInfoList = context.packageManager.queryIntentActivities(
intent, PackageManager.MATCH_DEFAULT_ONLY
)
for (resolveInfo in resInfoList) {
val packageName = resolveInfo.activityInfo.packageName
context.grantUriPermission(
packageName,
uri,
"text/plain"
flags
)
}
val flags =
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
val resInfoList = it.packageManager.queryIntentActivities(
intent, PackageManager.MATCH_DEFAULT_ONLY
)
for (resolveInfo in resInfoList) {
val packageName = resolveInfo.activityInfo.packageName
it.grantUriPermission(
packageName,
uri,
flags
)
}
try {
activity?.startActivity(intent)
} catch (e: Exception) {
println(e)
}
try {
activity?.startActivity(intent)
} catch (e: Exception) {
println(e)
}
}
private fun updateExcludeFromRecents(value: Boolean?) {
if (context == null) return
val am = getSystemService(context!!, ActivityManager::class.java)
val am = getSystemService(context, ActivityManager::class.java)
val task = am?.appTasks?.firstOrNull {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
it.taskInfo.taskId == activity?.taskId
@@ -236,7 +256,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
private suspend fun getPackageIcon(packageName: String): String? {
val packageManager = context?.packageManager
val packageManager = context.packageManager
if (iconMap[packageName] == null) {
iconMap[packageName] = try {
packageManager?.getApplicationIcon(packageName)?.getBase64()
@@ -248,32 +268,139 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
return iconMap[packageName]
}
private suspend fun getPackages(): String {
return withContext(Dispatchers.Default) {
val packageManager = context?.packageManager
val packages: List<Package>? =
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
it.packageName != context?.packageName
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|| it.packageName == "android"
private fun getPackages(): List<Package> {
val packageManager = context.packageManager
if (packages.isNotEmpty()) return packages;
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
it.packageName != context.packageName
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|| it.packageName == "android"
}?.map {
Package(
packageName = it.packageName,
label = it.applicationInfo.loadLabel(packageManager).toString(),
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1
)
}
}?.map {
Package(
packageName = it.packageName,
label = it.applicationInfo.loadLabel(packageManager).toString(),
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1,
firstInstallTime = it.firstInstallTime
)
}?.let { packages.addAll(it) }
return packages;
}
private suspend fun getPackagesToJson(): String {
return withContext(Dispatchers.Default) {
Gson().toJson(getPackages())
}
}
private suspend fun getChinaPackageNames(): String {
return withContext(Dispatchers.Default) {
val packages: List<String> =
getPackages().map { it.packageName }.filter { isChinaPackage(it) }
Gson().toJson(packages)
}
}
fun requestGc() {
channel.invokeMethod("gc", null)
fun requestVpnPermission(context: Context, callBack: () -> Unit) {
vpnCallBack = callBack
val intent = VpnService.prepare(context)
if (intent != null) {
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
return;
}
vpnCallBack?.invoke()
}
fun requestNotificationsPermission(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permission = ContextCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
)
if (permission != PackageManager.PERMISSION_GRANTED) {
if (isBlockNotification) return
if (activity == null) return
ActivityCompat.requestPermissions(
activity!!,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_REQUEST_CODE
)
return
}
}
}
private fun isChinaPackage(packageName: String): Boolean {
val packageManager = context.packageManager ?: return false
skipPrefixList.forEach {
if (packageName == it || packageName.startsWith("$it.")) return false
}
val packageManagerFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
PackageManager.MATCH_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
} else {
@Suppress("DEPRECATION")
PackageManager.GET_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
}
if (packageName.matches(chinaAppRegex)) {
return true
}
try {
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackageInfo(
packageName,
PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
)
} else {
@Suppress("DEPRECATION") packageManager.getPackageInfo(
packageName, packageManagerFlags
)
}
mutableListOf<ComponentInfo>().apply {
packageInfo.services?.let { addAll(it) }
packageInfo.activities?.let { addAll(it) }
packageInfo.receivers?.let { addAll(it) }
packageInfo.providers?.let { addAll(it) }
}.forEach {
if (it.name.matches(chinaAppRegex)) return true
}
ZipFile(File(packageInfo.applicationInfo.publicSourceDir)).use {
for (packageEntry in it.entries()) {
if (packageEntry.name.startsWith("firebase-")) return false
}
for (packageEntry in it.entries()) {
if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
".dex"
))
) {
continue
}
if (packageEntry.size > 15000000) {
return true
}
val input = it.getInputStream(packageEntry).buffered()
val dexFile = try {
DexBackedDexFile.fromInputStream(null, input)
} catch (e: Exception) {
return false
}
for (clazz in dexFile.classes) {
val clazzName =
clazz.type.substring(1, clazz.type.length - 1).replace("/", ".")
.replace("$", ".")
if (clazzName.matches(chinaAppRegex)) return true
}
}
}
} catch (_: Exception) {
return false
}
return false
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity;
binding.addActivityResultListener(::onActivityResult)
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
}
override fun onDetachedFromActivityForConfigChanges() {
@@ -288,4 +415,25 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
channel.invokeMethod("exit", null)
activity = null
}
}
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
if (resultCode == FlutterActivity.RESULT_OK) {
GlobalState.initServiceEngine(context)
vpnCallBack?.invoke()
}
}
return true
}
private fun onRequestPermissionsResultListener(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
): Boolean {
if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
isBlockNotification = true
}
return true
}
}

View File

@@ -1,220 +0,0 @@
package com.follow.clash.plugins
import android.Manifest
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.net.VpnService
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.follow.clash.GlobalState
import com.follow.clash.RunState
import com.follow.clash.models.Props
import com.follow.clash.services.FlClashVpnService
import com.google.gson.Gson
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
private lateinit var flutterMethodChannel: MethodChannel
val VPN_PERMISSION_REQUEST_CODE = 1001
val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
private var activity: Activity? = null
private var context: Context? = null
private var flClashVpnService: FlClashVpnService? = null
private var port: Int = 7890
private var props: Props? = null
private var isBlockNotification: Boolean = false
private var isStart: Boolean = false
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = service as FlClashVpnService.LocalBinder
flClashVpnService = binder.getService()
if (isStart) {
startVpn()
} else {
flClashVpnService?.initServiceEngine()
}
}
override fun onServiceDisconnected(arg: ComponentName) {
flClashVpnService = null
}
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "proxy")
flutterMethodChannel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
flutterMethodChannel.setMethodCallHandler(null)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
"initService" -> {
isStart = false
initService()
requestNotificationsPermission()
result.success(true)
}
"startProxy" -> {
isStart = true
port = call.argument<Int>("port")!!
val args = call.argument<String>("args")
props =
if (args != null) Gson().fromJson(args, Props::class.java) else null
startVpn()
result.success(true)
}
"stopProxy" -> {
stopVpn()
result.success(true)
}
"setProtect" -> {
val fd = call.argument<Int>("fd")
if (fd != null) {
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)
}
else -> {
result.notImplemented()
}
}
private fun initService() {
val intent = VpnService.prepare(context)
if (intent != null) {
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
} else {
if (flClashVpnService != null) {
flClashVpnService!!.initServiceEngine()
} else {
bindService()
}
}
}
private fun startVpn() {
if (flClashVpnService == null) {
bindService()
return
}
if (GlobalState.runState.value == RunState.START) return
GlobalState.runState.value = RunState.START
val intent = VpnService.prepare(context)
if (intent != null) {
stopVpn()
return
}
val fd = flClashVpnService?.start(port, props)
flutterMethodChannel.invokeMethod("started", fd)
}
private fun stopVpn() {
if (GlobalState.runState.value == RunState.STOP) return
GlobalState.runState.value = RunState.STOP
flClashVpnService?.stop()
GlobalState.destroyServiceEngine()
}
private fun startForeground(title: String, content: String) {
if (GlobalState.runState.value != RunState.START) return
flClashVpnService?.startForeground(title, content)
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
binding.addActivityResultListener(::onActivityResult)
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
}
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
if (resultCode == FlutterActivity.RESULT_OK) {
bindService()
} else {
stopVpn()
}
}
return true
}
private fun onRequestPermissionsResultListener(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
): Boolean {
if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
isBlockNotification = true
}
return false
}
private fun requestNotificationsPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permission = context?.let {
ContextCompat.checkSelfPermission(
it,
Manifest.permission.POST_NOTIFICATIONS
)
}
if (permission != PackageManager.PERMISSION_GRANTED) {
if (isBlockNotification) return
if (activity == null) return
ActivityCompat.requestPermissions(
activity!!,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_REQUEST_CODE
)
}
}
}
override fun onDetachedFromActivityForConfigChanges() {
activity = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onDetachedFromActivity() {
activity = null
}
private fun bindService() {
val intent = Intent(context, FlClashVpnService::class.java)
context?.bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
}

View File

@@ -0,0 +1,49 @@
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
import io.flutter.plugin.common.MethodChannel
class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var flutterMethodChannel: MethodChannel
private lateinit var context: Context
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "service")
flutterMethodChannel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
flutterMethodChannel.setMethodCallHandler(null)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
"init" -> {
GlobalState.getCurrentAppPlugin()?.requestNotificationsPermission(context)
GlobalState.initServiceEngine(context)
result.success(true)
}
"destroy" -> {
handleDestroy()
result.success(true)
}
else -> {
result.notImplemented()
}
}
private fun handleDestroy() {
GlobalState.getCurrentVPNPlugin()?.stop()
GlobalState.destroyServiceEngine()
}
}

View File

@@ -0,0 +1,272 @@
package com.follow.clash.plugins
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import android.os.IBinder
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.resolveDns
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 {
private lateinit var flutterMethodChannel: MethodChannel
private lateinit var context: Context
private var flClashService: BaseServiceInterface? = null
private var port: Int = 7890
private var props: Props? = null
private lateinit var scope: CoroutineScope
private val connectivity by lazy {
context.getSystemService<ConnectivityManager>()
}
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
flClashService = when (service) {
is FlClashVpnService.LocalBinder -> service.getService()
is FlClashService.LocalBinder -> service.getService()
else -> throw Exception("invalid binder")
}
start()
}
override fun onServiceDisconnected(arg: ComponentName) {
flClashService = null
}
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
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)
}
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()
}
}
}
@SuppressLint("ForegroundServiceType")
fun handleStartVpn() {
GlobalState.getCurrentAppPlugin()?.requestVpnPermission(context) {
start()
}
}
fun requestGc() {
flutterMethodChannel.invokeMethod("gc", null)
}
val networks = mutableSetOf<Network>()
fun onUpdateNetwork() {
val dns = networks.flatMap { network ->
connectivity?.resolveDns(network) ?: emptyList()
}
.toSet()
.joinToString(",")
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 {
if (GlobalState.runState.value != RunState.START) return
flClashService?.startForeground(title, content)
}
}
private fun start() {
if (flClashService == null) {
bindService()
return
}
GlobalState.runLock.withLock {
if (GlobalState.runState.value == RunState.START) return
GlobalState.runState.value = RunState.START
val tunProps = flClashService?.start(port, props)
flutterMethodChannel.invokeMethod(
"started",
Gson().toJson(tunProps, TunProps::class.java)
)
}
}
fun stop() {
GlobalState.runLock.withLock {
if (GlobalState.runState.value == RunState.STOP) return
GlobalState.runState.value = RunState.STOP
flClashService?.stop()
}
GlobalState.destroyServiceEngine()
}
private fun bindService() {
val intent = when (props?.enable == true) {
true -> Intent(context, FlClashVpnService::class.java)
false -> Intent(context, FlClashService::class.java)
}
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
}

View File

@@ -0,0 +1,104 @@
package com.follow.clash.services
import android.annotation.SuppressLint
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import com.follow.clash.BaseServiceInterface
import com.follow.clash.MainActivity
import com.follow.clash.models.Props
@SuppressLint("WrongConstant")
class FlClashService : Service(), BaseServiceInterface {
private val binder = LocalBinder()
inner class LocalBinder : Binder() {
fun getService(): FlClashService = this@FlClashService
}
override fun onBind(intent: Intent): IBinder {
return binder
}
override fun onUnbind(intent: Intent?): Boolean {
return super.onUnbind(intent)
}
private val CHANNEL = "FlClash"
private val notificationId: Int = 1
private val notificationBuilder: NotificationCompat.Builder by lazy {
val intent = Intent(this, MainActivity::class.java)
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
with(NotificationCompat.Builder(this, CHANNEL)) {
setSmallIcon(com.follow.clash.R.drawable.ic_stat_name)
setContentTitle("FlClash")
setContentIntent(pendingIntent)
setCategory(NotificationCompat.CATEGORY_SERVICE)
priority = NotificationCompat.PRIORITY_MIN
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
}
setOngoing(true)
setShowWhen(false)
setOnlyAlertOnce(true)
setAutoCancel(true)
}
}
override fun start(port: Int, props: Props?) = null
override fun stop() {
stopSelf()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
stopForeground(STOP_FOREGROUND_REMOVE)
}
}
@SuppressLint("ForegroundServiceType", "WrongConstant")
override fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
var channel = manager?.getNotificationChannel(CHANNEL)
if (channel == null) {
channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
manager?.createNotificationChannel(channel)
}
}
val notification =
notificationBuilder.setContentTitle(title).setContentText(content).build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(notificationId, notification)
}
}
}

View File

@@ -67,15 +67,15 @@ class FlClashTileService : TileService() {
activityTransfer()
if (GlobalState.runState.value == RunState.STOP) {
GlobalState.runState.value = RunState.PENDING
val titlePlugin = GlobalState.getCurrentTitlePlugin()
if (titlePlugin != null) {
titlePlugin.handleStart()
val tilePlugin = GlobalState.getCurrentTilePlugin()
if (tilePlugin != null) {
tilePlugin.handleStart()
} else {
GlobalState.initServiceEngine(applicationContext)
}
} else if (GlobalState.runState.value == RunState.START) {
GlobalState.runState.value = RunState.PENDING
GlobalState.getCurrentTitlePlugin()?.handleStop()
GlobalState.getCurrentTilePlugin()?.handleStop()
}
}

View File

@@ -1,11 +1,13 @@
package com.follow.clash.services
import android.annotation.SuppressLint
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.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
@@ -15,51 +17,68 @@ import android.os.Parcel
import android.os.RemoteException
import android.util.Log
import androidx.core.app.NotificationCompat
import com.follow.clash.BaseServiceInterface
import com.follow.clash.GlobalState
import com.follow.clash.MainActivity
import com.follow.clash.R
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
class FlClashVpnService : VpnService() {
private val CHANNEL = "FlClash"
@SuppressLint("WrongConstant")
class FlClashVpnService : VpnService(), BaseServiceInterface {
private val notificationId: Int = 1
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()
initServiceEngine()
GlobalState.initServiceEngine(applicationContext)
}
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() {
}
}
}
addDnsServer("172.16.0.2")
setSession("FlClash")
setBlocking(false)
if (Build.VERSION.SDK_INT >= 29) {
@@ -93,15 +111,36 @@ class FlClashVpnService : VpnService() {
)
)
}
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 stop() {
stopSelf()
stopForeground()
fun updateUnderlyingNetworks( networks: Array<Network>){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
this.setUnderlyingNetworks(networks)
}
}
override fun stop() {
stopSelf()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
stopForeground(STOP_FOREGROUND_REMOVE)
}
}
private val CHANNEL = "FlClash"
private val notificationId: Int = 1
private val notificationBuilder: NotificationCompat.Builder by lazy {
val intent = Intent(this, MainActivity::class.java)
@@ -120,6 +159,22 @@ class FlClashVpnService : VpnService() {
PendingIntent.FLAG_UPDATE_CURRENT
)
}
val stopPendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this,
0,
Intent("com.follow.clash.action.STOP"),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this,
0,
Intent("com.follow.clash.action.STOP"),
PendingIntent.FLAG_UPDATE_CURRENT
)
}
with(NotificationCompat.Builder(this, CHANNEL)) {
setSmallIcon(R.drawable.ic_stat_name)
setContentTitle("FlClash")
@@ -133,19 +188,12 @@ class FlClashVpnService : VpnService() {
setShowWhen(false)
setOnlyAlertOnce(true)
setAutoCancel(true)
addAction(0, "Stop", stopPendingIntent);
}
}
fun initServiceEngine() {
GlobalState.initServiceEngine(applicationContext)
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
GlobalState.getCurrentAppPlugin()?.requestGc()
}
fun startForeground(title: String, content: String) {
@SuppressLint("ForegroundServiceType", "WrongConstant")
override fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
var channel = manager?.getNotificationChannel(CHANNEL)
@@ -157,17 +205,16 @@ class FlClashVpnService : VpnService() {
}
val notification =
notificationBuilder.setContentTitle(title).setContentText(content).build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(notificationId, notification)
}
}
private fun stopForeground() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
stopForeground(STOP_FOREGROUND_REMOVE)
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
GlobalState.getCurrentVPNPlugin()?.requestGc()
}
private val binder = LocalBinder()
@@ -180,7 +227,7 @@ class FlClashVpnService : VpnService() {
val isSuccess = super.onTransact(code, data, reply, flags)
if (!isSuccess) {
CoroutineScope(Dispatchers.Main).launch {
GlobalState.getCurrentTitlePlugin()?.handleStop()
GlobalState.getCurrentTilePlugin()?.handleStop()
}
}
return isSuccess
@@ -190,7 +237,6 @@ class FlClashVpnService : VpnService() {
}
}
override fun onBind(intent: Intent): IBinder {
return binder
}

BIN
assets/fonts/Icons.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@@ -3,6 +3,18 @@ package main
import "C"
import (
"context"
"errors"
route "github.com/metacubex/mihomo/hub/route"
"math"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"time"
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/adapter/outboundgroup"
@@ -15,61 +27,18 @@ 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"
"github.com/metacubex/mihomo/tunnel"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"time"
)
//type healthCheckSchema struct {
// Enable bool `provider:"enable"`
// URL string `provider:"url"`
// Interval int `provider:"interval"`
// TestTimeout int `provider:"timeout,omitempty"`
// Lazy bool `provider:"lazy,omitempty"`
// ExpectedStatus string `provider:"expected-status,omitempty"`
//}
//type proxyProviderSchema struct {
// Type string `provider:"type"`
// Path string `provider:"path,omitempty"`
// URL string `provider:"url,omitempty"`
// Proxy string `provider:"proxy,omitempty"`
// Interval int `provider:"interval,omitempty"`
// Filter string `provider:"filter,omitempty"`
// ExcludeFilter string `provider:"exclude-filter,omitempty"`
// ExcludeType string `provider:"exclude-type,omitempty"`
// DialerProxy string `provider:"dialer-proxy,omitempty"`
//
// HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
// Override ap.OverrideSchema `provider:"override,omitempty"`
// Header map[string][]string `provider:"header,omitempty"`
//}
//
//type ruleProviderSchema struct {
// Type string `provider:"type"`
// Behavior string `provider:"behavior"`
// Path string `provider:"path,omitempty"`
// URL string `provider:"url,omitempty"`
// Proxy string `provider:"proxy,omitempty"`
// Format string `provider:"format,omitempty"`
// Interval int `provider:"interval,omitempty"`
//}
type ConfigExtendedParams struct {
IsPatch bool `json:"is-patch"`
IsCompatible bool `json:"is-compatible"`
SelectedMap map[string]string `json:"selected-map"`
TestURL *string `json:"test-url"`
OverrideDns bool `json:"override-dns"`
}
type GenerateConfigParams struct {
@@ -102,6 +71,12 @@ type ExternalProvider struct {
UpdateAt time.Time `json:"update-at"`
}
type ExternalProviders []ExternalProvider
func (a ExternalProviders) Len() int { return len(a) }
func (a ExternalProviders) Less(i, j int) bool { return a[i].Name < a[j].Name }
func (a ExternalProviders) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
var b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
func restartExecutable(execPath string) {
@@ -190,35 +165,67 @@ func getRawConfigWithId(id string) *config.RawConfig {
return prof
}
func getExternalProvidersRaw() map[string]ExternalProvider {
externalProviders := make(map[string]ExternalProvider)
func getExternalProvidersRaw() map[string]cp.Provider {
eps := make(map[string]cp.Provider)
for n, p := range tunnel.Providers() {
if p.VehicleType() != cp.Compatible {
p := p.(*provider.ProxySetProvider)
externalProviders[n] = ExternalProvider{
Name: n,
Type: p.Type().String(),
VehicleType: p.VehicleType().String(),
Count: p.Count(),
Path: p.Vehicle().Path(),
UpdateAt: p.UpdatedAt,
}
eps[n] = p
}
}
for n, p := range tunnel.RuleProviders() {
if p.VehicleType() != cp.Compatible {
p := p.(*rp.RuleSetProvider)
externalProviders[n] = ExternalProvider{
Name: n,
Type: p.Type().String(),
VehicleType: p.VehicleType().String(),
Count: p.Count(),
Path: p.Vehicle().Path(),
UpdateAt: p.UpdatedAt,
}
eps[n] = p
}
}
return externalProviders
return eps
}
func toExternalProvider(p cp.Provider) (*ExternalProvider, error) {
switch p.(type) {
case *provider.ProxySetProvider:
psp := p.(*provider.ProxySetProvider)
return &ExternalProvider{
Name: psp.Name(),
Type: psp.Type().String(),
VehicleType: psp.VehicleType().String(),
Count: psp.Count(),
Path: psp.Vehicle().Path(),
UpdateAt: psp.UpdatedAt(),
}, nil
case *rp.RuleSetProvider:
rsp := p.(*rp.RuleSetProvider)
return &ExternalProvider{
Name: rsp.Name(),
Type: rsp.Type().String(),
VehicleType: rsp.VehicleType().String(),
Count: rsp.Count(),
Path: rsp.Vehicle().Path(),
UpdateAt: rsp.UpdatedAt(),
}, nil
default:
return nil, errors.New("not external provider")
}
}
func sideUpdateExternalProvider(p cp.Provider, bytes []byte) error {
switch p.(type) {
case *provider.ProxySetProvider:
psp := p.(*provider.ProxySetProvider)
elm, same, err := psp.SideUpdate(bytes)
if err == nil && !same {
psp.OnUpdate(elm)
}
return nil
case rp.RuleSetProvider:
rsp := p.(*rp.RuleSetProvider)
elm, same, err := rsp.SideUpdate(bytes)
if err == nil && !same {
rsp.OnUpdate(elm)
}
return nil
default:
return errors.New("not external provider")
}
}
func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig {
@@ -373,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 = ""
@@ -380,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
@@ -398,48 +410,90 @@ 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)
listener.SetAllowLan(general.AllowLan)
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
inbound.SetAllowedIPs(general.LanAllowedIPs)
inbound.SetDisAllowedIPs(general.LanDisAllowedIPs)
listener.SetBindAddress(general.BindAddress)
route.ReStartServer(controller.ExternalController)
tunnel.SetSniffing(general.Sniffing)
tunnel.SetFindProcessMode(general.FindProcessMode)
dialer.SetTcpConcurrent(general.TCPConcurrent)
dialer.DefaultInterface.Store(general.Interface)
adapter.UnifiedDelay.Store(general.UnifiedDelay)
tunnel.SetMode(general.Mode)
log.SetLevel(general.LogLevel)
resolver.DisableIPv6 = !general.IPv6
}
var isRunning = false
var runLock sync.Mutex
func updateListeners(general *config.General, listeners map[string]constant.InboundListener) {
if !isRunning {
return
}
runLock.Lock()
defer runLock.Unlock()
listener.PatchInboundListeners(listeners, tunnel.Tunnel, true)
listener.SetAllowLan(general.AllowLan)
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
inbound.SetAllowedIPs(general.LanAllowedIPs)
inbound.SetDisAllowedIPs(general.LanDisAllowedIPs)
listener.SetBindAddress(general.BindAddress)
listener.ReCreateHTTP(general.Port, tunnel.Tunnel)
listener.ReCreateSocks(general.SocksPort, tunnel.Tunnel)
listener.ReCreateRedir(general.RedirPort, tunnel.Tunnel)
listener.ReCreateAutoRedir(general.EBpf.AutoRedir, tunnel.Tunnel)
listener.ReCreateTProxy(general.TProxyPort, tunnel.Tunnel)
listener.ReCreateTun(general.Tun, tunnel.Tunnel)
listener.ReCreateMixed(general.MixedPort, tunnel.Tunnel)
listener.ReCreateShadowSocks(general.ShadowSocksConfig, tunnel.Tunnel)
listener.ReCreateVmess(general.VmessConfig, tunnel.Tunnel)
listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel)
tunnel.SetMode(general.Mode)
log.SetLevel(general.LogLevel)
listener.ReCreateTun(general.Tun, tunnel.Tunnel)
}
func stopListeners() {
listener.StopListener()
}
func hcCompatibleProvider(proxyProviders map[string]cp.ProxyProvider) {
wg := sync.WaitGroup{}
ch := make(chan struct{}, math.MaxInt)
for _, proxyProvider := range proxyProviders {
proxyProvider := proxyProvider
if proxyProvider.VehicleType() == cp.Compatible {
log.Infoln("Start initial Compatible provider %s", proxyProvider.Name())
wg.Add(1)
ch <- struct{}{}
go func() {
defer func() { <-ch; wg.Done() }()
if err := proxyProvider.Initial(); err != nil {
log.Errorln("initial Compatible provider %s error: %v", proxyProvider.Name(), err)
}
}()
}
}
resolver.DisableIPv6 = !general.IPv6
}
func patchSelectGroup() {
@@ -467,12 +521,8 @@ func patchSelectGroup() {
}
}
var applyLock sync.Mutex
func applyConfig() error {
applyLock.Lock()
defer applyLock.Unlock()
cfg, err := config.ParseRawConfig(currentConfig)
cfg, err := config.ParseRawConfig(currentRawConfig)
if err != nil {
cfg, _ = config.ParseRawConfig(config.DefaultRawConfig())
}
@@ -480,12 +530,17 @@ 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, true)
hub.UltraApplyConfig(cfg)
patchSelectGroup()
}
updateListeners(cfg.General, cfg.Listeners)
if isRunning {
hcCompatibleProvider(cfg.Providers)
}
externalProviders = getExternalProvidersRaw()
return err
}

20
core/dns.go Normal file
View 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()
}()
}

View File

@@ -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
)

View File

@@ -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=

View File

@@ -5,35 +5,56 @@ package main
*/
import "C"
import (
"context"
bridge "core/dart-bridge"
"encoding/json"
"fmt"
"github.com/metacubex/mihomo/common/utils"
"os"
"runtime"
"sort"
"sync"
"time"
"unsafe"
"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"
cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/log"
rp "github.com/metacubex/mihomo/rules/provider"
"github.com/metacubex/mihomo/tunnel"
"github.com/metacubex/mihomo/tunnel/statistic"
"golang.org/x/net/context"
"os"
"runtime"
"time"
"unsafe"
)
var currentConfig = config.DefaultRawConfig()
var currentRawConfig = config.DefaultRawConfig()
var configParams = ConfigExtendedParams{}
var externalProviders = map[string]cp.Provider{}
var isInit = false
//export start
func start() {
runLock.Lock()
defer runLock.Unlock()
isRunning = true
}
//export stop
func stop() {
runLock.Lock()
go func() {
defer runLock.Unlock()
isRunning = false
stopListeners()
}()
}
//export initClash
func initClash(homeDirStr *C.char) bool {
if !isInit {
@@ -57,10 +78,10 @@ func restartClash() bool {
//export shutdownClash
func shutdownClash() bool {
stopListeners()
executor.Shutdown()
runtime.GC()
isInit = false
currentConfig = nil
return true
}
@@ -86,11 +107,15 @@ func validateConfig(s *C.char, port C.longlong) {
}()
}
var updateLock sync.Mutex
//export updateConfig
func updateConfig(s *C.char, port C.longlong) {
i := int64(port)
paramsString := C.GoString(s)
go func() {
updateLock.Lock()
defer updateLock.Unlock()
var params = &GenerateConfigParams{}
err := json.Unmarshal([]byte(paramsString), params)
if err != nil {
@@ -99,7 +124,7 @@ func updateConfig(s *C.char, port C.longlong) {
}
configParams = params.Params
prof := decorationConfig(params.ProfileId, params.Config)
currentConfig = prof
currentRawConfig = prof
err = applyConfig()
if err != nil {
bridge.SendToPort(i, err.Error())
@@ -200,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
}
@@ -311,34 +338,16 @@ func getProvider(name *C.char) *C.char {
//export getExternalProviders
func getExternalProviders() *C.char {
externalProviders := make(map[string]ExternalProvider)
for n, p := range tunnel.Providers() {
if p.VehicleType() != cp.Compatible {
p := p.(*provider.ProxySetProvider)
externalProviders[n] = ExternalProvider{
Name: n,
Type: p.Type().String(),
VehicleType: p.VehicleType().String(),
Count: p.Count(),
Path: p.Vehicle().Path(),
UpdateAt: p.UpdatedAt,
}
eps := make([]ExternalProvider, 0)
for _, p := range externalProviders {
externalProvider, err := toExternalProvider(p)
if err != nil {
continue
}
eps = append(eps, *externalProvider)
}
for n, p := range tunnel.RuleProviders() {
if p.VehicleType() != cp.Compatible {
p := p.(*rp.RuleSetProvider)
externalProviders[n] = ExternalProvider{
Name: n,
Type: p.Type().String(),
VehicleType: p.VehicleType().String(),
Count: p.Count(),
Path: p.Vehicle().Path(),
UpdateAt: p.UpdatedAt,
}
}
}
data, err := json.Marshal(externalProviders)
sort.Sort(ExternalProviders(eps))
data, err := json.Marshal(eps)
if err != nil {
return C.CString("")
}
@@ -348,69 +357,49 @@ func getExternalProviders() *C.char {
//export getExternalProvider
func getExternalProvider(name *C.char) *C.char {
externalProviderName := C.GoString(name)
externalProviders := getExternalProvidersRaw()
externalProvider, exist := externalProviders[externalProviderName]
if !exist {
return C.CString("")
}
data, err := json.Marshal(externalProvider)
e, err := toExternalProvider(externalProvider)
if err != nil {
return C.CString("")
}
data, err := json.Marshal(e)
if err != nil {
return C.CString("")
}
return C.CString(string(data))
}
//export updateExternalProvider
func updateExternalProvider(providerName *C.char, providerType *C.char, port C.longlong) {
//export updateGeoData
func updateGeoData(geoType *C.char, geoName *C.char, port C.longlong) {
i := int64(port)
providerNameString := C.GoString(providerName)
providerTypeString := C.GoString(providerType)
geoTypeString := C.GoString(geoType)
geoNameString := C.GoString(geoName)
go func() {
switch providerTypeString {
case "Proxy":
providers := tunnel.Providers()
proxyProvider, exist := providers[providerNameString].(*provider.ProxySetProvider)
if !exist {
bridge.SendToPort(i, "proxy provider is not exist")
return
}
err := proxyProvider.Update()
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "Rule":
providers := tunnel.RuleProviders()
ruleProvider, exist := providers[providerNameString].(*rp.RuleSetProvider)
if !exist {
bridge.SendToPort(i, "rule provider is not exist")
return
}
err := ruleProvider.Update()
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
path := constant.Path.Resolve(geoNameString)
switch geoTypeString {
case "MMDB":
err := updater.UpdateMMDB(constant.Path.Resolve(providerNameString))
err := updater.UpdateMMDBWithPath(path)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "ASN":
err := updater.UpdateASN(constant.Path.Resolve(providerNameString))
err := updater.UpdateASNWithPath(path)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "GeoIp":
err := updater.UpdateGeoIp(constant.Path.Resolve(providerNameString))
err := updater.UpdateGeoIpWithPath(path)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "GeoSite":
err := updater.UpdateGeoSite(constant.Path.Resolve(providerNameString))
err := updater.UpdateGeoSiteWithPath(path)
if err != nil {
bridge.SendToPort(i, err.Error())
return
@@ -420,65 +409,44 @@ func updateExternalProvider(providerName *C.char, providerType *C.char, port C.l
}()
}
//func sideLoadExternalProvider(providerName *C.char, providerType *C.char, data *C.char, port C.longlong) {
// i := int64(port)
// bytes := []byte(C.GoString(data))
// providerNameString := C.GoString(providerName)
// providerTypeString := C.GoString(providerType)
// go func() {
// switch providerTypeString {
// case "Proxy":
// providers := tunnel.Providers()
// proxyProvider, exist := providers[providerNameString].(*provider.ProxySetProvider)
// if exist {
// bridge.SendToPort(i, "proxy provider is not exist")
// return
// }
// err := proxyProvider.Update()
// if err != nil {
// bridge.SendToPort(i, err.Error())
// return
// }
// case "Rule":
// providers := tunnel.RuleProviders()
// ruleProvider, exist := providers[providerNameString].(*rp.RuleSetProvider)
// if exist {
// bridge.SendToPort(i, "proxy provider is not exist")
// return
// }
// err := ruleProvider.Update()
// if err != nil {
// bridge.SendToPort(i, err.Error())
// return
// }
// case "MMDB":
// err := updater.UpdateMMDB(constant.Path.Resolve(providerNameString))
// if err != nil {
// bridge.SendToPort(i, err.Error())
// return
// }
// case "ASN":
// err := updater.UpdateASN(constant.Path.Resolve(providerNameString))
// if err != nil {
// bridge.SendToPort(i, err.Error())
// return
// }
// case "GeoIp":
// err := updater.UpdateGeoIp(constant.Path.Resolve(providerNameString))
// if err != nil {
// bridge.SendToPort(i, err.Error())
// return
// }
// case "GeoSite":
// err := updater.UpdateGeoSite(constant.Path.Resolve(providerNameString))
// if err != nil {
// bridge.SendToPort(i, err.Error())
// return
// }
// }
// bridge.SendToPort(i, "")
// }()
//}
//export updateExternalProvider
func updateExternalProvider(providerName *C.char, port C.longlong) {
i := int64(port)
providerNameString := C.GoString(providerName)
go func() {
externalProvider, exist := externalProviders[providerNameString]
if !exist {
bridge.SendToPort(i, "external provider is not exist")
return
}
err := externalProvider.Update()
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
bridge.SendToPort(i, "")
}()
}
//export sideLoadExternalProvider
func sideLoadExternalProvider(providerName *C.char, data *C.char, port C.longlong) {
i := int64(port)
bytes := []byte(C.GoString(data))
providerNameString := C.GoString(providerName)
go func() {
externalProvider, exist := externalProviders[providerNameString]
if !exist {
bridge.SendToPort(i, "external provider is not exist")
return
}
err := sideUpdateExternalProvider(externalProvider, bytes)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
bridge.SendToPort(i, "")
}()
}
//export initNativeApiBridge
func initNativeApiBridge(api unsafe.Pointer) {

View File

@@ -14,6 +14,7 @@ type AccessControl struct {
}
type AndroidProps struct {
Enable bool `json:"enable"`
AccessControl *AccessControl `json:"accessControl"`
AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"`

View File

@@ -6,19 +6,20 @@ import "C"
import (
"core/platform"
t "core/tun"
"encoding/json"
"errors"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore"
"github.com/metacubex/mihomo/listener/sing_tun"
"strconv"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/log"
)
var tunLock sync.Mutex
var tun *t.Tun
var runTime *time.Time
type FdMap struct {
@@ -34,38 +35,49 @@ 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
paramsString := C.GoString(s)
if paramsString == "" {
tunLock.Lock()
defer tunLock.Unlock()
now := time.Now()
runTime = &now
SendMessage(Message{
Type: StartedMessage,
Data: strconv.FormatInt(runTime.UnixMilli(), 10),
})
return
}
initSocketHook()
go func() {
tunLock.Lock()
defer tunLock.Unlock()
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()
@@ -88,15 +100,15 @@ func getRunTime() *C.char {
//export stopTun
func stopTun() {
removeSocketHook()
go func() {
tunLock.Lock()
defer tunLock.Unlock()
runTime = nil
if tun != nil {
tun.Close()
tun = nil
if tunListener != nil {
_ = tunListener.Close()
}
}()
}
@@ -123,18 +135,12 @@ func markSocket(fd Fd) {
})
}
var fdCounter int64 = 0
func init() {
func initSocketHook() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() {
return errBlocked
}
return conn.Control(func(fd uintptr) {
if tun == nil {
return
}
fdInt := int64(fd)
timeout := time.After(100 * time.Millisecond)
id := atomic.AddInt64(&fdCounter, 1)
@@ -159,3 +165,7 @@ func init() {
})
}
}
func removeSocketHook() {
dialer.DefaultSocketHook = nil
}

View File

@@ -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()
}

View File

@@ -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: "",
}
}

View File

@@ -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.TunMixed,
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
}

View File

@@ -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
}

View File

@@ -1,10 +1,10 @@
import 'dart:async';
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/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
@@ -27,6 +27,9 @@ runAppWithPreferences(
ChangeNotifierProvider<Config>(
create: (_) => config,
),
ChangeNotifierProvider<AppFlowingState>(
create: (_) => AppFlowingState(),
),
ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>(
create: (_) => appState,
update: (_, config, clashConfig, appState) {
@@ -52,6 +55,7 @@ class Application extends StatefulWidget {
class ApplicationState extends State<Application> {
late SystemColorSchemes systemColorSchemes;
Timer? timer;
final _pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
@@ -80,7 +84,9 @@ class ApplicationState extends State<Application> {
@override
void initState() {
super.initState();
_initTimer();
globalState.appController = AppController(context);
globalState.measure = Measure.of(context);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final currentContext = globalState.navigatorKey.currentContext;
if (currentContext != null) {
@@ -88,25 +94,55 @@ class ApplicationState extends State<Application> {
}
await globalState.appController.init();
globalState.appController.initLink();
_updateGroups();
});
}
_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: 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,
@@ -120,75 +156,73 @@ class ApplicationState extends State<Application> {
});
}
_updateGroups() {
if (globalState.groupsUpdateTimer != null) {
globalState.groupsUpdateTimer?.cancel();
globalState.groupsUpdateTimer = null;
}
globalState.groupsUpdateTimer ??= Timer.periodic(
httpTimeoutDuration,
(timer) async {
await globalState.appController.updateGroupDebounce();
},
);
}
@override
Widget build(context) {
return AppStateContainer(
child: ClashContainer(
child: Selector2<AppState, Config, ApplicationSelectorState>(
selector: (_, appState, config) => ApplicationSelectorState(
locale: config.locale,
themeMode: config.themeMode,
primaryColor: config.primaryColor,
prueBlack: config.prueBlack,
),
builder: (_, state, child) {
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) {
_updateSystemColorSchemes(lightDynamic, darkDynamic);
return MaterialApp(
debugShowCheckedModeBanner: false,
navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate
],
builder: (_, child) {
return _buildApp(child!);
},
scrollBehavior: BaseScrollBehavior(),
title: appName,
locale: other.getLocaleForString(state.locale),
supportedLocales: AppLocalizations.delegate.supportedLocales,
themeMode: state.themeMode,
theme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
return _buildApp(
AppStateManager(
child: ClashManager(
child: Selector2<AppState, Config, ApplicationSelectorState>(
selector: (_, appState, config) => ApplicationSelectorState(
locale: config.locale,
themeMode: config.themeMode,
primaryColor: config.primaryColor,
prueBlack: config.prueBlack,
),
builder: (_, state, child) {
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) {
_updateSystemColorSchemes(lightDynamic, darkDynamic);
return MaterialApp(
navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate
],
builder: (_, child) {
return LayoutBuilder(
builder: (_, container) {
final appController = globalState.appController;
final maxWidth = container.maxWidth;
if (appController.appState.viewWidth != maxWidth) {
globalState.appController.updateViewWidth(maxWidth);
}
return _buildPage(child!);
},
);
},
scrollBehavior: BaseScrollBehavior(),
title: appName,
locale: other.getLocaleForString(state.locale),
supportedLocales:
AppLocalizations.delegate.supportedLocales,
themeMode: state.themeMode,
theme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
),
),
),
darkTheme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
).toPrueBlack(state.prueBlack),
),
home: child,
);
},
);
},
child: const HomePage(),
darkTheme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
).toPrueBlack(state.prueBlack),
),
home: child,
);
},
);
},
child: const HomePage(),
),
),
),
);
@@ -199,5 +233,6 @@ class ApplicationState extends State<Application> {
linkManager.destroy();
await globalState.appController.savePreferences();
super.dispose();
_cancelTimer();
}
}

View File

@@ -140,8 +140,7 @@ class ClashCore {
clashFFI.freeCString(externalProvidersRaw);
return Isolate.run<List<ExternalProvider>>(() {
final externalProviders =
(json.decode(externalProvidersRawString) as Map<String, dynamic>)
.values
(json.decode(externalProvidersRawString) as List<dynamic>)
.map(
(item) => ExternalProvider.fromJson(item),
)
@@ -150,7 +149,7 @@ class ClashCore {
});
}
ExternalProvider getExternalProvider(String externalProviderName) {
ExternalProvider? getExternalProvider(String externalProviderName) {
final externalProviderNameChar =
externalProviderName.toNativeUtf8().cast<Char>();
final externalProviderRaw =
@@ -159,12 +158,37 @@ class ClashCore {
final externalProviderRawString =
externalProviderRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(externalProviderRaw);
if (externalProviderRawString.isEmpty) return null;
return ExternalProvider.fromJson(json.decode(externalProviderRawString));
}
Future<String> updateExternalProvider({
Future<String> updateGeoData({
required String geoType,
required String geoName,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final geoTypeChar = geoType.toNativeUtf8().cast<Char>();
final geoNameChar = geoName.toNativeUtf8().cast<Char>();
clashFFI.updateGeoData(
geoTypeChar,
geoNameChar,
receiver.sendPort.nativePort,
);
malloc.free(geoTypeChar);
malloc.free(geoNameChar);
return completer.future;
}
Future<String> sideLoadExternalProvider({
required String providerName,
required String providerType,
required String data,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
@@ -175,14 +199,34 @@ class ClashCore {
}
});
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
final providerTypeChar = providerType.toNativeUtf8().cast<Char>();
clashFFI.updateExternalProvider(
final dataChar = data.toNativeUtf8().cast<Char>();
clashFFI.sideLoadExternalProvider(
providerNameChar,
dataChar,
receiver.sendPort.nativePort,
);
malloc.free(providerNameChar);
malloc.free(dataChar);
return completer.future;
}
Future<String> updateExternalProvider({
required String providerName,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
clashFFI.updateExternalProvider(
providerNameChar,
providerTypeChar,
receiver.sendPort.nativePort,
);
malloc.free(providerNameChar);
malloc.free(providerTypeChar);
return completer.future;
}
@@ -193,6 +237,14 @@ class ClashCore {
malloc.free(paramsChar);
}
start() {
clashFFI.start();
}
stop() {
clashFFI.stop();
}
Future<Delay> getDelay(String proxyName) {
final delayParams = {
"proxy-name": proxyName,
@@ -270,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() {

View File

@@ -5144,6 +5144,22 @@ class ClashFFI {
late final __FCmulcr =
__FCmulcrPtr.asFunction<_Fcomplex Function(_Fcomplex, double)>();
void start() {
return _start();
}
late final _startPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('start');
late final _start = _startPtr.asFunction<void Function()>();
void stop() {
return _stop();
}
late final _stopPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stop');
late final _stop = _stopPtr.asFunction<void Function()>();
int initClash(
ffi.Pointer<ffi.Char> homeDirStr,
) {
@@ -5248,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,
) {
@@ -5400,24 +5430,61 @@ class ClashFFI {
late final _getExternalProvider = _getExternalProviderPtr
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
void updateGeoData(
ffi.Pointer<ffi.Char> geoType,
ffi.Pointer<ffi.Char> geoName,
int port,
) {
return _updateGeoData(
geoType,
geoName,
port,
);
}
late final _updateGeoDataPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.LongLong)>>('updateGeoData');
late final _updateGeoData = _updateGeoDataPtr.asFunction<
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
void updateExternalProvider(
ffi.Pointer<ffi.Char> providerName,
ffi.Pointer<ffi.Char> providerType,
int port,
) {
return _updateExternalProvider(
providerName,
providerType,
port,
);
}
late final _updateExternalProviderPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('updateExternalProvider');
late final _updateExternalProvider = _updateExternalProviderPtr
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
void sideLoadExternalProvider(
ffi.Pointer<ffi.Char> providerName,
ffi.Pointer<ffi.Char> data,
int port,
) {
return _sideLoadExternalProvider(
providerName,
data,
port,
);
}
late final _sideLoadExternalProviderPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.LongLong)>>('updateExternalProvider');
late final _updateExternalProvider = _updateExternalProviderPtr.asFunction<
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
ffi.LongLong)>>('sideLoadExternalProvider');
late final _sideLoadExternalProvider =
_sideLoadExternalProviderPtr.asFunction<
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
void initNativeApiBridge(
ffi.Pointer<ffi.Void> api,
@@ -5514,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();

View File

@@ -23,6 +23,9 @@ export 'app_localizations.dart';
export 'function.dart';
export 'package.dart';
export 'measure.dart';
export 'service.dart';
export 'windows.dart';
export 'iterable.dart';
export 'scroll.dart';
export 'scroll.dart';
export 'icons.dart';
export 'http.dart';
export 'keyboard.dart';

View File

@@ -17,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",

View File

@@ -11,7 +11,7 @@ extension BuildContextExtension on BuildContext {
return MediaQuery.of(this).size;
}
double get width {
double get viewWidth {
return appSize.width;
}

View File

@@ -1,17 +1,14 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
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:path/path.dart';
import 'package:webdav_client/webdav_client.dart';
class DAVClient {
late Client client;
Completer<bool> pingCompleter = Completer();
late String fileName;
DAVClient(DAV dav) {
client = newClient(
@@ -19,6 +16,7 @@ class DAVClient {
user: dav.user,
password: dav.password,
);
fileName = dav.fileName;
client.setHeaders(
{
'accept-charset': 'utf-8',
@@ -34,8 +32,6 @@ class DAVClient {
Future<bool> _ping() async {
try {
await client.ping();
await client.mkdir("/$appName");
await client.mkdir("/$appName/$profilesDirectoryName");
return true;
} catch (_) {
return false;
@@ -44,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");
@@ -53,6 +49,7 @@ class DAVClient {
}
Future<List<int>> recovery() async {
await client.mkdir("$root");
final data = await client.read(backupFile);
return data;
}

25
lib/common/http.dart Normal file
View File

@@ -0,0 +1,25 @@
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 appController = globalState.appController;
final port = appController.clashConfig.mixedPort;
final isStart = appController.appFlowingState.isStart;
if (!isStart) return "DIRECT";
if (appController.appState.groups.isEmpty) {
return "DIRECT";
}
return "PROXY localhost:$port";
};
return client;
}
}

6
lib/common/icons.dart Normal file
View File

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

View File

@@ -62,6 +62,6 @@ extension DoubleListExt on List<double> {
}
}
return -1; // 这行理论上不会执行到,但为了完整性保留
return -1;
}
}

106
lib/common/keyboard.dart Normal file
View 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';
}
}

View File

@@ -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;
@@ -24,18 +26,77 @@ class AutoLaunch {
return await launchAtStartup.isEnabled();
}
Future<bool> get windowsIsEnable async {
final res = await Process.run(
'schtasks',
['/Query', '/TN', appName, '/V', "/FO", "LIST"],
runInShell: true,
);
return res.stdout.toString().contains(Platform.resolvedExecutable);
}
Future<bool> enable() async {
if (Platform.isWindows) {
await windowsDisable();
}
return await launchAtStartup.enable();
}
windowsDisable() async {
final res = await Process.run(
'schtasks',
[
'/Delete',
'/TN',
appName,
'/F',
],
runInShell: true,
);
return res.exitCode == 0;
}
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 isEnable = await this.isEnable;
if (isEnable == value) return;
if (value == true) {
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 (await isEnable == isAutoLaunch) return;
if (isAutoLaunch == true) {
enable();
} else {
disable();

View File

@@ -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;
}
}

View File

@@ -3,23 +3,23 @@ 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 = TextScaler.linear(
WidgetsBinding.instance.platformDispatcher.textScaleFactor);
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;

View File

@@ -1,12 +1,12 @@
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'dart:typed_data';
import 'package:fl_clash/common/app_localizations.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/constant.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;
@@ -84,7 +84,7 @@ class Other {
if (charA == charB) {
return sortByChar(a.substring(1), b.substring(1));
} else {
return charA.compareTo(charB);
return charA.compareToLower(charB);
}
}
@@ -100,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) {
@@ -131,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;
@@ -192,27 +204,28 @@ class Other {
return ViewMode.desktop;
}
int getColumns(ViewMode viewMode, int currentColumns) {
final targetColumnsArray = viewModeColumnsMap[viewMode]!;
if (targetColumnsArray.contains(currentColumns)) {
return currentColumns;
}
return targetColumnsArray.first;
}
String getColumnsTextForInt(int number){
return switch(number){
1 => appLocalizations.oneColumn,
2 => appLocalizations.twoColumns,
3 => appLocalizations.threeColumns,
4 => appLocalizations.fourColumns,
int() => throw UnimplementedError(),
int getProxiesColumns(double viewWidth, ProxiesLayout proxiesLayout) {
final columns = max((viewWidth / 300).ceil(), 2);
return switch (proxiesLayout) {
ProxiesLayout.tight => columns + 1,
ProxiesLayout.standard => columns,
ProxiesLayout.loose => columns - 1,
};
}
String getBackupFileName(){
int getProfilesColumns(double viewWidth) {
return max((viewWidth / 400).floor(), 1);
}
String getBackupFileName() {
return "${appName}_backup_${DateTime.now().show}.zip";
}
Size getScreenSize() {
final view = WidgetsBinding.instance.platformDispatcher.views.first;
return view.physicalSize / view.devicePixelRatio;
}
}
final other = Other();

View File

@@ -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;
}

View File

@@ -1,37 +1,4 @@
import 'package:fl_clash/common/datetime.dart';
import 'package:fl_clash/plugins/proxy.dart';
import 'package:proxy/proxy.dart' as proxy_plugin;
import 'package:proxy/proxy_platform_interface.dart';
import 'package:fl_clash/common/system.dart';
import 'package:proxy/proxy.dart';
class ProxyManager {
static ProxyManager? _instance;
late ProxyPlatform _proxy;
ProxyManager._internal() {
_proxy = proxy ?? proxy_plugin.Proxy();
}
bool get isStart => startTime != null && startTime!.isBeforeNow;
DateTime? get startTime => _proxy.startTime;
Future<bool?> startProxy({required int port}) async {
return await _proxy.startProxy(port);
}
Future<bool?> stopProxy() async {
return await _proxy.stopProxy();
}
Future<DateTime?> updateStartTime() async {
if (_proxy is! Proxy) return null;
return await (_proxy as Proxy).updateStartTime();
}
factory ProxyManager() {
_instance ??= ProxyManager._internal();
return _instance!;
}
}
final proxyManager = ProxyManager();
final proxy = system.isDesktop ? Proxy() : null;

View File

@@ -1,66 +1,57 @@
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.options = BaseOptions(
headers: {"User-Agent": globalState.appController.clashConfig.globalUa},
);
_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(
url,
options: Options(
headers: {
"User-Agent": globalState.appController.clashConfig.globalUa
},
responseType: ResponseType.bytes,
),
)
.timeout(
httpTimeoutDuration * 2,
httpTimeoutDuration * 6,
);
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",
@@ -86,10 +77,13 @@ class Request {
};
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
for (final source in _ipInfoSources.entries) {
for (final source in _ipInfoSources.entries.toList()..shuffle(Random())) {
try {
final response = await _dio
.get<Map<String, dynamic>>(source.key, cancelToken: cancelToken)
.get<Map<String, dynamic>>(
source.key,
cancelToken: cancelToken,
)
.timeout(
httpTimeoutDuration,
);
@@ -97,6 +91,9 @@ class Request {
return source.value(response.data!);
}
} catch (e) {
if (cancelToken?.isCancelled == true) {
throw "cancelled";
}
continue;
}
}

View File

@@ -1,110 +0,0 @@
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';
typedef CreateServiceNative = IntPtr Function(
IntPtr hSCManager,
Pointer<Utf16> lpServiceName,
Pointer<Utf16> lpDisplayName,
Uint32 dwDesiredAccess,
Uint32 dwServiceType,
Uint32 dwStartType,
Uint32 dwErrorControl,
Pointer<Utf16> lpBinaryPathName,
Pointer<Utf16> lpLoadOrderGroup,
Pointer<Uint32> lpdwTagId,
Pointer<Utf16> lpDependencies,
Pointer<Utf16> lpServiceStartName,
Pointer<Utf16> lpPassword,
);
typedef CreateServiceDart = int Function(
int hSCManager,
Pointer<Utf16> lpServiceName,
Pointer<Utf16> lpDisplayName,
int dwDesiredAccess,
int dwServiceType,
int dwStartType,
int dwErrorControl,
Pointer<Utf16> lpBinaryPathName,
Pointer<Utf16> lpLoadOrderGroup,
Pointer<Uint32> lpdwTagId,
Pointer<Utf16> lpDependencies,
Pointer<Utf16> lpServiceStartName,
Pointer<Utf16> lpPassword,
);
const _SERVICE_ALL_ACCESS = 0xF003F;
const _SERVICE_WIN32_OWN_PROCESS = 0x00000010;
const _SERVICE_AUTO_START = 0x00000002;
const _SERVICE_ERROR_NORMAL = 0x00000001;
typedef GetLastErrorNative = Uint32 Function();
typedef GetLastErrorDart = int Function();
class Service {
static Service? _instance;
late DynamicLibrary _advapi32;
Service._internal() {
_advapi32 = DynamicLibrary.open('advapi32.dll');
}
factory Service() {
_instance ??= Service._internal();
return _instance!;
}
Future<void> createService() async {
final int scManager = OpenSCManager(nullptr, nullptr, _SERVICE_ALL_ACCESS);
if (scManager == 0) return;
final serviceName = 'FlClash Service'.toNativeUtf16();
final displayName = 'FlClash Service'.toNativeUtf16();
final binaryPathName = "C:\\Application\\Clash.Verge_1.6.6_x64_portable\\resources\\clash-verge-service.exe".toNativeUtf16();
final createService =
_advapi32.lookupFunction<CreateServiceNative, CreateServiceDart>(
'CreateServiceW',
);
final getLastError = DynamicLibrary.open('kernel32.dll')
.lookupFunction<GetLastErrorNative, GetLastErrorDart>('GetLastError');
final serviceHandle = createService(
scManager,
serviceName,
displayName,
_SERVICE_ALL_ACCESS,
_SERVICE_WIN32_OWN_PROCESS,
_SERVICE_AUTO_START,
_SERVICE_ERROR_NORMAL,
binaryPathName,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
);
print("serviceHandle $serviceHandle");
final errorCode = GetLastError();
print('Error code: $errorCode');
final result = StartService(serviceHandle, 0, nullptr);
if (result == 0) {
print('Failed to start the service.');
} else {
print('Service started successfully.');
}
calloc.free(serviceName);
calloc.free(displayName);
calloc.free(binaryPathName);
}
}
final service = Platform.isWindows ? Service() : null;

View File

@@ -2,4 +2,10 @@ extension StringExtension on String {
bool get isUrl {
return RegExp(r'^(http|https|ftp)://').hasMatch(this);
}
int compareToLower(String other) {
return toLowerCase().compareTo(
other.toLowerCase(),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:flutter/services.dart';
@@ -18,6 +19,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();

16
lib/common/window.dart Normal file → Executable file
View 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,9 +28,9 @@ 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);
});
@@ -41,6 +39,11 @@ class Window {
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);
}
}

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

@@ -0,0 +1,59 @@
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
class Windows {
static Windows? _instance;
late DynamicLibrary _shell32;
Windows._internal() {
_shell32 = DynamicLibrary.open('shell32.dll');
}
factory Windows() {
_instance ??= Windows._internal();
return _instance!;
}
bool runas(String command, String arguments) {
final commandPtr = command.toNativeUtf16();
final argumentsPtr = arguments.toNativeUtf16();
final operationPtr = 'runas'.toNativeUtf16();
final shellExecute = _shell32.lookupFunction<
Int32 Function(
Pointer<Utf16> hwnd,
Pointer<Utf16> lpOperation,
Pointer<Utf16> lpFile,
Pointer<Utf16> lpParameters,
Pointer<Utf16> lpDirectory,
Int32 nShowCmd),
int Function(
Pointer<Utf16> hwnd,
Pointer<Utf16> lpOperation,
Pointer<Utf16> lpFile,
Pointer<Utf16> lpParameters,
Pointer<Utf16> lpDirectory,
int nShowCmd)>('ShellExecuteW');
final result = shellExecute(
nullptr,
operationPtr,
commandPtr,
argumentsPtr,
nullptr,
1,
);
calloc.free(commandPtr);
calloc.free(argumentsPtr);
calloc.free(operationPtr);
if (result <= 32) {
return false;
}
return true;
}
}
final windows = Platform.isWindows ? Windows() : null;

View File

@@ -19,33 +19,36 @@ import 'common/common.dart';
class AppController {
final BuildContext context;
late AppState appState;
late AppFlowingState appFlowingState;
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>();
config = context.read<Config>();
clashConfig = context.read<ClashConfig>();
appFlowingState = context.read<AppFlowingState>();
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);
}
Future<void> updateSystemProxy(bool isStart) async {
updateStatus(bool isStart) async {
if (isStart) {
await globalState.startSystemProxy(
appState: appState,
await globalState.handleStart(
config: config,
clashConfig: clashConfig,
);
@@ -55,14 +58,13 @@ class AppController {
updateRunTime,
updateTraffic,
];
if (Platform.isAndroid) return;
await applyProfile(isPrue: true);
applyProfileDebounce();
} else {
await globalState.stopSystemProxy();
await globalState.handleStop();
clashCore.resetTraffic();
appState.traffics = [];
appState.totalTraffic = Traffic();
appState.runTime = null;
appFlowingState.traffics = [];
appFlowingState.totalTraffic = Traffic();
appFlowingState.runTime = null;
addCheckIpNumDebounce();
}
}
@@ -72,18 +74,19 @@ class AppController {
}
updateRunTime() {
if (proxyManager.startTime != null) {
final startTimeStamp = proxyManager.startTime!.millisecondsSinceEpoch;
final startTime = globalState.startTime;
if (startTime != null) {
final startTimeStamp = startTime.millisecondsSinceEpoch;
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
appState.runTime = nowTimeStamp - startTimeStamp;
appFlowingState.runTime = nowTimeStamp - startTimeStamp;
} else {
appState.runTime = null;
appFlowingState.runTime = null;
}
}
updateTraffic() {
globalState.updateTraffic(
appState: appState,
appFlowingState: appFlowingState,
);
}
@@ -101,7 +104,8 @@ class AppController {
final updateId = config.profiles.first.id;
changeProfile(updateId);
} else {
updateSystemProxy(false);
changeProfile(null);
updateStatus(false);
}
}
}
@@ -161,7 +165,7 @@ class AppController {
try {
updateProfile(profile);
} catch (e) {
appState.addLog(
appFlowingState.addLog(
Log(
logLevel: LogLevel.info,
payload: e.toString(),
@@ -227,7 +231,8 @@ class AppController {
}
handleExit() async {
await updateSystemProxy(false);
await updateStatus(false);
await proxy?.stopProxy();
await savePreferences();
clashCore.shutdown();
system.exit();
@@ -238,7 +243,7 @@ class AppController {
clashCore.startLog();
} else {
clashCore.stopLog();
appState.logs = [];
appFlowingState.logs = [];
}
}
@@ -292,15 +297,21 @@ class AppController {
}
init() async {
final isDisclaimerAccepted = await handlerDisclaimer();
if (!isDisclaimerAccepted) {
system.exit();
}
updateLogStatus();
if (!config.silentLaunch) {
window?.show();
}
await proxyManager.updateStartTime();
if (proxyManager.isStart) {
await updateSystemProxy(true);
if (Platform.isAndroid) {
globalState.updateStartTime();
}
if (globalState.isStart) {
await updateStatus(true);
} else {
await updateSystemProxy(config.autoRun);
await updateStatus(config.autoRun);
}
autoUpdateProfiles();
autoCheckUpdate();
@@ -364,6 +375,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);
@@ -413,8 +469,6 @@ class AppController {
addProfileFormURL(url);
}
int get columns => other.getColumns(appState.viewMode, config.proxiesColumns);
updateViewWidth(double width) {
WidgetsBinding.instance.addPostFrameCallback((_) {
appState.viewWidth = width;
@@ -424,7 +478,10 @@ class AppController {
List<Proxy> _sortOfName(List<Proxy> proxies) {
return List.of(proxies)
..sort(
(a, b) => other.sortByChar(a.name, b.name),
(a, b) => other.sortByChar(
other.getPinyin(a.name),
other.getPinyin(b.name),
),
);
}
@@ -478,6 +535,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(!appFlowingState.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,

View File

@@ -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 }
@@ -52,6 +56,8 @@ enum TunStack { gvisor, system, mixed }
enum AccessControlMode { acceptSelected, rejectSelected }
enum AccessSortType { none, name, time }
enum ProfileType { file, url }
enum ResultType { success, error }
@@ -84,4 +90,65 @@ enum CommonCardType { plain, filled }
enum ProxiesType { tab, list }
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,
}

View File

@@ -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),

View File

@@ -1,4 +1,5 @@
import 'package:collection/collection.dart';
import 'dart:convert';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
@@ -6,15 +7,9 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
extension AccessControlExtension on AccessControl {
List<String> get currentList => switch (mode) {
AccessControlMode.acceptSelected => acceptList,
AccessControlMode.rejectSelected => rejectList,
};
}
class AccessFragment extends StatefulWidget {
const AccessFragment({super.key});
@@ -23,9 +18,13 @@ class AccessFragment extends StatefulWidget {
}
class _AccessFragmentState extends State<AccessFragment> {
List<String> acceptList = [];
List<String> rejectList = [];
@override
void initState() {
super.initState();
_updateInitList();
WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appController.appState;
if (appState.packages.isEmpty) {
@@ -36,297 +35,83 @@ class _AccessFragmentState extends State<AccessFragment> {
});
}
Widget _buildAppProxyModePopup() {
final items = [
CommonPopupMenuItem(
action: AccessControlMode.rejectSelected,
label: appLocalizations.blacklistMode,
),
CommonPopupMenuItem(
action: AccessControlMode.acceptSelected,
label: appLocalizations.whitelistMode,
),
];
return Selector<Config, AccessControlMode>(
selector: (_, config) => config.accessControl.mode,
builder: (context, mode, __) {
return CommonPopupMenu<AccessControlMode>.radio(
icon: Icon(
Icons.mode_standby,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
items: items,
onSelected: (value) {
final config = context.read<Config>();
config.accessControl = config.accessControl.copyWith(
mode: value,
);
},
selectedValue: mode,
);
},
);
_updateInitList() {
final accessControl = globalState.appController.config.accessControl;
acceptList = accessControl.acceptList;
rejectList = accessControl.rejectList;
}
Widget _buildFilterSystemAppButton() {
return Selector<Config, bool>(
selector: (_, config) => config.accessControl.isFilterSystemApp,
builder: (context, isFilterSystemApp, __) {
final tooltip = isFilterSystemApp
? appLocalizations.cancelFilterSystemApp
: appLocalizations.filterSystemApp;
return IconButton(
tooltip: tooltip,
onPressed: () {
final config = context.read<Config>();
config.accessControl = config.accessControl.copyWith(
isFilterSystemApp: !isFilterSystemApp,
);
},
icon: isFilterSystemApp
? const Icon(Icons.filter_list_off)
: const Icon(Icons.filter_list),
);
},
);
}
Widget _buildSearchButton(List<Package> packages) {
Widget _buildSearchButton() {
return IconButton(
tooltip: appLocalizations.search,
onPressed: () {
showSearch(
context: context,
delegate: AccessControlSearchDelegate(
packages: packages,
acceptList: acceptList,
rejectList: rejectList,
),
).then((_) => {setState(() {})});
).then((_) => setState(() {
_updateInitList();
}));
},
icon: const Icon(Icons.search),
);
}
// Widget _buildSelectedAllButton({
// required bool isAccessControl,
// required bool isSelectedAll,
// required List<String> allValueList,
// }) {
// final tooltip = isSelectedAll
// ? appLocalizations.cancelSelectAll
// : appLocalizations.selectAll;
// return AbsorbPointer(
// absorbing: !isAccessControl,
// child: FloatingActionButton(
// tooltip: tooltip,
// onPressed: () {
// final config = globalState.appController.config;
// final isAccept =
// config.accessControl.mode == AccessControlMode.acceptSelected;
//
// if (isSelectedAll) {
// config.accessControl = switch (isAccept) {
// true => config.accessControl.copyWith(
// acceptList: [],
// ),
// false => config.accessControl.copyWith(
// rejectList: [],
// ),
// };
// } else {
// config.accessControl = switch (isAccept) {
// true => config.accessControl.copyWith(
// acceptList: allValueList,
// ),
// false => config.accessControl.copyWith(
// rejectList: allValueList,
// ),
// };
// }
// },
// child: isSelectedAll
// ? const Icon(Icons.deselect)
// : const Icon(Icons.select_all),
// ),
// );
// }
Widget _buildPackageList() {
return Selector<AppState, List<Package>>(
selector: (_, appState) => appState.packages,
builder: (_, packages, ___) {
final accessControl = globalState.appController.config.accessControl;
final acceptList = accessControl.acceptList;
final rejectList = accessControl.rejectList;
final acceptPackages = packages.sorted((a, b) {
final isSelectA = acceptList.contains(a.packageName);
final isSelectB = acceptList.contains(b.packageName);
if (isSelectA && isSelectB) return 0;
if (isSelectA) return -1;
if (isSelectB) return 1;
return 0;
});
final rejectPackages = packages.sorted((a, b) {
final isSelectA = rejectList.contains(a.packageName);
final isSelectB = rejectList.contains(b.packageName);
if (isSelectA && isSelectB) return 0;
if (isSelectA) return -1;
if (isSelectB) return 1;
return 0;
});
return Selector<Config, PackageListSelectorState>(
selector: (_, config) => PackageListSelectorState(
accessControl: config.accessControl,
isAccessControl: config.isAccessControl,
),
builder: (context, state, __) {
final accessControl = state.accessControl;
final isAccessControl = state.isAccessControl;
final isFilterSystemApp = accessControl.isFilterSystemApp;
final accessControlMode = accessControl.mode;
final packages =
accessControlMode == AccessControlMode.acceptSelected
? acceptPackages
: rejectPackages;
final currentList = accessControl.currentList;
final currentPackages = isFilterSystemApp
? packages
.where((element) => element.isSystem == false)
.toList()
: packages;
final packageNameList =
currentPackages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList);
final describe =
accessControlMode == AccessControlMode.acceptSelected
? appLocalizations.accessControlAllowDesc
: appLocalizations.accessControlNotAllowDesc;
return DisabledMask(
status: !isAccessControl,
child: Column(
children: [
AbsorbPointer(
absorbing: !isAccessControl,
child: Padding(
padding: const EdgeInsets.only(
top: 4,
bottom: 4,
left: 16,
right: 8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: IntrinsicHeight(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Row(
children: [
Flexible(
child: Text(
appLocalizations.selected,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
const Flexible(
child: SizedBox(
width: 8,
),
),
Flexible(
child: Text(
"${valueList.length}",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
],
),
),
Flexible(
child: Text(describe),
)
],
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Flexible(
child: _buildSearchButton(currentPackages)),
Flexible(child: _buildFilterSystemAppButton()),
Flexible(child: _buildAppProxyModePopup()),
],
),
],
),
),
),
Expanded(
flex: 1,
child: currentPackages.isEmpty
? const Center(
child: CircularProgressIndicator(),
)
: ListView.builder(
itemCount: currentPackages.length,
itemBuilder: (_, index) {
final package = currentPackages[index];
return PackageListItem(
key: Key(package.packageName),
package: package,
value: valueList.contains(package.packageName),
isActive: isAccessControl,
onChanged: (value) {
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
final config =
globalState.appController.config;
if (accessControlMode ==
AccessControlMode.acceptSelected) {
config.accessControl =
config.accessControl.copyWith(
acceptList: valueList,
);
} else {
config.accessControl =
config.accessControl.copyWith(
rejectList: valueList,
);
}
},
);
},
),
),
],
Widget _buildSelectedAllButton({
required bool isSelectedAll,
required List<String> allValueList,
}) {
final tooltip = isSelectedAll
? appLocalizations.cancelSelectAll
: appLocalizations.selectAll;
return IconButton(
tooltip: tooltip,
onPressed: () {
final config = globalState.appController.config;
final isAccept =
config.accessControl.mode == AccessControlMode.acceptSelected;
if (isSelectedAll) {
config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith(
acceptList: [],
),
false => config.accessControl.copyWith(
rejectList: [],
),
};
} else {
config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith(
acceptList: allValueList,
),
false => config.accessControl.copyWith(
rejectList: allValueList,
),
};
}
},
icon: isSelectedAll
? const Icon(Icons.deselect)
: const Icon(Icons.select_all),
);
}
Widget _buildSettingButton() {
return IconButton(
onPressed: () {
showSheet(
title: appLocalizations.proxiesSetting,
context: context,
builder: (_) {
return AccessControlWidget(
context: context,
);
},
);
},
icon: const Icon(Icons.tune),
);
}
@@ -363,7 +148,170 @@ class _AccessFragmentState extends State<AccessFragment> {
],
);
},
child: _buildPackageList(),
child: Selector<AppState, List<Package>>(
selector: (_, appState) => appState.packages,
builder: (_, packages, ___) {
return Selector2<AppState, Config, PackageListSelectorState>(
selector: (_, appState, config) => PackageListSelectorState(
accessControl: config.accessControl,
isAccessControl: config.isAccessControl,
packages: appState.packages,
),
builder: (context, state, __) {
final accessControl = state.accessControl;
final isAccessControl = state.isAccessControl;
final accessControlMode = accessControl.mode;
final packages = state.getList(
accessControlMode == AccessControlMode.acceptSelected
? acceptList
: rejectList,
);
final currentList = accessControl.currentList;
final packageNameList =
packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList);
final describe =
accessControlMode == AccessControlMode.acceptSelected
? appLocalizations.accessControlAllowDesc
: appLocalizations.accessControlNotAllowDesc;
return DisabledMask(
status: !isAccessControl,
child: Column(
children: [
AbsorbPointer(
absorbing: !isAccessControl,
child: Padding(
padding: const EdgeInsets.only(
top: 4,
bottom: 4,
left: 16,
right: 8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: IntrinsicHeight(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Row(
children: [
Flexible(
child: Text(
appLocalizations.selected,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
const Flexible(
child: SizedBox(
width: 8,
),
),
Flexible(
child: Text(
"${valueList.length}",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
],
),
),
Flexible(
child: Text(describe),
)
],
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Flexible(
child: _buildSearchButton(),
),
Flexible(
child: _buildSelectedAllButton(
isSelectedAll: valueList.length ==
packageNameList.length,
allValueList: packageNameList,
),
),
Flexible(
child: _buildSettingButton(),
),
],
),
],
),
),
),
Expanded(
flex: 1,
child: packages.isEmpty
? const Center(
child: CircularProgressIndicator(),
)
: ListView.builder(
itemCount: packages.length,
itemBuilder: (_, index) {
final package = packages[index];
return PackageListItem(
key: Key(package.packageName),
package: package,
value:
valueList.contains(package.packageName),
isActive: isAccessControl,
onChanged: (value) {
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
final config =
globalState.appController.config;
if (accessControlMode ==
AccessControlMode.acceptSelected) {
config.accessControl =
config.accessControl.copyWith(
acceptList: valueList,
);
} else {
config.accessControl =
config.accessControl.copyWith(
rejectList: valueList,
);
}
},
);
},
),
),
],
),
);
},
);
},
),
);
}
}
@@ -430,23 +378,14 @@ class PackageListItem extends StatelessWidget {
}
class AccessControlSearchDelegate extends SearchDelegate {
final List<Package> packages;
List<String> acceptList = [];
List<String> rejectList = [];
AccessControlSearchDelegate({
required this.packages,
required this.acceptList,
required this.rejectList,
});
List<Package> get _results {
final lowQuery = query.toLowerCase();
return packages
.where(
(package) =>
package.label.toLowerCase().contains(lowQuery) ||
package.packageName.contains(lowQuery),
)
.toList();
}
@override
List<Widget>? buildActions(BuildContext context) {
return [
@@ -476,26 +415,39 @@ class AccessControlSearchDelegate extends SearchDelegate {
);
}
Widget _packageList(List<Package> packages) {
return Selector<Config, PackageListSelectorState>(
selector: (_, config) => PackageListSelectorState(
Widget _packageList() {
final lowQuery = query.toLowerCase();
return Selector2<AppState, Config, PackageListSelectorState>(
selector: (_, appState, config) => PackageListSelectorState(
packages: appState.packages,
accessControl: config.accessControl,
isAccessControl: config.isAccessControl,
),
builder: (context, state, __) {
final accessControl = state.accessControl;
final isAccessControl = state.isAccessControl;
final accessControlMode = accessControl.mode;
final packages = state.getList(
accessControlMode == AccessControlMode.acceptSelected
? acceptList
: rejectList,
);
final queryPackages = packages
.where(
(package) =>
package.label.toLowerCase().contains(lowQuery) ||
package.packageName.contains(lowQuery),
)
.toList();
final isAccessControl = state.isAccessControl;
final currentList = accessControl.currentList;
final packageNameList =
this.packages.map((e) => e.packageName).toList();
final packageNameList = packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList);
return DisabledMask(
status: !isAccessControl,
child: ListView.builder(
itemCount: packages.length,
itemCount: queryPackages.length,
itemBuilder: (_, index) {
final package = packages[index];
final package = queryPackages[index];
return PackageListItem(
key: Key(package.packageName),
package: package,
@@ -533,6 +485,268 @@ class AccessControlSearchDelegate extends SearchDelegate {
@override
Widget buildSuggestions(BuildContext context) {
return _packageList(_results);
return _packageList();
}
}
class AccessControlWidget extends StatelessWidget {
final BuildContext context;
const AccessControlWidget({
super.key,
required this.context,
});
IconData _getIconWithAccessControlMode(AccessControlMode mode) {
return switch (mode) {
AccessControlMode.acceptSelected => Icons.adjust_outlined,
AccessControlMode.rejectSelected => Icons.block_outlined,
};
}
String _getTextWithAccessControlMode(AccessControlMode mode) {
return switch (mode) {
AccessControlMode.acceptSelected => appLocalizations.whitelistMode,
AccessControlMode.rejectSelected => appLocalizations.blacklistMode,
};
}
String _getTextWithAccessSortType(AccessSortType type) {
return switch (type) {
AccessSortType.none => appLocalizations.defaultText,
AccessSortType.name => appLocalizations.name,
AccessSortType.time => appLocalizations.time,
};
}
IconData _getIconWithProxiesSortType(AccessSortType type) {
return switch (type) {
AccessSortType.none => Icons.sort,
AccessSortType.name => Icons.sort_by_alpha,
AccessSortType.time => Icons.timeline,
};
}
String _getTextWithIsFilterSystemApp(bool isFilterSystemApp) {
return switch (isFilterSystemApp) {
true => appLocalizations.onlyOtherApps,
false => appLocalizations.allApps,
};
}
List<Widget> _buildModeSetting() {
return generateSection(
title: appLocalizations.mode,
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, AccessControlMode>(
selector: (_, config) => config.accessControl.mode,
builder: (_, accessControlMode, __) {
return Wrap(
spacing: 16,
children: [
for (final item in AccessControlMode.values)
SettingInfoCard(
Info(
label: _getTextWithAccessControlMode(item),
iconData: _getIconWithAccessControlMode(item),
),
isSelected: accessControlMode == item,
onPressed: () {
final config = globalState.appController.config;
config.accessControl = config.accessControl.copyWith(
mode: item,
);
},
)
],
);
},
),
)
],
);
}
List<Widget> _buildSortSetting() {
return generateSection(
title: appLocalizations.sort,
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, AccessSortType>(
selector: (_, config) => config.accessControl.sort,
builder: (_, accessSortType, __) {
return Wrap(
spacing: 16,
children: [
for (final item in AccessSortType.values)
SettingInfoCard(
Info(
label: _getTextWithAccessSortType(item),
iconData: _getIconWithProxiesSortType(item),
),
isSelected: accessSortType == item,
onPressed: () {
final config = globalState.appController.config;
config.accessControl = config.accessControl.copyWith(
sort: item,
);
},
),
],
);
},
),
),
],
);
}
List<Widget> _buildSourceSetting() {
return generateSection(
title: appLocalizations.source,
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, bool>(
selector: (_, config) => config.accessControl.isFilterSystemApp,
builder: (_, isFilterSystemApp, __) {
return Wrap(
spacing: 16,
children: [
for (final item in [false, true])
SettingTextCard(
_getTextWithIsFilterSystemApp(item),
isSelected: isFilterSystemApp == item,
onPressed: () {
final config = globalState.appController.config;
config.accessControl = config.accessControl.copyWith(
isFilterSystemApp: item,
);
},
)
],
);
},
),
)
],
);
}
_intelligentSelected() async {
final appState = globalState.appController.appState;
final config = globalState.appController.config;
final accessControl = config.accessControl;
final packageNames = appState.packages
.where(
(item) =>
accessControl.isFilterSystemApp ? item.isSystem == false : true,
)
.map((item) => item.packageName);
Navigator.of(context).pop();
final commonScaffoldState = context.commonScaffoldState;
if (commonScaffoldState?.mounted != true) return;
final selectedPackageNames =
(await commonScaffoldState?.loadingRun<List<String>>(
() async {
return await app?.getChinaPackageNames() ?? [];
},
))
?.toSet() ??
{};
final acceptList = packageNames
.where((item) => !selectedPackageNames.contains(item))
.toList();
final rejectList = packageNames
.where((item) => selectedPackageNames.contains(item))
.toList();
config.accessControl = accessControl.copyWith(
acceptList: acceptList,
rejectList: rejectList,
);
}
_copyToClipboard() async {
await globalState.safeRun(() {
final data = globalState.appController.config.accessControl.toJson();
Clipboard.setData(
ClipboardData(
text: json.encode(data),
),
);
});
if (!context.mounted) return;
Navigator.of(context).pop();
}
_pasteToClipboard() async {
await globalState.safeRun(() async {
final config = globalState.appController.config;
final data = await Clipboard.getData('text/plain');
final text = data?.text;
if (text == null) return;
config.accessControl = AccessControl.fromJson(
json.decode(text),
);
});
if (!context.mounted) return;
Navigator.of(context).pop();
}
List<Widget> _buildActionSetting() {
return generateSection(
title: appLocalizations.action,
items: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
child: Wrap(
runSpacing: 16,
spacing: 16,
children: [
CommonChip(
avatar: const Icon(Icons.auto_awesome),
label: appLocalizations.intelligentSelected,
onPressed: _intelligentSelected,
),
CommonChip(
avatar: const Icon(Icons.paste),
label: appLocalizations.clipboardImport,
onPressed: _pasteToClipboard,
),
CommonChip(
avatar: const Icon(Icons.content_copy),
label: appLocalizations.clipboardExport,
onPressed: _copyToClipboard,
)
],
),
)
],
);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
..._buildModeSetting(),
..._buildSortSetting(),
..._buildSourceSetting(),
..._buildActionSetting(),
],
),
);
}
}

View File

@@ -76,7 +76,7 @@ class ApplicationSettingFragment extends StatelessWidget {
selector: (_, config) => config.autoRun,
builder: (_, autoRun, child) {
return ListItem.switchItem(
leading: const Icon(Icons.start),
leading: const Icon(Icons.not_started),
title: Text(appLocalizations.autoRun),
subtitle: Text(appLocalizations.autoRunDesc),
delegate: SwitchDelegate(

View File

@@ -1,13 +1,10 @@
import 'dart:io';
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/card.dart';
import 'package:fl_clash/widgets/fade_box.dart';
import 'package:fl_clash/widgets/list.dart';
import 'package:fl_clash/widgets/text.dart';
@@ -75,10 +72,11 @@ class BackupAndRecovery extends StatelessWidget {
final res = await commonScaffoldState?.loadingRun<bool>(
() async {
final backupData = await globalState.appController.backupData();
await picker.saveFile(
final value = await picker.saveFile(
other.getBackupFileName(),
Uint8List.fromList(backupData),
);
if (value == null) return false;
return true;
},
title: appLocalizations.backup,
@@ -205,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);

View File

@@ -1,694 +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;
globalState.appController.updateClashConfigDebounce();
} 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;
appController.updateClashConfigDebounce();
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;
appController.updateClashConfigDebounce();
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;
globalState.appController.updateClashConfigDebounce();
} 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.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 appController = globalState.appController;
appController.config.allowBypass = value;
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.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 appController = globalState.appController;
appController.config.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;
appController.updateClashConfigDebounce();
},
),
);
},
),
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;
globalState.appController.updateClashConfigDebounce();
},
),
);
},
),
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;
appController.updateClashConfigDebounce();
},
),
);
},
),
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;
appController.updateClashConfigDebounce();
},
),
);
},
),
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;
appController.updateClashConfigDebounce();
},
),
);
},
),
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;
appController.updateClashConfigDebounce();
},
),
);
},
),
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 : '';
appController.updateClashConfigDebounce();
},
),
);
},
),
],
);
}
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);
globalState.appController.updateClashConfigDebounce();
},
),
);
},
),
],
);
}
@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),
)
],
);
}
}

View File

@@ -0,0 +1,61 @@
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(),
];

View 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(),
);
}
}

View File

@@ -0,0 +1,850 @@
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 OverrideItem extends StatelessWidget {
const OverrideItem({super.key});
_initActions(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
globalState.showMessage(
title: appLocalizations.resetDns,
message: TextSpan(
text: appLocalizations.dnsResetTip,
),
onTab: () {
globalState.appController.clashConfig.dns = const Dns();
Navigator.of(context).pop();
});
},
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(),
];

View 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();

View 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(),
];

View File

@@ -1,6 +1,8 @@
import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/fragments/dashboard/intranet_ip.dart';
import 'package:fl_clash/fragments/dashboard/status_switch.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'package:fl_clash/widgets/widgets.dart';
@@ -28,34 +30,51 @@ class _DashboardFragmentState extends State<DashboardFragment> {
child: Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16).copyWith(
bottom: 88,
),
child: Selector<AppState, double>(
selector: (_, appState) => appState.viewWidth,
builder: (_, viewWidth, ___) {
// final viewMode = other.getViewMode(viewWidth);
// final isDesktop = viewMode == ViewMode.desktop;
final columns = max(4 * ((viewWidth / 350).ceil()), 8);
final int switchCount = (4 / columns) * viewWidth < 200 ? 8 : 4;
return Grid(
crossAxisCount: max(4 * ((viewWidth / 350).ceil()), 8),
crossAxisCount: columns,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: const [
GridItem(
children: [
const GridItem(
crossAxisCellCount: 8,
child: NetworkSpeed(),
),
GridItem(
// if (Platform.isAndroid)
// GridItem(
// crossAxisCellCount: switchCount,
// child: const VPNSwitch(),
// ),
if (system.isDesktop) ...[
GridItem(
crossAxisCellCount: switchCount,
child: const TUNSwitch(),
),
GridItem(
crossAxisCellCount: switchCount,
child: const ProxySwitch(),
),
],
const GridItem(
crossAxisCellCount: 4,
child: OutboundMode(),
),
GridItem(
const GridItem(
crossAxisCellCount: 4,
child: NetworkDetection(),
),
GridItem(
const GridItem(
crossAxisCellCount: 4,
child: TrafficUsage(),
),
GridItem(
const GridItem(
crossAxisCellCount: 4,
child: IntranetIP(),
),

View File

@@ -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, __) {

View File

@@ -1,4 +1,3 @@
import 'package:country_flags/country_flags.dart';
import 'package:dio/dio.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
@@ -15,28 +14,40 @@ class NetworkDetection extends StatefulWidget {
}
class _NetworkDetectionState extends State<NetworkDetection> {
final ipInfoNotifier = ValueNotifier<IpInfo?>(null);
final timeoutNotifier = ValueNotifier<bool>(false);
final networkDetectionState = ValueNotifier<NetworkDetectionState>(
const NetworkDetectionState(
isTesting: true,
ipInfo: null,
),
);
bool? _preIsStart;
Function? _checkIpDebounce;
CancelToken? cancelToken;
_checkIp() async {
final appState = globalState.appController.appState;
final appFlowingState = globalState.appController.appFlowingState;
final isInit = appState.isInit;
final isStart = appState.isStart;
if (!isInit) return;
timeoutNotifier.value = false;
final isStart = appFlowingState.isStart;
if (_preIsStart == false && _preIsStart == isStart) return;
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: true,
ipInfo: null,
);
_preIsStart = isStart;
ipInfoNotifier.value = null;
final ipInfo = await request.checkIp();
if (ipInfo == null) {
timeoutNotifier.value = true;
return;
} else {
timeoutNotifier.value = false;
if (cancelToken != null) {
cancelToken!.cancel();
cancelToken = null;
}
ipInfoNotifier.value = ipInfo;
cancelToken = CancelToken();
try {
final ipInfo = await request.checkIp(cancelToken: cancelToken);
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: false,
ipInfo: ipInfo,
);
} catch (_) {}
}
_checkIpContainer(Widget child) {
@@ -57,17 +68,28 @@ class _NetworkDetectionState extends State<NetworkDetection> {
@override
void dispose() {
super.dispose();
ipInfoNotifier.dispose();
timeoutNotifier.dispose();
networkDetectionState.dispose();
}
String countryCodeToEmoji(String countryCode) {
final String code = countryCode.toUpperCase();
if (code.length != 2) {
return countryCode;
}
final int firstLetter = code.codeUnitAt(0) - 0x41 + 0x1F1E6;
final int secondLetter = code.codeUnitAt(1) - 0x41 + 0x1F1E6;
return String.fromCharCode(firstLetter) + String.fromCharCode(secondLetter);
}
@override
Widget build(BuildContext context) {
_checkIpDebounce = debounce(_checkIp);
_checkIpDebounce ??= debounce(_checkIp);
return _checkIpContainer(
ValueListenableBuilder<IpInfo?>(
valueListenable: ipInfoNotifier,
builder: (_, ipInfo, __) {
ValueListenableBuilder<NetworkDetectionState>(
valueListenable: networkDetectionState,
builder: (_, state, __) {
final ipInfo = state.ipInfo;
final isTesting = state.isTesting;
return CommonCard(
onPressed: () {},
child: Column(
@@ -88,37 +110,38 @@ class _NetworkDetectionState extends State<NetworkDetection> {
Flexible(
flex: 1,
child: FadeBox(
child: ipInfo != null
? CountryFlag.fromCountryCode(
ipInfo.countryCode,
width: 24,
height: 24,
child: isTesting
? Text(
appLocalizations.checking,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style:
Theme.of(context).textTheme.titleMedium,
)
: ValueListenableBuilder(
valueListenable: timeoutNotifier,
builder: (_, timeout, __) {
if (timeout) {
return Text(
appLocalizations.checkError,
: ipInfo != null
? Container(
alignment: Alignment.centerLeft,
height: globalState
.measure.titleMediumHeight,
child: Text(
countryCodeToEmoji(
ipInfo.countryCode),
style: Theme.of(context)
.textTheme
.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
}
return TooltipText(
text: Text(
appLocalizations.checking,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleMedium,
.titleLarge
?.copyWith(
fontFamily: "Twemoji",
),
),
);
},
),
)
: Text(
appLocalizations.checkError,
style: Theme.of(context)
.textTheme
.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
],
@@ -126,9 +149,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(
@@ -151,28 +172,24 @@ class _NetworkDetectionState extends State<NetworkDetection> {
),
],
)
: ValueListenableBuilder(
valueListenable: timeoutNotifier,
builder: (_, timeout, __) {
if (timeout) {
return Text(
"timeout",
style: context.textTheme.titleLarge
?.copyWith(color: Colors.red)
.toSoftBold
.toMinus,
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
}
return Container(
padding: const EdgeInsets.all(2),
child: const AspectRatio(
aspectRatio: 1,
child: CircularProgressIndicator(),
),
);
},
: FadeBox(
child: isTesting == false && ipInfo == null
? Text(
"timeout",
style: context.textTheme.titleLarge
?.copyWith(color: Colors.red)
.toSoftBold
.toMinus,
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Container(
padding: const EdgeInsets.all(2),
child: const AspectRatio(
aspectRatio: 1,
child: CircularProgressIndicator(),
),
),
),
),
)

View File

@@ -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,
@@ -114,10 +114,10 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
onPressed: () {},
info: Info(
label: appLocalizations.networkSpeed,
iconData: Icons.speed,
iconData: Icons.speed_sharp,
),
child: Selector<AppState, List<Traffic>>(
selector: (_, appState) => appState.traffics,
child: Selector<AppFlowingState, List<Traffic>>(
selector: (_, appFlowingState) => appFlowingState.traffics,
builder: (_, traffics, __) {
return Container(
padding: const EdgeInsets.all(16),

View File

@@ -15,7 +15,6 @@ class OutboundMode extends StatelessWidget {
final clashConfig = appController.clashConfig;
if (value == null || clashConfig.mode == value) return;
clashConfig.mode = value;
await appController.updateClashConfig();
appController.addCheckIpNumDebounce();
}
@@ -28,7 +27,7 @@ class OutboundMode extends StatelessWidget {
onPressed: () {},
info: Info(
label: appLocalizations.outboundMode,
iconData: Icons.call_split,
iconData: Icons.call_split_sharp,
),
child: Padding(
padding: const EdgeInsets.only(bottom: 16),

View File

@@ -34,10 +34,10 @@ class _StartButtonState extends State<StartButton>
handleSwitchStart() {
final appController = globalState.appController;
if (isStart == appController.appState.isStart) {
if (isStart == appController.appFlowingState.isStart) {
isStart = !isStart;
updateController();
appController.updateSystemProxy(isStart);
appController.updateStatus(isStart);
}
}
@@ -50,10 +50,10 @@ class _StartButtonState extends State<StartButton>
}
Widget _updateControllerContainer(Widget child) {
return Selector<AppState, bool>(
selector: (_, appState) => appState.isStart,
return Selector<AppFlowingState, bool>(
selector: (_, appFlowingState) => appFlowingState.isStart,
builder: (_, isStart, child) {
if(isStart != this.isStart){
if (isStart != this.isStart) {
this.isStart = isStart;
updateController();
}
@@ -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(
@@ -127,8 +127,8 @@ class _StartButtonState extends State<StartButton>
);
},
child: _updateControllerContainer(
Selector<AppState, int?>(
selector: (_, appState) => appState.runTime,
Selector<AppFlowingState, int?>(
selector: (_, appFlowingState) => appFlowingState.runTime,
builder: (_, int? value, __) {
final text = other.getTimeText(value);
return Text(

View File

@@ -0,0 +1,125 @@
import 'package:fl_clash/common/app_localizations.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// class VPNSwitch extends StatelessWidget {
// const VPNSwitch({super.key});
//
// @override
// Widget build(BuildContext context) {
// return SwitchContainer(
// info: const Info(
// label: "VPN",
// iconData: Icons.stacked_line_chart,
// ),
// child: Selector<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});
@override
Widget build(BuildContext context) {
return SwitchContainer(
info: Info(
label: appLocalizations.tun,
iconData: Icons.stacked_line_chart,
),
child: Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tun.enable,
builder: (_, enable, __) {
return LocaleBuilder(
builder: (_) => Switch(
value: enable,
onChanged: (value) {
final clashConfig = globalState.appController.clashConfig;
clashConfig.tun = clashConfig.tun.copyWith(
enable: value,
);
},
),
);
},
),
);
}
}
class ProxySwitch extends StatelessWidget {
const ProxySwitch({super.key});
@override
Widget build(BuildContext context) {
return SwitchContainer(
info: Info(
label: appLocalizations.systemProxy,
iconData: Icons.shuffle,
),
child: Selector<Config, bool>(
selector: (_, config) => config.desktopProps.systemProxy,
builder: (_, systemProxy, __) {
return LocaleBuilder(
builder: (_) => Switch(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: systemProxy,
onChanged: (value) {
final config = globalState.appController.config;
config.desktopProps =
config.desktopProps.copyWith(systemProxy: value);
},
),
);
},
),
);
}
}
class SwitchContainer extends StatelessWidget {
final Info info;
final Widget child;
const SwitchContainer({
super.key,
required this.info,
required this.child,
});
@override
Widget build(BuildContext context) {
return CommonCard(
onPressed: () {},
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoHeader(
info: info,
actions: [
child,
],
),
],
),
);
}
}

View File

@@ -56,8 +56,8 @@ class TrafficUsage extends StatelessWidget {
label: appLocalizations.trafficUsage,
iconData: Icons.data_saver_off,
),
child: Selector<AppState, Traffic>(
selector: (_, appState) => appState.totalTraffic,
child: Selector<AppFlowingState, Traffic>(
selector: (_, appFlowingState) => appFlowingState.totalTraffic,
builder: (_, totalTraffic, __) {
final upTotalTrafficValue = totalTraffic.up;
final downTotalTrafficValue = totalTraffic.down;

View File

@@ -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
View 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: () {},
);
}
}

View File

@@ -29,14 +29,14 @@ class _LogsFragmentState extends State<LogsFragment> {
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appController.appState;
logsNotifier.value = logsNotifier.value.copyWith(logs: appState.logs);
final appFlowingState = globalState.appController.appFlowingState;
logsNotifier.value = logsNotifier.value.copyWith(logs: appFlowingState.logs);
if (timer != null) {
timer?.cancel();
timer = null;
}
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
final logs = appState.logs;
final logs = appFlowingState.logs;
if (!const ListEquality<Log>().equals(
logsNotifier.value.logs,
logs,

View File

@@ -11,8 +11,6 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'view_profile.dart';
class EditProfile extends StatefulWidget {
final Profile profile;
final BuildContext context;

View File

@@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/profiles/edit_profile.dart';
import 'package:fl_clash/models/models.dart';
@@ -37,32 +39,24 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
);
}
_getColumns(ViewMode viewMode) {
switch (viewMode) {
case ViewMode.mobile:
return 1;
case ViewMode.laptop:
return 1;
case ViewMode.desktop:
return 2;
}
}
_updateProfiles() async {
final appController = globalState.appController;
final config = appController.config;
final profiles = appController.config.profiles;
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 (_) {
} catch (e) {
messages.add("${profile.label ?? profile.id}: $e \n");
config.setProfile(
profile.copyWith(
isUpdating: false,
@@ -71,15 +65,27 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
}
},
);
final titleMedium = context.textTheme.titleMedium;
await Future.wait(updateProfiles);
if (messages.isNotEmpty) {
globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(
children: [
for (final message in messages)
TextSpan(text: message, style: titleMedium)
],
),
);
}
}
_initScaffoldState() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
if (!mounted) return;
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
if (!context.mounted) return;
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
@@ -87,6 +93,24 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
},
icon: const Icon(Icons.sync),
),
const SizedBox(
width: 8,
),
IconButton(
onPressed: () {
final profiles = globalState.appController.config.profiles;
showSheet(
title: appLocalizations.profilesSort,
context: context,
builder: (_) => SizedBox(
height: 400,
child: ReorderableProfiles(profiles: profiles),
),
);
},
icon: const Icon(Icons.sort),
iconSize: 26,
),
];
},
);
@@ -116,7 +140,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
selector: (_, appState, config) => ProfilesSelectorState(
profiles: config.profiles,
currentProfileId: config.currentProfileId,
viewMode: appState.viewMode,
columns: other.getProfilesColumns(appState.viewWidth),
),
builder: (context, state, child) {
if (state.profiles.isEmpty) {
@@ -124,7 +148,6 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
label: appLocalizations.nullProfileDesc,
);
}
final columns = _getColumns(state.viewMode);
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
@@ -137,7 +160,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
child: Grid(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: columns,
crossAxisCount: state.columns,
children: [
for (int i = 0; i < state.profiles.length; i++)
GridItem(
@@ -145,8 +168,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
key: Key(state.profiles[i].id),
profile: state.profiles[i],
groupValue: state.currentProfileId,
onChanged:
globalState.appController.changeProfile,
onChanged: globalState.appController.changeProfile,
),
),
],
@@ -204,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(
@@ -244,6 +266,7 @@ class ProfileItem extends StatelessWidget {
LinearProgressIndicator(
minHeight: 6,
value: progress,
backgroundColor: context.colorScheme.primary.toSoft(),
),
const SizedBox(
height: 8,
@@ -372,3 +395,132 @@ class ProfileItem extends StatelessWidget {
);
}
}
class ReorderableProfiles extends StatefulWidget {
final List<Profile> profiles;
const ReorderableProfiles({
super.key,
required this.profiles,
});
@override
State<ReorderableProfiles> createState() => _ReorderableProfilesState();
}
class _ReorderableProfilesState extends State<ReorderableProfiles> {
late List<Profile> profiles;
@override
void initState() {
super.initState();
profiles = List.from(widget.profiles);
}
Widget proxyDecorator(
Widget child,
int index,
Animation<double> animation,
) {
final profile = profiles[index];
return AnimatedBuilder(
animation: animation,
builder: (_, Widget? child) {
final double animValue = Curves.easeInOut.transform(animation.value);
final double scale = lerpDouble(1, 1.02, animValue)!;
return Transform.scale(
scale: scale,
child: child,
);
},
child: Container(
key: Key(profile.id),
padding: const EdgeInsets.symmetric(vertical: 4),
child: CommonCard(
type: CommonCardType.filled,
child: ListTile(
contentPadding: const EdgeInsets.only(
right: 44,
left: 16,
),
title: Text(profile.label ?? profile.id),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
flex: 1,
child: ReorderableListView.builder(
buildDefaultDragHandles: false,
padding: const EdgeInsets.all(12),
proxyDecorator: proxyDecorator,
onReorder: (int oldIndex, int newIndex) {
if (oldIndex == newIndex) return;
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final profile = profiles.removeAt(oldIndex);
profiles.insert(newIndex, profile);
});
},
itemBuilder: (_, index) {
final profile = profiles[index];
return Container(
key: Key(profile.id),
padding: const EdgeInsets.symmetric(vertical: 4),
child: CommonCard(
type: CommonCardType.filled,
child: ListTile(
contentPadding: const EdgeInsets.only(
right: 16,
left: 16,
),
title: Text(profile.label ?? profile.id),
trailing: ReorderableDragStartListener(
index: index,
child: const Icon(Icons.drag_handle),
),
),
),
);
},
itemCount: profiles.length,
),
),
Container(
padding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 24,
),
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,
),
],
),
),
),
],
);
}
}

View File

@@ -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,
),
),
),
);
@@ -69,23 +80,21 @@ class ProxyCard extends StatelessWidget {
if (type == ProxyCardType.min) {
return SizedBox(
height: measure.bodyMediumHeight * 1,
child: Text(
child: EmojiText(
proxy.name,
maxLines: 1,
style: context.textTheme.bodyMedium?.copyWith(
overflow: TextOverflow.ellipsis,
),
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodyMedium,
),
);
} else {
return SizedBox(
height: measure.bodyMediumHeight * 2,
child: Text(
child: EmojiText(
proxy.name,
maxLines: 2,
style: context.textTheme.bodyMedium?.copyWith(
overflow: TextOverflow.ellipsis,
),
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodyMedium,
),
);
}
@@ -121,10 +130,10 @@ 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(
return currentSelectedProxyNameBuilder(
groupName: groupName,
builder: (currentGroupName) {
return Stack(
@@ -155,14 +164,12 @@ class ProxyCard extends StatelessWidget {
proxy.name,
),
builder: (_, desc, __) {
return TooltipText(
text: Text(
desc,
style: context.textTheme.bodySmall?.copyWith(
overflow: TextOverflow.ellipsis,
color: context.textTheme.bodySmall?.color
?.toLight(),
),
return EmojiText(
desc,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall?.copyWith(
color: context.textTheme.bodySmall?.color
?.toLight(),
),
);
},
@@ -207,23 +214,9 @@ class ProxyCard extends StatelessWidget {
config.currentSelectedMap[groupName];
return selectedProxyName ?? '';
},
builder: (_, value, __) {
builder: (_, value, child) {
if (value != proxy.name) return Container();
return Positioned.fill(
child: Container(
alignment: Alignment.topRight,
margin: const EdgeInsets.all(8),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
shape: BoxShape.circle,
color:
Theme.of(context).colorScheme.secondaryContainer,
),
child: const SelectIcon(),
),
),
);
return child!;
},
child: Positioned.fill(
child: Container(

View File

@@ -1,14 +1,12 @@
import 'dart:math';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/constant.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
Widget currentGroupProxyNameBuilder({
Widget currentSelectedProxyNameBuilder({
required String groupName,
required Widget Function(String currentGroupName) builder,
}) {
@@ -18,19 +16,19 @@ Widget currentGroupProxyNameBuilder({
final selectedProxyName = config.currentSelectedMap[groupName];
return group?.getCurrentSelectedName(selectedProxyName ?? "") ?? "";
},
builder: (_, currentGroupName, ___) {
return builder(currentGroupName);
builder: (_, currentSelectedProxyName, ___) {
return builder(currentSelectedProxyName);
},
);
}
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++;
}
@@ -61,7 +79,10 @@ double getScrollToSelectedOffset({
required List<Proxy> proxies,
}) {
final appController = globalState.appController;
final columns = appController.columns;
final columns = other.getProxiesColumns(
appController.appState.viewWidth,
appController.config.proxiesLayout,
);
final proxyCardType = appController.config.proxyCardType;
final selectedName = appController.getCurrentSelectedName(groupName);
final findSelectedIndex = proxies.indexWhere(
@@ -69,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;
}

View File

@@ -1,9 +1,14 @@
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';
import 'package:provider/provider.dart';
@@ -216,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,
);
@@ -237,7 +246,10 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
currentUnfoldSet: config.currentUnfoldSet,
proxyCardType: config.proxyCardType,
proxiesSortType: config.proxiesSortType,
columns: globalState.appController.columns,
columns: other.getProxiesColumns(
appState.viewWidth,
config.proxiesLayout,
),
sortNum: appState.sortNum,
);
},
@@ -252,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,
),
),
),
),
],
);
},
);
}),
],
),
);
],
);
},
);
}),
],
),
);
});
},
);
}
@@ -349,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;
@@ -408,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),
@@ -415,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: Text(
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: currentSelectedProxyNameBuilder(
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,
),
],
),
),
)
],
),
),

View File

@@ -1,3 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/app.dart';
@@ -50,7 +53,6 @@ class _ProvidersState extends State<Providers> {
);
await clashCore.updateExternalProvider(
providerName: provider.name,
providerType: provider.type,
);
appState.setProvider(
clashCore.getExternalProvider(provider.name),
@@ -58,6 +60,7 @@ class _ProvidersState extends State<Providers> {
},
);
await Future.wait(updateProviders);
await globalState.appController.updateGroupDebounce();
}
@override
@@ -65,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,
]);
},
);
}
@@ -91,28 +105,48 @@ class ProviderItem extends StatelessWidget {
required this.provider,
});
_handleUpdateProfile() async {
await globalState.safeRun<void>(updateProvider);
_handleUpdateProvider() async {
await globalState.safeRun<void>(() async {
final appState = globalState.appController.appState;
if (provider.vehicleType != "HTTP") return;
await globalState.safeRun(() async {
appState.setProvider(
provider.copyWith(
isUpdating: true,
),
);
final message = await clashCore.updateExternalProvider(
providerName: provider.name,
);
if (message.isNotEmpty) throw message;
});
appState.setProvider(
clashCore.getExternalProvider(provider.name),
);
});
await globalState.appController.updateGroupDebounce();
}
updateProvider() async {
final appState = globalState.appController.appState;
if (provider.vehicleType != "HTTP") return;
await globalState.safeRun(() async {
appState.setProvider(
provider.copyWith(
isUpdating: true,
),
_handleSideLoadProvider() async {
await globalState.safeRun<void>(() async {
final platformFile = await picker.pickerFile();
final appState = globalState.appController.appState;
final bytes = platformFile?.bytes;
if (bytes == null) return;
final file = await File(provider.path).create(recursive: true);
await file.writeAsBytes(bytes);
final providerName = provider.name;
var message = await clashCore.sideLoadExternalProvider(
providerName: providerName,
data: utf8.decode(bytes),
);
final message = await clashCore.updateExternalProvider(
providerName: provider.name,
providerType: provider.type,
if (message.isNotEmpty) throw message;
appState.setProvider(
clashCore.getExternalProvider(provider.name),
);
if (message.isNotEmpty) throw message;
});
appState.setProvider(
clashCore.getExternalProvider(provider.name),
);
await globalState.appController.updateGroupDebounce();
}
String _buildProviderDesc() {
@@ -153,18 +187,16 @@ class ProviderItem extends StatelessWidget {
runSpacing: 6,
spacing: 12,
children: [
// CommonChip(
// avatar: const Icon(Icons.upload),
// label: appLocalizations.upload,
// onPressed: () {},
// ),
CommonChip(
avatar: const Icon(Icons.upload),
label: appLocalizations.upload,
onPressed: _handleSideLoadProvider,
),
if (provider.vehicleType == "HTTP")
CommonChip(
avatar: const Icon(Icons.sync),
label: appLocalizations.sync,
onPressed: () {
_handleUpdateProfile();
},
onPressed: _handleUpdateProvider,
),
],
),

View File

@@ -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(

View File

@@ -33,6 +33,14 @@ class ProxiesSettingWidget extends StatelessWidget {
};
}
String getTextForProxiesLayout(ProxiesLayout proxiesLayout) {
return switch (proxiesLayout) {
ProxiesLayout.tight => appLocalizations.tight,
ProxiesLayout.standard => appLocalizations.standard,
ProxiesLayout.loose => appLocalizations.loose,
};
}
List<Widget> _buildStyleSetting() {
return generateSection(
title: appLocalizations.style,
@@ -132,36 +140,28 @@ class ProxiesSettingWidget extends StatelessWidget {
);
}
List<Widget> _buildColumnsSetting() {
List<Widget> _buildLayoutSetting() {
return generateSection(
title: appLocalizations.columns,
title: appLocalizations.layout,
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
scrollDirection: Axis.horizontal,
child: Selector2<AppState, Config, ColumnsSelectorState>(
selector: (_, appState, config) => ColumnsSelectorState(
columns: config.proxiesColumns,
viewMode: appState.viewMode,
),
builder: (_, state, __) {
child: Selector< Config, ProxiesLayout>(
selector: (_, config) => config.proxiesLayout,
builder: (_, proxiesLayout, __) {
final config = globalState.appController.config;
final targetColumnsArray = viewModeColumnsMap[state.viewMode]!;
final currentColumns = other.getColumns(
state.viewMode,
state.columns,
);
return Wrap(
spacing: 16,
children: [
for (final item in targetColumnsArray)
for (final item in ProxiesLayout.values)
SettingTextCard(
other.getColumnsTextForInt(item),
isSelected: item == currentColumns,
getTextForProxiesLayout(item),
isSelected: item == proxiesLayout,
onPressed: () {
config.proxiesColumns = item;
config.proxiesLayout = item;
},
)
],
@@ -183,80 +183,10 @@ class ProxiesSettingWidget extends StatelessWidget {
children: [
..._buildStyleSetting(),
..._buildSortSetting(),
..._buildColumnsSetting(),
..._buildLayoutSetting(),
..._buildSizeSetting(),
],
),
);
}
}
class SettingInfoCard extends StatelessWidget {
final Info info;
final bool? isSelected;
final VoidCallback onPressed;
const SettingInfoCard(
this.info, {
super.key,
this.isSelected,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return CommonCard(
isSelected: isSelected,
onPressed: onPressed,
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Flexible(
child: Icon(info.iconData),
),
const SizedBox(
width: 8,
),
Flexible(
child: Text(
info.label,
style: context.textTheme.bodyMedium,
),
),
],
),
),
);
}
}
class SettingTextCard extends StatelessWidget {
final String text;
final bool? isSelected;
final VoidCallback onPressed;
const SettingTextCard(
this.text, {
super.key,
this.isSelected,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return CommonCard(
onPressed: onPressed,
isSelected: isSelected,
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
text,
style: context.textTheme.bodyMedium,
),
),
);
}
}

View File

@@ -1,7 +1,8 @@
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/proxies/setting.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
@@ -139,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,
@@ -267,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,
);
@@ -285,7 +289,10 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
return ProxyGroupSelectorState(
proxyCardType: config.proxyCardType,
proxiesSortType: config.proxiesSortType,
columns: globalState.appController.columns,
columns: other.getProxiesColumns(
appState.viewWidth,
config.proxiesLayout,
),
sortNum: appState.sortNum,
proxies: group.all,
groupType: group.type,
@@ -307,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: 96,
),
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,
);
},
),
),
);

View File

@@ -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) {
@@ -91,7 +100,6 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
final appController = globalState.appController;
appController.clashConfig.geoXUrl =
Map.from(appController.clashConfig.geoXUrl)..[geoItem.key] = newUrl;
appController.updateClashConfigDebounce();
} catch (e) {
globalState.showMessage(
title: geoItem.label,
@@ -182,9 +190,9 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
updateGeoDateItem() async {
isUpdating.value = true;
try {
final message = await clashCore.updateExternalProvider(
providerName: geoItem.fileName,
providerType: geoItem.label,
final message = await clashCore.updateGeoData(
geoName: geoItem.fileName,
geoType: geoItem.label,
);
if (message.isNotEmpty) throw message;
} catch (e) {
@@ -239,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
@@ -259,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;
@@ -286,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),

View File

@@ -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,
),
],
);
}

View File

@@ -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),
);
},
);

View File

@@ -37,7 +37,7 @@
"overrideDesc": "Override Proxy related config",
"allowLan": "AllowLan",
"allowLanDesc": "Allow access proxy through the LAN",
"tun": "TUN mode",
"tun": "TUN",
"tunDesc": "only effective in administrator mode",
"minimizeOnExit": "Minimize on exit",
"minimizeOnExitDesc": "Modify the default system exit event",
@@ -117,7 +117,7 @@
"logLevel": "LogLevel",
"show": "Show",
"exit": "Exit",
"systemProxy": "SystemProxy",
"systemProxy": "System proxy",
"project": "Project",
"core": "Core",
"tabAnimation": "Tab animation",
@@ -227,5 +227,82 @@
"remoteBackupDesc": "Backup local data to WebDAV",
"remoteRecoveryDesc": "Recovery data from WebDAV",
"localBackupDesc": "Backup local data to local",
"localRecoveryDesc": "Recovery data from file"
"localRecoveryDesc": "Recovery data from file",
"mode": "Mode",
"time": "Time",
"source": "Source",
"allApps": "All apps",
"onlyOtherApps": "Only third-party apps",
"action": "Action",
"intelligentSelected": "Intelligent selection",
"clipboardImport": "Clipboard import",
"clipboardExport": "Export clipboard",
"layout": "Layout",
"tight": "Tight",
"standard": "Standard",
"loose": "Loose",
"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",
"dnsResetTip": "Make sure to reset the DNS"
}

View File

@@ -37,7 +37,7 @@
"overrideDesc": "覆写代理相关配置",
"allowLan": "局域网代理",
"allowLanDesc": "允许通过局域网访问代理",
"tun": "TUN模式",
"tun": "虚拟网卡",
"tunDesc": "仅在管理员模式生效",
"minimizeOnExit": "退出时最小化",
"minimizeOnExitDesc": "修改系统默认退出事件",
@@ -227,5 +227,82 @@
"remoteBackupDesc": "备份数据到WebDAV",
"remoteRecoveryDesc": "通过WebDAV恢复数据",
"localBackupDesc": "备份数据到本地",
"localRecoveryDesc": "通过文件恢复数据"
"localRecoveryDesc": "通过文件恢复数据",
"mode": "模式",
"time": "时间",
"source": "来源",
"allApps": "所有应用",
"onlyOtherApps": "仅第三方应用",
"action": "操作",
"intelligentSelected": "智能选择",
"clipboardImport": "剪贴板导入",
"clipboardExport": "导出剪贴板",
"layout": "布局",
"tight": "紧凑",
"standard": "标准",
"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": "暂无快捷键",
"dnsResetTip": "确定重置DNS"
}

View File

@@ -33,6 +33,12 @@ class MessageLookup extends MessageLookupByLibrary {
"account": MessageLookupByLibrary.simpleMessage("Account"),
"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":
@@ -40,6 +46,8 @@ 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"),
"allowBypassDesc": MessageLookupByLibrary.simpleMessage(
@@ -50,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"),
@@ -89,6 +99,10 @@ class MessageLookup extends MessageLookupByLibrary {
"checkUpdateError": MessageLookupByLibrary.simpleMessage(
"The current application is already the latest version"),
"checking": MessageLookupByLibrary.simpleMessage("Checking..."),
"clipboardExport":
MessageLookupByLibrary.simpleMessage("Export clipboard"),
"clipboardImport":
MessageLookupByLibrary.simpleMessage("Clipboard import"),
"columns": MessageLookupByLibrary.simpleMessage("Columns"),
"compatible":
MessageLookupByLibrary.simpleMessage("Compatibility mode"),
@@ -108,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"),
@@ -118,12 +136,21 @@ 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"),
"dnsResetTip":
MessageLookupByLibrary.simpleMessage("Make sure to reset the DNS"),
"doYouWantToPass":
MessageLookupByLibrary.simpleMessage("Do you want to pass"),
"domain": MessageLookupByLibrary.simpleMessage("Domain"),
"download": MessageLookupByLibrary.simpleMessage("Download"),
"edit": MessageLookupByLibrary.simpleMessage("Edit"),
"en": MessageLookupByLibrary.simpleMessage("English"),
@@ -143,6 +170,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"),
@@ -153,27 +187,46 @@ 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"),
"list": MessageLookupByLibrary.simpleMessage("List"),
"local": MessageLookupByLibrary.simpleMessage("Local"),
@@ -187,19 +240,33 @@ 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":
MessageLookupByLibrary.simpleMessage("Minimize on exit"),
"minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage(
"Modify the default system exit event"),
"minutes": MessageLookupByLibrary.simpleMessage("Minutes"),
"mode": MessageLookupByLibrary.simpleMessage("Mode"),
"months": MessageLookupByLibrary.simpleMessage("Months"),
"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"),
@@ -216,10 +283,13 @@ class MessageLookup extends MessageLookupByLibrary {
"No profile, Please add a profile"),
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"),
"oneColumn": MessageLookupByLibrary.simpleMessage("One column"),
"onlyOtherApps":
MessageLookupByLibrary.simpleMessage("Only third-party apps"),
"onlyStatisticsProxy":
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"),
@@ -227,6 +297,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"),
@@ -238,6 +311,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":
@@ -255,14 +332,22 @@ class MessageLookup extends MessageLookupByLibrary {
"profileUrlNullValidationDesc": MessageLookupByLibrary.simpleMessage(
"Please input the profile URL"),
"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"),
@@ -280,13 +365,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"),
@@ -300,12 +392,19 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Start in the background"),
"size": MessageLookupByLibrary.simpleMessage("Size"),
"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"),
"sync": MessageLookupByLibrary.simpleMessage("Sync"),
"systemProxy": MessageLookupByLibrary.simpleMessage("SystemProxy"),
"systemProxy": MessageLookupByLibrary.simpleMessage("System proxy"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage(
"Attach HTTP proxy to VpnService"),
"tab": MessageLookupByLibrary.simpleMessage("Tab"),
@@ -322,10 +421,12 @@ class MessageLookup extends MessageLookupByLibrary {
"Set dark mode,adjust the color"),
"themeMode": MessageLookupByLibrary.simpleMessage("Theme mode"),
"threeColumns": MessageLookupByLibrary.simpleMessage("Three columns"),
"tight": MessageLookupByLibrary.simpleMessage("Tight"),
"time": MessageLookupByLibrary.simpleMessage("Time"),
"tip": MessageLookupByLibrary.simpleMessage("tip"),
"tools": MessageLookupByLibrary.simpleMessage("Tools"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"),
"tun": MessageLookupByLibrary.simpleMessage("TUN mode"),
"tun": MessageLookupByLibrary.simpleMessage("TUN"),
"tunDesc": MessageLookupByLibrary.simpleMessage(
"only effective in administrator mode"),
"twoColumns": MessageLookupByLibrary.simpleMessage("Two columns"),
@@ -341,7 +442,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"),

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