Compare commits

...

24 Commits

Author SHA1 Message Date
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
chen08209
7fc54c5295 Optimize provider page
Optimize delay test

Support local backup and recovery
2024-08-05 18:17:05 +08:00
chen08209
00a78b5fb4 Fix android tile service issues 2024-08-01 23:51:28 +08:00
chen08209
8cdaf30de0 Fix linux core build error 2024-07-31 21:24:31 +08:00
chen08209
f39b9cf933 Add proxy-only traffic statistics
Update core

Optimize more details
2024-07-31 21:05:16 +08:00
chen08209
9df1ff46c2 Merge pull request #140 from txyyh/main
添加自建 F-Droid 仓库相关 workflow
2024-07-29 16:48:04 +08:00
txyyh
fcbbbdc698 Rename readme fingerprint 2024-07-29 16:45:12 +08:00
txyyh
3ba8355772 Rename workflow deploy repo name 2024-07-29 16:42:25 +08:00
txyyh
f6b97f82ae Add download guide to README 2024-07-29 16:39:52 +08:00
txyyh
13ac20f273 Add push release files to fdroid-repo 2024-07-29 16:39:52 +08:00
159 changed files with 13482 additions and 4870 deletions

View File

@@ -87,7 +87,7 @@ jobs:
- name: Setup Flutter - name: Setup Flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
flutter-version: '3.x' flutter-version: 3.22.x
channel: 'stable' channel: 'stable'
cache: true cache: true
@@ -136,8 +136,29 @@ jobs:
gitchangelog "${pre}.." >> release.md 2>&1 || echo "Error in gitchangelog" gitchangelog "${pre}.." >> release.md 2>&1 || echo "Error in gitchangelog"
echo -e "\n\n</details>" >> release.md echo -e "\n\n</details>" >> release.md
fi fi
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: ./dist/* files: ./dist/*
body_path: './release.md' 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: ./tmp/
destination-github-username: chen08209
destination-repository-name: FlClash-fdroid-repo
user-name: 'github-actions[bot]'
user-email: 'github-actions[bot]@users.noreply.github.com'
target-branch: action-pr
commit-message: Update from ${{ github.ref_name }}
target-directory: /tmp/

View File

@@ -38,6 +38,10 @@ on Mobile:
✨ Support subscription link, Dark mode ✨ Support subscription link, Dark mode
## Download
<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 ## Contact
[Telegram](https://t.me/+G-veVtwBOl4wODc1) [Telegram](https://t.me/+G-veVtwBOl4wODc1)

View File

@@ -38,6 +38,11 @@ on Mobile:
✨ 支持一键导入订阅, 深色模式 ✨ 支持一键导入订阅, 深色模式
## Download
<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 ## Contact
[Telegram](https://t.me/+G-veVtwBOl4wODc1) [Telegram](https://t.me/+G-veVtwBOl4wODc1)

View File

@@ -102,6 +102,9 @@ flutter {
dependencies { dependencies {
implementation 'androidx.core:core-splashscreen:1.0.1' implementation 'androidx.core:core-splashscreen:1.0.1'
implementation 'com.google.code.gson:gson:2.10' 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"> xmlns:tools="http://schemas.android.com/tools">
<uses-feature <uses-feature
android:name="android.hardware.touchscreen" android:name="android.hardware.touchscreen"
android:required="false" /> android:required="false" />
<uses-feature <uses-feature
android:name="android.hardware.camera" android:name="android.hardware.camera"
android:required="false" /> android:required="false" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <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.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_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" /> tools:ignore="SystemPermissionTypo" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" <uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" /> tools:ignore="QueryAllPackagesPermission" />
<application <application
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:networkSecurityConfig="@xml/network_security_config"
android:extractNativeLibs="true"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher"
android:label="FlClash" android:label="FlClash"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="tiramisu"> tools:targetApi="tiramisu">
<activity <activity
android:name="com.follow.clash.MainActivity" android:name="com.follow.clash.MainActivity"
@@ -56,17 +58,17 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="clash"/> <data android:scheme="clash" />
<data android:scheme="clashmeta"/> <data android:scheme="clashmeta" />
<data android:scheme="flclash"/> <data android:scheme="flclash" />
<data android:host="install-config"/> <data android:host="install-config" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- <meta-data--> <!-- <meta-data-->
<!-- android:name="io.flutter.embedding.android.EnableImpeller"--> <!-- android:name="io.flutter.embedding.android.EnableImpeller"-->
<!-- android:value="true" />--> <!-- android:value="true" />-->
<activity <activity
android:name=".TempActivity" android:name=".TempActivity"
@@ -75,8 +77,8 @@
<service <service
android:name=".services.FlClashTileService" android:name=".services.FlClashTileService"
android:exported="true" android:exported="true"
android:icon="@drawable/ic_stat_name"
android:foregroundServiceType="specialUse" android:foregroundServiceType="specialUse"
android:icon="@drawable/ic_stat_name"
android:label="FlClash" android:label="FlClash"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"> android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter> <intent-filter>
@@ -86,6 +88,29 @@
android:name="android.service.quicksettings.TOGGLEABLE_TILE" android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" /> android:value="true" />
</service> </service>
<provider
android:name=".FilesProvider"
android:authorities="${applicationId}.files"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS"
android:process=":background">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<service <service
android:name=".services.FlClashVpnService" android:name=".services.FlClashVpnService"
android:exported="false" android:exported="false"
@@ -96,6 +121,11 @@
</intent-filter> </intent-filter>
</service> </service>
<service
android:name=".services.FlClashService"
android:exported="false"
android:foregroundServiceType="specialUse" />
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />

View File

@@ -0,0 +1,9 @@
package com.follow.clash
import com.follow.clash.models.Props
interface BaseServiceInterface {
fun start(port: Int, props: Props?): Int?
fun stop()
fun startForeground(title: String, content: String)
}

View File

@@ -0,0 +1,112 @@
package com.follow.clash
import android.database.Cursor
import android.database.MatrixCursor
import android.os.CancellationSignal
import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract.Document
import android.provider.DocumentsContract.Root
import android.provider.DocumentsProvider
import java.io.File
import java.io.FileNotFoundException
class FilesProvider : DocumentsProvider() {
companion object {
private const val DEFAULT_ROOT_ID = "0"
private val DEFAULT_DOCUMENT_COLUMNS = arrayOf(
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_FLAGS,
Document.COLUMN_SIZE,
)
private val DEFAULT_ROOT_COLUMNS = arrayOf(
Root.COLUMN_ROOT_ID,
Root.COLUMN_FLAGS,
Root.COLUMN_ICON,
Root.COLUMN_TITLE,
Root.COLUMN_SUMMARY,
Root.COLUMN_DOCUMENT_ID
)
}
override fun onCreate(): Boolean {
return true
}
override fun queryRoots(projection: Array<String>?): Cursor {
return MatrixCursor(projection ?: DEFAULT_ROOT_COLUMNS).apply {
newRow().apply {
add(Root.COLUMN_ROOT_ID, DEFAULT_ROOT_ID)
add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY)
add(Root.COLUMN_ICON, R.mipmap.ic_launcher)
add(Root.COLUMN_TITLE, context!!.getString(R.string.fl_clash))
add(Root.COLUMN_SUMMARY, "Data")
add(Root.COLUMN_DOCUMENT_ID, "/")
}
}
}
override fun queryChildDocuments(
parentDocumentId: String,
projection: Array<String>?,
sortOrder: String?
): Cursor {
val result = MatrixCursor(resolveDocumentProjection(projection))
val parentFile = if (parentDocumentId == "/") {
context?.filesDir
} else {
File(parentDocumentId)
} ?: throw FileNotFoundException("Parent directory not found")
parentFile.listFiles()?.forEach { file ->
includeFile(result, file)
}
return result
}
override fun queryDocument(documentId: String, projection: Array<String>?): Cursor {
val result = MatrixCursor(resolveDocumentProjection(projection))
val file = File(documentId)
includeFile(result, file)
return result
}
override fun openDocument(
documentId: String,
mode: String,
signal: CancellationSignal?
): ParcelFileDescriptor {
val file = File(documentId)
val accessMode = ParcelFileDescriptor.parseMode(mode)
return ParcelFileDescriptor.open(file, accessMode)
}
private fun includeFile(result: MatrixCursor, file: File) {
result.newRow().apply {
add(Document.COLUMN_DOCUMENT_ID, file.absolutePath)
add(Document.COLUMN_DISPLAY_NAME, file.name)
add(Document.COLUMN_SIZE, file.length())
add(
Document.COLUMN_FLAGS,
Document.FLAG_SUPPORTS_WRITE or Document.FLAG_SUPPORTS_DELETE
)
add(Document.COLUMN_MIME_TYPE, getDocumentType(file))
}
}
private fun getDocumentType(file: File): String {
return if (file.isDirectory) {
Document.MIME_TYPE_DIR
} else {
"application/octet-stream"
}
}
private fun resolveDocumentProjection(projection: Array<String>?): Array<String> {
return projection ?: DEFAULT_DOCUMENT_COLUMNS
}
}

View File

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

View File

@@ -2,7 +2,8 @@ package com.follow.clash
import com.follow.clash.plugins.AppPlugin 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 com.follow.clash.plugins.TilePlugin
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
@@ -12,7 +13,8 @@ class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
flutterEngine.plugins.add(AppPlugin()) flutterEngine.plugins.add(AppPlugin())
flutterEngine.plugins.add(ProxyPlugin()) flutterEngine.plugins.add(VpnPlugin())
flutterEngine.plugins.add(ServicePlugin())
flutterEngine.plugins.add(TilePlugin()) flutterEngine.plugins.add(TilePlugin())
GlobalState.flutterEngine = flutterEngine GlobalState.flutterEngine = flutterEngine
} }

View File

@@ -1,18 +1,28 @@
package com.follow.clash.extensions package com.follow.clash.extensions
import android.annotation.SuppressLint
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build
import android.system.OsConstants.IPPROTO_TCP import android.system.OsConstants.IPPROTO_TCP
import android.system.OsConstants.IPPROTO_UDP import android.system.OsConstants.IPPROTO_UDP
import android.util.Base64 import android.util.Base64
import java.net.URL import androidx.core.app.NotificationCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import com.follow.clash.MainActivity
import com.follow.clash.R
import com.follow.clash.models.Metadata import com.follow.clash.models.Metadata
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.net.InetAddress
import java.net.InetSocketAddress
suspend fun Drawable.getBase64(): String { suspend fun Drawable.getBase64(): String {
@@ -31,7 +41,6 @@ fun Metadata.getProtocol(): Int? {
return null return null
} }
fun String.getInetSocketAddress(): InetSocketAddress { private val CHANNEL = "FlClash"
val url = URL("https://$this")
return InetSocketAddress(InetAddress.getByName(url.host), url.port) private val notificationId: Int = 1
}

View File

@@ -1,7 +1,10 @@
package com.follow.clash.models package com.follow.clash.models
import java.util.Date
data class Package( data class Package(
val packageName: String, val packageName: String,
val label: String, val label: String,
val isSystem:Boolean val isSystem: Boolean,
val firstInstallTime: Long,
) )

View File

@@ -12,6 +12,7 @@ data class AccessControl(
) )
data class Props( data class Props(
val enable: Boolean?,
val accessControl: AccessControl?, val accessControl: AccessControl?,
val allowBypass: Boolean?, val allowBypass: Boolean?,
val systemProxy: Boolean?, val systemProxy: Boolean?,

View File

@@ -4,12 +4,19 @@ import android.Manifest
import android.app.Activity import android.app.Activity
import android.app.ActivityManager import android.app.ActivityManager
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.VpnService
import android.os.Build import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService 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 androidx.core.content.getSystemService
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.extensions.getBase64 import com.follow.clash.extensions.getBase64
@@ -17,6 +24,7 @@ import com.follow.clash.extensions.getProtocol
import com.follow.clash.models.Package import com.follow.clash.models.Package
import com.follow.clash.models.Process import com.follow.clash.models.Process
import com.google.gson.Gson import com.google.gson.Gson
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
@@ -28,8 +36,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.util.zip.ZipFile
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware { class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
@@ -37,7 +46,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private var toast: Toast? = null private var toast: Toast? = null
private var context: Context? = null private lateinit var context: Context
private lateinit var channel: MethodChannel private lateinit var channel: MethodChannel
@@ -45,14 +54,78 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private var connectivity: ConnectivityManager? = null private var connectivity: ConnectivityManager? = null
private var vpnCallBack: (() -> Unit)? = null
private val iconMap = mutableMapOf<String, String?>() 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) { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
scope = CoroutineScope(Dispatchers.Default) scope = CoroutineScope(Dispatchers.Default)
context = flutterPluginBinding.applicationContext; context = flutterPluginBinding.applicationContext;
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app") channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
channel.setMethodCallHandler(this) channel.setMethodCallHandler(this)
} }
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
@@ -61,7 +134,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
private fun tip(message: String?) { private fun tip(message: String?) {
if(GlobalState.flutterEngine == null){ if (GlobalState.flutterEngine == null) {
if (toast != null) { if (toast != null) {
toast!!.cancel() toast!!.cancel()
} }
@@ -85,7 +158,13 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
"getPackages" -> { "getPackages" -> {
scope.launch { scope.launch {
result.success(getPackages()) result.success(getPackagesToJson())
}
}
"getChinaPackageNames" -> {
scope.launch {
result.success(getChinaPackageNames())
} }
} }
@@ -104,7 +183,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
if (iconMap["default"] == null) { if (iconMap["default"] == null) {
iconMap["default"] = iconMap["default"] =
context?.packageManager?.defaultActivityIcon?.getBase64() context.packageManager?.defaultActivityIcon?.getBase64()
} }
result.success(iconMap["default"]) result.success(iconMap["default"])
return@launch return@launch
@@ -131,12 +210,8 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
result.success(null) result.success(null)
return@withContext return@withContext
} }
if (context == null) {
result.success(null)
return@withContext
}
if (connectivity == null) { if (connectivity == null) {
connectivity = context!!.getSystemService<ConnectivityManager>() connectivity = context.getSystemService<ConnectivityManager>()
} }
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort) val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
val dst = InetSocketAddress( val dst = InetSocketAddress(
@@ -152,7 +227,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
result.success(null) result.success(null)
return@withContext return@withContext
} }
val packages = context?.packageManager?.getPackagesForUid(uid) val packages = context.packageManager?.getPackagesForUid(uid)
result.success(packages?.first()) result.success(packages?.first())
} }
} }
@@ -164,15 +239,56 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
result.success(true) result.success(true)
} }
"openFile" -> {
val path = call.argument<String>("path")!!
openFile(path)
result.success(true)
}
else -> { else -> {
result.notImplemented(); result.notImplemented();
} }
} }
} }
private fun openFile(path: String) {
val file = File(path)
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileProvider",
file
)
val intent = Intent(Intent.ACTION_VIEW).setDataAndType(
uri,
"text/plain"
)
val flags =
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
val resInfoList = context.packageManager.queryIntentActivities(
intent, PackageManager.MATCH_DEFAULT_ONLY
)
for (resolveInfo in resInfoList) {
val packageName = resolveInfo.activityInfo.packageName
context.grantUriPermission(
packageName,
uri,
flags
)
}
try {
activity?.startActivity(intent)
} catch (e: Exception) {
println(e)
}
}
private fun updateExcludeFromRecents(value: Boolean?) { 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 { val task = am?.appTasks?.firstOrNull {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
it.taskInfo.taskId == activity?.taskId it.taskInfo.taskId == activity?.taskId
@@ -189,7 +305,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
private suspend fun getPackageIcon(packageName: String): String? { private suspend fun getPackageIcon(packageName: String): String? {
val packageManager = context?.packageManager val packageManager = context.packageManager
if (iconMap[packageName] == null) { if (iconMap[packageName] == null) {
iconMap[packageName] = try { iconMap[packageName] = try {
packageManager?.getApplicationIcon(packageName)?.getBase64() packageManager?.getApplicationIcon(packageName)?.getBase64()
@@ -201,32 +317,144 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
return iconMap[packageName] return iconMap[packageName]
} }
private suspend fun getPackages(): String { private fun getPackages(): List<Package> {
return withContext(Dispatchers.Default) { val packageManager = context.packageManager
val packageManager = context?.packageManager if (packages.isNotEmpty()) return packages;
val packages: List<Package>? = packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter { it.packageName != context.packageName
it.packageName != context?.packageName || it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true || it.packageName == "android"
|| it.packageName == "android"
}?.map { }?.map {
Package( Package(
packageName = it.packageName, packageName = it.packageName,
label = it.applicationInfo.loadLabel(packageManager).toString(), label = it.applicationInfo.loadLabel(packageManager).toString(),
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1 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) Gson().toJson(packages)
} }
} }
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
}
fun requestGc() { fun requestGc() {
channel.invokeMethod("gc", null) channel.invokeMethod("gc", null)
} }
override fun onAttachedToActivity(binding: ActivityPluginBinding) { override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity; activity = binding.activity;
binding.addActivityResultListener(::onActivityResult)
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
} }
override fun onDetachedFromActivityForConfigChanges() { override fun onDetachedFromActivityForConfigChanges() {
@@ -241,4 +469,25 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
channel.invokeMethod("exit", null) channel.invokeMethod("exit", null)
activity = 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,47 @@
package com.follow.clash.plugins
import android.content.Context
import com.follow.clash.GlobalState
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var flutterMethodChannel: MethodChannel
private lateinit var context: Context
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "service")
flutterMethodChannel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
flutterMethodChannel.setMethodCallHandler(null)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
"init" -> {
GlobalState.getCurrentAppPlugin()?.requestNotificationsPermission(context)
GlobalState.initServiceEngine(context)
result.success(true)
}
"destroy" -> {
handleDestroy()
result.success(true)
}
else -> {
result.notImplemented()
}
}
private fun handleDestroy() {
GlobalState.getCurrentVPNPlugin()?.stop()
GlobalState.destroyServiceEngine()
}
}

View File

@@ -0,0 +1,143 @@
package com.follow.clash.plugins
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.util.Log
import com.follow.clash.BaseServiceInterface
import com.follow.clash.GlobalState
import com.follow.clash.RunState
import com.follow.clash.models.Props
import com.follow.clash.services.FlClashService
import com.follow.clash.services.FlClashVpnService
import com.google.gson.Gson
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import kotlin.concurrent.withLock
class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var flutterMethodChannel: MethodChannel
private lateinit var context: Context
private var flClashService: BaseServiceInterface? = null
private var port: Int = 7890
private var props: Props? = null
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
flClashService = when (service) {
is FlClashVpnService.LocalBinder -> service.getService()
is FlClashService.LocalBinder -> service.getService()
else -> throw Exception("invalid binder")
}
start()
}
override fun onServiceDisconnected(arg: ComponentName) {
flClashService = null
}
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "vpn")
flutterMethodChannel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
flutterMethodChannel.setMethodCallHandler(null)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
"start" -> {
port = call.argument<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)
}
else -> {
result.notImplemented()
}
}
@SuppressLint("ForegroundServiceType")
fun handleStartVpn() {
GlobalState.getCurrentAppPlugin()?.requestVpnPermission(context) {
start()
}
}
@SuppressLint("ForegroundServiceType")
private fun startForeground(title: String, content: String) {
GlobalState.runLock.withLock {
if (GlobalState.runState.value != RunState.START) return
flClashService?.startForeground(title, content)
}
}
private fun start() {
if (flClashService == null) {
bindService()
return
}
GlobalState.runLock.withLock {
if (GlobalState.runState.value == RunState.START) return
GlobalState.runState.value = RunState.START
val fd = flClashService?.start(port, props)
flutterMethodChannel.invokeMethod("started", fd)
}
}
fun stop() {
GlobalState.runLock.withLock {
if (GlobalState.runState.value == RunState.STOP) return
GlobalState.runState.value = RunState.STOP
flClashService?.stop()
}
GlobalState.destroyServiceEngine()
}
private fun bindService() {
val intent = when (props?.enable == true) {
true -> Intent(context, FlClashVpnService::class.java)
false -> Intent(context, FlClashService::class.java)
}
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
}

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?): Int? = null
override fun stop() {
stopSelf()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
stopForeground(STOP_FOREGROUND_REMOVE)
}
}
@SuppressLint("ForegroundServiceType", "WrongConstant")
override fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
var channel = manager?.getNotificationChannel(CHANNEL)
if (channel == null) {
channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
manager?.createNotificationChannel(channel)
}
}
val notification =
notificationBuilder.setContentTitle(title).setContentText(content).build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(notificationId, notification)
}
}
}

View File

@@ -1,5 +1,6 @@
package com.follow.clash.services package com.follow.clash.services
import android.annotation.SuppressLint
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
@@ -15,6 +16,7 @@ import android.os.Parcel
import android.os.RemoteException import android.os.RemoteException
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.follow.clash.BaseServiceInterface
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.MainActivity import com.follow.clash.MainActivity
import com.follow.clash.R import com.follow.clash.R
@@ -25,10 +27,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class FlClashVpnService : VpnService() { @SuppressLint("WrongConstant")
private val CHANNEL = "FlClash" class FlClashVpnService : VpnService(), BaseServiceInterface {
private val notificationId: Int = 1
private val passList = listOf( private val passList = listOf(
"*zhihu.com", "*zhihu.com",
@@ -52,10 +52,10 @@ class FlClashVpnService : VpnService() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
initServiceEngine() GlobalState.initServiceEngine(applicationContext)
} }
fun start(port: Int, props: Props?): Int? { override fun start(port: Int, props: Props?): Int? {
return with(Builder()) { return with(Builder()) {
addAddress("172.16.0.1", 30) addAddress("172.16.0.1", 30)
setMtu(9000) setMtu(9000)
@@ -97,11 +97,18 @@ class FlClashVpnService : VpnService() {
} }
} }
fun stop() {
override fun stop() {
stopSelf() stopSelf()
stopForeground() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
stopForeground(STOP_FOREGROUND_REMOVE)
}
} }
private val CHANNEL = "FlClash"
private val notificationId: Int = 1
private val notificationBuilder: NotificationCompat.Builder by lazy { private val notificationBuilder: NotificationCompat.Builder by lazy {
val intent = Intent(this, MainActivity::class.java) val intent = Intent(this, MainActivity::class.java)
@@ -136,16 +143,8 @@ class FlClashVpnService : VpnService() {
} }
} }
fun initServiceEngine() { @SuppressLint("ForegroundServiceType", "WrongConstant")
GlobalState.initServiceEngine(applicationContext) override fun startForeground(title: String, content: String) {
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
GlobalState.getCurrentAppPlugin()?.requestGc()
}
fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java) val manager = getSystemService(NotificationManager::class.java)
var channel = manager?.getNotificationChannel(CHANNEL) var channel = manager?.getNotificationChannel(CHANNEL)
@@ -157,17 +156,16 @@ class FlClashVpnService : VpnService() {
} }
val notification = val notification =
notificationBuilder.setContentTitle(title).setContentText(content).build() 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) startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else { } else {
startForeground(notificationId, notification) startForeground(notificationId, notification)
} }
} }
private fun stopForeground() { override fun onTrimMemory(level: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { super.onTrimMemory(level)
stopForeground(STOP_FOREGROUND_REMOVE) GlobalState.getCurrentAppPlugin()?.requestGc()
}
} }
private val binder = LocalBinder() private val binder = LocalBinder()
@@ -190,7 +188,6 @@ class FlClashVpnService : VpnService() {
} }
} }
override fun onBind(intent: Intent): IBinder { override fun onBind(intent: Intent): IBinder {
return binder return binder
} }

View File

@@ -0,0 +1,6 @@
<paths>
<files-path
name="files"
path="."/>
</paths>

BIN
assets/fonts/Icons.ttf Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -2,20 +2,10 @@ package main
import "C" import "C"
import ( import (
"github.com/metacubex/mihomo/adapter" "context"
"github.com/metacubex/mihomo/adapter/inbound" "errors"
"github.com/metacubex/mihomo/adapter/outboundgroup" route "github.com/metacubex/mihomo/hub/route"
ap "github.com/metacubex/mihomo/adapter/provider" "math"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
"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"
"github.com/metacubex/mihomo/tunnel"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -24,54 +14,37 @@ import (
"sync" "sync"
"syscall" "syscall"
"time" "time"
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/adapter/outboundgroup"
"github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/common/batch"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/hub"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/listener"
"github.com/metacubex/mihomo/log"
rp "github.com/metacubex/mihomo/rules/provider"
"github.com/metacubex/mihomo/tunnel"
) )
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 { type ConfigExtendedParams struct {
IsPatch bool `json:"is-patch"` IsPatch bool `json:"is-patch"`
IsCompatible bool `json:"is-compatible"` IsCompatible bool `json:"is-compatible"`
SelectedMap map[string]string `json:"selected-map"` SelectedMap map[string]string `json:"selected-map"`
TestURL *string `json:"test-url"` TestURL *string `json:"test-url"`
OverrideDns bool `json:"override-dns"`
} }
type GenerateConfigParams struct { type GenerateConfigParams struct {
ProfilePath *string `json:"profile-path"` ProfileId string `json:"profile-id"`
Config config.RawConfig `json:"config" ` Config config.RawConfig `json:"config" `
Params ConfigExtendedParams `json:"params"` Params ConfigExtendedParams `json:"params"`
} }
type ChangeProxyParams struct { type ChangeProxyParams struct {
@@ -93,9 +66,19 @@ type ExternalProvider struct {
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
VehicleType string `json:"vehicle-type"` VehicleType string `json:"vehicle-type"`
Count int `json:"count"`
Path string `json:"path"`
UpdateAt time.Time `json:"update-at"` 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) { func restartExecutable(execPath string) {
var err error var err error
executor.Shutdown() executor.Shutdown()
@@ -145,26 +128,108 @@ func removeFile(path string) error {
return nil return nil
} }
func getRawConfigWithPath(path *string) *config.RawConfig { func getProfilePath(id string) string {
if path == nil { return filepath.Join(constant.Path.HomeDir(), "profiles", id+".yaml")
}
func getProfileProvidersPath(id string) string {
return filepath.Join(constant.Path.HomeDir(), "providers", id)
}
func getRawConfigWithId(id string) *config.RawConfig {
path := getProfilePath(id)
bytes, err := readFile(path)
if err != nil {
log.Errorln("profile is not exist")
return config.DefaultRawConfig() return config.DefaultRawConfig()
} else { }
bytes, err := readFile(*path) prof, err := config.UnmarshalRawConfig(bytes)
if err != nil { if err != nil {
log.Errorln("getProfile readFile error %v", err) log.Errorln("unmarshalRawConfig error %v", err)
return config.DefaultRawConfig() return config.DefaultRawConfig()
}
for _, mapping := range prof.ProxyProvider {
value, exist := mapping["path"].(string)
if !exist {
continue
} }
prof, err := config.UnmarshalRawConfig(bytes) mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
if err != nil { }
log.Errorln("getProfile UnmarshalRawConfig error %v", err) for _, mapping := range prof.RuleProvider {
return config.DefaultRawConfig() value, exist := mapping["path"].(string)
if !exist {
continue
} }
return prof mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
}
return prof
}
func getExternalProvidersRaw() map[string]cp.Provider {
eps := make(map[string]cp.Provider)
for n, p := range tunnel.Providers() {
if p.VehicleType() != cp.Compatible {
eps[n] = p
}
}
for n, p := range tunnel.RuleProviders() {
if p.VehicleType() != cp.Compatible {
eps[n] = p
}
}
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 decorationConfig(profilePath *string, cfg config.RawConfig) *config.RawConfig { func sideUpdateExternalProvider(p cp.Provider, bytes []byte) error {
prof := getRawConfigWithPath(profilePath) 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 {
prof := getRawConfigWithId(profileId)
overwriteConfig(prof, cfg) overwriteConfig(prof, cfg)
return prof return prof
} }
@@ -315,6 +380,12 @@ func generateProxyGroupAndRule(proxyGroup *[]map[string]any, rule *[]string) {
*rule = computedRule *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) { func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) {
targetConfig.ExternalController = patchConfig.ExternalController targetConfig.ExternalController = patchConfig.ExternalController
targetConfig.ExternalUI = "" targetConfig.ExternalUI = ""
@@ -322,11 +393,11 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.ExternalUIURL = "" targetConfig.ExternalUIURL = ""
targetConfig.TCPConcurrent = patchConfig.TCPConcurrent targetConfig.TCPConcurrent = patchConfig.TCPConcurrent
targetConfig.UnifiedDelay = patchConfig.UnifiedDelay targetConfig.UnifiedDelay = patchConfig.UnifiedDelay
//targetConfig.GeodataMode = false
targetConfig.IPv6 = patchConfig.IPv6 targetConfig.IPv6 = patchConfig.IPv6
targetConfig.LogLevel = patchConfig.LogLevel targetConfig.LogLevel = patchConfig.LogLevel
targetConfig.Port = 0 targetConfig.Port = 0
targetConfig.SocksPort = 0 targetConfig.SocksPort = 0
targetConfig.KeepAliveInterval = patchConfig.KeepAliveInterval
targetConfig.MixedPort = patchConfig.MixedPort targetConfig.MixedPort = patchConfig.MixedPort
targetConfig.FindProcessMode = patchConfig.FindProcessMode targetConfig.FindProcessMode = patchConfig.FindProcessMode
targetConfig.AllowLan = patchConfig.AllowLan targetConfig.AllowLan = patchConfig.AllowLan
@@ -339,48 +410,90 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.Profile.StoreSelected = false targetConfig.Profile.StoreSelected = false
targetConfig.GeoXUrl = patchConfig.GeoXUrl targetConfig.GeoXUrl = patchConfig.GeoXUrl
targetConfig.GlobalUA = patchConfig.GlobalUA targetConfig.GlobalUA = patchConfig.GlobalUA
if targetConfig.DNS.Enable == false { genHosts(targetConfig.Hosts, patchConfig.Hosts)
if configParams.OverrideDns {
targetConfig.DNS = patchConfig.DNS targetConfig.DNS = patchConfig.DNS
} else {
if targetConfig.DNS.Enable == false {
targetConfig.DNS.Enable = true
}
} }
//if runtime.GOOS == "android" { //if runtime.GOOS == "android" {
// targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, "dhcp://"+dns.SystemDNSPlaceholder) // targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, "dhcp://"+dns.SystemDNSPlaceholder)
//} else if runtime.GOOS == "windows" { //} else if runtime.GOOS == "windows" {
// targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, dns.SystemDNSPlaceholder) // targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, dns.SystemDNSPlaceholder)
//} //}
if configParams.IsCompatible == false { //if configParams.IsCompatible == false {
targetConfig.ProxyProvider = make(map[string]map[string]any) // targetConfig.ProxyProvider = make(map[string]map[string]any)
targetConfig.RuleProvider = make(map[string]map[string]any) // targetConfig.RuleProvider = make(map[string]map[string]any)
generateProxyGroupAndRule(&targetConfig.ProxyGroup, &targetConfig.Rule) // generateProxyGroupAndRule(&targetConfig.ProxyGroup, &targetConfig.Rule)
} //}
} }
func patchConfig(general *config.General) { func patchConfig(general *config.General, controller *config.Controller) {
log.Infoln("[Apply] patch") log.Infoln("[Apply] patch")
route.ReStartServer(general.ExternalController) route.ReStartServer(controller.ExternalController)
listener.SetAllowLan(general.AllowLan)
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
inbound.SetAllowedIPs(general.LanAllowedIPs)
inbound.SetDisAllowedIPs(general.LanDisAllowedIPs)
listener.SetBindAddress(general.BindAddress)
tunnel.SetSniffing(general.Sniffing) tunnel.SetSniffing(general.Sniffing)
tunnel.SetFindProcessMode(general.FindProcessMode) tunnel.SetFindProcessMode(general.FindProcessMode)
dialer.SetTcpConcurrent(general.TCPConcurrent) dialer.SetTcpConcurrent(general.TCPConcurrent)
dialer.DefaultInterface.Store(general.Interface) dialer.DefaultInterface.Store(general.Interface)
adapter.UnifiedDelay.Store(general.UnifiedDelay) 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.ReCreateHTTP(general.Port, tunnel.Tunnel)
listener.ReCreateSocks(general.SocksPort, tunnel.Tunnel) listener.ReCreateSocks(general.SocksPort, tunnel.Tunnel)
listener.ReCreateRedir(general.RedirPort, tunnel.Tunnel) listener.ReCreateRedir(general.RedirPort, tunnel.Tunnel)
listener.ReCreateAutoRedir(general.EBpf.AutoRedir, tunnel.Tunnel)
listener.ReCreateTProxy(general.TProxyPort, tunnel.Tunnel) listener.ReCreateTProxy(general.TProxyPort, tunnel.Tunnel)
listener.ReCreateTun(general.Tun, tunnel.Tunnel)
listener.ReCreateMixed(general.MixedPort, tunnel.Tunnel) listener.ReCreateMixed(general.MixedPort, tunnel.Tunnel)
listener.ReCreateShadowSocks(general.ShadowSocksConfig, tunnel.Tunnel) listener.ReCreateShadowSocks(general.ShadowSocksConfig, tunnel.Tunnel)
listener.ReCreateVmess(general.VmessConfig, tunnel.Tunnel) listener.ReCreateVmess(general.VmessConfig, tunnel.Tunnel)
listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel) listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel)
tunnel.SetMode(general.Mode) listener.ReCreateTun(general.Tun, tunnel.Tunnel)
log.SetLevel(general.LogLevel) }
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() { func patchSelectGroup() {
@@ -408,12 +521,8 @@ func patchSelectGroup() {
} }
} }
var applyLock sync.Mutex func applyConfig() error {
cfg, err := config.ParseRawConfig(currentRawConfig)
func applyConfig() {
applyLock.Lock()
defer applyLock.Unlock()
cfg, err := config.ParseRawConfig(currentConfig)
if err != nil { if err != nil {
cfg, _ = config.ParseRawConfig(config.DefaultRawConfig()) cfg, _ = config.ParseRawConfig(config.DefaultRawConfig())
} }
@@ -421,11 +530,17 @@ func applyConfig() {
constant.DefaultTestURL = *configParams.TestURL constant.DefaultTestURL = *configParams.TestURL
} }
if configParams.IsPatch { if configParams.IsPatch {
patchConfig(cfg.General) patchConfig(cfg.General, cfg.Controller)
} else { } else {
closeConnections() closeConnections()
runtime.GC() runtime.GC()
hub.UltraApplyConfig(cfg, true) hub.UltraApplyConfig(cfg)
patchSelectGroup() patchSelectGroup()
} }
updateListeners(cfg.General, cfg.Listeners)
if isRunning {
hcCompatibleProvider(cfg.Providers)
}
externalProviders = getExternalProvidersRaw()
return err
} }

View File

@@ -7,31 +7,28 @@ replace github.com/metacubex/mihomo => ./Clash.Meta
require ( require (
github.com/Kr328/tun2socket v0.0.0-20220414050025-d07c78d06d34 github.com/Kr328/tun2socket v0.0.0-20220414050025-d07c78d06d34
github.com/metacubex/mihomo v1.17.1 github.com/metacubex/mihomo v1.17.1
github.com/miekg/dns v1.1.61 github.com/miekg/dns v1.1.62
golang.org/x/net v0.26.0 golang.org/x/sync v0.8.0
golang.org/x/sync v0.7.0
) )
require ( require (
github.com/3andne/restls-client-go v0.1.6 // indirect github.com/3andne/restls-client-go v0.1.6 // indirect
github.com/RyuaNerin/go-krypto v1.2.4 // indirect github.com/RyuaNerin/go-krypto v1.2.4 // indirect
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/ajg/form v1.5.1 // indirect github.com/ajg/form v1.5.1 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect github.com/andybalholm/brotli v1.0.6 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // 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/cloudflare/circl v1.3.7 // indirect
github.com/coreos/go-iptables v0.7.0 // 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/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gaukas/godicttls v0.0.4 // 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/cors v1.2.1 // indirect
github.com/go-chi/render v1.0.3 // indirect github.com/go-chi/render v1.0.3 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
@@ -39,31 +36,33 @@ require (
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // 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/btree v1.1.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/hashicorp/yamux v0.1.1 // 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/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // 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/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec // 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.46.1-0.20240807232329-1c6cb2d67f58 // indirect
github.com/metacubex/randv2 v0.2.0 // indirect github.com/metacubex/randv2 v0.2.0 // indirect
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72 // indirect github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 // indirect
github.com/metacubex/sing-shadowsocks v0.2.6 // indirect github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.0 // indirect github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
github.com/metacubex/sing-tun v0.2.7-0.20240627012306-9d1f5fc0b45e // indirect github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1 // indirect
github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f // 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/sing-wireguard v0.0.0-20240826061955-1e4e67afe5cd // indirect
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 // indirect github.com/metacubex/tfo-go v0.0.0-20240830120620-c5e019b67785 // indirect
github.com/metacubex/utls v1.6.6 // indirect github.com/metacubex/utls v1.6.6 // indirect
github.com/mroth/weightedrand/v2 v2.1.0 // indirect github.com/mroth/weightedrand/v2 v2.1.0 // indirect
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
@@ -72,18 +71,18 @@ require (
github.com/oschwald/maxminddb-golang v1.12.0 // indirect github.com/oschwald/maxminddb-golang v1.12.0 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // 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/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // 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/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/sagernet/sing v0.5.0-alpha.10 // indirect github.com/sagernet/sing v0.5.0-alpha.13 // indirect
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 // indirect github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 // indirect
github.com/sagernet/sing-shadowtls v0.1.4 // indirect github.com/sagernet/sing-shadowtls v0.1.4 // indirect
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e // 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/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
@@ -96,16 +95,18 @@ require (
github.com/vishvananda/netns v0.0.4 // indirect github.com/vishvananda/netns v0.0.4 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // indirect
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
go.uber.org/mock v0.4.0 // indirect go.uber.org/mock v0.4.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.24.0 // indirect golang.org/x/crypto v0.26.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
golang.org/x/mod v0.18.0 // indirect golang.org/x/mod v0.20.0 // indirect
golang.org/x/sys v0.21.0 // indirect golang.org/x/net v0.28.0 // indirect
golang.org/x/text v0.16.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/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 google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.3.0 // indirect lukechampine.com/blake3 v1.3.0 // indirect

View File

@@ -7,8 +7,6 @@ github.com/RyuaNerin/go-krypto v1.2.4 h1:mXuNdK6M317aPV0llW6Xpjbo4moOlPF7Yxz4tb4
github.com/RyuaNerin/go-krypto v1.2.4/go.mod h1:QqCYkoutU3yInyD9INt2PGolVRsc3W4oraQadVGXJ/8= github.com/RyuaNerin/go-krypto v1.2.4/go.mod h1:QqCYkoutU3yInyD9INt2PGolVRsc3W4oraQadVGXJ/8=
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok=
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
@@ -21,8 +19,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/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/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/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 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8= github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
@@ -30,8 +26,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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 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 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
@@ -42,14 +38,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/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 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA=
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok= 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 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 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 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= 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.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.0.14/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 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 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
@@ -67,8 +61,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/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 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= 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.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk=
github.com/gofrs/uuid/v5 v5.2.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= 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.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 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
@@ -84,20 +78,16 @@ 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 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= 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/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-20240812123929-b105c29bd1b5 h1:GkMacU5ftc+IEg1449N3UEy2XLDz58W4fkrRu2fibb8=
github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic= 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/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.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= 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 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 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 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= 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= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
@@ -108,32 +98,36 @@ 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/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 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= 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 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= 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 h1:HxreOiFTUrJXJautEo8rnE1uKTVGY8wtZepY1Tii/Nc=
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec/go.mod h1:8BVmQ+3cxjqzWElafm24rb2Ae4jRI6vAXNXWqWjfrXw= 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.46.1-0.20240807232329-1c6cb2d67f58 h1:T6OxROLZBr9SOQxN5TzUslv81hEREy/dEgaUKVjaG7U=
github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e/go.mod h1:Yza2H7Ax1rxWPUcJx0vW+oAt9EsPuSiyQFhFabUPzwU= github.com/metacubex/quic-go v0.46.1-0.20240807232329-1c6cb2d67f58/go.mod h1:Yza2H7Ax1rxWPUcJx0vW+oAt9EsPuSiyQFhFabUPzwU=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= 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-20240827003841-cd97758ed8b4 h1:HobpULaPK6OoxrHMmgcwLkwwIduXVmwdcznwUfH1GQM=
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8= github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
github.com/metacubex/sing-shadowsocks v0.2.6 h1:6oEB3QcsFYnNiFeoevcXrCwJ3sAablwVSgtE9R3QeFQ= github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4=
github.com/metacubex/sing-shadowsocks v0.2.6/go.mod h1:zIkMeSnb8Mbf4hdqhw0pjzkn1d99YJ3JQm/VBg5WMTg= github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
github.com/metacubex/sing-shadowsocks2 v0.2.0 h1:hqwT/AfI5d5UdPefIzR6onGHJfDXs5zgOM5QSgaM/9A= github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
github.com/metacubex/sing-shadowsocks2 v0.2.0/go.mod h1:LCKF6j1P94zN8ZS+LXRK1gmYTVGB3squivBSXAFnOg8= github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
github.com/metacubex/sing-tun v0.2.7-0.20240627012306-9d1f5fc0b45e h1:o+zohxPRo45P35fS9u1zfdBgr+L/7S0ObGU6YjbVBIc= github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1 h1:ypfofGDZbP8p3Y4P/m74JYu7sQViesi3c8nbmT6cS0Y=
github.com/metacubex/sing-tun v0.2.7-0.20240627012306-9d1f5fc0b45e/go.mod h1:WwJGbCx7bQcBzuQXiDOJvZH27R0kIjKNNlISIWsL6kM= github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1/go.mod h1:olbEx9yVcaw5tHTNlRamRoxmMKcvDvcVS1YLnQGzvWE=
github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f h1:QjXrHKbTMBip/C+R79bvbfr42xH1gZl3uFb0RELdZiQ= github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY= 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-20240826061955-1e4e67afe5cd h1:r7alry8u4qlUFLNMwGvG1A8ZcfPM6AMSmrm6E2yKdB4=
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a/go.mod h1:uY+BYb0UEknLrqvbGcwi9i++KgrKxsurysgI6G1Pveo= github.com/metacubex/sing-wireguard v0.0.0-20240826061955-1e4e67afe5cd/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-20240830120620-c5e019b67785 h1:NNmI+ZV0DzNuqaAInRQuZFLHlWVuyHeow8jYpdKjHjo=
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts= 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 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8=
github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo= 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.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ= 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 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU= 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= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
@@ -156,23 +150,21 @@ 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/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 h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 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.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
github.com/puzpuzpuz/xsync/v3 v3.2.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= 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 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= 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 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= 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/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
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/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= 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/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 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= 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.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing v0.5.0-alpha.10 h1:kuHl10gpjbKQAdQfyogQU3u0CVnpqC3wrAHe/+BFaXc= github.com/sagernet/sing v0.5.0-alpha.13 h1:fpR4TFZfu/9V3LbHSAnnnwcaXGMF8ijmAAPoY2WHSKw=
github.com/sagernet/sing v0.5.0-alpha.10/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= 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 h1:5bCAkvDDzSMITiHFjolBwpdqYsvycdTu71FsMEFXQ14=
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ= 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= github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
@@ -181,8 +173,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/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 h1:iGH0RMv2FzELOFNFQtvsxH7NPmlo7X5JizEK51UCojo=
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e/go.mod h1:YbL4TKHRR6APYQv3U2RGfwLDpPYSyWz6oUlpISBEzBE= 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.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= 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 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
@@ -218,6 +210,8 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 h1:UNrDfkQqiEYzdMlNsVvBYOAJWZjdktqFE9tQh5BT2+4=
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7/go.mod h1:E+rxHvJG9H6PUdzq9NRG6csuLN3XUx98BfGOVWNYnXs=
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo=
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
@@ -226,21 +220,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= 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-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.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= 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/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.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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-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.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.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= 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.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -257,18 +251,18 @@ 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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.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.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= 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.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 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 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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.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.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= 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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=

View File

@@ -5,38 +5,55 @@ package main
*/ */
import "C" import "C"
import ( import (
"context"
bridge "core/dart-bridge" bridge "core/dart-bridge"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/metacubex/mihomo/common/utils"
"os"
"runtime"
"sort"
"sync"
"time"
"unsafe"
"github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/adapter/outboundgroup"
"github.com/metacubex/mihomo/adapter/provider" "github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/common/structure" "github.com/metacubex/mihomo/component/updater"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/geodata"
"github.com/metacubex/mihomo/component/mmdb"
"github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant"
cp "github.com/metacubex/mihomo/constant/provider" cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/hub/executor" "github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
rp "github.com/metacubex/mihomo/rules/provider"
"github.com/metacubex/mihomo/tunnel" "github.com/metacubex/mihomo/tunnel"
"github.com/metacubex/mihomo/tunnel/statistic" "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 configParams = ConfigExtendedParams{}
var externalProviders = map[string]cp.Provider{}
var isInit = false var isInit = false
var currentProfileName = "" //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 //export initClash
func initClash(homeDirStr *C.char) bool { func initClash(homeDirStr *C.char) bool {
@@ -61,10 +78,10 @@ func restartClash() bool {
//export shutdownClash //export shutdownClash
func shutdownClash() bool { func shutdownClash() bool {
stopListeners()
executor.Shutdown() executor.Shutdown()
runtime.GC() runtime.GC()
isInit = false isInit = false
currentConfig = nil
return true return true
} }
@@ -76,16 +93,6 @@ func forceGc() {
}() }()
} }
//export setCurrentProfileName
func setCurrentProfileName(s *C.char) {
currentProfileName = C.GoString(s)
}
//export getCurrentProfileName
func getCurrentProfileName() *C.char {
return C.CString(currentProfileName)
}
//export validateConfig //export validateConfig
func validateConfig(s *C.char, port C.longlong) { func validateConfig(s *C.char, port C.longlong) {
i := int64(port) i := int64(port)
@@ -100,11 +107,15 @@ func validateConfig(s *C.char, port C.longlong) {
}() }()
} }
var updateLock sync.Mutex
//export updateConfig //export updateConfig
func updateConfig(s *C.char, port C.longlong) { func updateConfig(s *C.char, port C.longlong) {
i := int64(port) i := int64(port)
paramsString := C.GoString(s) paramsString := C.GoString(s)
go func() { go func() {
updateLock.Lock()
defer updateLock.Unlock()
var params = &GenerateConfigParams{} var params = &GenerateConfigParams{}
err := json.Unmarshal([]byte(paramsString), params) err := json.Unmarshal([]byte(paramsString), params)
if err != nil { if err != nil {
@@ -112,43 +123,23 @@ func updateConfig(s *C.char, port C.longlong) {
return return
} }
configParams = params.Params configParams = params.Params
prof := decorationConfig(params.ProfilePath, params.Config) prof := decorationConfig(params.ProfileId, params.Config)
currentConfig = prof currentRawConfig = prof
applyConfig() err = applyConfig()
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
bridge.SendToPort(i, "") bridge.SendToPort(i, "")
}() }()
} }
//export clearEffect //export clearEffect
func clearEffect(s *C.char) { func clearEffect(s *C.char) {
path := C.GoString(s) id := C.GoString(s)
go func() { go func() {
rawCfg := getRawConfigWithPath(&path) _ = removeFile(getProfilePath(id))
for _, mapping := range rawCfg.RuleProvider { _ = removeFile(getProfileProvidersPath(id))
schema := &ruleProviderSchema{}
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
if err := decoder.Decode(mapping, schema); err != nil {
return
}
if schema.Type == "http" {
_ = removeFile(constant.Path.Resolve(schema.Path))
}
}
for _, mapping := range rawCfg.ProxyProvider {
schema := &proxyProviderSchema{
HealthCheck: healthCheckSchema{
Lazy: true,
},
}
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
if err := decoder.Decode(mapping, schema); err != nil {
return
}
if schema.Type == "http" {
_ = removeFile(constant.Path.Resolve(schema.Path))
}
}
_ = removeFile(path)
}() }()
} }
@@ -164,35 +155,36 @@ func getProxies() *C.char {
//export changeProxy //export changeProxy
func changeProxy(s *C.char) { func changeProxy(s *C.char) {
paramsString := C.GoString(s) paramsString := C.GoString(s)
go func() { var params = &ChangeProxyParams{}
var params = &ChangeProxyParams{} err := json.Unmarshal([]byte(paramsString), params)
err := json.Unmarshal([]byte(paramsString), params) if err != nil {
if err != nil { log.Infoln("Unmarshal ChangeProxyParams %v", err)
log.Infoln("Unmarshal ChangeProxyParams %v", err) }
} groupName := *params.GroupName
groupName := *params.GroupName proxyName := *params.ProxyName
proxyName := *params.ProxyName proxies := tunnel.ProxiesWithProviders()
proxies := tunnel.ProxiesWithProviders() group, ok := proxies[groupName]
group, ok := proxies[groupName] if !ok {
if !ok { return
return }
} adapterProxy := group.(*adapter.Proxy)
adapterProxy := group.(*adapter.Proxy) selector, ok := adapterProxy.ProxyAdapter.(outboundgroup.SelectAble)
selector, ok := adapterProxy.ProxyAdapter.(*outboundgroup.Selector) if !ok {
if !ok { return
return }
} if proxyName == "" {
selector.ForceSet(proxyName)
} else {
err = selector.Set(proxyName) err = selector.Set(proxyName)
if err == nil { }
log.Infoln("[Selector] %s selected %s", groupName, proxyName) if err == nil {
} log.Infoln("[SelectAble] %s selected %s", groupName, proxyName)
}() }
} }
//export getTraffic //export getTraffic
func getTraffic() *C.char { func getTraffic() *C.char {
up, down := statistic.DefaultManager.Now() up, down := statistic.DefaultManager.Current(state.OnlyProxy)
traffic := map[string]int64{ traffic := map[string]int64{
"up": up, "up": up,
"down": down, "down": down,
@@ -207,7 +199,7 @@ func getTraffic() *C.char {
//export getTotalTraffic //export getTotalTraffic
func getTotalTraffic() *C.char { func getTotalTraffic() *C.char {
up, down := statistic.DefaultManager.Total() up, down := statistic.DefaultManager.Total(state.OnlyProxy)
traffic := map[string]int64{ traffic := map[string]int64{
"up": up, "up": up,
"down": down, "down": down,
@@ -229,16 +221,18 @@ func resetTraffic() {
func asyncTestDelay(s *C.char, port C.longlong) { func asyncTestDelay(s *C.char, port C.longlong) {
i := int64(port) i := int64(port)
paramsString := C.GoString(s) paramsString := C.GoString(s)
go func() { b.Go(paramsString, func() (bool, error) {
var params = &TestDelayParams{} var params = &TestDelayParams{}
err := json.Unmarshal([]byte(paramsString), params) err := json.Unmarshal([]byte(paramsString), params)
if err != nil { if err != nil {
return bridge.SendToPort(i, "")
return false, nil
} }
expectedStatus, err := utils.NewUnsignedRanges[uint16]("") expectedStatus, err := utils.NewUnsignedRanges[uint16]("")
if err != nil { if err != nil {
return bridge.SendToPort(i, "")
return false, nil
} }
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(params.Timeout)) ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(params.Timeout))
@@ -255,7 +249,7 @@ func asyncTestDelay(s *C.char, port C.longlong) {
delayData.Value = -1 delayData.Value = -1
data, _ := json.Marshal(delayData) data, _ := json.Marshal(delayData)
bridge.SendToPort(i, string(data)) bridge.SendToPort(i, string(data))
return return false, nil
} }
delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus) delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus)
@@ -263,14 +257,14 @@ func asyncTestDelay(s *C.char, port C.longlong) {
delayData.Value = -1 delayData.Value = -1
data, _ := json.Marshal(delayData) data, _ := json.Marshal(delayData)
bridge.SendToPort(i, string(data)) bridge.SendToPort(i, string(data))
return return false, nil
} }
delayData.Value = int32(delay) delayData.Value = int32(delay)
data, _ := json.Marshal(delayData) data, _ := json.Marshal(delayData)
bridge.SendToPort(i, string(data)) bridge.SendToPort(i, string(data))
return return false, nil
}() })
} }
//export getVersionInfo //export getVersionInfo
@@ -344,78 +338,67 @@ func getProvider(name *C.char) *C.char {
//export getExternalProviders //export getExternalProviders
func getExternalProviders() *C.char { func getExternalProviders() *C.char {
externalProviders := make([]ExternalProvider, 0) eps := make([]ExternalProvider, 0)
providers := tunnel.Providers() for _, p := range externalProviders {
for n, p := range providers { externalProvider, err := toExternalProvider(p)
if p.VehicleType() != cp.Compatible { if err != nil {
p := p.(*provider.ProxySetProvider) continue
externalProviders = append(externalProviders, ExternalProvider{
Name: n,
Type: p.Type().String(),
VehicleType: p.VehicleType().String(),
UpdateAt: p.UpdatedAt,
})
} }
eps = append(eps, *externalProvider)
} }
for n, p := range tunnel.RuleProviders() { sort.Sort(ExternalProviders(eps))
if p.VehicleType() != cp.Compatible { data, err := json.Marshal(eps)
p := p.(*rp.RuleSetProvider)
externalProviders = append(externalProviders, ExternalProvider{
Name: n,
Type: p.Type().String(),
VehicleType: p.VehicleType().String(),
UpdateAt: p.UpdatedAt,
})
}
}
data, err := json.Marshal(externalProviders)
if err != nil { if err != nil {
return C.CString("") return C.CString("")
} }
return C.CString(string(data)) return C.CString(string(data))
} }
//export updateExternalProvider //export getExternalProvider
func updateExternalProvider(providerName *C.char, providerType *C.char, port C.longlong) { func getExternalProvider(name *C.char) *C.char {
externalProviderName := C.GoString(name)
externalProvider, exist := externalProviders[externalProviderName]
if !exist {
return C.CString("")
}
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 updateGeoData
func updateGeoData(geoType *C.char, geoName *C.char, port C.longlong) {
i := int64(port) i := int64(port)
providerNameString := C.GoString(providerName) geoTypeString := C.GoString(geoType)
providerTypeString := C.GoString(providerType) geoNameString := C.GoString(geoName)
go func() { go func() {
switch providerTypeString { switch geoTypeString {
case "Proxy":
providers := tunnel.Providers()
err := providers[providerNameString].Update()
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "Rule":
providers := tunnel.RuleProviders()
err := providers[providerNameString].Update()
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "MMDB": case "MMDB":
err := mmdb.DownloadMMDB(constant.Path.Resolve(providerNameString)) err := updater.UpdateMMDB(constant.Path.Resolve(geoNameString))
if err != nil { if err != nil {
bridge.SendToPort(i, err.Error()) bridge.SendToPort(i, err.Error())
return return
} }
case "ASN": case "ASN":
err := mmdb.DownloadASN(constant.Path.Resolve(providerNameString)) err := updater.UpdateASN(constant.Path.Resolve(geoNameString))
if err != nil { if err != nil {
bridge.SendToPort(i, err.Error()) bridge.SendToPort(i, err.Error())
return return
} }
case "GeoIp": case "GeoIp":
err := geodata.DownloadGeoIP(constant.Path.Resolve(providerNameString)) err := updater.UpdateGeoIp(constant.Path.Resolve(geoNameString))
if err != nil { if err != nil {
bridge.SendToPort(i, err.Error()) bridge.SendToPort(i, err.Error())
return return
} }
case "GeoSite": case "GeoSite":
err := geodata.DownloadGeoSite(constant.Path.Resolve(providerNameString)) err := updater.UpdateGeoSite(constant.Path.Resolve(geoNameString))
if err != nil { if err != nil {
bridge.SendToPort(i, err.Error()) bridge.SendToPort(i, err.Error())
return return
@@ -425,6 +408,45 @@ func updateExternalProvider(providerName *C.char, providerType *C.char, port C.l
}() }()
} }
//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 //export initNativeApiBridge
func initNativeApiBridge(api unsafe.Pointer) { func initNativeApiBridge(api unsafe.Pointer) {
bridge.InitDartApi(api) bridge.InitDartApi(api)
@@ -462,7 +484,7 @@ func init() {
Data: c, Data: c,
}) })
} }
executor.DefaultProxyProviderLoadedHook = func(providerName string) { executor.DefaultProviderLoadedHook = func(providerName string) {
SendMessage(Message{ SendMessage(Message{
Type: LoadedMessage, Type: LoadedMessage,
Data: providerName, Data: providerName,

View File

@@ -1,5 +1,3 @@
//go:build android
package main package main
import "C" import "C"
@@ -16,16 +14,24 @@ type AccessControl struct {
} }
type AndroidProps struct { type AndroidProps struct {
Enable bool `json:"enable"`
AccessControl *AccessControl `json:"accessControl"` AccessControl *AccessControl `json:"accessControl"`
AllowBypass bool `json:"allowBypass"` AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"` SystemProxy bool `json:"systemProxy"`
} }
var androidProps AndroidProps type State struct {
AndroidProps
CurrentProfileName string `json:"currentProfileName"`
MixedPort int `json:"mixedPort"`
OnlyProxy bool `json:"onlyProxy"`
}
//export getAndroidProps var state State
func getAndroidProps() *C.char {
data, err := json.Marshal(androidProps) //export getState
func getState() *C.char {
data, err := json.Marshal(state)
if err != nil { if err != nil {
fmt.Println("Error:", err) fmt.Println("Error:", err)
return C.CString("") return C.CString("")
@@ -33,10 +39,10 @@ func getAndroidProps() *C.char {
return C.CString(string(data)) return C.CString(string(data))
} }
//export setAndroidProps //export setState
func setAndroidProps(s *C.char) { func setState(s *C.char) {
paramsString := C.GoString(s) paramsString := C.GoString(s)
err := json.Unmarshal([]byte(paramsString), &androidProps) err := json.Unmarshal([]byte(paramsString), &state)
if err != nil { if err != nil {
return return
} }

View File

@@ -7,14 +7,15 @@ import (
"core/platform" "core/platform"
t "core/tun" t "core/tun"
"errors" "errors"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore"
"strconv" "strconv"
"sync" "sync"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
"time" "time"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore"
) )
var tunLock sync.Mutex var tunLock sync.Mutex
@@ -40,6 +41,18 @@ var fdMap FdMap
func startTUN(fd C.int, port C.longlong) { func startTUN(fd C.int, port C.longlong) {
i := int64(port) i := int64(port)
ServicePort = i ServicePort = i
if fd == 0 {
tunLock.Lock()
defer tunLock.Unlock()
now := time.Now()
runTime = &now
SendMessage(Message{
Type: StartedMessage,
Data: strconv.FormatInt(runTime.UnixMilli(), 10),
})
return
}
initSocketHook()
go func() { go func() {
tunLock.Lock() tunLock.Lock()
defer tunLock.Unlock() defer tunLock.Unlock()
@@ -88,6 +101,7 @@ func getRunTime() *C.char {
//export stopTun //export stopTun
func stopTun() { func stopTun() {
removeSocketHook()
go func() { go func() {
tunLock.Lock() tunLock.Lock()
defer tunLock.Unlock() defer tunLock.Unlock()
@@ -125,7 +139,7 @@ func markSocket(fd Fd) {
var fdCounter int64 = 0 var fdCounter int64 = 0
func init() { func initSocketHook() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error { dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() { if platform.ShouldBlockConnection() {
return errBlocked return errBlocked
@@ -159,3 +173,7 @@ func init() {
}) })
} }
} }
func removeSocketHook() {
dialer.DefaultSocketHook = nil
}

View File

@@ -67,7 +67,6 @@ func Start(fd int, gateway, portal, dns string) (io.Closer, error) {
for stack.TCP().SetDeadline(time.Time{}) == nil { for stack.TCP().SetDeadline(time.Time{}) == nil {
conn, err := stack.TCP().Accept() conn, err := stack.TCP().Accept()
if err != nil { if err != nil {
log.Errorln("Accept connection: %v", err)
continue continue
} }
lAddr := conn.LocalAddr().(*net.TCPAddr) lAddr := conn.LocalAddr().(*net.TCPAddr)

View File

@@ -4,6 +4,7 @@ import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_clash/l10n/l10n.dart'; import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/proxy_container.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
@@ -88,7 +89,6 @@ class ApplicationState extends State<Application> {
} }
await globalState.appController.init(); await globalState.appController.init();
globalState.appController.initLink(); globalState.appController.initLink();
_updateGroups();
}); });
} }
@@ -96,7 +96,9 @@ class ApplicationState extends State<Application> {
if (system.isDesktop) { if (system.isDesktop) {
return WindowContainer( return WindowContainer(
child: TrayContainer( child: TrayContainer(
child: app, child: ProxyContainer(
child: app,
),
), ),
); );
} }
@@ -107,6 +109,17 @@ class ApplicationState extends State<Application> {
); );
} }
_buildPage(Widget page) {
if (system.isDesktop) {
return WindowHeaderContainer(
child: page,
);
}
return VpnContainer(
child: page,
);
}
_updateSystemColorSchemes( _updateSystemColorSchemes(
ColorScheme? lightDynamic, ColorScheme? lightDynamic,
ColorScheme? darkDynamic, ColorScheme? darkDynamic,
@@ -120,74 +133,66 @@ 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.updateGroups();
},
);
}
@override @override
Widget build(context) { Widget build(context) {
return AppStateContainer( return _buildApp(
child: ClashMessageContainer( AppStateContainer(
child: Selector2<AppState, Config, ApplicationSelectorState>( child: ClashContainer(
selector: (_, appState, config) => ApplicationSelectorState( child: Selector2<AppState, Config, ApplicationSelectorState>(
locale: config.locale, selector: (_, appState, config) => ApplicationSelectorState(
themeMode: config.themeMode, locale: config.locale,
primaryColor: config.primaryColor, 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 MediaContainer(
child: _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(),
), ),
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,
),
),
darkTheme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
),
),
home: child,
);
},
);
},
child: const HomePage(),
), ),
), ),
); );

View File

@@ -100,22 +100,6 @@ class ClashCore {
); );
} }
setProfileName(String profileName) {
final profileNameChar = profileName.toNativeUtf8().cast<Char>();
clashFFI.setCurrentProfileName(
profileNameChar,
);
malloc.free(profileNameChar);
}
getProfileName() {
final currentProfileNameRaw = clashFFI.getCurrentProfileName();
final currentProfileName =
currentProfileNameRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(currentProfileNameRaw);
return currentProfileName;
}
Future<List<Group>> getProxiesGroups() { Future<List<Group>> getProxiesGroups() {
final proxiesRaw = clashFFI.getProxies(); final proxiesRaw = clashFFI.getProxies();
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString(); final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
@@ -128,8 +112,7 @@ class ClashCore {
UsedProxy.GLOBAL.name, UsedProxy.GLOBAL.name,
...(proxies[UsedProxy.GLOBAL.name]["all"] as List).where((e) { ...(proxies[UsedProxy.GLOBAL.name]["all"] as List).where((e) {
final proxy = proxies[e] ?? {}; final proxy = proxies[e] ?? {};
return GroupTypeExtension.valueList.contains(proxy['type']) && return GroupTypeExtension.valueList.contains(proxy['type']);
proxy['hidden'] != true;
}) })
]; ];
final groupsRaw = groupNames.map((groupName) { final groupsRaw = groupNames.map((groupName) {
@@ -142,7 +125,11 @@ class ClashCore {
.toList(); .toList();
return group; return group;
}).toList(); }).toList();
return groupsRaw.map((e) => Group.fromJson(e)).toList(); return groupsRaw
.map(
(e) => Group.fromJson(e),
)
.toList();
}); });
} }
@@ -162,9 +149,46 @@ class ClashCore {
}); });
} }
Future<String> updateExternalProvider({ ExternalProvider? getExternalProvider(String externalProviderName) {
final externalProviderNameChar =
externalProviderName.toNativeUtf8().cast<Char>();
final externalProviderRaw =
clashFFI.getExternalProvider(externalProviderNameChar);
malloc.free(externalProviderNameChar);
final externalProviderRawString =
externalProviderRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(externalProviderRaw);
if(externalProviderRawString.isEmpty) return null;
return ExternalProvider.fromJson(json.decode(externalProviderRawString));
}
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 providerName,
required String providerType, required String data,
}) { }) {
final completer = Completer<String>(); final completer = Completer<String>();
final receiver = ReceivePort(); final receiver = ReceivePort();
@@ -175,14 +199,34 @@ class ClashCore {
} }
}); });
final providerNameChar = providerName.toNativeUtf8().cast<Char>(); final providerNameChar = providerName.toNativeUtf8().cast<Char>();
final providerTypeChar = providerType.toNativeUtf8().cast<Char>(); final dataChar = data.toNativeUtf8().cast<Char>();
clashFFI.updateExternalProvider( 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, providerNameChar,
providerTypeChar,
receiver.sendPort.nativePort, receiver.sendPort.nativePort,
); );
malloc.free(providerNameChar); malloc.free(providerNameChar);
malloc.free(providerTypeChar);
return completer.future; return completer.future;
} }
@@ -193,6 +237,14 @@ class ClashCore {
malloc.free(paramsChar); malloc.free(paramsChar);
} }
start() {
clashFFI.start();
}
stop() {
clashFFI.stop();
}
Future<Delay> getDelay(String proxyName) { Future<Delay> getDelay(String proxyName) {
final delayParams = { final delayParams = {
"proxy-name": proxyName, "proxy-name": proxyName,
@@ -213,21 +265,13 @@ class ClashCore {
receiver.sendPort.nativePort, receiver.sendPort.nativePort,
); );
malloc.free(delayParamsChar); malloc.free(delayParamsChar);
Future.delayed(httpTimeoutDuration + moreDuration, () {
receiver.close();
if (!completer.isCompleted) {
completer.complete(
Delay(name: proxyName, value: -1),
);
}
});
return completer.future; return completer.future;
} }
clearEffect(String path) { clearEffect(String profileId) {
final pathChar = path.toNativeUtf8().cast<Char>(); final profileIdChar = profileId.toNativeUtf8().cast<Char>();
clashFFI.clearEffect(pathChar); clashFFI.clearEffect(profileIdChar);
malloc.free(pathChar); malloc.free(profileIdChar);
} }
VersionInfo getVersionInfo() { VersionInfo getVersionInfo() {
@@ -237,19 +281,19 @@ class ClashCore {
return VersionInfo.fromJson(versionInfo); return VersionInfo.fromJson(versionInfo);
} }
setProps(Props props) { setState(CoreState state) {
final propsChar = json.encode(props).toNativeUtf8().cast<Char>(); final stateChar = json.encode(state).toNativeUtf8().cast<Char>();
clashFFI.setAndroidProps(propsChar); clashFFI.setState(stateChar);
malloc.free(propsChar); malloc.free(stateChar);
} }
Props getProps() { CoreState getState() {
final androidPropsRaw = clashFFI.getAndroidProps(); final stateRaw = clashFFI.getState();
final androidProps = json.decode( final state = json.decode(
androidPropsRaw.cast<Utf8>().toDartString(), stateRaw.cast<Utf8>().toDartString(),
); );
clashFFI.freeCString(androidPropsRaw); clashFFI.freeCString(stateRaw);
return Props.fromJson(androidProps); return CoreState.fromJson(state);
} }
Traffic getTraffic() { Traffic getTraffic() {

View File

@@ -5144,6 +5144,22 @@ class ClashFFI {
late final __FCmulcr = late final __FCmulcr =
__FCmulcrPtr.asFunction<_Fcomplex Function(_Fcomplex, double)>(); __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( int initClash(
ffi.Pointer<ffi.Char> homeDirStr, ffi.Pointer<ffi.Char> homeDirStr,
) { ) {
@@ -5190,30 +5206,6 @@ class ClashFFI {
_lookup<ffi.NativeFunction<ffi.Void Function()>>('forceGc'); _lookup<ffi.NativeFunction<ffi.Void Function()>>('forceGc');
late final _forceGc = _forceGcPtr.asFunction<void Function()>(); late final _forceGc = _forceGcPtr.asFunction<void Function()>();
void setCurrentProfileName(
ffi.Pointer<ffi.Char> s,
) {
return _setCurrentProfileName(
s,
);
}
late final _setCurrentProfileNamePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'setCurrentProfileName');
late final _setCurrentProfileName = _setCurrentProfileNamePtr
.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> getCurrentProfileName() {
return _getCurrentProfileName();
}
late final _getCurrentProfileNamePtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getCurrentProfileName');
late final _getCurrentProfileName =
_getCurrentProfileNamePtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void validateConfig( void validateConfig(
ffi.Pointer<ffi.Char> s, ffi.Pointer<ffi.Char> s,
int port, int port,
@@ -5409,24 +5401,76 @@ class ClashFFI {
late final _getExternalProviders = late final _getExternalProviders =
_getExternalProvidersPtr.asFunction<ffi.Pointer<ffi.Char> Function()>(); _getExternalProvidersPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
ffi.Pointer<ffi.Char> getExternalProvider(
ffi.Pointer<ffi.Char> name,
) {
return _getExternalProvider(
name,
);
}
late final _getExternalProviderPtr = _lookup<
ffi.NativeFunction<
ffi.Pointer<ffi.Char> Function(
ffi.Pointer<ffi.Char>)>>('getExternalProvider');
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( void updateExternalProvider(
ffi.Pointer<ffi.Char> providerName, ffi.Pointer<ffi.Char> providerName,
ffi.Pointer<ffi.Char> providerType,
int port, int port,
) { ) {
return _updateExternalProvider( return _updateExternalProvider(
providerName, providerName,
providerType,
port, port,
); );
} }
late final _updateExternalProviderPtr = _lookup< 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.NativeFunction<
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.LongLong)>>('updateExternalProvider'); ffi.LongLong)>>('sideLoadExternalProvider');
late final _updateExternalProvider = _updateExternalProviderPtr.asFunction< late final _sideLoadExternalProvider =
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>(); _sideLoadExternalProviderPtr.asFunction<
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
void initNativeApiBridge( void initNativeApiBridge(
ffi.Pointer<ffi.Void> api, ffi.Pointer<ffi.Void> api,
@@ -5499,29 +5543,28 @@ class ClashFFI {
late final _setProcessMap = late final _setProcessMap =
_setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>(); _setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> getAndroidProps() { ffi.Pointer<ffi.Char> getState() {
return _getAndroidProps(); return _getState();
} }
late final _getAndroidPropsPtr = late final _getStatePtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>( _lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('getState');
'getAndroidProps'); late final _getState =
late final _getAndroidProps = _getStatePtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
_getAndroidPropsPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void setAndroidProps( void setState(
ffi.Pointer<ffi.Char> s, ffi.Pointer<ffi.Char> s,
) { ) {
return _setAndroidProps( return _setState(
s, s,
); );
} }
late final _setAndroidPropsPtr = late final _setStatePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>( _lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'setAndroidProps'); 'setState');
late final _setAndroidProps = late final _setState =
_setAndroidPropsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>(); _setStatePtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
void startTUN( void startTUN(
int fd, int fd,

28
lib/common/archive.dart Normal file
View File

@@ -0,0 +1,28 @@
import 'dart:convert';
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:path/path.dart';
extension ArchiveExt on Archive {
addDirectoryToArchive(String dirPath, String parentPath) {
final dir = Directory(dirPath);
final entities = dir.listSync(recursive: false);
for (final entity in entities) {
final relativePath = relative(entity.path, from: parentPath);
if (entity is File) {
final data = entity.readAsBytesSync();
final archiveFile = ArchiveFile(relativePath, data.length, data);
addFile(archiveFile);
} else if (entity is Directory) {
addDirectoryToArchive(entity.path, parentPath);
}
}
}
add<T>(String name, T raw) {
final data = json.encode(raw);
addFile(
ArchiveFile(name, data.length, data),
);
}
}

View File

@@ -16,4 +16,21 @@ extension ColorExtension on Color {
toLittle() { toLittle() {
return withOpacity(0.03); return withOpacity(0.03);
} }
}
Color darken([double amount = .1]) {
assert(amount >= 0 && amount <= 1);
final hsl = HSLColor.fromColor(this);
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
return hslDark.toColor();
}
}
extension ColorSchemeExtension on ColorScheme {
ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack
? copyWith(
surface: Colors.black,
background: Colors.black,
surfaceContainer: surfaceContainer.darken(0.05),
)
: this;
}

View File

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

View File

@@ -1,4 +1,3 @@
import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
@@ -8,7 +7,7 @@ import 'system.dart';
const appName = "FlClash"; const appName = "FlClash";
const coreName = "clash.meta"; const coreName = "clash.meta";
const packageName = "FlClash"; const packageName = "com.follow.clash";
const httpTimeoutDuration = Duration(milliseconds: 5000); const httpTimeoutDuration = Duration(milliseconds: 5000);
const moreDuration = Duration(milliseconds: 100); const moreDuration = Duration(milliseconds: 100);
const animateDuration = Duration(milliseconds: 100); const animateDuration = Duration(milliseconds: 100);
@@ -17,7 +16,7 @@ const mmdbFileName = "geoip.metadb";
const asnFileName = "ASN.mmdb"; const asnFileName = "ASN.mmdb";
const geoIpFileName = "GeoIP.dat"; const geoIpFileName = "GeoIP.dat";
const geoSiteFileName = "GeoSite.dat"; const geoSiteFileName = "GeoSite.dat";
final double kHeaderHeight = system.isDesktop ? (Platform.isMacOS ? 28 : 40) : 0; final double kHeaderHeight = system.isDesktop ? 40 : 0;
const GeoXMap defaultGeoXMap = { const GeoXMap defaultGeoXMap = {
"mmdb": "mmdb":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb", "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",

View File

@@ -1,16 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:typed_data';
import 'package:fl_clash/common/common.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/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:path/path.dart';
import 'package:webdav_client/webdav_client.dart'; import 'package:webdav_client/webdav_client.dart';
class DAVClient { class DAVClient {
late Client client; late Client client;
Completer<bool> pingCompleter = Completer(); Completer<bool> pingCompleter = Completer();
late String fileName;
DAVClient(DAV dav) { DAVClient(DAV dav) {
client = newClient( client = newClient(
@@ -18,6 +16,7 @@ class DAVClient {
user: dav.user, user: dav.user,
password: dav.password, password: dav.password,
); );
fileName = dav.fileName;
client.setHeaders( client.setHeaders(
{ {
'accept-charset': 'utf-8', 'accept-charset': 'utf-8',
@@ -33,8 +32,6 @@ class DAVClient {
Future<bool> _ping() async { Future<bool> _ping() async {
try { try {
await client.ping(); await client.ping();
await client.mkdir("/$appName");
await client.mkdir("/$appName/$profilesDirectoryName");
return true; return true;
} catch (_) { } catch (_) {
return false; return false;
@@ -43,65 +40,17 @@ class DAVClient {
get root => "/$appName"; get root => "/$appName";
get remoteConfig => "$root/$configKey.json"; get backupFile => "$root/$fileName";
get remoteClashConfig => "$root/$clashConfigKey.json"; backup(Uint8List data) async {
get remoteProfiles => "$root/$profilesDirectoryName";
backup() async {
final appController = globalState.appController;
final config = appController.config;
final clashConfig = appController.clashConfig;
await client.mkdir("$root"); await client.mkdir("$root");
client.write( await client.write("$backupFile", data);
remoteConfig,
utf8.encode(
json.encode(config.toJson()),
),
);
client.write(
remoteClashConfig,
utf8.encode(
json.encode(clashConfig.toJson()),
),
);
await client.remove(remoteProfiles);
for (final profile in config.profiles) {
final path = await appPath.getProfilePath(profile.id);
if (path == null) continue;
await client.writeFromFile(
path,
"$remoteProfiles/${basename(path)}",
);
}
return true; return true;
} }
recovery({required RecoveryOption recoveryOption}) async { Future<List<int>> recovery() async {
final profiles = await client.readDir(remoteProfiles); await client.mkdir("$root");
final profilesPath = await appPath.getProfilesPath(); final data = await client.read(backupFile);
for (final file in profiles) { return data;
await client.read2File(
"$remoteProfiles/${file.name}",
join(
profilesPath,
file.name,
),
);
}
final configRaw = utf8.decode((await client.read(remoteConfig)));
final clashConfigRaw = utf8.decode(await client.read(remoteClashConfig));
final config = Config.fromJson(json.decode(configRaw));
final clashConfig = ClashConfig.fromJson(json.decode(clashConfigRaw));
if(recoveryOption == RecoveryOption.onlyProfiles){
globalState.appController.config.update(config, RecoveryOption.onlyProfiles);
}else{
globalState.appController.config.update(config, RecoveryOption.all);
globalState.appController.clashConfig.update(clashConfig);
}
await globalState.appController.applyProfile();
globalState.appController.savePreferences();
return true;
} }
} }

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

@@ -0,0 +1,21 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import '../state.dart';
class FlClashHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
final client = super.createHttpClient(context);
client.badCertificateCallback = (_, __, ___) => true;
client.findProxy = (url) {
debugPrint("find $url");
final port = globalState.appController.clashConfig.mixedPort;
final isStart = globalState.appController.appState.isStart;
if (!isStart) return "DIRECT";
return "PROXY localhost:$port";
};
return client;
}
}

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

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

View File

@@ -1,9 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:fl_clash/models/models.dart' hide Process;
import 'package:launch_at_startup/launch_at_startup.dart'; import 'package:launch_at_startup/launch_at_startup.dart';
import 'constant.dart'; import 'constant.dart';
import 'system.dart'; import 'system.dart';
import 'windows.dart';
class AutoLaunch { class AutoLaunch {
static AutoLaunch? _instance; static AutoLaunch? _instance;
@@ -24,18 +26,77 @@ class AutoLaunch {
return await launchAtStartup.isEnabled(); 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 { Future<bool> enable() async {
if (Platform.isWindows) {
await windowsDisable();
}
return await launchAtStartup.enable(); 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 { Future<bool> disable() async {
return await launchAtStartup.disable(); return await launchAtStartup.disable();
} }
updateStatus(bool value) async { updateStatus(AutoLaunchState state) async {
final isEnable = await this.isEnable; final isOpenTun = state.isOpenTun;
if (isEnable == value) return; final isAutoLaunch = state.isAutoLaunch;
if (value == true) { 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(); enable();
} else { } else {
disable(); disable();

View File

@@ -2,4 +2,17 @@ extension ListExtension<T> on List<T> {
List<T> intersection(List<T> list) { List<T> intersection(List<T> list) {
return where((item) => list.contains(item)).toList(); 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,21 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class Measure { class Measure {
Measure.of(this.context); final TextScaler _textScale;
late BuildContext context;
final _textScaleFactor = Measure.of(this.context) : _textScale = MediaQuery.of(context).textScaler;
WidgetsBinding.instance.platformDispatcher.textScaleFactor;
Size computeTextSize(Text text) { Size computeTextSize(Text text) {
final textPainter = TextPainter( final textPainter = TextPainter(
text: TextSpan(text: text.data, style: text.style), text: TextSpan(text: text.data, style: text.style),
maxLines: text.maxLines, maxLines: text.maxLines,
textScaler: TextScaler.linear(_textScaleFactor), textScaler: _textScale,
textDirection: text.textDirection ?? TextDirection.ltr, textDirection: text.textDirection ?? TextDirection.ltr,
)..layout(); )..layout();
return textPainter.size; return textPainter.size;
} }
late BuildContext context;
double? _bodyMediumHeight; double? _bodyMediumHeight;
double? _bodySmallHeight; double? _bodySmallHeight;
double? _labelSmallHeight; double? _labelSmallHeight;

View File

@@ -44,7 +44,7 @@ class Navigation {
modes: [NavigationItemMode.desktop, NavigationItemMode.more], modes: [NavigationItemMode.desktop, NavigationItemMode.more],
), ),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.swap_vert_circle), icon: Icon(Icons.storage),
label: "resources", label: "resources",
description: "resourcesDesc", description: "resourcesDesc",
keep: false, keep: false,

View File

@@ -1,11 +1,12 @@
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'dart:math';
import 'dart:typed_data'; 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:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lpinyin/lpinyin.dart';
import 'package:zxing2/qrcode.dart'; import 'package:zxing2/qrcode.dart';
import 'package:image/image.dart' as img; import 'package:image/image.dart' as img;
@@ -83,7 +84,7 @@ class Other {
if (charA == charB) { if (charA == charB) {
return sortByChar(a.substring(1), b.substring(1)); return sortByChar(a.substring(1), b.substring(1));
} else { } else {
return charA.compareTo(charB); return charA.compareToLower(charB);
} }
} }
@@ -130,6 +131,12 @@ class Other {
return build1.compareTo(build2); return build1.compareTo(build2);
} }
String getPinyin(String value) {
return value.isNotEmpty
? PinyinHelper.getFirstWordPinyin(value.substring(0, 1))
: "";
}
Future<String?> parseQRCode(Uint8List? bytes) { Future<String?> parseQRCode(Uint8List? bytes) {
return Isolate.run<String?>(() { return Isolate.run<String?>(() {
if (bytes == null) return null; if (bytes == null) return null;
@@ -191,22 +198,21 @@ class Other {
return ViewMode.desktop; return ViewMode.desktop;
} }
int getColumns(ViewMode viewMode, int currentColumns) { int getProxiesColumns(double viewWidth, ProxiesLayout proxiesLayout) {
final targetColumnsArray = viewModeColumnsMap[viewMode]!; final columns = max((viewWidth / 300).ceil(), 2);
if (targetColumnsArray.contains(currentColumns)) { return switch (proxiesLayout) {
return currentColumns; ProxiesLayout.tight => columns - 1,
} ProxiesLayout.standard => columns,
return targetColumnsArray.first; ProxiesLayout.loose => columns + 1,
};
} }
String getColumnsTextForInt(int number){ int getProfilesColumns(double viewWidth) {
return switch(number){ return max((viewWidth / 400).floor(), 1);
1 => appLocalizations.oneColumn, }
2 => appLocalizations.twoColumns,
3 => appLocalizations.threeColumns, String getBackupFileName() {
4 => appLocalizations.fourColumns, return "${appName}_backup_${DateTime.now().show}.zip";
int() => throw UnimplementedError(),
};
} }
} }

View File

@@ -9,6 +9,7 @@ import 'constant.dart';
class AppPath { class AppPath {
static AppPath? _instance; static AppPath? _instance;
Completer<Directory> cacheDir = Completer(); Completer<Directory> cacheDir = Completer();
Completer<Directory> downloadDir = Completer();
// Future<Directory> _createDesktopCacheDir() async { // Future<Directory> _createDesktopCacheDir() async {
// final path = join(dirname(Platform.resolvedExecutable), 'cache'); // final path = join(dirname(Platform.resolvedExecutable), 'cache');
@@ -23,6 +24,9 @@ class AppPath {
getApplicationSupportDirectory().then((value) { getApplicationSupportDirectory().then((value) {
cacheDir.complete(value); cacheDir.complete(value);
}); });
getDownloadsDirectory().then((value) {
downloadDir.complete(value);
});
// if (Platform.isAndroid) { // if (Platform.isAndroid) {
// getApplicationSupportDirectory().then((value) { // getApplicationSupportDirectory().then((value) {
// cacheDir.complete(value); // cacheDir.complete(value);
@@ -39,6 +43,11 @@ class AppPath {
return _instance!; return _instance!;
} }
Future<String> getDownloadDirPath() async {
final directory = await downloadDir.future;
return directory.path;
}
Future<String> getHomeDirPath() async { Future<String> getHomeDirPath() async {
final directory = await cacheDir.future; final directory = await cacheDir.future;
return directory.path; return directory.path;

View File

@@ -1,22 +1,31 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
class Picker { class Picker {
Future<PlatformFile?> pickerConfigFile() async { Future<PlatformFile?> pickerFile() async {
final filePickerResult = await FilePicker.platform.pickFiles( final filePickerResult = await FilePicker.platform.pickFiles(
withData: true, withData: true,
allowMultiple: false, allowMultiple: false,
initialDirectory: await appPath.getDownloadDirPath(),
); );
return filePickerResult?.files.first; return filePickerResult?.files.first;
} }
Future<PlatformFile?> pickerGeoDataFile() async { Future<String?> saveFile(String fileName, Uint8List bytes) async {
final filePickerResult = await FilePicker.platform.pickFiles( final path = await FilePicker.platform.saveFile(
withData: true, fileName: fileName,
allowMultiple: false, initialDirectory: await appPath.getDownloadDirPath(),
bytes: Platform.isAndroid ? bytes : null,
); );
return filePickerResult?.files.first; if (!Platform.isAndroid && path != null) {
final file = await File(path).create(recursive: true);
await file.writeAsBytes(bytes);
}
return path;
} }
Future<String?> pickerConfigQRCode() async { Future<String?> pickerConfigQRCode() async {

View File

@@ -1,37 +1,4 @@
import 'package:fl_clash/common/datetime.dart'; import 'package:fl_clash/common/system.dart';
import 'package:fl_clash/plugins/proxy.dart'; import 'package:proxy/proxy.dart';
import 'package:proxy/proxy.dart' as proxy_plugin;
import 'package:proxy/proxy_platform_interface.dart';
class ProxyManager { final proxy = system.isDesktop ? Proxy() : null;
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();

View File

@@ -1,66 +1,57 @@
import 'dart:io'; import 'dart:math';
import 'dart:typed_data';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/ip.dart'; import 'package:fl_clash/models/ip.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/cupertino.dart';
class Request { class Request {
late final Dio _dio; late final Dio _dio;
int? _port; String? userAgent;
bool _isStart = false;
Request() { Request() {
_dio = Dio(); _dio = Dio();
_dio.options = BaseOptions(
headers: {"User-Agent": globalState.appController.clashConfig.globalUa},
);
_dio.interceptors.add( _dio.interceptors.add(
InterceptorsWrapper( InterceptorsWrapper(
onRequest: (options, handler) { onRequest: (options, handler) {
_updateAdapter();
return handler.next(options); // 继续请求 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 { Future<Response> getFileResponseForUrl(String url) async {
final response = await _dio final response = await _dio
.get( .get(
url, url,
options: Options( options: Options(
headers: {
"User-Agent": globalState.appController.clashConfig.globalUa
},
responseType: ResponseType.bytes, responseType: ResponseType.bytes,
), ),
) )
.timeout( .timeout(
httpTimeoutDuration * 2, httpTimeoutDuration * 6,
); );
return response; 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 { Future<Map<String, dynamic>?> checkForUpdate() async {
final response = await _dio.get( final response = await _dio.get(
"https://api.github.com/repos/$repository/releases/latest", "https://api.github.com/repos/$repository/releases/latest",
@@ -85,11 +76,14 @@ class Request {
"https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson, "https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
}; };
Future<IpInfo?> checkIp(CancelToken? cancelToken) async { Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
for (final source in _ipInfoSources.entries) { for (final source in _ipInfoSources.entries.toList()..shuffle(Random())) {
try { try {
final response = await _dio final response = await _dio
.get<Map<String, dynamic>>(source.key, cancelToken: cancelToken) .get<Map<String, dynamic>>(
source.key,
cancelToken: cancelToken,
)
.timeout( .timeout(
httpTimeoutDuration, httpTimeoutDuration,
); );
@@ -97,6 +91,9 @@ class Request {
return source.value(response.data!); return source.value(response.data!);
} }
} catch (e) { } catch (e) {
if (cancelToken?.isCancelled == true) {
throw "cancelled";
}
continue; 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 { bool get isUrl {
return RegExp(r'^(http|https|ftp)://').hasMatch(this); return RegExp(r'^(http|https|ftp)://').hasMatch(this);
} }
int compareToLower(String other) {
return toLowerCase().compareTo(
other.toLowerCase(),
);
}
} }

View File

@@ -18,6 +18,12 @@ class System {
bool get isDesktop => bool get isDesktop =>
Platform.isWindows || Platform.isMacOS || Platform.isLinux; 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;
}
back() async { back() async {
await app?.moveTaskToBack(); await app?.moveTaskToBack();
await window?.hide(); await window?.hide();

View File

@@ -20,6 +20,7 @@ class Window {
WindowOptions windowOptions = WindowOptions( WindowOptions windowOptions = WindowOptions(
size: Size(props.width, props.height), size: Size(props.width, props.height),
minimumSize: const Size(380, 500), minimumSize: const Size(380, 500),
windowButtonVisibility: false,
titleBarStyle: TitleBarStyle.hidden, titleBarStyle: TitleBarStyle.hidden,
); );
if (props.left != null || props.top != null) { if (props.left != null || props.top != null) {

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

@@ -1,8 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:archive/archive.dart';
import 'package:fl_clash/common/archive.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:path/path.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -15,9 +21,10 @@ class AppController {
late AppState appState; late AppState appState;
late Config config; late Config config;
late ClashConfig clashConfig; late ClashConfig clashConfig;
late Measure measure;
late Function updateClashConfigDebounce; late Function updateClashConfigDebounce;
late Function updateGroupDebounce;
late Function addCheckIpNumDebounce; late Function addCheckIpNumDebounce;
late Function applyProfileDebounce;
AppController(this.context) { AppController(this.context) {
appState = context.read<AppState>(); appState = context.read<AppState>();
@@ -26,16 +33,20 @@ class AppController {
updateClashConfigDebounce = debounce<Function()>(() async { updateClashConfigDebounce = debounce<Function()>(() async {
await updateClashConfig(); await updateClashConfig();
}); });
applyProfileDebounce = debounce<Function()>(() async {
await applyProfile(isPrue: true);
});
addCheckIpNumDebounce = debounce(() { addCheckIpNumDebounce = debounce(() {
appState.checkIpNum++; appState.checkIpNum++;
}); });
measure = Measure.of(context); updateGroupDebounce = debounce(() async {
await updateGroups();
});
} }
Future<void> updateSystemProxy(bool isStart) async { updateStatus(bool isStart) async {
if (isStart) { if (isStart) {
await globalState.startSystemProxy( await globalState.handleStart(
appState: appState,
config: config, config: config,
clashConfig: clashConfig, clashConfig: clashConfig,
); );
@@ -45,12 +56,14 @@ class AppController {
updateRunTime, updateRunTime,
updateTraffic, updateTraffic,
]; ];
applyProfileDebounce();
} else { } else {
await globalState.stopSystemProxy(); await globalState.handleStop();
clashCore.resetTraffic(); clashCore.resetTraffic();
appState.traffics = []; appState.traffics = [];
appState.totalTraffic = Traffic(); appState.totalTraffic = Traffic();
appState.runTime = null; appState.runTime = null;
addCheckIpNumDebounce();
} }
} }
@@ -59,8 +72,9 @@ class AppController {
} }
updateRunTime() { updateRunTime() {
if (proxyManager.startTime != null) { final startTime = globalState.startTime;
final startTimeStamp = proxyManager.startTime!.millisecondsSinceEpoch; if (startTime != null) {
final startTimeStamp = startTime.millisecondsSinceEpoch;
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch; final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
appState.runTime = nowTimeStamp - startTimeStamp; appState.runTime = nowTimeStamp - startTimeStamp;
} else { } else {
@@ -82,22 +96,22 @@ class AppController {
deleteProfile(String id) async { deleteProfile(String id) async {
config.deleteProfileById(id); config.deleteProfileById(id);
final profilePath = await appPath.getProfilePath(id); clashCore.clearEffect(id);
if (profilePath == null) return;
clashCore.clearEffect(profilePath);
if (config.currentProfileId == id) { if (config.currentProfileId == id) {
if (config.profiles.isNotEmpty) { if (config.profiles.isNotEmpty) {
final updateId = config.profiles.first.id; final updateId = config.profiles.first.id;
changeProfile(updateId); changeProfile(updateId);
} else { } else {
updateSystemProxy(false); updateStatus(false);
} }
} }
} }
Future<void> updateProfile(Profile profile) async { Future<void> updateProfile(Profile profile) async {
await profile.update(); final newProfile = await profile.update();
config.setProfile(await profile.update()); config.setProfile(
newProfile.copyWith(isUpdating: false),
);
} }
Future<void> updateClashConfig({bool isPatch = true}) async { Future<void> updateClashConfig({bool isPatch = true}) async {
@@ -108,32 +122,30 @@ class AppController {
); );
} }
Future applyProfile() async { Future applyProfile({bool isPrue = false}) async {
final commonScaffoldState = globalState.homeScaffoldKey.currentState; if (isPrue) {
if (commonScaffoldState?.mounted != true) return;
commonScaffoldState?.loadingRun(() async {
await globalState.applyProfile( await globalState.applyProfile(
appState: appState, appState: appState,
config: config, config: config,
clashConfig: clashConfig, clashConfig: clashConfig,
); );
}); } else {
} final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
Future rawApplyProfile() async { await commonScaffoldState?.loadingRun(() async {
await globalState.applyProfile( await globalState.applyProfile(
appState: appState, appState: appState,
config: config, config: config,
clashConfig: clashConfig, clashConfig: clashConfig,
); );
});
}
addCheckIpNumDebounce();
} }
changeProfile(String? value) async { changeProfile(String? value) async {
if (value == config.currentProfileId) return; if (value == config.currentProfileId) return;
config.currentProfileId = value; config.currentProfileId = value;
await applyProfile();
appState.delayMap = {};
saveConfigPreferences();
} }
autoUpdateProfiles() async { autoUpdateProfiles() async {
@@ -192,8 +204,19 @@ class AppController {
await preferences.saveClashConfig(clashConfig); await preferences.saveClashConfig(clashConfig);
} }
changeProxy({
required String groupName,
required String proxyName,
}) {
globalState.changeProxy(
config: config,
groupName: groupName,
proxyName: proxyName,
);
addCheckIpNumDebounce();
}
handleBackOrExit() async { handleBackOrExit() async {
print(config.isMinimizeOnExit);
if (config.isMinimizeOnExit) { if (config.isMinimizeOnExit) {
if (system.isDesktop) { if (system.isDesktop) {
await savePreferences(); await savePreferences();
@@ -205,7 +228,8 @@ class AppController {
} }
handleExit() async { handleExit() async {
await updateSystemProxy(false); await updateStatus(false);
await proxy?.stopProxy();
await savePreferences(); await savePreferences();
clashCore.shutdown(); clashCore.shutdown();
system.exit(); system.exit();
@@ -274,36 +298,26 @@ class AppController {
if (!config.silentLaunch) { if (!config.silentLaunch) {
window?.show(); window?.show();
} }
final commonScaffoldState = globalState.homeScaffoldKey.currentState; if (Platform.isAndroid) {
if (commonScaffoldState?.mounted == true) { globalState.updateStartTime();
await commonScaffoldState?.loadingRun(() async {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
}, title: appLocalizations.init);
} else {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
} }
await afterInit(); if (globalState.isStart) {
} await updateStatus(true);
afterInit() async {
await proxyManager.updateStartTime();
if (proxyManager.isStart) {
await updateSystemProxy(true);
} else { } else {
await updateSystemProxy(config.autoRun); await updateStatus(config.autoRun);
} }
autoUpdateProfiles(); autoUpdateProfiles();
autoCheckUpdate(); autoCheckUpdate();
} }
updateTray() {
globalState.updateTray(
appState: appState,
config: config,
clashConfig: clashConfig,
);
}
setDelay(Delay delay) { setDelay(Delay delay) {
appState.setDelay(delay); appState.setDelay(delay);
} }
@@ -362,6 +376,10 @@ class AppController {
); );
} }
showSnackBar(String message) {
globalState.showSnackBar(context, message: message);
}
addProfileFormURL(String url) async { addProfileFormURL(String url) async {
if (globalState.navigatorKey.currentState?.canPop() ?? false) { if (globalState.navigatorKey.currentState?.canPop() ?? false) {
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst); globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
@@ -383,7 +401,11 @@ class AppController {
} }
addProfileFormFile() async { addProfileFormFile() async {
final platformFile = await globalState.safeRun(picker.pickerConfigFile); final platformFile = await globalState.safeRun(picker.pickerFile);
final bytes = platformFile?.bytes;
if (bytes == null) {
return null;
}
if (!context.mounted) return; if (!context.mounted) return;
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst); globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
toProfiles(); toProfiles();
@@ -392,10 +414,6 @@ class AppController {
final profile = await commonScaffoldState?.loadingRun<Profile?>( final profile = await commonScaffoldState?.loadingRun<Profile?>(
() async { () async {
await Future.delayed(const Duration(milliseconds: 300)); await Future.delayed(const Duration(milliseconds: 300));
final bytes = platformFile?.bytes;
if (bytes == null) {
return null;
}
return await Profile.normal(label: platformFile?.name).saveFile(bytes); return await Profile.normal(label: platformFile?.name).saveFile(bytes);
}, },
title: "${appLocalizations.add}${appLocalizations.profile}", title: "${appLocalizations.add}${appLocalizations.profile}",
@@ -411,8 +429,6 @@ class AppController {
addProfileFormURL(url); addProfileFormURL(url);
} }
int get columns => other.getColumns(appState.viewMode, config.proxiesColumns);
updateViewWidth(double width) { updateViewWidth(double width) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
appState.viewWidth = width; appState.viewWidth = width;
@@ -422,7 +438,10 @@ class AppController {
List<Proxy> _sortOfName(List<Proxy> proxies) { List<Proxy> _sortOfName(List<Proxy> proxies) {
return List.of(proxies) return List.of(proxies)
..sort( ..sort(
(a, b) => other.sortByChar(a.name, b.name), (a, b) => other.sortByChar(
other.getPinyin(a.name),
other.getPinyin(b.name),
),
); );
} }
@@ -456,6 +475,67 @@ class AppController {
String getCurrentSelectedName(String groupName) { String getCurrentSelectedName(String groupName) {
final group = appState.getGroupWithName(groupName); final group = appState.getGroupWithName(groupName);
return config.currentSelectedMap[groupName] ?? group?.now ?? ''; return group?.getCurrentSelectedName(
config.currentSelectedMap[groupName] ?? '') ??
'';
}
Future<List<int>> backupData() async {
final homeDirPath = await appPath.getHomeDirPath();
final profilesPath = await appPath.getProfilesPath();
final configJson = config.toJson();
final clashConfigJson = clashConfig.toJson();
return Isolate.run<List<int>>(() async {
final archive = Archive();
archive.add("config.json", configJson);
archive.add("clashConfig.json", clashConfigJson);
await archive.addDirectoryToArchive(profilesPath, homeDirPath);
final zipEncoder = ZipEncoder();
return zipEncoder.encode(archive) ?? [];
});
}
recoveryData(
List<int> data,
RecoveryOption recoveryOption,
) async {
final archive = await Isolate.run<Archive>(() {
final zipDecoder = ZipDecoder();
return zipDecoder.decodeBytes(data);
});
final homeDirPath = await appPath.getHomeDirPath();
final configs =
archive.files.where((item) => item.name.endsWith(".json")).toList();
final profiles =
archive.files.where((item) => !item.name.endsWith(".json"));
final configIndex =
configs.indexWhere((config) => config.name == "config.json");
final clashConfigIndex =
configs.indexWhere((config) => config.name == "clashConfig.json");
if (configIndex == -1 || clashConfigIndex == -1) throw "invalid backup.zip";
final configFile = configs[configIndex];
final clashConfigFile = configs[clashConfigIndex];
final tempConfig = Config.fromJson(
json.decode(
utf8.decode(configFile.content),
),
);
final tempClashConfig = ClashConfig.fromJson(
json.decode(
utf8.decode(clashConfigFile.content),
),
);
for (final profile in profiles) {
final filePath = join(homeDirPath, profile.name);
final file = File(filePath);
await file.create(recursive: true);
await file.writeAsBytes(profile.content);
}
if (recoveryOption == RecoveryOption.onlyProfiles) {
config.update(tempConfig, RecoveryOption.onlyProfiles);
} else {
config.update(tempConfig, RecoveryOption.all);
clashConfig.update(tempClashConfig);
}
} }
} }

View File

@@ -1,5 +1,7 @@
// ignore_for_file: constant_identifier_names // ignore_for_file: constant_identifier_names
import 'package:freezed_annotation/freezed_annotation.dart';
enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay } enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay }
enum GroupName { GLOBAL, Proxy, Auto, Fallback } enum GroupName { GLOBAL, Proxy, Auto, Fallback }
@@ -52,6 +54,8 @@ enum TunStack { gvisor, system, mixed }
enum AccessControlMode { acceptSelected, rejectSelected } enum AccessControlMode { acceptSelected, rejectSelected }
enum AccessSortType { none, name, time }
enum ProfileType { file, url } enum ProfileType { file, url }
enum ResultType { success, error } enum ResultType { success, error }
@@ -84,4 +88,17 @@ enum CommonCardType { plain, filled }
enum ProxiesType { tab, list } enum ProxiesType { tab, list }
enum ProxiesLayout { loose, standard, tight }
enum ProxyCardType { expand, shrink, min } enum ProxyCardType { expand, shrink, min }
enum DnsMode {
normal,
@JsonValue("fake-ip")
fakeIp,
@JsonValue("redir-host")
redirHost,
hosts
}

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/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.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/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.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 { class AccessFragment extends StatefulWidget {
const AccessFragment({super.key}); const AccessFragment({super.key});
@@ -23,198 +18,164 @@ class AccessFragment extends StatefulWidget {
} }
class _AccessFragmentState extends State<AccessFragment> { class _AccessFragmentState extends State<AccessFragment> {
final packagesListenable = ValueNotifier<List<Package>>([]); List<String> acceptList = [];
List<String> rejectList = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_updateInitList();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(const Duration(milliseconds: 300), () async { final appState = globalState.appController.appState;
packagesListenable.value = await app?.getPackages() ?? []; if (appState.packages.isEmpty) {
}); Future.delayed(const Duration(milliseconds: 300), () async {
appState.packages = await app?.getPackages() ?? [];
});
}
}); });
} }
@override _updateInitList() {
void dispose() { final accessControl = globalState.appController.config.accessControl;
super.dispose(); acceptList = accessControl.acceptList;
packagesListenable.dispose(); rejectList = accessControl.rejectList;
} }
Widget _buildAppProxyModePopup() { Widget _buildSearchButton() {
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,
);
},
);
}
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) {
return IconButton( return IconButton(
tooltip: appLocalizations.search, tooltip: appLocalizations.search,
onPressed: () { onPressed: () {
showSearch( showSearch(
context: context, context: context,
delegate: AccessControlSearchDelegate( delegate: AccessControlSearchDelegate(
packages: packages, acceptList: acceptList,
rejectList: rejectList,
), ),
).then((_) => {setState(() {})}); ).then((_) => setState(() {
_updateInitList();
}));
}, },
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
); );
} }
Widget _buildSelectedAllButton({ Widget _buildSelectedAllButton({
required bool isAccessControl,
required bool isSelectedAll, required bool isSelectedAll,
required List<String> allValueList, required List<String> allValueList,
}) { }) {
final tooltip = isSelectedAll final tooltip = isSelectedAll
? appLocalizations.cancelSelectAll ? appLocalizations.cancelSelectAll
: appLocalizations.selectAll; : appLocalizations.selectAll;
return AbsorbPointer( return IconButton(
absorbing: !isAccessControl, tooltip: tooltip,
child: FloatingActionButton( onPressed: () {
tooltip: tooltip, final config = globalState.appController.config;
onPressed: () { final isAccept =
final config = globalState.appController.config; config.accessControl.mode == AccessControlMode.acceptSelected;
final isAccept = if (isSelectedAll) {
config.accessControl.mode == AccessControlMode.acceptSelected; config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith(
if (isSelectedAll) { acceptList: [],
config.accessControl = switch (isAccept) { ),
true => config.accessControl.copyWith( false => config.accessControl.copyWith(
acceptList: [], rejectList: [],
), ),
false => config.accessControl.copyWith( };
rejectList: [], } else {
), config.accessControl = switch (isAccept) {
}; true => config.accessControl.copyWith(
} else { acceptList: allValueList,
config.accessControl = switch (isAccept) { ),
true => config.accessControl.copyWith( false => config.accessControl.copyWith(
acceptList: allValueList, rejectList: allValueList,
), ),
false => config.accessControl.copyWith( };
rejectList: allValueList, }
), },
}; icon: isSelectedAll
} ? const Icon(Icons.deselect)
}, : const Icon(Icons.select_all),
child: isSelectedAll
? const Icon(Icons.deselect)
: const Icon(Icons.select_all),
),
); );
} }
Widget _buildPackageList() { Widget _buildSettingButton() {
return ValueListenableBuilder( return IconButton(
valueListenable: packagesListenable, onPressed: () {
builder: (_, packages, ___) { showSheet(
final accessControl = globalState.appController.config.accessControl; title: appLocalizations.proxiesSetting,
final acceptList = accessControl.acceptList; context: context,
final rejectList = accessControl.rejectList; builder: (_) {
final acceptPackages = packages.sorted((a, b) { return AccessControlWidget(
final isSelectA = acceptList.contains(a.packageName); context: context,
final isSelectB = acceptList.contains(b.packageName); );
if (isSelectA && isSelectB) return 0; },
if (isSelectA) return -1; );
if (isSelectB) return 1; },
return 0; icon: const Icon(Icons.tune),
}); );
final rejectPackages = packages.sorted((a, b) { }
final isSelectA = rejectList.contains(a.packageName);
final isSelectB = rejectList.contains(b.packageName); @override
if (isSelectA && isSelectB) return 0; Widget build(BuildContext context) {
if (isSelectA) return -1; return Selector<Config, bool>(
if (isSelectB) return 1; selector: (_, config) => config.isAccessControl,
return 0; builder: (_, isAccessControl, child) {
}); return Column(
return Selector<Config, PackageListSelectorState>( mainAxisSize: MainAxisSize.max,
selector: (_, config) => PackageListSelectorState( children: [
accessControl: config.accessControl, Flexible(
isAccessControl: config.isAccessControl, flex: 0,
), child: ListItem.switchItem(
builder: (context, state, __) { title: Text(appLocalizations.appAccessControl),
final accessControl = state.accessControl; delegate: SwitchDelegate(
final isAccessControl = state.isAccessControl; value: isAccessControl,
final isFilterSystemApp = accessControl.isFilterSystemApp; onChanged: (isAccessControl) {
final accessControlMode = accessControl.mode; final config = context.read<Config>();
final packages = config.isAccessControl = isAccessControl;
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: FloatLayout(
floatingWidget: FloatWrapper(
child: _buildSelectedAllButton(
isAccessControl: isAccessControl,
isSelectedAll: valueList.length == packageNameList.length,
allValueList: packageNameList,
),
), ),
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Divider(
height: 12,
),
),
Flexible(
child: child!,
),
],
);
},
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( child: Column(
children: [ children: [
AbsorbPointer( AbsorbPointer(
@@ -285,9 +246,18 @@ class _AccessFragmentState extends State<AccessFragment> {
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
Flexible( Flexible(
child: _buildSearchButton(currentPackages)), child: _buildSearchButton(),
Flexible(child: _buildFilterSystemAppButton()), ),
Flexible(child: _buildAppProxyModePopup()), Flexible(
child: _buildSelectedAllButton(
isSelectedAll: valueList.length ==
packageNameList.length,
allValueList: packageNameList,
),
),
Flexible(
child: _buildSettingButton(),
),
], ],
), ),
], ],
@@ -296,92 +266,52 @@ class _AccessFragmentState extends State<AccessFragment> {
), ),
Expanded( Expanded(
flex: 1, flex: 1,
child: FadeBox( child: packages.isEmpty
key: const Key("fade_box"), ? const Center(
child: currentPackages.isEmpty child: CircularProgressIndicator(),
? const Center( )
child: CircularProgressIndicator(), : ListView.builder(
) itemCount: packages.length,
: ListView.builder( itemBuilder: (_, index) {
itemCount: currentPackages.length, final package = packages[index];
itemBuilder: (_, index) { return PackageListItem(
final package = currentPackages[index]; key: Key(package.packageName),
return PackageListItem( package: package,
key: Key(package.packageName), value:
package: package, valueList.contains(package.packageName),
value: isActive: isAccessControl,
valueList.contains(package.packageName), onChanged: (value) {
isActive: isAccessControl, if (value == true) {
onChanged: (value) { valueList.add(package.packageName);
if (value == true) { } else {
valueList.add(package.packageName); valueList.remove(package.packageName);
} else { }
valueList.remove(package.packageName); final config =
} globalState.appController.config;
final config = if (accessControlMode ==
globalState.appController.config; AccessControlMode.acceptSelected) {
if (accessControlMode == config.accessControl =
AccessControlMode.acceptSelected) { config.accessControl.copyWith(
config.accessControl = acceptList: valueList,
config.accessControl.copyWith( );
acceptList: valueList, } else {
); config.accessControl =
} else { config.accessControl.copyWith(
config.accessControl = rejectList: valueList,
config.accessControl.copyWith( );
rejectList: valueList, }
); },
} );
}, },
); ),
},
),
),
), ),
], ],
), ),
), );
); },
}, );
); },
}, ),
);
}
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.isAccessControl,
builder: (_, isAccessControl, child) {
return Column(
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
flex: 0,
child: ListItem.switchItem(
title: Text(appLocalizations.appAccessControl),
delegate: SwitchDelegate(
value: isAccessControl,
onChanged: (isAccessControl) {
final config = context.read<Config>();
config.isAccessControl = isAccessControl;
},
),
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Divider(
height: 12,
),
),
Flexible(
child: child!,
),
],
);
},
child: _buildPackageList(),
); );
} }
} }
@@ -448,23 +378,14 @@ class PackageListItem extends StatelessWidget {
} }
class AccessControlSearchDelegate extends SearchDelegate { class AccessControlSearchDelegate extends SearchDelegate {
final List<Package> packages; List<String> acceptList = [];
List<String> rejectList = [];
AccessControlSearchDelegate({ 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 @override
List<Widget>? buildActions(BuildContext context) { List<Widget>? buildActions(BuildContext context) {
return [ return [
@@ -494,26 +415,39 @@ class AccessControlSearchDelegate extends SearchDelegate {
); );
} }
Widget _packageList(List<Package> packages) { Widget _packageList() {
return Selector<Config, PackageListSelectorState>( final lowQuery = query.toLowerCase();
selector: (_, config) => PackageListSelectorState( return Selector2<AppState, Config, PackageListSelectorState>(
selector: (_, appState, config) => PackageListSelectorState(
packages: appState.packages,
accessControl: config.accessControl, accessControl: config.accessControl,
isAccessControl: config.isAccessControl, isAccessControl: config.isAccessControl,
), ),
builder: (context, state, __) { builder: (context, state, __) {
final accessControl = state.accessControl; final accessControl = state.accessControl;
final isAccessControl = state.isAccessControl;
final accessControlMode = accessControl.mode; 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 currentList = accessControl.currentList;
final packageNameList = final packageNameList = packages.map((e) => e.packageName).toList();
this.packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList); final valueList = currentList.intersection(packageNameList);
return DisabledMask( return DisabledMask(
status: !isAccessControl, status: !isAccessControl,
child: ListView.builder( child: ListView.builder(
itemCount: packages.length, itemCount: queryPackages.length,
itemBuilder: (_, index) { itemBuilder: (_, index) {
final package = packages[index]; final package = queryPackages[index];
return PackageListItem( return PackageListItem(
key: Key(package.packageName), key: Key(package.packageName),
package: package, package: package,
@@ -551,6 +485,268 @@ class AccessControlSearchDelegate extends SearchDelegate {
@override @override
Widget buildSuggestions(BuildContext context) { 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, selector: (_, config) => config.autoRun,
builder: (_, autoRun, child) { builder: (_, autoRun, child) {
return ListItem.switchItem( return ListItem.switchItem(
leading: const Icon(Icons.start), leading: const Icon(Icons.not_started),
title: Text(appLocalizations.autoRun), title: Text(appLocalizations.autoRun),
subtitle: Text(appLocalizations.autoRunDesc), subtitle: Text(appLocalizations.autoRunDesc),
delegate: SwitchDelegate( delegate: SwitchDelegate(

View File

@@ -1,3 +1,5 @@
import 'dart:typed_data';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/dav_client.dart'; import 'package:fl_clash/common/dav_client.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
@@ -10,16 +12,9 @@ import 'package:fl_clash/widgets/text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class BackupAndRecovery extends StatefulWidget { class BackupAndRecovery extends StatelessWidget {
const BackupAndRecovery({super.key}); const BackupAndRecovery({super.key});
@override
State<BackupAndRecovery> createState() => _BackupAndRecoveryState();
}
class _BackupAndRecoveryState extends State<BackupAndRecovery> {
DAVClient? _client;
_showAddWebDAV(DAV? dav) async { _showAddWebDAV(DAV? dav) async {
await globalState.showCommonDialog<String>( await globalState.showCommonDialog<String>(
child: WebDAVFormDialog( child: WebDAVFormDialog(
@@ -28,11 +23,15 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
); );
} }
_backup() async { _backupOnWebDAV(BuildContext context, DAVClient client) async {
final commonScaffoldState = context.commonScaffoldState; final commonScaffoldState = context.commonScaffoldState;
final res = await commonScaffoldState?.loadingRun<bool>(() async { final res = await commonScaffoldState?.loadingRun<bool>(
return await _client?.backup(); () async {
}); final backupData = await globalState.appController.backupData();
return await client.backup(Uint8List.fromList(backupData));
},
title: appLocalizations.backup,
);
if (res != true) return; if (res != true) return;
globalState.showMessage( globalState.showMessage(
title: appLocalizations.backup, title: appLocalizations.backup,
@@ -40,11 +39,20 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
); );
} }
_recovery(RecoveryOption recoveryOption) async { _recoveryOnWebDAV(
BuildContext context,
DAVClient client,
RecoveryOption recoveryOption,
) async {
final commonScaffoldState = context.commonScaffoldState; final commonScaffoldState = context.commonScaffoldState;
final res = await commonScaffoldState?.loadingRun<bool>(() async { final res = await commonScaffoldState?.loadingRun<bool>(
return await _client?.recovery(recoveryOption: recoveryOption); () async {
}); final data = await client.recovery();
await globalState.appController.recoveryData(data, recoveryOption);
return true;
},
title: appLocalizations.recovery,
);
if (res != true) return; if (res != true) return;
globalState.showMessage( globalState.showMessage(
title: appLocalizations.recovery, title: appLocalizations.recovery,
@@ -52,12 +60,66 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
); );
} }
_handleRecovery() async { _handleRecoveryOnWebDAV(BuildContext context, DAVClient client) async {
final recoveryOption = await globalState.showCommonDialog<RecoveryOption>( final recoveryOption = await globalState.showCommonDialog<RecoveryOption>(
child: const RecoveryOptionsDialog(), child: const RecoveryOptionsDialog(),
); );
if (recoveryOption == null) return; if (recoveryOption == null || !context.mounted) return;
_recovery(recoveryOption); _recoveryOnWebDAV(context, client, recoveryOption);
}
_backupOnLocal(BuildContext context) async {
final commonScaffoldState = context.commonScaffoldState;
final res = await commonScaffoldState?.loadingRun<bool>(
() async {
final backupData = await globalState.appController.backupData();
final value = await picker.saveFile(
other.getBackupFileName(),
Uint8List.fromList(backupData),
);
if (value == null) return false;
return true;
},
title: appLocalizations.backup,
);
if (res != true) return;
globalState.showMessage(
title: appLocalizations.backup,
message: TextSpan(text: appLocalizations.backupSuccess),
);
}
_recoveryOnLocal(
BuildContext context,
RecoveryOption recoveryOption,
) async {
final file = await picker.pickerFile();
final data = file?.bytes;
if (data == null || !context.mounted) return;
final commonScaffoldState = context.commonScaffoldState;
final res = await commonScaffoldState?.loadingRun<bool>(
() async {
await globalState.appController.recoveryData(
List<int>.from(data),
recoveryOption,
);
return true;
},
title: appLocalizations.recovery,
);
if (res != true) return;
globalState.showMessage(
title: appLocalizations.recovery,
message: TextSpan(text: appLocalizations.recoverySuccess),
);
}
_handleRecoveryOnLocal(BuildContext context) async {
final recoveryOption = await globalState.showCommonDialog<RecoveryOption>(
child: const RecoveryOptionsDialog(),
);
if (recoveryOption == null || !context.mounted) return;
_recoveryOnLocal(context, recoveryOption);
} }
@override @override
@@ -65,12 +127,11 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
return Selector<Config, DAV?>( return Selector<Config, DAV?>(
selector: (_, config) => config.dav, selector: (_, config) => config.dav,
builder: (_, dav, __) { builder: (_, dav, __) {
if (dav == null) { final client = dav != null ? DAVClient(dav) : null;
return ListView( return ListView(
children: [ children: [
ListHeader( ListHeader(title: appLocalizations.remote),
title: appLocalizations.account, if (dav == null)
),
ListItem( ListItem(
leading: const Icon(Icons.account_box), leading: const Icon(Icons.account_box),
title: Text(appLocalizations.noInfo), title: Text(appLocalizations.noInfo),
@@ -83,95 +144,113 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
appLocalizations.bind, appLocalizations.bind,
), ),
), ),
)
else ...[
ListItem(
leading: const Icon(Icons.account_box),
title: TooltipText(
text: Text(
dav.user,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(appLocalizations.connectivity),
FutureBuilder<bool>(
future: client!.pingCompleter.future,
builder: (_, snapshot) {
return Center(
child: FadeBox(
child: snapshot.connectionState ==
ConnectionState.waiting
? const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 1,
),
)
: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: snapshot.data == true
? Colors.green
: Colors.red,
),
width: 12,
height: 12,
),
),
);
},
),
],
),
),
trailing: FilledButton.tonal(
onPressed: () {
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.edit,
),
),
),
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);
},
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.remoteBackupDesc),
),
ListItem(
onTap: () {
_handleRecoveryOnWebDAV(context, client);
},
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.remoteRecoveryDesc),
), ),
], ],
); ListHeader(title: appLocalizations.local),
}
_client = DAVClient(dav);
final pingFuture = _client!.pingCompleter.future;
return ListView(
children: [
ListHeader(title: appLocalizations.account),
ListItem( ListItem(
leading: const Icon(Icons.account_box), onTap: () {
title: TooltipText( _backupOnLocal(context);
text: Text(
dav.user,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(appLocalizations.connectivity),
FutureBuilder<bool>(
future: pingFuture,
builder: (_, snapshot) {
return Center(
child: FadeBox(
key: const Key("fade_box_1"),
child: snapshot.connectionState == ConnectionState.waiting
? const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 1,
),
)
: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: snapshot.data == true
? Colors.green
: Colors.red,
),
width: 12,
height: 12,
),
),
);
},
),
],
),
),
trailing: FilledButton.tonal(
onPressed: () {
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.edit,
),
),
),
FutureBuilder<bool>(
future: pingFuture,
builder: (_, snapshot) {
return FadeBox(
key: const Key("fade_box_2"),
child: snapshot.data == true
? Column(
children: [
ListHeader(
title: appLocalizations.backupAndRecovery),
ListItem(
onTap: _backup,
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.backupDesc),
),
ListItem(
onTap: _handleRecovery,
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.recoveryDesc),
),
],
)
: Container(),
);
}, },
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.localBackupDesc),
),
ListItem(
onTap: () {
_handleRecoveryOnLocal(context);
},
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.localRecoveryDesc),
), ),
], ],
); );
@@ -180,6 +259,50 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
} }
} }
class RecoveryOptionsDialog extends StatefulWidget {
const RecoveryOptionsDialog({super.key});
@override
State<RecoveryOptionsDialog> createState() => _RecoveryOptionsDialogState();
}
class _RecoveryOptionsDialogState extends State<RecoveryOptionsDialog> {
_handleOnTab(RecoveryOption? value) {
if (value == null) return;
Navigator.of(context).pop(value);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(appLocalizations.recovery),
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
),
content: SizedBox(
width: 250,
child: Wrap(
children: [
ListItem(
onTap: () {
_handleOnTab(RecoveryOption.onlyProfiles);
},
title: Text(appLocalizations.recoveryProfiles),
),
ListItem(
onTap: () {
_handleOnTab(RecoveryOption.all);
},
title: Text(appLocalizations.recoveryAll),
)
],
),
),
);
}
}
class WebDAVFormDialog extends StatefulWidget { class WebDAVFormDialog extends StatefulWidget {
final DAV? dav; final DAV? dav;
@@ -238,7 +361,7 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
children: [ children: [
TextFormField( TextFormField(
controller: uriController, controller: uriController,
maxLines: 2, maxLines: 5,
minLines: 1, minLines: 1,
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: const Icon(Icons.link), prefixIcon: const Icon(Icons.link),
@@ -313,47 +436,3 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
); );
} }
} }
class RecoveryOptionsDialog extends StatefulWidget {
const RecoveryOptionsDialog({super.key});
@override
State<RecoveryOptionsDialog> createState() => _RecoveryOptionsDialogState();
}
class _RecoveryOptionsDialogState extends State<RecoveryOptionsDialog> {
_handleOnTab(RecoveryOption? value) {
if (value == null) return;
Navigator.of(context).pop(value);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(appLocalizations.recovery),
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
),
content: SizedBox(
width: 250,
child: Wrap(
children: [
ListItem(
onTap: () {
_handleOnTab(RecoveryOption.onlyProfiles);
},
title: Text(appLocalizations.recoveryProfiles),
),
ListItem(
onTap: () {
_handleOnTab(RecoveryOption.all);
},
title: Text(appLocalizations.recoveryAll),
)
],
),
),
);
}
}

View File

@@ -1,574 +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(),
),
);
}
}
}
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.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<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),
)
],
);
}
}

View File

@@ -0,0 +1,89 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/config.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' show dirname, join;
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;
},
),
);
},
);
}
}
class UWPLoopbackUtil extends StatelessWidget {
const UWPLoopbackUtil({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.onlyProxy,
builder: (_, onlyProxy, __) {
return ListItem(
leading: const Icon(Icons.lock_open),
title: Text(appLocalizations.loopback),
subtitle: Text(appLocalizations.loopbackDesc),
onTap: () {
windows?.runas(
'"${join(dirname(Platform.resolvedExecutable), "EnableLoopback.exe")}"',
"",
);
},
);
},
);
}
}
final appItems = [
if (Platform.isWindows) const UWPLoopbackUtil(),
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,854 @@
import 'package:collection/collection.dart';
import 'package:fl_clash/common/app_localizations.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class OverrideItem extends StatelessWidget {
const OverrideItem({super.key});
_initActions(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
globalState.appController.clashConfig.dns = const Dns();
},
tooltip: appLocalizations.resetDns,
icon: const Icon(
Icons.replay,
),
)
];
});
}
@override
Widget build(BuildContext context) {
_initActions(context);
return Selector<Config, bool>(
selector: (_, config) => config.overrideDns,
builder: (_, override, __) {
return ListItem.switchItem(
title: Text(appLocalizations.overrideDns),
subtitle: Text(appLocalizations.overrideDnsDesc),
delegate: SwitchDelegate(
value: override,
onChanged: (bool value) async {
final config = globalState.appController.config;
config.overrideDns = value;
},
),
);
},
);
}
}
class DnsDisabledContainer extends StatelessWidget {
final Widget child;
const DnsDisabledContainer(this.child, {
super.key,
});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.overrideDns,
builder: (_, enable, child) {
return AbsorbPointer(
absorbing: !enable,
child: DisabledMask(
status: !enable,
child: Container(
color: context.colorScheme.surface,
child: child!,
),
),
);
},
child: child,
);
}
}
class StatusItem extends StatelessWidget {
const StatusItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.enable,
builder: (_, enable, __) {
return ListItem.switchItem(
title: Text(appLocalizations.status),
subtitle: Text(appLocalizations.statusDesc),
delegate: SwitchDelegate(
value: enable,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
enable: value,
);
},
),
);
},
);
}
}
class PreferH3Item extends StatelessWidget {
const PreferH3Item({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.preferH3,
builder: (_, preferH3, __) {
return ListItem.switchItem(
title: const Text("PreferH3"),
subtitle: Text(appLocalizations.preferH3Desc),
delegate: SwitchDelegate(
value: preferH3,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
preferH3: value,
);
},
),
);
},
);
}
}
class IPv6Item extends StatelessWidget {
const IPv6Item({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.ipv6,
builder: (_, ipv6, __) {
return ListItem.switchItem(
title: const Text("IPv6"),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
ipv6: value,
);
},
),
);
},
);
}
}
class RespectRulesItem extends StatelessWidget {
const RespectRulesItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.respectRules,
builder: (_, respectRules, __) {
return ListItem.switchItem(
title: Text(appLocalizations.respectRules),
subtitle: Text(appLocalizations.respectRulesDesc),
delegate: SwitchDelegate(
value: respectRules,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
respectRules: value,
);
},
),
);
},
);
}
}
class DnsModeItem extends StatelessWidget {
const DnsModeItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, DnsMode>(
selector: (_, clashConfig) => clashConfig.dns.enhancedMode,
builder: (_, enhancedMode, __) {
return ListItem<DnsMode>.options(
title: Text(appLocalizations.dnsMode),
subtitle: Text(enhancedMode.name),
delegate: OptionsDelegate(
title: appLocalizations.dnsMode,
options: DnsMode.values,
onChanged: (value) {
if (value == null) {
return;
}
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(enhancedMode: value);
},
textBuilder: (dnsMode) => dnsMode.name,
value: enhancedMode,
),
);
},
);
}
}
class FakeIpRangeItem extends StatelessWidget {
const FakeIpRangeItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, String>(
selector: (_, clashConfig) => clashConfig.dns.fakeIpRange,
builder: (_, fakeIpRange, __) {
return ListItem.input(
title: Text(appLocalizations.fakeipRange),
subtitle: Text(fakeIpRange),
delegate: InputDelegate(
title: appLocalizations.fakeipRange,
value: fakeIpRange,
onChanged: (String? value) {
if (value != null) {
try {
final clashConfig = globalState.appController.clashConfig;
clashConfig.dns = clashConfig.dns.copyWith(
fakeIpRange: value,
);
} catch (e) {
globalState.showMessage(
title: appLocalizations.fakeipRange,
message: TextSpan(
text: e.toString(),
),
);
}
}
},
),
);
},
);
}
}
class FakeIpFilterItem extends StatelessWidget {
const FakeIpFilterItem({super.key});
@override
Widget build(BuildContext context) {
return ListItem.open(
title: Text(appLocalizations.fakeipFilter),
delegate: OpenDelegate(
isBlur: false,
title: appLocalizations.fakeipFilter,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.fakeIpFilter,
shouldRebuild: (prev, next) =>
!const ListEquality<String>().equals(prev, next),
builder: (_, fakeIpFilter, __) {
return UpdatePage(
title: appLocalizations.fakeipFilter,
items: fakeIpFilter,
titleBuilder: (item) => Text(item),
onRemove: (value) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fakeIpFilter: List.from(dns.fakeIpFilter)
..remove(value),
);
},
onAdd: (value) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
if (fakeIpFilter.contains(value)) return;
clashConfig.dns = dns.copyWith(
fakeIpFilter: List.from(dns.fakeIpFilter)
..add(value),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class DefaultNameserverItem extends StatelessWidget {
const DefaultNameserverItem({super.key});
@override
Widget build(BuildContext context) {
return ListItem.open(
title: Text(appLocalizations.defaultNameserver),
subtitle: Text(appLocalizations.defaultNameserverDesc),
delegate: OpenDelegate(
isBlur: false,
title: appLocalizations.defaultNameserver,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.defaultNameserver,
shouldRebuild: (prev, next) =>
!const ListEquality<String>().equals(prev, next),
builder: (_, defaultNameserver, __) {
return UpdatePage(
title: appLocalizations.defaultNameserver,
items: defaultNameserver,
titleBuilder: (item) => Text(item),
onRemove: (value) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
defaultNameserver: List.from(dns.defaultNameserver)
..remove(value),
);
},
onAdd: (value) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
if (defaultNameserver.contains(value)) return;
clashConfig.dns = dns.copyWith(
defaultNameserver: List.from(dns.defaultNameserver)
..add(value),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class NameserverItem extends StatelessWidget {
const NameserverItem({super.key});
@override
Widget build(BuildContext context) {
return ListItem.open(
title: Text(appLocalizations.nameserver),
subtitle: Text(appLocalizations.nameserverDesc),
delegate: OpenDelegate(
title: appLocalizations.nameserver,
isBlur: false,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.nameserver,
shouldRebuild: (prev, next) =>
!const ListEquality<String>().equals(prev, next),
builder: (_, nameserver, __) {
return UpdatePage(
title: "域名服务器",
items: nameserver,
titleBuilder: (item) => Text(item),
onRemove: (value) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
nameserver: List.from(dns.nameserver)
..remove(value),
);
},
onAdd: (value) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
if (nameserver.contains(value)) return;
clashConfig.dns = dns.copyWith(
nameserver: List.from(dns.nameserver)
..add(value),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class UseHostsItem extends StatelessWidget {
const UseHostsItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.useHosts,
builder: (_, useHosts, __) {
return ListItem.switchItem(
title: Text(appLocalizations.useHosts),
delegate: SwitchDelegate(
value: useHosts,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
useHosts: value,
);
},
),
);
},
);
}
}
class UseSystemHostsItem extends StatelessWidget {
const UseSystemHostsItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.useSystemHosts,
builder: (_, useSystemHosts, __) {
return ListItem.switchItem(
title: Text(appLocalizations.useSystemHosts),
delegate: SwitchDelegate(
value: useSystemHosts,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
useSystemHosts: value,
);
},
),
);
},
);
}
}
class NameserverPolicyItem extends StatelessWidget {
const NameserverPolicyItem({super.key});
@override
Widget build(BuildContext context) {
return ListItem.open(
title: Text(appLocalizations.nameserverPolicy),
subtitle: Text(appLocalizations.nameserverPolicyDesc),
delegate: OpenDelegate(
isBlur: false,
title: appLocalizations.nameserverPolicy,
widget: Selector<ClashConfig, Map<String, String>>(
selector: (_, clashConfig) => clashConfig.dns.nameserverPolicy,
shouldRebuild: (prev, next) =>
!const MapEquality<String, String>().equals(prev, next),
builder: (_, nameserverPolicy, __) {
return UpdatePage(
title: appLocalizations.nameserverPolicy,
items: nameserverPolicy.entries,
titleBuilder: (item) => Text(item.key),
subtitleBuilder: (item) => Text(item.value),
isMap: true,
onRemove: (value) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
nameserverPolicy: Map.from(dns.nameserverPolicy)
..remove(value.key),
);
},
onAdd: (value) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
nameserverPolicy: Map.from(dns.nameserverPolicy)
..addEntries([value]),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class ProxyServerNameserverItem extends StatelessWidget {
const ProxyServerNameserverItem({super.key});
@override
Widget build(BuildContext context) {
return ListItem.open(
title: Text(appLocalizations.proxyNameserver),
subtitle: Text(appLocalizations.proxyNameserverDesc),
delegate: OpenDelegate(
isBlur: false,
title: appLocalizations.proxyNameserver,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.proxyServerNameserver,
shouldRebuild: (prev, next) =>
!const ListEquality<String>().equals(prev, next),
builder: (_, proxyServerNameserver, __) {
return UpdatePage(
title: appLocalizations.proxyNameserver,
items: proxyServerNameserver,
titleBuilder: (item) => Text(item),
onRemove: (value) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
proxyServerNameserver: List.from(dns.proxyServerNameserver)
..remove(value),
);
},
onAdd: (value) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
if (proxyServerNameserver.contains(value)) return;
clashConfig.dns = dns.copyWith(
proxyServerNameserver: List.from(dns.proxyServerNameserver)
..add(value),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class FallbackItem extends StatelessWidget {
const FallbackItem({super.key});
@override
Widget build(BuildContext context) {
return ListItem.open(
title: Text(appLocalizations.fallback),
subtitle: Text(appLocalizations.fallbackDesc),
delegate: OpenDelegate(
isBlur: false,
title: appLocalizations.fallback,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.fallback,
shouldRebuild: (prev, next) =>
!const ListEquality<String>().equals(prev, next),
builder: (_, fallback, __) {
return UpdatePage(
title: appLocalizations.fallback,
items: fallback,
titleBuilder: (item) => Text(item),
onRemove: (value) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallback: List.from(dns.fallback)
..remove(value),
);
},
onAdd: (value) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
if (fallback.contains(value)) return;
clashConfig.dns = dns.copyWith(
fallback: List.from(dns.fallback)
..add(value),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class GeoipItem extends StatelessWidget {
const GeoipItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoip,
builder: (_, geoip, __) {
return ListItem.switchItem(
title: const Text("Geoip"),
delegate: SwitchDelegate(
value: geoip,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(geoip: value),
);
},
),
);
},
);
}
}
class GeoipCodeItem extends StatelessWidget {
const GeoipCodeItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, String>(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoipCode,
builder: (_, geoipCode, __) {
return ListItem.input(
title: Text(appLocalizations.geoipCode),
subtitle: Text(geoipCode),
delegate: InputDelegate(
title: appLocalizations.geoipCode,
value: geoipCode,
onChanged: (String? value) {
if (value != null) {
try {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(
geoipCode: value,
),
);
} catch (e) {
globalState.showMessage(
title: appLocalizations.geoipCode,
message: TextSpan(
text: e.toString(),
),
);
}
}
},
),
);
},
);
}
}
class GeositeItem extends StatelessWidget {
const GeositeItem({super.key});
@override
Widget build(BuildContext context) {
return ListItem.open(
title: const Text("Geosite"),
delegate: OpenDelegate(
isBlur: false,
title: "Geosite",
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geosite,
shouldRebuild: (prev, next) =>
!const ListEquality<String>().equals(prev, next),
builder: (_, geosite, __) {
return UpdatePage(
title: "Geosite",
items: geosite,
titleBuilder: (item) => Text(item),
onRemove: (value) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(
geosite: List.from(geosite)
..remove(value),
),
);
},
onAdd: (value) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(
geosite: List.from(geosite)
..add(value),
),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class IpcidrItem extends StatelessWidget {
const IpcidrItem({super.key});
@override
Widget build(BuildContext context) {
return ListItem.open(
title: Text(appLocalizations.ipcidr),
delegate: OpenDelegate(
isBlur: false,
title: appLocalizations.ipcidr,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.ipcidr,
shouldRebuild: (prev, next) =>
!const ListEquality<String>().equals(prev, next),
builder: (_, ipcidr, __) {
return UpdatePage(
title: appLocalizations.ipcidr,
items: ipcidr,
titleBuilder: (item) => Text(item),
onRemove: (value) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(
ipcidr: List.from(ipcidr)
..remove(value),
),
);
},
onAdd: (value) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(
ipcidr: List.from(ipcidr)
..add(value),
),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class DomainItem extends StatelessWidget {
const DomainItem({super.key});
@override
Widget build(BuildContext context) {
return ListItem.open(
title: Text(appLocalizations.domain),
delegate: OpenDelegate(
isBlur: false,
title: appLocalizations.domain,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.domain,
shouldRebuild: (prev, next) =>
!const ListEquality<String>().equals(prev, next),
builder: (_, domain, __) {
return UpdatePage(
title: appLocalizations.domain,
items: domain,
titleBuilder: (item) => Text(item),
onRemove: (value) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(
domain: List.from(domain)
..remove(value),
),
);
},
onAdd: (value) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(
domain: List.from(domain)
..add(value),
),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class DnsOptions extends StatelessWidget {
const DnsOptions({super.key});
@override
Widget build(BuildContext context) {
return DnsDisabledContainer(
Column(
children: generateSection(
title: appLocalizations.options,
items: [
const StatusItem(),
const UseHostsItem(),
const UseSystemHostsItem(),
const IPv6Item(),
const RespectRulesItem(),
const PreferH3Item(),
const DnsModeItem(),
const FakeIpRangeItem(),
const FakeIpFilterItem(),
const DefaultNameserverItem(),
const NameserverPolicyItem(),
const NameserverItem(),
const FallbackItem(),
const ProxyServerNameserverItem(),
],
),
),
);
}
}
class FallbackFilterOptions extends StatelessWidget {
const FallbackFilterOptions({super.key});
@override
Widget build(BuildContext context) {
return DnsDisabledContainer(
Column(
children: generateSection(
title: appLocalizations.fallbackFilter,
items: [
const GeoipItem(),
const GeoipCodeItem(),
const GeositeItem(),
const IpcidrItem(),
const DomainItem(),
],
),
),
);
}
}
const dnsItems = <Widget>[
OverrideItem(),
DnsOptions(),
FallbackFilterOptions(),
];

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

@@ -51,18 +51,6 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
final commonScaffoldState = final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>(); context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [ commonScaffoldState?.actions = [
IconButton(
onPressed: () {
clashCore.closeConnections();
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: clashCore.getConnections(),
);
},
icon: const Icon(Icons.delete_sweep_outlined),
),
const SizedBox(
width: 8,
),
IconButton( IconButton(
onPressed: () { onPressed: () {
showSearch( showSearch(
@@ -74,6 +62,18 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
}, },
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
), ),
const SizedBox(
width: 8,
),
IconButton(
onPressed: () {
clashCore.closeConnections();
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: clashCore.getConnections(),
);
},
icon: const Icon(Icons.delete_sweep_outlined),
),
]; ];
}, },
); );

View File

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

View File

@@ -53,7 +53,7 @@ class _IntranetIPState extends State<IntranetIP> {
}, },
child: Container( child: Container(
padding: const EdgeInsets.all(16).copyWith(top: 0), padding: const EdgeInsets.all(16).copyWith(top: 0),
height: globalState.appController.measure.titleLargeHeight + 24 - 2, height: globalState.measure.titleLargeHeight + 24 - 2,
child: ValueListenableBuilder( child: ValueListenableBuilder(
valueListenable: ipNotifier, valueListenable: ipNotifier,
builder: (_, value, __) { builder: (_, value, __) {

View File

@@ -1,4 +1,3 @@
import 'package:country_flags/country_flags.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
@@ -15,51 +14,51 @@ class NetworkDetection extends StatefulWidget {
} }
class _NetworkDetectionState extends State<NetworkDetection> { class _NetworkDetectionState extends State<NetworkDetection> {
final ipInfoNotifier = ValueNotifier<IpInfo?>(null); final networkDetectionState = ValueNotifier<NetworkDetectionState>(
final timeoutNotifier = ValueNotifier<bool>(false); const NetworkDetectionState(
isTesting: true,
ipInfo: null,
),
);
bool? _preIsStart; bool? _preIsStart;
CancelToken? cancelToken;
Function? _checkIpDebounce; Function? _checkIpDebounce;
CancelToken? cancelToken;
_checkIp( _checkIp() async {
bool isInit, final appState = globalState.appController.appState;
bool isStart, final isInit = appState.isInit;
) async {
if (!isInit) return; if (!isInit) return;
timeoutNotifier.value = false; final isStart = appState.isStart;
if (_preIsStart == false && _preIsStart == isStart) return; if (_preIsStart == false && _preIsStart == isStart) return;
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: true,
ipInfo: null,
);
_preIsStart = isStart;
if (cancelToken != null) { if (cancelToken != null) {
cancelToken!.cancel(); cancelToken!.cancel();
cancelToken = null; cancelToken = null;
} }
ipInfoNotifier.value = null; cancelToken = CancelToken();
final ipInfo = await request.checkIp(cancelToken); try {
if (ipInfo == null) { final ipInfo = await request.checkIp(cancelToken: cancelToken);
timeoutNotifier.value = true; networkDetectionState.value = networkDetectionState.value.copyWith(
return; isTesting: false,
} else { ipInfo: ipInfo,
timeoutNotifier.value = false; );
} } catch (_) {}
_preIsStart = isStart;
ipInfoNotifier.value = ipInfo;
} }
_checkIpContainer(Widget child) { _checkIpContainer(Widget child) {
_checkIpDebounce = debounce(_checkIp); return Selector<AppState, num>(
return Selector2<AppState, Config, CheckIpSelectorState>( selector: (_, appState) {
selector: (_, appState, config) { return appState.checkIpNum;
return CheckIpSelectorState(
isInit: appState.isInit,
selectedMap: appState.selectedMap,
isStart: appState.isStart,
checkIpNum: appState.checkIpNum,
);
}, },
builder: (_, state, __) { builder: (_, checkIpNum, child) {
if (_checkIpDebounce != null) { if (_checkIpDebounce != null) {
_checkIpDebounce!([state.isInit, state.isStart]); _checkIpDebounce!();
} }
return child; return child!;
}, },
child: child, child: child,
); );
@@ -68,16 +67,28 @@ class _NetworkDetectionState extends State<NetworkDetection> {
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
ipInfoNotifier.dispose(); networkDetectionState.dispose();
timeoutNotifier.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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_checkIpDebounce ??= debounce(_checkIp);
return _checkIpContainer( return _checkIpContainer(
ValueListenableBuilder<IpInfo?>( ValueListenableBuilder<NetworkDetectionState>(
valueListenable: ipInfoNotifier, valueListenable: networkDetectionState,
builder: (_, ipInfo, __) { builder: (_, state, __) {
final ipInfo = state.ipInfo;
final isTesting = state.isTesting;
return CommonCard( return CommonCard(
onPressed: () {}, onPressed: () {},
child: Column( child: Column(
@@ -98,37 +109,38 @@ class _NetworkDetectionState extends State<NetworkDetection> {
Flexible( Flexible(
flex: 1, flex: 1,
child: FadeBox( child: FadeBox(
child: ipInfo != null child: isTesting
? CountryFlag.fromCountryCode( ? Text(
ipInfo.countryCode, appLocalizations.checking,
width: 24, maxLines: 1,
height: 24, overflow: TextOverflow.ellipsis,
style:
Theme.of(context).textTheme.titleMedium,
) )
: ValueListenableBuilder( : ipInfo != null
valueListenable: timeoutNotifier, ? Container(
builder: (_, timeout, __) { alignment: Alignment.centerLeft,
if (timeout) { height: globalState
return Text( .measure.titleMediumHeight,
appLocalizations.checkError, child: Text(
countryCodeToEmoji(
ipInfo.countryCode),
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.titleMedium, .titleLarge
maxLines: 1, ?.copyWith(
overflow: TextOverflow.ellipsis, fontFamily: "Twemoji",
); ),
}
return TooltipText(
text: Text(
appLocalizations.checking,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleMedium,
), ),
); )
}, : Text(
), appLocalizations.checkError,
style: Theme.of(context)
.textTheme
.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
), ),
), ),
], ],
@@ -136,9 +148,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
), ),
), ),
Container( Container(
height: globalState.appController.measure.titleLargeHeight + height: globalState.measure.titleLargeHeight + 24 - 2,
24 -
2,
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
padding: const EdgeInsets.all(16).copyWith(top: 0), padding: const EdgeInsets.all(16).copyWith(top: 0),
child: FadeBox( child: FadeBox(
@@ -161,28 +171,24 @@ class _NetworkDetectionState extends State<NetworkDetection> {
), ),
], ],
) )
: ValueListenableBuilder( : FadeBox(
valueListenable: timeoutNotifier, child: isTesting == false && ipInfo == null
builder: (_, timeout, __) { ? Text(
if (timeout) { "timeout",
return Text( style: context.textTheme.titleLarge
"timeout", ?.copyWith(color: Colors.red)
style: context.textTheme.titleLarge .toSoftBold
?.copyWith(color: Colors.red) .toMinus,
.toSoftBold maxLines: 1,
.toMinus, overflow: TextOverflow.ellipsis,
maxLines: 1, )
overflow: TextOverflow.ellipsis, : Container(
); padding: const EdgeInsets.all(2),
} child: const AspectRatio(
return Container( aspectRatio: 1,
padding: const EdgeInsets.all(2), child: CircularProgressIndicator(),
child: const AspectRatio( ),
aspectRatio: 1, ),
child: CircularProgressIndicator(),
),
);
},
), ),
), ),
) )

View File

@@ -59,7 +59,7 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
style: bodyMedium, style: bodyMedium,
maxLines: 1, maxLines: 1,
); );
final size = globalState.appController.measure.computeTextSize(valueText); final size = globalState.measure.computeTextSize(valueText);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@@ -114,7 +114,7 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
onPressed: () {}, onPressed: () {},
info: Info( info: Info(
label: appLocalizations.networkSpeed, label: appLocalizations.networkSpeed,
iconData: Icons.speed, iconData: Icons.speed_sharp,
), ),
child: Selector<AppState, List<Traffic>>( child: Selector<AppState, List<Traffic>>(
selector: (_, appState) => appState.traffics, selector: (_, appState) => appState.traffics,

View File

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

View File

@@ -37,7 +37,7 @@ class _StartButtonState extends State<StartButton>
if (isStart == appController.appState.isStart) { if (isStart == appController.appState.isStart) {
isStart = !isStart; isStart = !isStart;
updateController(); updateController();
appController.updateSystemProxy(isStart); appController.updateStatus(isStart);
} }
} }
@@ -53,7 +53,7 @@ class _StartButtonState extends State<StartButton>
return Selector<AppState, bool>( return Selector<AppState, bool>(
selector: (_, appState) => appState.isStart, selector: (_, appState) => appState.isStart,
builder: (_, isStart, child) { builder: (_, isStart, child) {
if(isStart != this.isStart){ if (isStart != this.isStart) {
this.isStart = isStart; this.isStart = isStart;
updateController(); updateController();
} }
@@ -74,7 +74,7 @@ class _StartButtonState extends State<StartButton>
if (!state.isInit || !state.hasProfile) { if (!state.isInit || !state.hasProfile) {
return Container(); return Container();
} }
final textWidth = globalState.appController.measure final textWidth = globalState.measure
.computeTextSize( .computeTextSize(
Text( Text(
other.getTimeDifference( other.getTimeDifference(

View File

@@ -0,0 +1,121 @@
import 'package:fl_clash/common/app_localizations.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// class VPNSwitch extends StatelessWidget {
// const VPNSwitch({super.key});
//
// @override
// Widget build(BuildContext context) {
// return SwitchContainer(
// info: const Info(
// label: "VPN",
// iconData: Icons.stacked_line_chart,
// ),
// child: Selector<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 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 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

@@ -5,9 +5,9 @@ export 'profiles/profiles.dart';
export 'logs.dart'; export 'logs.dart';
export 'connections.dart'; export 'connections.dart';
export 'access.dart'; export 'access.dart';
export 'config.dart'; export 'config/config.dart';
export 'application_setting.dart'; export 'application_setting.dart';
export 'about.dart'; export 'about.dart';
export 'backup_and_recovery.dart'; export 'backup_and_recovery.dart';
export 'resources.dart'; export 'resources.dart';
export 'requests.dart'; export 'requests.dart';

View File

@@ -1,10 +1,15 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
class EditProfile extends StatefulWidget { class EditProfile extends StatefulWidget {
final Profile profile; final Profile profile;
@@ -26,6 +31,8 @@ class _EditProfileState extends State<EditProfile> {
late TextEditingController autoUpdateDurationController; late TextEditingController autoUpdateDurationController;
late bool autoUpdate; late bool autoUpdate;
final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final fileInfoNotifier = ValueNotifier<FileInfo?>(null);
Uint8List? fileData;
@override @override
void initState() { void initState() {
@@ -36,12 +43,16 @@ class _EditProfileState extends State<EditProfile> {
autoUpdateDurationController = TextEditingController( autoUpdateDurationController = TextEditingController(
text: widget.profile.autoUpdateDuration.inMinutes.toString(), text: widget.profile.autoUpdateDuration.inMinutes.toString(),
); );
appPath.getProfilePath(widget.profile.id).then((path) async {
if (path == null) return;
fileInfoNotifier.value = await _getFileInfo(path);
});
} }
_handleConfirm() { _handleConfirm() async {
if (!_formKey.currentState!.validate()) return; if (!_formKey.currentState!.validate()) return;
final config = widget.context.read<Config>(); final config = widget.context.read<Config>();
final profile = widget.profile.copyWith( var profile = widget.profile.copyWith(
url: urlController.text, url: urlController.text,
label: labelController.text, label: labelController.text,
autoUpdate: autoUpdate, autoUpdate: autoUpdate,
@@ -52,7 +63,11 @@ class _EditProfileState extends State<EditProfile> {
), ),
); );
final hasUpdate = widget.profile.url != profile.url; final hasUpdate = widget.profile.url != profile.url;
config.setProfile(profile); if (fileData != null) {
config.setProfile(await profile.saveFile(fileData!));
} else {
config.setProfile(profile);
}
if (hasUpdate) { if (hasUpdate) {
globalState.homeScaffoldKey.currentState?.loadingRun( globalState.homeScaffoldKey.currentState?.loadingRun(
() async { () async {
@@ -62,7 +77,9 @@ class _EditProfileState extends State<EditProfile> {
}, },
); );
} }
Navigator.of(context).pop(); if (mounted) {
Navigator.of(context).pop();
}
} }
_setAutoUpdate(bool value) { _setAutoUpdate(bool value) {
@@ -72,6 +89,47 @@ class _EditProfileState extends State<EditProfile> {
}); });
} }
Future<FileInfo?> _getFileInfo(path) async {
final file = File(path);
if (!await file.exists()) {
return null;
}
final lastModified = await file.lastModified();
final size = await file.length();
return FileInfo(
size: size,
lastModified: lastModified,
);
}
_editProfileFile() async {
final profilePath = await appPath.getProfilePath(widget.profile.id);
if (profilePath == null) return;
globalState.safeRun(() async {
if (Platform.isAndroid) {
await app?.openFile(
profilePath,
);
return;
}
await launchUrl(
Uri.file(
profilePath,
),
);
});
}
_uploadProfileFile() async {
final platformFile = await globalState.safeRun(picker.pickerFile);
if (platformFile?.bytes == null) return;
fileData = platformFile?.bytes;
fileInfoNotifier.value = fileInfoNotifier.value?.copyWith(
size: fileData?.length ?? 0,
lastModified: DateTime.now(),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final items = [ final items = [
@@ -141,7 +199,51 @@ class _EditProfileState extends State<EditProfile> {
}, },
), ),
), ),
] ],
ValueListenableBuilder<FileInfo?>(
valueListenable: fileInfoNotifier,
builder: (_, fileInfo, __) {
return FadeBox(
child: fileInfo == null
? Container()
: ListItem(
title: Text(
appLocalizations.profile,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 4,
),
Text(
fileInfo.desc,
),
const SizedBox(
height: 8,
),
Wrap(
runSpacing: 6,
spacing: 12,
children: [
CommonChip(
avatar: const Icon(Icons.edit),
label: appLocalizations.edit,
onPressed: _editProfileFile,
),
CommonChip(
avatar: const Icon(Icons.upload),
label: appLocalizations.upload,
onPressed: _uploadProfileFile,
),
],
),
],
),
),
);
},
),
]; ];
return FloatLayout( return FloatLayout(
floatingWidget: FloatWrapper( floatingWidget: FloatWrapper(
@@ -159,7 +261,9 @@ class _EditProfileState extends State<EditProfile> {
vertical: 16, vertical: 16,
), ),
child: ListView.separated( child: ListView.separated(
primary: true, padding: kMaterialListPadding.copyWith(
bottom: 72,
),
itemBuilder: (_, index) { itemBuilder: (_, index) {
return items[index]; return items[index];
}, },

View File

@@ -1,6 +1,7 @@
import 'dart:ui';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/profiles/edit_profile.dart'; import 'package:fl_clash/fragments/profiles/edit_profile.dart';
import 'package:fl_clash/fragments/profiles/view_profile.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
@@ -16,7 +17,6 @@ enum ProfileActions {
edit, edit,
update, update,
delete, delete,
view,
} }
class ProfilesFragment extends StatefulWidget { class ProfilesFragment extends StatefulWidget {
@@ -27,11 +27,8 @@ class ProfilesFragment extends StatefulWidget {
} }
class _ProfilesFragmentState extends State<ProfilesFragment> { class _ProfilesFragmentState extends State<ProfilesFragment> {
final hasPadding = ValueNotifier<bool>(false);
Function? applyConfigDebounce; Function? applyConfigDebounce;
List<GlobalObjectKey<_ProfileItemState>> profileItemKeys = [];
_handleShowAddExtendPage() { _handleShowAddExtendPage() {
showExtendPage( showExtendPage(
globalState.navigatorKey.currentState!.context, globalState.navigatorKey.currentState!.context,
@@ -42,29 +39,53 @@ 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 { _updateProfiles() async {
final updateProfiles = profileItemKeys.map<Future>( final appController = globalState.appController;
(key) async => await key.currentState?.updateProfile(false)); 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.applyProfileDebounce();
}
} catch (e) {
messages.add("${profile.label ?? profile.id}: $e \n");
config.setProfile(
profile.copyWith(
isUpdating: false,
),
);
}
},
);
final titleMedium = context.textTheme.titleMedium;
await Future.wait(updateProfiles); 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() { _initScaffoldState() {
WidgetsBinding.instance.addPostFrameCallback( WidgetsBinding.instance.addPostFrameCallback(
(_) { (_) {
if (!mounted) return;
final commonScaffoldState = final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>(); context.findAncestorStateOfType<CommonScaffoldState>();
if (!context.mounted) return;
commonScaffoldState?.actions = [ commonScaffoldState?.actions = [
IconButton( IconButton(
onPressed: () { onPressed: () {
@@ -72,35 +93,29 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
}, },
icon: const Icon(Icons.sync), 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,
),
]; ];
}, },
); );
} }
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
hasPadding.dispose();
}
_changeProfile(String? id) async {
final appController = globalState.appController;
final config = appController.config;
if (id == config.currentProfileId) return;
config.currentProfileId = id;
applyConfigDebounce ??= debounce<Function()>(() async {
await appController.applyProfile();
appController.appState.delayMap = {};
appController.saveConfigPreferences();
});
applyConfigDebounce!();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FloatLayout( return FloatLayout(
@@ -125,7 +140,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
selector: (_, appState, config) => ProfilesSelectorState( selector: (_, appState, config) => ProfilesSelectorState(
profiles: config.profiles, profiles: config.profiles,
currentProfileId: config.currentProfileId, currentProfileId: config.currentProfileId,
viewMode: appState.viewMode, columns: other.getProfilesColumns(appState.viewWidth),
), ),
builder: (context, state, child) { builder: (context, state, child) {
if (state.profiles.isEmpty) { if (state.profiles.isEmpty) {
@@ -133,47 +148,30 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
label: appLocalizations.nullProfileDesc, label: appLocalizations.nullProfileDesc,
); );
} }
profileItemKeys = state.profiles
.map(
(profile) => GlobalObjectKey<_ProfileItemState>(profile.id))
.toList();
final columns = _getColumns(state.viewMode);
return Align( return Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: NotificationListener<ScrollNotification>( child: SingleChildScrollView(
onNotification: (scrollNotification) { padding: const EdgeInsets.only(
hasPadding.value = left: 16,
scrollNotification.metrics.maxScrollExtent > 0; right: 16,
return true; top: 16,
}, bottom: 88,
child: ValueListenableBuilder( ),
valueListenable: hasPadding, child: Grid(
builder: (_, hasPadding, __) { mainAxisSpacing: 16,
return SingleChildScrollView( crossAxisSpacing: 16,
padding: EdgeInsets.only( crossAxisCount: state.columns,
left: 16, children: [
right: 16, for (int i = 0; i < state.profiles.length; i++)
top: 16, GridItem(
bottom: 16 + (hasPadding ? 72 : 0), child: ProfileItem(
key: Key(state.profiles[i].id),
profile: state.profiles[i],
groupValue: state.currentProfileId,
onChanged: globalState.appController.changeProfile,
),
), ),
child: Grid( ],
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: columns,
children: [
for (int i = 0; i < state.profiles.length; i++)
GridItem(
child: ProfileItem(
key: profileItemKeys[i],
profile: state.profiles[i],
groupValue: state.currentProfileId,
onChanged: _changeProfile,
),
),
],
),
);
},
), ),
), ),
); );
@@ -184,7 +182,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
} }
} }
class ProfileItem extends StatefulWidget { class ProfileItem extends StatelessWidget {
final Profile profile; final Profile profile;
final String? groupValue; final String? groupValue;
final void Function(String? value) onChanged; final void Function(String? value) onChanged;
@@ -196,289 +194,333 @@ class ProfileItem extends StatefulWidget {
required this.onChanged, required this.onChanged,
}); });
@override _handleDeleteProfile(BuildContext context) async {
State<ProfileItem> createState() => _ProfileItemState(); globalState.showMessage(
} title: appLocalizations.tip,
message: TextSpan(
class _ProfileItemState extends State<ProfileItem> { text: appLocalizations.deleteProfileTip,
final isUpdating = ValueNotifier<bool>(false); ),
onTab: () async {
_handleDeleteProfile() async { await globalState.appController.deleteProfile(profile.id);
globalState.appController.deleteProfile(widget.profile.id); if (context.mounted) {
Navigator.of(context).pop();
}
},
);
} }
_handleUpdateProfile() async { _handleUpdateProfile() async {
await globalState.safeRun<void>(updateProfile); await globalState.safeRun<void>(updateProfile);
} }
Future updateProfile([isSingle = true]) async { Future updateProfile() async {
isUpdating.value = true; final appController = globalState.appController;
try { final config = appController.config;
final appController = globalState.appController; if (profile.type == ProfileType.file) return;
await appController.updateProfile(widget.profile); await globalState.safeRun(() async {
if (widget.profile.id == appController.config.currentProfile?.id && try {
!appController.appState.isStart) { config.setProfile(
globalState.appController.rawApplyProfile(); profile.copyWith(
} isUpdating: true,
} catch (e) { ),
isUpdating.value = false; );
if (!isSingle) { await appController.updateProfile(profile);
return e.toString(); if (profile.id == appController.config.currentProfile?.id) {
} else { appController.applyProfileDebounce();
}
} catch (e) {
config.setProfile(
profile.copyWith(
isUpdating: false,
),
);
rethrow; rethrow;
} }
} });
isUpdating.value = false;
return null;
} }
_handleShowEditExtendPage() { _handleShowEditExtendPage(BuildContext context) {
showExtendPage( showExtendPage(
context, context,
body: EditProfile( body: EditProfile(
profile: widget.profile, profile: profile,
context: context, context: context,
), ),
title: "${appLocalizations.edit}${appLocalizations.profile}", title: "${appLocalizations.edit}${appLocalizations.profile}",
); );
} }
_handleViewProfile() { List<Widget> _buildUserInfo(BuildContext context, UserInfo userInfo) {
Navigator.of(context).push( final use = userInfo.upload + userInfo.download;
MaterialPageRoute( final total = userInfo.total;
builder: (context) => ViewProfile( if (total == 0) {
profile: widget.profile, return [];
), }
final useShow = TrafficValue(value: use).show;
final totalShow = TrafficValue(value: total).show;
final progress = total == 0 ? 0.0 : use / total;
final expireShow = userInfo.expire == 0
? appLocalizations.infiniteTime
: DateTime.fromMillisecondsSinceEpoch(userInfo.expire * 1000).show;
return [
LinearProgressIndicator(
minHeight: 6,
value: progress,
backgroundColor: context.colorScheme.primary.toSoft(),
), ),
); const SizedBox(
height: 8,
),
Text(
"$useShow / $totalShow · $expireShow",
style: context.textTheme.labelMedium?.toLight,
),
const SizedBox(
height: 4,
),
];
} }
_buildTitle(Profile profile) { List<Widget> _buildUrlProfileInfo(BuildContext context) {
final textTheme = context.textTheme; final userInfo = profile.userInfo;
return Container( return [
padding: const EdgeInsets.symmetric(vertical: 4), const SizedBox(
child: Column( height: 8,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
profile.label ?? profile.id,
style: textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Text(
profile.lastUpdateDate?.lastUpdateTimeDesc ?? '',
style: textTheme.labelMedium?.toLight,
),
],
),
Builder(builder: (context) {
final userInfo = profile.userInfo ?? const UserInfo();
final use = userInfo.upload + userInfo.download;
final total = userInfo.total;
final useShow = TrafficValue(value: use).show;
final totalShow = TrafficValue(value: total).show;
final progress = total == 0 ? 0.0 : use / total;
final expireShow = userInfo.expire == 0
? appLocalizations.infiniteTime
: DateTime.fromMillisecondsSinceEpoch(userInfo.expire * 1000)
.show;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
margin: const EdgeInsets.symmetric(
vertical: 8,
),
child: LinearProgressIndicator(
minHeight: 6,
value: progress,
),
),
Text(
"$useShow / $totalShow",
style: textTheme.labelMedium?.toLight,
),
const SizedBox(
height: 2,
),
Row(
children: [
Text(
expireShow,
style: textTheme.labelMedium?.toLighter,
),
],
)
],
);
// final child = switch (userInfo != null) {
// true => () {
// final use = userInfo!.upload + userInfo.download;
// final total = userInfo.total;
// final useShow = TrafficValue(value: use).show;
// final totalShow = TrafficValue(value: total).show;
// final progress = total == 0 ? 0.0 : use / total;
// final expireShow = userInfo.expire == 0
// ? appLocalizations.infiniteTime
// : DateTime.fromMillisecondsSinceEpoch(
// userInfo.expire * 1000)
// .show;
// return Column(
// mainAxisSize: MainAxisSize.min,
// crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Container(
// margin: const EdgeInsets.symmetric(
// vertical: 8,
// ),
// child: LinearProgressIndicator(
// minHeight: 6,
// value: progress,
// ),
// ),
// Text(
// "$useShow / $totalShow",
// style: textTheme.labelMedium?.toLight(),
// ),
// const SizedBox(
// height: 2,
// ),
// Row(
// children: [
// Text(
// appLocalizations.expirationTime,
// style: textTheme.labelMedium?.toLighter(),
// ),
// const SizedBox(
// width: 4,
// ),
// Text(
// expireShow,
// style: textTheme.labelMedium?.toLighter(),
// ),
// ],
// )
// ],
// );
// }(),
// false => Column(
// children: [
// Padding(
// padding: const EdgeInsets.only(top: 8),
// child: CommonChip(
// onPressed: _handleViewProfile,
// avatar: const Icon(Icons.remove_red_eye),
// label: appLocalizations.view,
// ),
// ),
// ],
// ),
// };
// final measure = globalState.appController.measure;
// final height = 6 + 8 * 2 + 2 + measure.labelMediumHeight * 2;
// return SizedBox(
// height: height,
// child: child,
// );
}),
],
), ),
); if (userInfo != null) ..._buildUserInfo(context, userInfo),
Text(
profile.lastUpdateDate?.lastUpdateTimeDesc ?? "",
style: context.textTheme.labelMedium?.toLight,
),
];
} }
@override List<Widget> _buildFileProfileInfo(BuildContext context) {
void dispose() { return [
isUpdating.dispose(); const SizedBox(
super.dispose(); height: 8,
),
Text(
profile.lastUpdateDate?.lastUpdateTimeDesc ?? "",
style: context.textTheme.labelMedium?.toLight,
),
];
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final profile = widget.profile;
final groupValue = widget.groupValue;
final onChanged = widget.onChanged;
return CommonCard( return CommonCard(
child: ListItem.radio( isSelected: profile.id == groupValue,
onPressed: () {
onChanged(profile.id);
},
child: ListItem(
key: Key(profile.id), key: Key(profile.id),
horizontalTitleGap: 16, horizontalTitleGap: 16,
delegate: RadioDelegate<String?>(
value: profile.id,
groupValue: groupValue,
onChanged: onChanged,
),
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
trailing: SizedBox( trailing: SizedBox(
height: 48, height: 40,
width: 48, width: 40,
child: ValueListenableBuilder( child: FadeBox(
valueListenable: isUpdating, child: profile.isUpdating
builder: (_, isUpdating, ___) { ? const Padding(
return FadeBox( padding: EdgeInsets.all(8),
child: isUpdating child: CircularProgressIndicator(),
? const Padding( )
padding: EdgeInsets.all(8), : CommonPopupMenu<ProfileActions>(
child: CircularProgressIndicator(), items: [
) CommonPopupMenuItem(
: CommonPopupMenu<ProfileActions>( action: ProfileActions.edit,
items: [ label: appLocalizations.edit,
CommonPopupMenuItem( iconData: Icons.edit,
action: ProfileActions.edit, ),
label: appLocalizations.edit, if (profile.type == ProfileType.url)
iconData: Icons.edit, CommonPopupMenuItem(
), action: ProfileActions.update,
if (profile.type == ProfileType.url) label: appLocalizations.update,
CommonPopupMenuItem( iconData: Icons.sync,
action: ProfileActions.update, ),
label: appLocalizations.update, CommonPopupMenuItem(
iconData: Icons.sync, action: ProfileActions.delete,
), label: appLocalizations.delete,
CommonPopupMenuItem( iconData: Icons.delete,
action: ProfileActions.view, ),
label: appLocalizations.view, ],
iconData: Icons.visibility, onSelected: (ProfileActions? action) async {
), switch (action) {
CommonPopupMenuItem( case ProfileActions.edit:
action: ProfileActions.delete, _handleShowEditExtendPage(context);
label: appLocalizations.delete, break;
iconData: Icons.delete, case ProfileActions.delete:
), _handleDeleteProfile(context);
], break;
onSelected: (ProfileActions? action) async { case ProfileActions.update:
switch (action) { _handleUpdateProfile();
case ProfileActions.edit: break;
_handleShowEditExtendPage(); case null:
break; break;
case ProfileActions.delete: }
_handleDeleteProfile(); },
break; ),
case ProfileActions.update: ),
_handleUpdateProfile(); ),
break; title: Container(
case ProfileActions.view: padding: const EdgeInsets.symmetric(vertical: 4),
_handleViewProfile(); child: Column(
break; crossAxisAlignment: CrossAxisAlignment.start,
case null: mainAxisAlignment: MainAxisAlignment.center,
break; children: [
} Text(
}, profile.label ?? profile.id,
)); style: context.textTheme.titleMedium,
}, maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
...switch (profile.type) {
ProfileType.file => _buildFileProfileInfo(context),
ProfileType.url => _buildUrlProfileInfo(context),
},
],
),
],
), ),
), ),
title: _buildTitle(profile),
tileTitleAlignment: ListTileTitleAlignment.titleHeight, tileTitleAlignment: ListTileTitleAlignment.titleHeight,
), ),
); );
} }
} }
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: 8,
horizontal: 24,
),
child: FilledButton(
onPressed: () {
Navigator.of(context).pop();
globalState.appController.config.profiles = profiles;
},
style: ButtonStyle(
padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(vertical: 16),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
appLocalizations.confirm,
),
],
),
),
),
],
);
}
}

View File

@@ -7,7 +7,7 @@ import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:re_editor/re_editor.dart'; import 'package:re_editor/re_editor.dart';
import 'package:re_highlight/languages/yaml.dart'; import 'package:re_highlight/languages/yaml.dart';
import 'package:re_highlight/styles/intellij-light.dart'; import 'package:re_highlight/styles/atom-one-light.dart';
class ViewProfile extends StatefulWidget { class ViewProfile extends StatefulWidget {
final Profile profile; final Profile profile;
@@ -23,29 +23,27 @@ class ViewProfile extends StatefulWidget {
class _ViewProfileState extends State<ViewProfile> { class _ViewProfileState extends State<ViewProfile> {
bool readOnly = true; bool readOnly = true;
CodeLineEditingController? controller; final CodeLineEditingController _controller = CodeLineEditingController();
final contentNotifier = ValueNotifier<String>("");
final key = GlobalKey<CommonScaffoldState>(); final key = GlobalKey<CommonScaffoldState>();
final _focusNode = FocusNode();
String? rawText;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async { appPath.getProfilePath(widget.profile.id).then((path) async {
final profilePath = await appPath.getProfilePath(widget.profile.id); if (path == null) return;
if (profilePath == null) { final file = File(path);
return; rawText = await file.readAsString();
} _controller.text = rawText ?? "";
final file = File(profilePath);
final text = await file.readAsString();
contentNotifier.value = text;
}); });
} }
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
contentNotifier.dispose(); _controller.dispose();
controller?.dispose(); _focusNode.dispose();
} }
Profile get profile => widget.profile; Profile get profile => widget.profile;
@@ -56,16 +54,9 @@ class _ViewProfileState extends State<ViewProfile> {
readOnly = false; readOnly = false;
}); });
} else { } else {
final text = controller?.text; if (_controller.text == rawText) return;
if (text == null || text == contentNotifier.value) {
setState(() {
readOnly = true;
});
return;
}
contentNotifier.value = text;
final newProfile = await key.currentState?.loadingRun<Profile>(() async { final newProfile = await key.currentState?.loadingRun<Profile>(() async {
return await profile.saveFileWithString(text); return await profile.saveFileWithString(_controller.text);
}); });
if (newProfile == null) return; if (newProfile == null) return;
globalState.appController.config.setProfile(newProfile); globalState.appController.config.setProfile(newProfile);
@@ -81,74 +72,67 @@ class _ViewProfileState extends State<ViewProfile> {
key: key, key: key,
actions: [ actions: [
IconButton( IconButton(
onPressed: controller?.undo, onPressed: _controller.undo,
icon: const Icon(Icons.undo), icon: const Icon(Icons.undo),
), ),
IconButton( IconButton(
onPressed: controller?.redo, onPressed: _controller.redo,
icon: const Icon(Icons.redo), icon: const Icon(Icons.redo),
), ),
if (!widget.profile.realAutoUpdate) IconButton(
IconButton( onPressed: _handleChangeReadOnly,
onPressed: _handleChangeReadOnly, icon: readOnly ? const Icon(Icons.edit) : const Icon(Icons.save),
icon: readOnly ? const Icon(Icons.edit) : const Icon(Icons.save), ),
),
const SizedBox(
width: 8,
)
], ],
body: ValueListenableBuilder( body: CodeEditor(
valueListenable: contentNotifier, readOnly: readOnly,
builder: (_, value, __) { focusNode: _focusNode,
if (value.isEmpty) return Container(); scrollbarBuilder: (context, child, details) {
controller = CodeLineEditingController.fromText(value); return Scrollbar(
return CodeEditor( controller: details.controller,
autofocus: false, thickness: 8,
readOnly: readOnly, radius: const Radius.circular(2),
scrollbarBuilder: (context, child, details) { interactive: true,
return Scrollbar( child: child,
controller: details.controller,
thickness: 8,
radius: const Radius.circular(2),
interactive: true,
child: child,
);
},
showCursorWhenReadOnly: false,
controller: controller,
toolbarController:
!readOnly ? const ContextMenuControllerImpl() : null,
shortcutsActivatorsBuilder:
const DefaultCodeShortcutsActivatorsBuilder(),
indicatorBuilder:
(context, editingController, chunkController, notifier) {
return Row(
children: [
DefaultCodeLineNumber(
controller: editingController,
notifier: notifier,
),
DefaultCodeChunkIndicator(
width: 20,
controller: chunkController,
notifier: notifier,
)
],
);
},
style: CodeEditorStyle(
fontSize: 14,
codeTheme: CodeHighlightTheme(
languages: {
'yaml': CodeHighlightThemeMode(
mode: langYaml,
)
},
theme: intellijLightTheme,
),
),
); );
}, },
showCursorWhenReadOnly: false,
controller: _controller,
shortcutsActivatorsBuilder:
const DefaultCodeShortcutsActivatorsBuilder(),
indicatorBuilder: (
context,
editingController,
chunkController,
notifier,
) {
return Row(
children: [
DefaultCodeLineNumber(
controller: editingController,
notifier: notifier,
),
DefaultCodeChunkIndicator(
width: 20,
controller: chunkController,
notifier: notifier,
)
],
);
},
toolbarController:
!readOnly ? ContextMenuControllerImpl(_focusNode) : null,
style: CodeEditorStyle(
fontSize: 14,
codeTheme: CodeHighlightTheme(
languages: {
'yaml': CodeHighlightThemeMode(
mode: langYaml,
)
},
theme: atomOneLightTheme,
),
),
), ),
title: widget.profile.label ?? widget.profile.id, title: widget.profile.label ?? widget.profile.id,
); );
@@ -164,10 +148,38 @@ class ContextMenuItemWidget extends PopupMenuItem<void> {
} }
class ContextMenuControllerImpl implements SelectionToolbarController { class ContextMenuControllerImpl implements SelectionToolbarController {
const ContextMenuControllerImpl(); OverlayEntry? _overlayEntry;
final FocusNode focusNode;
ContextMenuControllerImpl(
this.focusNode,
);
_removeOverLayEntry() {
_overlayEntry?.remove();
_overlayEntry = null;
}
@override @override
void hide(BuildContext context) {} void hide(BuildContext context) {
// _removeOverLayEntry();
}
_handleCut(CodeLineEditingController controller) {
controller.cut();
_removeOverLayEntry();
}
_handleCopy(CodeLineEditingController controller) async {
await controller.copy();
_removeOverLayEntry();
}
_handlePaste(CodeLineEditingController controller) {
controller.paste();
_removeOverLayEntry();
}
@override @override
void show({ void show({
@@ -181,27 +193,40 @@ class ContextMenuControllerImpl implements SelectionToolbarController {
if (controller.selectedText.isEmpty) { if (controller.selectedText.isEmpty) {
return; return;
} }
showMenu( _removeOverLayEntry();
context: context, final relativeRect = RelativeRect.fromSize(
position: RelativeRect.fromSize( (anchors.primaryAnchor) &
(anchors.secondaryAnchor ?? anchors.primaryAnchor) & const Size(150, double.infinity),
const Size(150, double.infinity), MediaQuery.of(context).size,
MediaQuery.of(context).size,
),
items: [
ContextMenuItemWidget(
text: appLocalizations.cut,
onTap: controller.cut,
),
ContextMenuItemWidget(
text: appLocalizations.copy,
onTap: controller.copy,
),
ContextMenuItemWidget(
text: appLocalizations.paste,
onTap: controller.paste,
),
],
); );
_overlayEntry ??= OverlayEntry(
builder: (context) => ValueListenableBuilder<CodeLineEditingValue>(
valueListenable: controller,
builder: (_, __, child) {
if (controller.selectedText.isEmpty) {
_removeOverLayEntry();
}
return child!;
},
child: Positioned(
left: relativeRect.left,
top: relativeRect.top,
child: Material(
color: Colors.transparent,
child: GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(focusNode);
},
child: Container(
width: 200,
height: 200,
color: Colors.green,
),
),
),
),
),
);
Overlay.of(context).insert(_overlayEntry!);
} }
} }

View File

@@ -1,6 +1,6 @@
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/proxies/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
@@ -10,7 +10,7 @@ import 'package:provider/provider.dart';
class ProxyCard extends StatelessWidget { class ProxyCard extends StatelessWidget {
final String groupName; final String groupName;
final Proxy proxy; final Proxy proxy;
final bool isSelected; final GroupType groupType;
final CommonCardType style; final CommonCardType style;
final ProxyCardType type; final ProxyCardType type;
@@ -18,12 +18,12 @@ class ProxyCard extends StatelessWidget {
super.key, super.key,
required this.groupName, required this.groupName,
required this.proxy, required this.proxy,
required this.isSelected, required this.groupType,
this.style = CommonCardType.plain, this.style = CommonCardType.plain,
required this.type, required this.type,
}); });
Measure get measure => globalState.appController.measure; Measure get measure => globalState.measure;
Widget _buildDelayText() { Widget _buildDelayText() {
return SizedBox( return SizedBox(
@@ -69,123 +69,176 @@ class ProxyCard extends StatelessWidget {
if (type == ProxyCardType.min) { if (type == ProxyCardType.min) {
return SizedBox( return SizedBox(
height: measure.bodyMediumHeight * 1, height: measure.bodyMediumHeight * 1,
child: Text( child: EmojiText(
proxy.name, proxy.name,
maxLines: 1, maxLines: 1,
style: context.textTheme.bodyMedium?.copyWith( overflow: TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium,
),
), ),
); );
} else { } else {
return SizedBox( return SizedBox(
height: measure.bodyMediumHeight * 2, height: measure.bodyMediumHeight * 2,
child: Text( child: EmojiText(
proxy.name, proxy.name,
maxLines: 2, maxLines: 2,
style: context.textTheme.bodyMedium?.copyWith( overflow: TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium,
),
), ),
); );
} }
} }
_changeProxy(BuildContext context) { _changeProxy(BuildContext context) async {
final appController = globalState.appController; final appController = globalState.appController;
final group = appController.appState.getGroupWithName(groupName)!; final isUrlTest = groupType == GroupType.URLTest;
if (group.type != GroupType.Selector) { final isSelector = groupType == GroupType.Selector;
globalState.showSnackBar( if (isUrlTest || isSelector) {
context, final currentProxyName =
message: appLocalizations.notSelectedTip, appController.config.currentSelectedMap[groupName];
final nextProxyName = switch (isUrlTest) {
true => currentProxyName == proxy.name ? "" : proxy.name,
false => proxy.name,
};
appController.config.updateCurrentSelectedMap(
groupName,
nextProxyName,
); );
appController.changeProxy(
groupName: groupName,
proxyName: nextProxyName,
);
await appController.updateGroupDebounce();
return; return;
} }
globalState.appController.config.updateCurrentSelectedMap( globalState.showSnackBar(
groupName, context,
proxy.name, message: appLocalizations.notSelectedTip,
);
globalState.changeProxy(
config: appController.config,
groupName: groupName,
proxyName: proxy.name,
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final measure = globalState.appController.measure; final measure = globalState.measure;
final delayText = _buildDelayText(); final delayText = _buildDelayText();
final proxyNameText = _buildProxyNameText(context); final proxyNameText = _buildProxyNameText(context);
return CommonCard( return currentGroupProxyNameBuilder(
type: style, groupName: groupName,
key: key, builder: (currentGroupName) {
onPressed: () { return Stack(
_changeProxy(context);
},
isSelected: isSelected,
child: Container(
padding: const EdgeInsets.all(12),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
proxyNameText, CommonCard(
const SizedBox( type: style,
height: 8, key: key,
), onPressed: () {
if (type == ProxyCardType.expand) ...[ _changeProxy(context);
SizedBox( },
height: measure.bodySmallHeight, isSelected: currentGroupName == proxy.name,
child: Selector<AppState, String>( child: Container(
selector: (context, appState) => appState.getDesc( padding: const EdgeInsets.all(12),
proxy.type, child: Column(
proxy.name, mainAxisSize: MainAxisSize.min,
), crossAxisAlignment: CrossAxisAlignment.start,
builder: (_, desc, __) {
return TooltipText(
text: Text(
desc,
style: context.textTheme.bodySmall?.copyWith(
overflow: TextOverflow.ellipsis,
color: context.textTheme.bodySmall?.color?.toLight(),
),
),
);
},
),
),
const SizedBox(
height: 8,
),
delayText,
] else
SizedBox(
height: measure.bodySmallHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Flexible( proxyNameText,
flex: 1, const SizedBox(
child: TooltipText( height: 8,
text: Text( ),
proxy.type, if (type == ProxyCardType.expand) ...[
style: context.textTheme.bodySmall?.copyWith( SizedBox(
overflow: TextOverflow.ellipsis, height: measure.bodySmallHeight,
color: child: Selector<AppState, String>(
context.textTheme.bodySmall?.color?.toLight(), selector: (context, appState) => appState.getDesc(
proxy.type,
proxy.name,
), ),
builder: (_, desc, __) {
return EmojiText(
desc,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall?.copyWith(
color: context.textTheme.bodySmall?.color
?.toLight(),
),
);
},
),
),
const SizedBox(
height: 8,
),
delayText,
] else
SizedBox(
height: measure.bodySmallHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
proxy.type,
style: context.textTheme.bodySmall?.copyWith(
overflow: TextOverflow.ellipsis,
color: context.textTheme.bodySmall?.color
?.toLight(),
),
),
),
),
delayText,
],
), ),
), ),
),
delayText,
], ],
), ),
), ),
),
if (groupType == GroupType.URLTest)
Selector<Config, String>(
selector: (_, config) {
final selectedProxyName =
config.currentSelectedMap[groupName];
return selectedProxyName ?? '';
},
builder: (_, value, __) {
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(),
),
),
);
},
child: 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(),
),
),
),
)
], ],
), );
), },
); );
} }
} }

View File

@@ -1,35 +1,36 @@
import 'dart:math'; import 'dart:math';
import 'package:fl_clash/clash/clash.dart'; 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/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
Widget currentProxyNameBuilder({ Widget currentGroupProxyNameBuilder({
required String groupName, required String groupName,
required Widget Function(String) builder, required Widget Function(String currentGroupName) builder,
}) { }) {
return Selector2<AppState, Config, String>( return Selector2<AppState, Config, String>(
selector: (_, appState, config) { selector: (_, appState, config) {
final group = appState.getGroupWithName(groupName); final group = appState.getGroupWithName(groupName);
return config.currentSelectedMap[groupName] ?? group?.now ?? ''; final selectedProxyName = config.currentSelectedMap[groupName];
return group?.getCurrentSelectedName(selectedProxyName ?? "") ?? "";
}, },
builder: (_, value, ___) { builder: (_, currentGroupName, ___) {
return builder(value); return builder(currentGroupName);
}, },
); );
} }
double get listHeaderHeight { double get listHeaderHeight {
final measure = globalState.appController.measure; final measure = globalState.measure;
return 24 + measure.titleMediumHeight + 4 + measure.bodyMediumHeight; return 24 + measure.titleMediumHeight + 4 + measure.bodyMediumHeight;
} }
double getItemHeight(ProxyCardType proxyCardType) { double getItemHeight(ProxyCardType proxyCardType) {
final measure = globalState.appController.measure; final measure = globalState.measure;
final baseHeight = final baseHeight =
12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8; 12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8;
return switch (proxyCardType) { return switch (proxyCardType) {
@@ -41,20 +42,21 @@ double getItemHeight(ProxyCardType proxyCardType) {
delayTest(List<Proxy> proxies) async { delayTest(List<Proxy> proxies) async {
final appController = globalState.appController; final appController = globalState.appController;
for (final proxy in proxies) { final delayProxies = proxies.map<Future>((proxy) async {
final proxyName = final proxyName = appController.appState.getRealProxyName(proxy.name);
appController.appState.getRealProxyName(proxy.name) ?? proxy.name;
globalState.appController.setDelay( globalState.appController.setDelay(
Delay( Delay(
name: proxyName, name: proxyName,
value: 0, value: 0,
), ),
); );
clashCore.getDelay(proxyName).then((delay) { globalState.appController.setDelay(await clashCore.getDelay(proxyName));
globalState.appController.setDelay(delay); }).toList();
});
final batchesDelayProxies = delayProxies.batch(100);
for (final batchDelayProxies in batchesDelayProxies) {
await Future.wait(batchDelayProxies);
} }
await Future.delayed(httpTimeoutDuration + moreDuration);
appController.appState.sortNum++; appController.appState.sortNum++;
} }
@@ -63,13 +65,16 @@ double getScrollToSelectedOffset({
required List<Proxy> proxies, required List<Proxy> proxies,
}) { }) {
final appController = globalState.appController; final appController = globalState.appController;
final columns = appController.columns; final columns = other.getProxiesColumns(
appController.appState.viewWidth,
appController.config.proxiesLayout,
);
final proxyCardType = appController.config.proxyCardType; final proxyCardType = appController.config.proxyCardType;
final selectedName = appController.getCurrentSelectedName(groupName); final selectedName = appController.getCurrentSelectedName(groupName);
final findSelectedIndex = proxies.indexWhere( final findSelectedIndex = proxies.indexWhere(
(proxy) => proxy.name == selectedName, (proxy) => proxy.name == selectedName,
); );
final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0; final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0;
final rows = ((selectedIndex - 1) / columns).ceil(); final rows = (selectedIndex / columns).floor();
return max(rows * (getItemHeight(proxyCardType) + 8) - 8, 0); return max(rows * (getItemHeight(proxyCardType) + 8) - 8, 0);
} }

View File

@@ -1,9 +1,12 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/card.dart'; import 'package:fl_clash/widgets/card.dart';
import 'package:fl_clash/widgets/fade_box.dart';
import 'package:fl_clash/widgets/text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -140,17 +143,13 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
final children = proxies final children = proxies
.map<Widget>( .map<Widget>(
(proxy) => Flexible( (proxy) => Flexible(
child: currentProxyNameBuilder( child: ProxyCard(
groupName: group.name, type: type,
builder: (currentProxyName) { groupType: group.type,
return ProxyCard( key: ValueKey('$groupName.${proxy.name}'),
type: type, proxy: proxy,
isSelected: currentProxyName == proxy.name, groupName: groupName,
key: ValueKey('$groupName.${proxy.name}'), ),
proxy: proxy,
groupName: groupName,
);
}),
), ),
) )
.fill( .fill(
@@ -209,6 +208,9 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
} }
_scrollToGroupSelected(String groupName) { _scrollToGroupSelected(String groupName) {
if (_controller.position.maxScrollExtent == 0) {
return;
}
final appController = globalState.appController; final appController = globalState.appController;
final currentGroups = appController.appState.currentGroups; final currentGroups = appController.appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList(); final groupNames = currentGroups.map((e) => e.name).toList();
@@ -238,7 +240,10 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
currentUnfoldSet: config.currentUnfoldSet, currentUnfoldSet: config.currentUnfoldSet,
proxyCardType: config.proxyCardType, proxyCardType: config.proxyCardType,
proxiesSortType: config.proxiesSortType, proxiesSortType: config.proxiesSortType,
columns: globalState.appController.columns, columns: other.getProxiesColumns(
appState.viewWidth,
config.proxiesLayout,
),
sortNum: appState.sortNum, sortNum: appState.sortNum,
); );
}, },
@@ -350,6 +355,8 @@ class _ListHeaderState extends State<ListHeader>
late Animation<double> _iconTurns; late Animation<double> _iconTurns;
var isLock = false; var isLock = false;
String get icon => widget.group.icon;
String get groupName => widget.group.name; String get groupName => widget.group.name;
String get groupType => widget.group.type.name; String get groupType => widget.group.type.name;
@@ -393,10 +400,23 @@ class _ListHeaderState extends State<ListHeader>
super.dispose(); super.dispose();
} }
@override
void didUpdateWidget(ListHeader oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.isExpand != widget.isExpand) {
if (isExpand) {
_animationController.value = 1.0;
} else {
_animationController.value = 0.0;
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CommonCard( return CommonCard(
key: widget.key, key: widget.key,
radius: 24,
type: CommonCardType.filled, type: CommonCardType.filled,
child: Container( child: Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
@@ -404,59 +424,96 @@ class _ListHeaderState extends State<ListHeader>
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Flexible( Flexible(
child: Column( child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( const SizedBox(
groupName, width: 4,
style: context.textTheme.titleMedium?.copyWith( ),
color: context.colorScheme.primary, 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( const SizedBox(
height: 4, width: 16,
), ),
Flexible( Flexible(
flex: 1, child: Column(
child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text( Text(
groupType, groupName,
style: context.textTheme.labelMedium?.toLight, style: context.textTheme.titleMedium,
),
const SizedBox(
height: 4,
), ),
Flexible( Flexible(
flex: 1, flex: 1,
child: currentProxyNameBuilder( child: Row(
groupName: groupName, mainAxisSize: MainAxisSize.min,
builder: (value) { mainAxisAlignment: MainAxisAlignment.start,
return Row( crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min, children: [
mainAxisAlignment: MainAxisAlignment.start, Text(
crossAxisAlignment: CrossAxisAlignment.center, groupType,
children: [ style: context.textTheme.labelMedium?.toLight,
if (value.isNotEmpty) ...[ ),
Flexible( Flexible(
flex: 1, flex: 1,
child: Text( child: currentGroupProxyNameBuilder(
overflow: TextOverflow.ellipsis, groupName: groupName,
" · $value", builder: (currentGroupName) {
style: context return Row(
.textTheme.labelMedium?.toLight, 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

@@ -0,0 +1,219 @@
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';
import 'package:fl_clash/models/ffi.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
typedef UpdatingMap = Map<String, bool>;
class Providers extends StatefulWidget {
const Providers({
super.key,
});
@override
State<Providers> createState() => _ProvidersState();
}
class _ProvidersState extends State<Providers> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback(
(_) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
_updateProviders();
},
icon: const Icon(
Icons.sync,
),
)
];
},
);
}
_updateProviders() async {
final appState = globalState.appController.appState;
final providers = globalState.appController.appState.providers;
final updateProviders = providers.map<Future>(
(provider) async {
appState.setProvider(
provider.copyWith(isUpdating: true),
);
await clashCore.updateExternalProvider(
providerName: provider.name,
);
appState.setProvider(
clashCore.getExternalProvider(provider.name),
);
},
);
await Future.wait(updateProviders);
await globalState.appController.updateGroupDebounce();
}
@override
Widget build(BuildContext context) {
return Selector<AppState, List<ExternalProvider>>(
selector: (_, appState) => appState.providers,
builder: (_, providers, ___) {
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,
]);
},
);
}
}
class ProviderItem extends StatelessWidget {
final ExternalProvider provider;
const ProviderItem({
super.key,
required this.provider,
});
_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();
}
_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),
);
if (message.isNotEmpty) throw message;
appState.setProvider(
clashCore.getExternalProvider(provider.name),
);
if (message.isNotEmpty) throw message;
});
await globalState.appController.updateGroupDebounce();
}
String _buildProviderDesc() {
final baseInfo =
"${provider.type}(${provider.vehicleType}) · ${provider.updateAt.lastUpdateTimeDesc}";
final count = provider.count;
return switch (count == 0) {
true => baseInfo,
false => "$baseInfo · $count${appLocalizations.entries}",
};
}
@override
Widget build(BuildContext context) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
title: Text(provider.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 4,
),
Text(
_buildProviderDesc(),
),
Text(
provider.path,
style: context.textTheme.bodyMedium?.toLight,
),
const SizedBox(
height: 8,
),
Wrap(
runSpacing: 6,
spacing: 12,
children: [
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: _handleUpdateProvider,
),
],
),
],
),
trailing: SizedBox(
height: 48,
width: 48,
child: FadeBox(
child: provider.isUpdating
? const Padding(
padding: EdgeInsets.all(8),
child: CircularProgressIndicator(),
)
: const SizedBox(),
),
),
);
}
}

View File

@@ -6,6 +6,7 @@ import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'providers.dart';
import 'setting.dart'; import 'setting.dart';
import 'tab.dart'; import 'tab.dart';
@@ -19,18 +20,37 @@ class ProxiesFragment extends StatefulWidget {
class _ProxiesFragmentState extends State<ProxiesFragment> { class _ProxiesFragmentState extends State<ProxiesFragment> {
final GlobalKey<ProxiesTabFragmentState> _proxiesTabKey = GlobalKey(); final GlobalKey<ProxiesTabFragmentState> _proxiesTabKey = GlobalKey();
_initActions(ProxiesType proxiesType) { _initActions(ProxiesType proxiesType, bool hasProvider) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState = final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>(); context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [ commonScaffoldState?.actions = [
if (hasProvider) ...[
IconButton(
onPressed: () {
showExtendPage(
isScaffold: true,
extendPageWidth: 360,
context,
body: const Providers(),
title: appLocalizations.providers,
);
},
icon: const Icon(
Icons.swap_vert_circle_outlined,
),
),
const SizedBox(
width: 8,
),
],
if (proxiesType == ProxiesType.tab) ...[ if (proxiesType == ProxiesType.tab) ...[
IconButton( IconButton(
onPressed: () { onPressed: () {
_proxiesTabKey.currentState?.scrollToGroupSelected(); _proxiesTabKey.currentState?.scrollToGroupSelected();
}, },
icon: const Icon( icon: const Icon(
Icons.gps_fixed, Icons.adjust_outlined,
), ),
), ),
const SizedBox( const SizedBox(
@@ -60,18 +80,18 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
return Selector<Config, ProxiesType>( return Selector<Config, ProxiesType>(
selector: (_, config) => config.proxiesType, selector: (_, config) => config.proxiesType,
builder: (_, proxiesType, __) { builder: (_, proxiesType, __) {
return Selector<AppState, bool>( return ProxiesActionsBuilder(
selector: (_, appState) => appState.currentLabel == 'proxies', builder: (state, child) {
builder: (_, isCurrent, child) { if (state.isCurrent) {
if (isCurrent) { _initActions(proxiesType, state.hasProvider);
_initActions(proxiesType);
} }
return switch (proxiesType) { return child!;
ProxiesType.tab => ProxiesTabFragment( },
key: _proxiesTabKey, child: switch (proxiesType) {
), ProxiesType.tab => ProxiesTabFragment(
ProxiesType.list => const ProxiesListFragment(), key: _proxiesTabKey,
}; ),
ProxiesType.list => const ProxiesListFragment(),
}, },
); );
}, },

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() { List<Widget> _buildStyleSetting() {
return generateSection( return generateSection(
title: appLocalizations.style, title: appLocalizations.style,
@@ -132,36 +140,28 @@ class ProxiesSettingWidget extends StatelessWidget {
); );
} }
List<Widget> _buildColumnsSetting() { List<Widget> _buildLayoutSetting() {
return generateSection( return generateSection(
title: appLocalizations.columns, title: appLocalizations.layout,
items: [ items: [
SingleChildScrollView( SingleChildScrollView(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
), ),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Selector2<AppState, Config, ColumnsSelectorState>( child: Selector< Config, ProxiesLayout>(
selector: (_, appState, config) => ColumnsSelectorState( selector: (_, config) => config.proxiesLayout,
columns: config.proxiesColumns, builder: (_, proxiesLayout, __) {
viewMode: appState.viewMode,
),
builder: (_, state, __) {
final config = globalState.appController.config; final config = globalState.appController.config;
final targetColumnsArray = viewModeColumnsMap[state.viewMode]!;
final currentColumns = other.getColumns(
state.viewMode,
state.columns,
);
return Wrap( return Wrap(
spacing: 16, spacing: 16,
children: [ children: [
for (final item in targetColumnsArray) for (final item in ProxiesLayout.values)
SettingTextCard( SettingTextCard(
other.getColumnsTextForInt(item), getTextForProxiesLayout(item),
isSelected: item == currentColumns, isSelected: item == proxiesLayout,
onPressed: () { onPressed: () {
config.proxiesColumns = item; config.proxiesLayout = item;
}, },
) )
], ],
@@ -183,80 +183,10 @@ class ProxiesSettingWidget extends StatelessWidget {
children: [ children: [
..._buildStyleSetting(), ..._buildStyleSetting(),
..._buildSortSetting(), ..._buildSortSetting(),
..._buildColumnsSetting(), ..._buildLayoutSetting(),
..._buildSizeSetting(), ..._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,6 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.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/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
@@ -262,54 +261,10 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
_controller.dispose(); _controller.dispose();
} }
Widget _buildTabGroupView({
required List<Proxy> proxies,
required int columns,
required ProxyCardType proxyCardType,
}) {
final sortedProxies = globalState.appController.getSortProxies(
proxies,
);
_lastProxies = sortedProxies;
return DelayTestButtonContainer(
onClick: () async {
await _delayTest(
proxies,
);
},
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),
),
itemCount: sortedProxies.length,
itemBuilder: (_, index) {
final proxy = sortedProxies[index];
return currentProxyNameBuilder(
builder: (value) {
return ProxyCard(
type: proxyCardType,
key: ValueKey('$groupName.${proxy.name}'),
isSelected: value == proxy.name,
proxy: proxy,
groupName: groupName,
);
},
groupName: groupName,
);
},
),
),
);
}
scrollToSelected() { scrollToSelected() {
if (_controller.position.maxScrollExtent == 0) {
return;
}
_controller.animateTo( _controller.animateTo(
16 + 16 +
getScrollToSelectedOffset( getScrollToSelectedOffset(
@@ -329,19 +284,53 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
return ProxyGroupSelectorState( return ProxyGroupSelectorState(
proxyCardType: config.proxyCardType, proxyCardType: config.proxyCardType,
proxiesSortType: config.proxiesSortType, proxiesSortType: config.proxiesSortType,
columns: globalState.appController.columns, columns: other.getProxiesColumns(
appState.viewWidth,
config.proxiesLayout,
),
sortNum: appState.sortNum, sortNum: appState.sortNum,
proxies: group.all, proxies: group.all,
groupType: group.type,
); );
}, },
builder: (_, state, __) { builder: (_, state, __) {
final proxies = state.proxies; final proxies = state.proxies;
final columns = state.columns; final columns = state.columns;
final proxyCardType = state.proxyCardType; final proxyCardType = state.proxyCardType;
return _buildTabGroupView( final sortedProxies = globalState.appController.getSortProxies(
proxies: proxies, proxies,
columns: columns, );
proxyCardType: proxyCardType, _lastProxies = sortedProxies;
return DelayTestButtonContainer(
onClick: () async {
await _delayTest(
proxies,
);
},
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),
),
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

@@ -22,126 +22,47 @@ class GeoItem {
}); });
} }
@immutable class Resources extends StatelessWidget {
class FileInfo {
final String size;
final DateTime lastModified;
const FileInfo({
required this.size,
required this.lastModified,
});
}
class Resources extends StatefulWidget {
const Resources({super.key}); const Resources({super.key});
@override @override
State<Resources> createState() => _ResourcesState(); Widget build(BuildContext context) {
}
class _ResourcesState extends State<Resources> {
List<ExternalProvider> externalProviders = [];
List<GlobalObjectKey<_ProviderItemState>> providerItemKeys = [];
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_syncExternalProviders();
});
}
_syncExternalProviders() async {
externalProviders = await clashCore.getExternalProviders();
if (mounted) {
setState(() {});
}
}
_updateProviders() async {
final updateProviders = providerItemKeys.map<Future>(
(key) async => await key.currentState?.updateProvider(false),
);
await Future.wait(updateProviders);
_syncExternalProviders();
}
List<Widget> _buildExternalProviderSection() {
List<GlobalObjectKey<_ProviderItemState>> keys = [];
final res = generateInfoSection(
info: Info(
iconData: Icons.source,
label: appLocalizations.externalResources,
),
actions: [
IconButton.filledTonal(
onPressed: () {
_updateProviders();
},
padding: const EdgeInsets.all(4),
iconSize: 20,
icon: const Icon(
Icons.sync,
),
)
],
items: externalProviders.map(
(externalProvider) {
final key =
GlobalObjectKey<_ProviderItemState>(externalProvider.name);
keys.add(key);
return ProviderItem(
key: key,
provider: externalProvider,
onUpdated: () {
_syncExternalProviders();
},
);
},
),
);
providerItemKeys = keys;
return res;
}
List<Widget> _buildGeoDataSection() {
const geoItems = <GeoItem>[ const geoItems = <GeoItem>[
GeoItem( GeoItem(
label: "GeoIp", label: "GeoIp",
fileName: geoIpFileName, fileName: geoIpFileName,
key: "geoip", key: "geoip",
), ),
GeoItem(label: "GeoSite", fileName: geoSiteFileName, key: "geosite"), GeoItem(
label: "GeoSite",
fileName: geoSiteFileName,
key: "geosite",
),
GeoItem( GeoItem(
label: "MMDB", label: "MMDB",
fileName: mmdbFileName, fileName: mmdbFileName,
key: "mmdb", key: "mmdb",
), ),
GeoItem(label: "ASN", fileName: asnFileName, key: "asn"), GeoItem(
label: "ASN",
fileName: asnFileName,
key: "asn",
),
]; ];
return generateInfoSection( return ListView.separated(
info: Info( itemBuilder: (_, index) {
iconData: Icons.storage, final geoItem = geoItems[index];
label: appLocalizations.geoData, return GeoDataListItem(
),
items: geoItems.map(
(geoItem) => GeoDataListItem(
geoItem: geoItem, geoItem: geoItem,
), );
), },
); separatorBuilder: (BuildContext context, int index) {
} return const Divider(
height: 0,
@override );
Widget build(BuildContext context) { },
return generateListView( itemCount: geoItems.length,
[
..._buildGeoDataSection(),
..._buildExternalProviderSection(),
],
); );
} }
} }
@@ -168,6 +89,7 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
child: UpdateGeoUrlFormDialog( child: UpdateGeoUrlFormDialog(
title: geoItem.label, title: geoItem.label,
url: url, url: url,
defaultValue: defaultGeoXMap[geoItem.key],
), ),
); );
if (newUrl != null && newUrl != url && mounted) { if (newUrl != null && newUrl != url && mounted) {
@@ -178,7 +100,6 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
final appController = globalState.appController; final appController = globalState.appController;
appController.clashConfig.geoXUrl = appController.clashConfig.geoXUrl =
Map.from(appController.clashConfig.geoXUrl)..[geoItem.key] = newUrl; Map.from(appController.clashConfig.geoXUrl)..[geoItem.key] = newUrl;
appController.updateClashConfigDebounce();
} catch (e) { } catch (e) {
globalState.showMessage( globalState.showMessage(
title: geoItem.label, title: geoItem.label,
@@ -196,27 +117,11 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
final lastModified = await file.lastModified(); final lastModified = await file.lastModified();
final size = await file.length(); final size = await file.length();
return FileInfo( return FileInfo(
size: TrafficValue(value: size).show, size: size,
lastModified: lastModified, lastModified: lastModified,
); );
} }
// _uploadGeoFile(String fileName) async {
// final res = await picker.pickerGeoDataFile();
// if (res == null || res.bytes == null) return;
// final homePath = await appPath.getHomeDirPath();
// final file = File(join(homePath, fileName));
// await file.writeAsBytes(
// res.bytes!,
// flush: true,
// );
// setState(() {});
// }
String _buildFileInfoDesc(FileInfo fileInfo) {
return "${fileInfo.size} · ${fileInfo.lastModified.lastUpdateTimeDesc}";
}
Widget _buildSubtitle(String url) { Widget _buildSubtitle(String url) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -240,7 +145,7 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
), ),
) )
: Text( : Text(
_buildFileInfoDesc(snapshot.data!), snapshot.data!.desc,
), ),
), ),
); );
@@ -253,9 +158,6 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
const SizedBox( const SizedBox(
height: 8, height: 8,
), ),
const SizedBox(
height: 8,
),
Wrap( Wrap(
runSpacing: 6, runSpacing: 6,
spacing: 12, spacing: 12,
@@ -288,9 +190,9 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
updateGeoDateItem() async { updateGeoDateItem() async {
isUpdating.value = true; isUpdating.value = true;
try { try {
final message = await clashCore.updateExternalProvider( final message = await clashCore.updateGeoData(
providerName: geoItem.fileName, geoName: geoItem.fileName,
providerType: geoItem.label, geoType: geoItem.label,
); );
if (message.isNotEmpty) throw message; if (message.isNotEmpty) throw message;
} catch (e) { } catch (e) {
@@ -342,125 +244,16 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
} }
} }
class ProviderItem extends StatefulWidget {
final ExternalProvider provider;
final Function onUpdated;
const ProviderItem({
super.key,
required this.provider,
required this.onUpdated,
});
@override
State<ProviderItem> createState() => _ProviderItemState();
}
class _ProviderItemState extends State<ProviderItem> {
final isUpdating = ValueNotifier<bool>(false);
ExternalProvider get provider => widget.provider;
_handleUpdateProfile() async {
await globalState.safeRun<void>(updateProvider);
widget.onUpdated();
}
updateProvider([isSingle = true]) async {
if (provider.vehicleType != "HTTP") return;
isUpdating.value = true;
try {
final message = await clashCore.updateExternalProvider(
providerName: provider.name,
providerType: provider.type,
);
if (message.isNotEmpty) throw message;
} catch (e) {
isUpdating.value = false;
if (!isSingle) {
return e.toString();
} else {
rethrow;
}
}
isUpdating.value = false;
return null;
}
String _buildProviderDesc() {
return "${provider.type} (${provider.vehicleType}) · ${provider.updateAt.lastUpdateTimeDesc}";
}
@override
void dispose() {
super.dispose();
isUpdating.dispose();
}
Widget _buildSubtitle() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 4,
),
Text(
_buildProviderDesc(),
),
if (provider.vehicleType == "HTTP") ...[
const SizedBox(
height: 8,
),
CommonChip(
avatar: const Icon(Icons.sync),
label: appLocalizations.sync,
onPressed: () {
_handleUpdateProfile();
},
),
],
],
);
}
@override
Widget build(BuildContext context) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
title: Text(provider.name),
subtitle: _buildSubtitle(),
trailing: SizedBox(
height: 48,
width: 48,
child: ValueListenableBuilder(
valueListenable: isUpdating,
builder: (_, isUpdating, ___) {
return FadeBox(
child: isUpdating
? const Padding(
padding: EdgeInsets.all(8),
child: CircularProgressIndicator(),
)
: const SizedBox(),
);
},
),
),
);
}
}
class UpdateGeoUrlFormDialog extends StatefulWidget { class UpdateGeoUrlFormDialog extends StatefulWidget {
final String title; final String title;
final String url; final String url;
final String? defaultValue;
const UpdateGeoUrlFormDialog({ const UpdateGeoUrlFormDialog({
super.key, super.key,
required this.title, required this.title,
required this.url, required this.url,
this.defaultValue
}); });
@override @override
@@ -476,6 +269,13 @@ class _UpdateGeoUrlFormDialogState extends State<UpdateGeoUrlFormDialog> {
urlController = TextEditingController(text: widget.url); urlController = TextEditingController(text: widget.url);
} }
_handleReset() async {
if (widget.defaultValue == null) {
return;
}
Navigator.of(context).pop<String>(widget.defaultValue);
}
_handleUpdate() async { _handleUpdate() async {
final url = urlController.value.text; final url = urlController.value.text;
if (url.isEmpty) return; if (url.isEmpty) return;
@@ -503,6 +303,16 @@ class _UpdateGeoUrlFormDialogState extends State<UpdateGeoUrlFormDialog> {
), ),
), ),
actions: [ actions: [
if (widget.defaultValue != null &&
urlController.value.text != widget.defaultValue) ...[
TextButton(
onPressed: _handleReset,
child: Text(appLocalizations.reset),
),
const SizedBox(
width: 4,
),
],
TextButton( TextButton(
onPressed: _handleUpdate, onPressed: _handleUpdate,
child: Text(appLocalizations.submit), child: Text(appLocalizations.submit),
@@ -510,4 +320,4 @@ class _UpdateGeoUrlFormDialogState extends State<UpdateGeoUrlFormDialog> {
], ],
); );
} }
} }

View File

@@ -26,9 +26,7 @@ class ThemeFragment extends StatelessWidget {
final previewCard = Padding( final previewCard = Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: CommonCard( child: CommonCard(
onPressed: (){ onPressed: () {},
},
info: Info( info: Info(
label: appLocalizations.preview, label: appLocalizations.preview,
iconData: Icons.looks, iconData: Icons.looks,
@@ -87,7 +85,6 @@ class ThemeColorsBox extends StatefulWidget {
} }
class _ThemeColorsBoxState extends State<ThemeColorsBox> { class _ThemeColorsBoxState extends State<ThemeColorsBox> {
Widget _themeModeCheckBox({ Widget _themeModeCheckBox({
bool? isSelected, bool? isSelected,
required ThemeModeItem themeModeItem, required ThemeModeItem themeModeItem,
@@ -229,6 +226,78 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
), ),
), ),
), ),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Selector<Config, bool>(
selector: (_, config) => config.prueBlack,
builder: (_, value, ___) {
return ListItem.switchItem(
leading: Icon(
Icons.contrast,
color: context.colorScheme.primary,
),
title: Text(appLocalizations.prueBlackMode),
delegate: SwitchDelegate(
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,7 +4,7 @@ import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/about.dart'; import 'package:fl_clash/fragments/about.dart';
import 'package:fl_clash/fragments/access.dart'; import 'package:fl_clash/fragments/access.dart';
import 'package:fl_clash/fragments/application_setting.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/l10n/l10n.dart'; import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
@@ -82,44 +82,23 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
builder: (_, localeString, __) { builder: (_, localeString, __) {
final subTitle = localeString ?? appLocalizations.defaultText; final subTitle = localeString ?? appLocalizations.defaultText;
final currentLocale = other.getLocaleForString(localeString); final currentLocale = other.getLocaleForString(localeString);
return ListTile( return ListItem<Locale?>.options(
leading: const Icon(Icons.language_outlined), leading: const Icon(Icons.language_outlined),
title: Text(appLocalizations.language), title: Text(appLocalizations.language),
subtitle: Text(Intl.message(subTitle)), subtitle: Text(Intl.message(subTitle)),
onTap: () { delegate: OptionsDelegate(
globalState.showCommonDialog( title: appLocalizations.language,
child: AlertDialog( options: [
title: Text(appLocalizations.language), null,
contentPadding: const EdgeInsets.symmetric( ...AppLocalizations.delegate.supportedLocales
horizontal: 8, ],
vertical: 16, onChanged: (Locale? value) {
), final config = context.read<Config>();
content: SizedBox( config.locale = value?.toString();
width: 250, },
child: Wrap( textBuilder: (locale) => _getLocaleString(locale),
children: [ value: currentLocale,
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)),
)
],
),
),
),
);
},
); );
}, },
), ),
@@ -159,11 +138,10 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
delegate: OpenDelegate( delegate: OpenDelegate(
title: appLocalizations.override, title: appLocalizations.override,
widget: const ConfigFragment(), widget: const ConfigFragment(),
extendPageWidth: 360,
), ),
), ),
ListItem.open( ListItem.open(
leading: const Icon(Icons.settings_applications), leading: const Icon(Icons.settings),
title: Text(appLocalizations.application), title: Text(appLocalizations.application),
subtitle: Text(appLocalizations.applicationDesc), subtitle: Text(appLocalizations.applicationDesc),
delegate: OpenDelegate( delegate: OpenDelegate(

View File

@@ -37,7 +37,7 @@
"overrideDesc": "Override Proxy related config", "overrideDesc": "Override Proxy related config",
"allowLan": "AllowLan", "allowLan": "AllowLan",
"allowLanDesc": "Allow access proxy through the LAN", "allowLanDesc": "Allow access proxy through the LAN",
"tun": "TUN mode", "tun": "TUN",
"tunDesc": "only effective in administrator mode", "tunDesc": "only effective in administrator mode",
"minimizeOnExit": "Minimize on exit", "minimizeOnExit": "Minimize on exit",
"minimizeOnExitDesc": "Modify the default system exit event", "minimizeOnExitDesc": "Modify the default system exit event",
@@ -66,6 +66,7 @@
"hours": "Hours", "hours": "Hours",
"days": "Days", "days": "Days",
"minutes": "Minutes", "minutes": "Minutes",
"seconds": "Seconds",
"ago": " Ago", "ago": " Ago",
"just": "Just", "just": "Just",
"qrcode": "QR code", "qrcode": "QR code",
@@ -116,7 +117,7 @@
"logLevel": "LogLevel", "logLevel": "LogLevel",
"show": "Show", "show": "Show",
"exit": "Exit", "exit": "Exit",
"systemProxy": "SystemProxy", "systemProxy": "System proxy",
"project": "Project", "project": "Project",
"core": "Core", "core": "Core",
"tabAnimation": "Tab animation", "tabAnimation": "Tab animation",
@@ -130,12 +131,10 @@
"notSelectedTip": "The current proxy group cannot be selected.", "notSelectedTip": "The current proxy group cannot be selected.",
"tip": "tip", "tip": "tip",
"backupAndRecovery": "Backup and Recovery", "backupAndRecovery": "Backup and Recovery",
"backupAndRecoveryDesc": "Sync data by WebDAV", "backupAndRecoveryDesc": "Sync data via WebDAV or file",
"account": "Account", "account": "Account",
"backup": "Backup", "backup": "Backup",
"backupDesc": "Backup local data to WebDAV",
"recovery": "Recovery", "recovery": "Recovery",
"recoveryDesc": "Recovery data from WebDAV",
"recoveryProfiles": "Only recovery profiles", "recoveryProfiles": "Only recovery profiles",
"recoveryAll": "Recovery all data", "recoveryAll": "Recovery all data",
"recoverySuccess": "Recovery success", "recoverySuccess": "Recovery success",
@@ -216,5 +215,78 @@
"externalLink": "External link", "externalLink": "External link",
"otherContributors": "Other contributors", "otherContributors": "Other contributors",
"autoCloseConnections": "Auto lose connections", "autoCloseConnections": "Auto lose connections",
"autoCloseConnectionsDesc": "Auto close connections after change node" "autoCloseConnectionsDesc": "Auto close connections after change node",
"onlyStatisticsProxy": "Only statistics proxy",
"onlyStatisticsProxyDesc": "When turned on, only statistics proxy traffic",
"deleteProfileTip": "Sure you want to delete the current profile?",
"prueBlackMode": "Prue black mode",
"keepAliveIntervalDesc": "Tcp keep alive interval",
"entries": " entries",
"local": "Local",
"remote": "Remote",
"remoteBackupDesc": "Backup local data to WebDAV",
"remoteRecoveryDesc": "Recovery data from WebDAV",
"localBackupDesc": "Backup local data to local",
"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"
} }

View File

@@ -37,7 +37,7 @@
"overrideDesc": "覆写代理相关配置", "overrideDesc": "覆写代理相关配置",
"allowLan": "局域网代理", "allowLan": "局域网代理",
"allowLanDesc": "允许通过局域网访问代理", "allowLanDesc": "允许通过局域网访问代理",
"tun": "TUN模式", "tun": "虚拟网卡",
"tunDesc": "仅在管理员模式生效", "tunDesc": "仅在管理员模式生效",
"minimizeOnExit": "退出时最小化", "minimizeOnExit": "退出时最小化",
"minimizeOnExitDesc": "修改系统默认退出事件", "minimizeOnExitDesc": "修改系统默认退出事件",
@@ -66,6 +66,7 @@
"hours": "小时", "hours": "小时",
"days": "天", "days": "天",
"minutes": "分钟", "minutes": "分钟",
"seconds": "秒",
"ago": "前", "ago": "前",
"just": "刚刚", "just": "刚刚",
"qrcode": "二维码", "qrcode": "二维码",
@@ -130,12 +131,10 @@
"notSelectedTip": "当前代理组无法选中", "notSelectedTip": "当前代理组无法选中",
"tip": "提示", "tip": "提示",
"backupAndRecovery": "备份与恢复", "backupAndRecovery": "备份与恢复",
"backupAndRecoveryDesc": "通过WebDAV同步数据", "backupAndRecoveryDesc": "通过WebDAV或者文件同步数据",
"account": "账号", "account": "账号",
"backup": "备份", "backup": "备份",
"backupDesc": "备份数据到WebDAV",
"recovery": "恢复", "recovery": "恢复",
"recoveryDesc": "从WebDAV恢复数据",
"recoveryProfiles": "仅恢复配置文件", "recoveryProfiles": "仅恢复配置文件",
"recoveryAll": "恢复所有数据", "recoveryAll": "恢复所有数据",
"recoverySuccess": "恢复成功", "recoverySuccess": "恢复成功",
@@ -216,5 +215,78 @@
"externalLink": "外部链接", "externalLink": "外部链接",
"otherContributors": "其他贡献者", "otherContributors": "其他贡献者",
"autoCloseConnections": "自动关闭连接", "autoCloseConnections": "自动关闭连接",
"autoCloseConnectionsDesc": "切换节点后自动关闭连接" "autoCloseConnectionsDesc": "切换节点后自动关闭连接",
"onlyStatisticsProxy": "仅统计代理",
"onlyStatisticsProxyDesc": "开启后,将只统计代理流量",
"deleteProfileTip": "确定要删除当前配置吗?",
"prueBlackMode": "纯黑模式",
"keepAliveIntervalDesc": "TCP保持活动间隔",
"entries": "个条目",
"local": "本地",
"remote": "远程",
"remoteBackupDesc": "备份数据到WebDAV",
"remoteRecoveryDesc": "通过WebDAV恢复数据",
"localBackupDesc": "备份数据到本地",
"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": "重置"
} }

View File

@@ -33,6 +33,7 @@ class MessageLookup extends MessageLookupByLibrary {
"account": MessageLookupByLibrary.simpleMessage("Account"), "account": MessageLookupByLibrary.simpleMessage("Account"),
"accountTip": "accountTip":
MessageLookupByLibrary.simpleMessage("Account cannot be empty"), MessageLookupByLibrary.simpleMessage("Account cannot be empty"),
"action": MessageLookupByLibrary.simpleMessage("Action"),
"add": MessageLookupByLibrary.simpleMessage("Add"), "add": MessageLookupByLibrary.simpleMessage("Add"),
"address": MessageLookupByLibrary.simpleMessage("Address"), "address": MessageLookupByLibrary.simpleMessage("Address"),
"addressHelp": "addressHelp":
@@ -40,6 +41,7 @@ class MessageLookup extends MessageLookupByLibrary {
"addressTip": MessageLookupByLibrary.simpleMessage( "addressTip": MessageLookupByLibrary.simpleMessage(
"Please enter a valid WebDAV address"), "Please enter a valid WebDAV address"),
"ago": MessageLookupByLibrary.simpleMessage(" Ago"), "ago": MessageLookupByLibrary.simpleMessage(" Ago"),
"allApps": MessageLookupByLibrary.simpleMessage("All apps"),
"allowBypass": MessageLookupByLibrary.simpleMessage( "allowBypass": MessageLookupByLibrary.simpleMessage(
"Allow applications to bypass VPN"), "Allow applications to bypass VPN"),
"allowBypassDesc": MessageLookupByLibrary.simpleMessage( "allowBypassDesc": MessageLookupByLibrary.simpleMessage(
@@ -50,6 +52,8 @@ class MessageLookup extends MessageLookupByLibrary {
"app": MessageLookupByLibrary.simpleMessage("App"), "app": MessageLookupByLibrary.simpleMessage("App"),
"appAccessControl": "appAccessControl":
MessageLookupByLibrary.simpleMessage("App access control"), MessageLookupByLibrary.simpleMessage("App access control"),
"appDesc": MessageLookupByLibrary.simpleMessage(
"Processing app related settings"),
"application": MessageLookupByLibrary.simpleMessage("Application"), "application": MessageLookupByLibrary.simpleMessage("Application"),
"applicationDesc": MessageLookupByLibrary.simpleMessage( "applicationDesc": MessageLookupByLibrary.simpleMessage(
"Modify application related settings"), "Modify application related settings"),
@@ -74,10 +78,8 @@ class MessageLookup extends MessageLookupByLibrary {
"backup": MessageLookupByLibrary.simpleMessage("Backup"), "backup": MessageLookupByLibrary.simpleMessage("Backup"),
"backupAndRecovery": "backupAndRecovery":
MessageLookupByLibrary.simpleMessage("Backup and Recovery"), MessageLookupByLibrary.simpleMessage("Backup and Recovery"),
"backupAndRecoveryDesc": "backupAndRecoveryDesc": MessageLookupByLibrary.simpleMessage(
MessageLookupByLibrary.simpleMessage("Sync data by WebDAV"), "Sync data via WebDAV or file"),
"backupDesc":
MessageLookupByLibrary.simpleMessage("Backup local data to WebDAV"),
"backupSuccess": MessageLookupByLibrary.simpleMessage("Backup success"), "backupSuccess": MessageLookupByLibrary.simpleMessage("Backup success"),
"bind": MessageLookupByLibrary.simpleMessage("Bind"), "bind": MessageLookupByLibrary.simpleMessage("Bind"),
"blacklistMode": MessageLookupByLibrary.simpleMessage("Blacklist mode"), "blacklistMode": MessageLookupByLibrary.simpleMessage("Blacklist mode"),
@@ -91,6 +93,10 @@ class MessageLookup extends MessageLookupByLibrary {
"checkUpdateError": MessageLookupByLibrary.simpleMessage( "checkUpdateError": MessageLookupByLibrary.simpleMessage(
"The current application is already the latest version"), "The current application is already the latest version"),
"checking": MessageLookupByLibrary.simpleMessage("Checking..."), "checking": MessageLookupByLibrary.simpleMessage("Checking..."),
"clipboardExport":
MessageLookupByLibrary.simpleMessage("Export clipboard"),
"clipboardImport":
MessageLookupByLibrary.simpleMessage("Clipboard import"),
"columns": MessageLookupByLibrary.simpleMessage("Columns"), "columns": MessageLookupByLibrary.simpleMessage("Columns"),
"compatible": "compatible":
MessageLookupByLibrary.simpleMessage("Compatibility mode"), MessageLookupByLibrary.simpleMessage("Compatibility mode"),
@@ -110,11 +116,17 @@ class MessageLookup extends MessageLookupByLibrary {
"dark": MessageLookupByLibrary.simpleMessage("Dark"), "dark": MessageLookupByLibrary.simpleMessage("Dark"),
"dashboard": MessageLookupByLibrary.simpleMessage("Dashboard"), "dashboard": MessageLookupByLibrary.simpleMessage("Dashboard"),
"days": MessageLookupByLibrary.simpleMessage("Days"), "days": MessageLookupByLibrary.simpleMessage("Days"),
"defaultNameserver":
MessageLookupByLibrary.simpleMessage("Default nameserver"),
"defaultNameserverDesc":
MessageLookupByLibrary.simpleMessage("For resolving DNS server"),
"defaultSort": MessageLookupByLibrary.simpleMessage("Sort by default"), "defaultSort": MessageLookupByLibrary.simpleMessage("Sort by default"),
"defaultText": MessageLookupByLibrary.simpleMessage("Default"), "defaultText": MessageLookupByLibrary.simpleMessage("Default"),
"delay": MessageLookupByLibrary.simpleMessage("Delay"), "delay": MessageLookupByLibrary.simpleMessage("Delay"),
"delaySort": MessageLookupByLibrary.simpleMessage("Sort by delay"), "delaySort": MessageLookupByLibrary.simpleMessage("Sort by delay"),
"delete": MessageLookupByLibrary.simpleMessage("Delete"), "delete": MessageLookupByLibrary.simpleMessage("Delete"),
"deleteProfileTip": MessageLookupByLibrary.simpleMessage(
"Sure you want to delete the current profile?"),
"desc": MessageLookupByLibrary.simpleMessage( "desc": MessageLookupByLibrary.simpleMessage(
"A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free."), "A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free."),
"direct": MessageLookupByLibrary.simpleMessage("Direct"), "direct": MessageLookupByLibrary.simpleMessage("Direct"),
@@ -122,11 +134,16 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Discover the new version"), MessageLookupByLibrary.simpleMessage("Discover the new version"),
"discovery": "discovery":
MessageLookupByLibrary.simpleMessage("Discovery a new version"), MessageLookupByLibrary.simpleMessage("Discovery a new version"),
"dnsDesc":
MessageLookupByLibrary.simpleMessage("Update DNS related settings"),
"dnsMode": MessageLookupByLibrary.simpleMessage("DNS mode"),
"doYouWantToPass": "doYouWantToPass":
MessageLookupByLibrary.simpleMessage("Do you want to pass"), MessageLookupByLibrary.simpleMessage("Do you want to pass"),
"domain": MessageLookupByLibrary.simpleMessage("Domain"),
"download": MessageLookupByLibrary.simpleMessage("Download"), "download": MessageLookupByLibrary.simpleMessage("Download"),
"edit": MessageLookupByLibrary.simpleMessage("Edit"), "edit": MessageLookupByLibrary.simpleMessage("Edit"),
"en": MessageLookupByLibrary.simpleMessage("English"), "en": MessageLookupByLibrary.simpleMessage("English"),
"entries": MessageLookupByLibrary.simpleMessage(" entries"),
"exclude": "exclude":
MessageLookupByLibrary.simpleMessage("Hidden from recent tasks"), MessageLookupByLibrary.simpleMessage("Hidden from recent tasks"),
"excludeDesc": MessageLookupByLibrary.simpleMessage( "excludeDesc": MessageLookupByLibrary.simpleMessage(
@@ -142,6 +159,13 @@ class MessageLookup extends MessageLookupByLibrary {
"externalLink": MessageLookupByLibrary.simpleMessage("External link"), "externalLink": MessageLookupByLibrary.simpleMessage("External link"),
"externalResources": "externalResources":
MessageLookupByLibrary.simpleMessage("External resources"), 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"), "file": MessageLookupByLibrary.simpleMessage("File"),
"fileDesc": "fileDesc":
MessageLookupByLibrary.simpleMessage("Directly upload profile"), MessageLookupByLibrary.simpleMessage("Directly upload profile"),
@@ -152,43 +176,74 @@ class MessageLookup extends MessageLookupByLibrary {
"There is a risk of flashback after opening"), "There is a risk of flashback after opening"),
"fourColumns": MessageLookupByLibrary.simpleMessage("Four columns"), "fourColumns": MessageLookupByLibrary.simpleMessage("Four columns"),
"general": MessageLookupByLibrary.simpleMessage("General"), "general": MessageLookupByLibrary.simpleMessage("General"),
"generalDesc":
MessageLookupByLibrary.simpleMessage("Overwrite general settings"),
"geoData": MessageLookupByLibrary.simpleMessage("GeoData"), "geoData": MessageLookupByLibrary.simpleMessage("GeoData"),
"geodataLoader": "geodataLoader":
MessageLookupByLibrary.simpleMessage("Geo Low Memory Mode"), MessageLookupByLibrary.simpleMessage("Geo Low Memory Mode"),
"geodataLoaderDesc": MessageLookupByLibrary.simpleMessage( "geodataLoaderDesc": MessageLookupByLibrary.simpleMessage(
"Enabling will use the Geo low memory loader"), "Enabling will use the Geo low memory loader"),
"geoipCode": MessageLookupByLibrary.simpleMessage("Geoip code"),
"global": MessageLookupByLibrary.simpleMessage("Global"), "global": MessageLookupByLibrary.simpleMessage("Global"),
"go": MessageLookupByLibrary.simpleMessage("Go"), "go": MessageLookupByLibrary.simpleMessage("Go"),
"goDownload": MessageLookupByLibrary.simpleMessage("Go to download"), "goDownload": MessageLookupByLibrary.simpleMessage("Go to download"),
"hostsDesc": MessageLookupByLibrary.simpleMessage("Add Hosts"),
"hours": MessageLookupByLibrary.simpleMessage("Hours"), "hours": MessageLookupByLibrary.simpleMessage("Hours"),
"importFromURL": "importFromURL":
MessageLookupByLibrary.simpleMessage("Import from URL"), MessageLookupByLibrary.simpleMessage("Import from URL"),
"infiniteTime": "infiniteTime":
MessageLookupByLibrary.simpleMessage("Long term effective"), MessageLookupByLibrary.simpleMessage("Long term effective"),
"init": MessageLookupByLibrary.simpleMessage("Init"), "init": MessageLookupByLibrary.simpleMessage("Init"),
"intelligentSelected":
MessageLookupByLibrary.simpleMessage("Intelligent selection"),
"intranetIP": MessageLookupByLibrary.simpleMessage("Intranet IP"), "intranetIP": MessageLookupByLibrary.simpleMessage("Intranet IP"),
"ipcidr": MessageLookupByLibrary.simpleMessage("Ipcidr"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage( "ipv6Desc": MessageLookupByLibrary.simpleMessage(
"When turned on it will be able to receive IPv6 traffic"), "When turned on it will be able to receive IPv6 traffic"),
"just": MessageLookupByLibrary.simpleMessage("Just"), "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"), "language": MessageLookupByLibrary.simpleMessage("Language"),
"layout": MessageLookupByLibrary.simpleMessage("Layout"),
"light": MessageLookupByLibrary.simpleMessage("Light"), "light": MessageLookupByLibrary.simpleMessage("Light"),
"list": MessageLookupByLibrary.simpleMessage("List"), "list": MessageLookupByLibrary.simpleMessage("List"),
"local": MessageLookupByLibrary.simpleMessage("Local"),
"localBackupDesc":
MessageLookupByLibrary.simpleMessage("Backup local data to local"),
"localRecoveryDesc":
MessageLookupByLibrary.simpleMessage("Recovery data from file"),
"logLevel": MessageLookupByLibrary.simpleMessage("LogLevel"), "logLevel": MessageLookupByLibrary.simpleMessage("LogLevel"),
"logcat": MessageLookupByLibrary.simpleMessage("Logcat"), "logcat": MessageLookupByLibrary.simpleMessage("Logcat"),
"logcatDesc": MessageLookupByLibrary.simpleMessage( "logcatDesc": MessageLookupByLibrary.simpleMessage(
"Disabling will hide the log entry"), "Disabling will hide the log entry"),
"logs": MessageLookupByLibrary.simpleMessage("Logs"), "logs": MessageLookupByLibrary.simpleMessage("Logs"),
"logsDesc": MessageLookupByLibrary.simpleMessage("Log capture records"), "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"), "min": MessageLookupByLibrary.simpleMessage("Min"),
"minimizeOnExit": "minimizeOnExit":
MessageLookupByLibrary.simpleMessage("Minimize on exit"), MessageLookupByLibrary.simpleMessage("Minimize on exit"),
"minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage( "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage(
"Modify the default system exit event"), "Modify the default system exit event"),
"minutes": MessageLookupByLibrary.simpleMessage("Minutes"), "minutes": MessageLookupByLibrary.simpleMessage("Minutes"),
"mode": MessageLookupByLibrary.simpleMessage("Mode"),
"months": MessageLookupByLibrary.simpleMessage("Months"), "months": MessageLookupByLibrary.simpleMessage("Months"),
"more": MessageLookupByLibrary.simpleMessage("More"), "more": MessageLookupByLibrary.simpleMessage("More"),
"name": MessageLookupByLibrary.simpleMessage("Name"), "name": MessageLookupByLibrary.simpleMessage("Name"),
"nameSort": MessageLookupByLibrary.simpleMessage("Sort by 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": "networkDetection":
MessageLookupByLibrary.simpleMessage("Network detection"), MessageLookupByLibrary.simpleMessage("Network detection"),
"networkSpeed": MessageLookupByLibrary.simpleMessage("Network speed"), "networkSpeed": MessageLookupByLibrary.simpleMessage("Network speed"),
@@ -208,6 +263,13 @@ class MessageLookup extends MessageLookupByLibrary {
"No profile, Please add a profile"), "No profile, Please add a profile"),
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"), "nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"),
"oneColumn": MessageLookupByLibrary.simpleMessage("One column"), "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"), "other": MessageLookupByLibrary.simpleMessage("Other"),
"otherContributors": "otherContributors":
MessageLookupByLibrary.simpleMessage("Other contributors"), MessageLookupByLibrary.simpleMessage("Other contributors"),
@@ -215,6 +277,9 @@ class MessageLookup extends MessageLookupByLibrary {
"override": MessageLookupByLibrary.simpleMessage("Override"), "override": MessageLookupByLibrary.simpleMessage("Override"),
"overrideDesc": MessageLookupByLibrary.simpleMessage( "overrideDesc": MessageLookupByLibrary.simpleMessage(
"Override Proxy related config"), "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"), "password": MessageLookupByLibrary.simpleMessage("Password"),
"passwordTip": "passwordTip":
MessageLookupByLibrary.simpleMessage("Password cannot be empty"), MessageLookupByLibrary.simpleMessage("Password cannot be empty"),
@@ -226,6 +291,8 @@ class MessageLookup extends MessageLookupByLibrary {
"pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage( "pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage(
"Please upload a valid QR code"), "Please upload a valid QR code"),
"port": MessageLookupByLibrary.simpleMessage("Port"), "port": MessageLookupByLibrary.simpleMessage("Port"),
"preferH3Desc": MessageLookupByLibrary.simpleMessage(
"Prioritize the use of DOH\'s http/3"),
"preview": MessageLookupByLibrary.simpleMessage("Preview"), "preview": MessageLookupByLibrary.simpleMessage("Preview"),
"profile": MessageLookupByLibrary.simpleMessage("Profile"), "profile": MessageLookupByLibrary.simpleMessage("Profile"),
"profileAutoUpdateIntervalInvalidValidationDesc": "profileAutoUpdateIntervalInvalidValidationDesc":
@@ -243,35 +310,55 @@ class MessageLookup extends MessageLookupByLibrary {
"profileUrlNullValidationDesc": MessageLookupByLibrary.simpleMessage( "profileUrlNullValidationDesc": MessageLookupByLibrary.simpleMessage(
"Please input the profile URL"), "Please input the profile URL"),
"profiles": MessageLookupByLibrary.simpleMessage("Profiles"), "profiles": MessageLookupByLibrary.simpleMessage("Profiles"),
"profilesSort": MessageLookupByLibrary.simpleMessage("Profiles sort"),
"project": MessageLookupByLibrary.simpleMessage("Project"), "project": MessageLookupByLibrary.simpleMessage("Project"),
"providers": MessageLookupByLibrary.simpleMessage("Providers"),
"proxies": MessageLookupByLibrary.simpleMessage("Proxies"), "proxies": MessageLookupByLibrary.simpleMessage("Proxies"),
"proxiesSetting": "proxiesSetting":
MessageLookupByLibrary.simpleMessage("Proxies setting"), MessageLookupByLibrary.simpleMessage("Proxies setting"),
"proxyGroup": MessageLookupByLibrary.simpleMessage("Proxy group"), "proxyGroup": MessageLookupByLibrary.simpleMessage("Proxy group"),
"proxyNameserver":
MessageLookupByLibrary.simpleMessage("Proxy nameserver"),
"proxyNameserverDesc": MessageLookupByLibrary.simpleMessage(
"Domain for resolving proxy nodes"),
"proxyPort": MessageLookupByLibrary.simpleMessage("ProxyPort"), "proxyPort": MessageLookupByLibrary.simpleMessage("ProxyPort"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage( "proxyPortDesc": MessageLookupByLibrary.simpleMessage(
"Set the Clash listening port"), "Set the Clash listening port"),
"proxyProviders":
MessageLookupByLibrary.simpleMessage("Proxy providers"),
"prueBlackMode":
MessageLookupByLibrary.simpleMessage("Prue black mode"),
"qrcode": MessageLookupByLibrary.simpleMessage("QR code"), "qrcode": MessageLookupByLibrary.simpleMessage("QR code"),
"qrcodeDesc": MessageLookupByLibrary.simpleMessage( "qrcodeDesc": MessageLookupByLibrary.simpleMessage(
"Scan QR code to obtain profile"), "Scan QR code to obtain profile"),
"recovery": MessageLookupByLibrary.simpleMessage("Recovery"), "recovery": MessageLookupByLibrary.simpleMessage("Recovery"),
"recoveryAll": "recoveryAll":
MessageLookupByLibrary.simpleMessage("Recovery all data"), MessageLookupByLibrary.simpleMessage("Recovery all data"),
"recoveryDesc":
MessageLookupByLibrary.simpleMessage("Recovery data from WebDAV"),
"recoveryProfiles": "recoveryProfiles":
MessageLookupByLibrary.simpleMessage("Only recovery profiles"), MessageLookupByLibrary.simpleMessage("Only recovery profiles"),
"recoverySuccess": "recoverySuccess":
MessageLookupByLibrary.simpleMessage("Recovery success"), MessageLookupByLibrary.simpleMessage("Recovery success"),
"remote": MessageLookupByLibrary.simpleMessage("Remote"),
"remoteBackupDesc":
MessageLookupByLibrary.simpleMessage("Backup local data to WebDAV"),
"remoteRecoveryDesc":
MessageLookupByLibrary.simpleMessage("Recovery data from WebDAV"),
"requests": MessageLookupByLibrary.simpleMessage("Requests"), "requests": MessageLookupByLibrary.simpleMessage("Requests"),
"requestsDesc": MessageLookupByLibrary.simpleMessage( "requestsDesc": MessageLookupByLibrary.simpleMessage(
"View recently request records"), "View recently request records"),
"reset": MessageLookupByLibrary.simpleMessage("Reset"),
"resetDns": MessageLookupByLibrary.simpleMessage("Reset Dns"),
"resources": MessageLookupByLibrary.simpleMessage("Resources"), "resources": MessageLookupByLibrary.simpleMessage("Resources"),
"resourcesDesc": MessageLookupByLibrary.simpleMessage( "resourcesDesc": MessageLookupByLibrary.simpleMessage(
"External resource related info"), "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"), "rule": MessageLookupByLibrary.simpleMessage("Rule"),
"ruleProviders": MessageLookupByLibrary.simpleMessage("Rule providers"),
"save": MessageLookupByLibrary.simpleMessage("Save"), "save": MessageLookupByLibrary.simpleMessage("Save"),
"search": MessageLookupByLibrary.simpleMessage("Search"), "search": MessageLookupByLibrary.simpleMessage("Search"),
"seconds": MessageLookupByLibrary.simpleMessage("Seconds"),
"selectAll": MessageLookupByLibrary.simpleMessage("Select all"), "selectAll": MessageLookupByLibrary.simpleMessage("Select all"),
"selected": MessageLookupByLibrary.simpleMessage("Selected"), "selected": MessageLookupByLibrary.simpleMessage("Selected"),
"settings": MessageLookupByLibrary.simpleMessage("Settings"), "settings": MessageLookupByLibrary.simpleMessage("Settings"),
@@ -282,12 +369,19 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Start in the background"), MessageLookupByLibrary.simpleMessage("Start in the background"),
"size": MessageLookupByLibrary.simpleMessage("Size"), "size": MessageLookupByLibrary.simpleMessage("Size"),
"sort": MessageLookupByLibrary.simpleMessage("Sort"), "sort": MessageLookupByLibrary.simpleMessage("Sort"),
"source": MessageLookupByLibrary.simpleMessage("Source"),
"standard": MessageLookupByLibrary.simpleMessage("Standard"),
"start": MessageLookupByLibrary.simpleMessage("Start"),
"startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."), "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..."), "stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."),
"style": MessageLookupByLibrary.simpleMessage("Style"), "style": MessageLookupByLibrary.simpleMessage("Style"),
"submit": MessageLookupByLibrary.simpleMessage("Submit"), "submit": MessageLookupByLibrary.simpleMessage("Submit"),
"sync": MessageLookupByLibrary.simpleMessage("Sync"), "sync": MessageLookupByLibrary.simpleMessage("Sync"),
"systemProxy": MessageLookupByLibrary.simpleMessage("SystemProxy"), "systemProxy": MessageLookupByLibrary.simpleMessage("System proxy"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage( "systemProxyDesc": MessageLookupByLibrary.simpleMessage(
"Attach HTTP proxy to VpnService"), "Attach HTTP proxy to VpnService"),
"tab": MessageLookupByLibrary.simpleMessage("Tab"), "tab": MessageLookupByLibrary.simpleMessage("Tab"),
@@ -304,10 +398,12 @@ class MessageLookup extends MessageLookupByLibrary {
"Set dark mode,adjust the color"), "Set dark mode,adjust the color"),
"themeMode": MessageLookupByLibrary.simpleMessage("Theme mode"), "themeMode": MessageLookupByLibrary.simpleMessage("Theme mode"),
"threeColumns": MessageLookupByLibrary.simpleMessage("Three columns"), "threeColumns": MessageLookupByLibrary.simpleMessage("Three columns"),
"tight": MessageLookupByLibrary.simpleMessage("Tight"),
"time": MessageLookupByLibrary.simpleMessage("Time"),
"tip": MessageLookupByLibrary.simpleMessage("tip"), "tip": MessageLookupByLibrary.simpleMessage("tip"),
"tools": MessageLookupByLibrary.simpleMessage("Tools"), "tools": MessageLookupByLibrary.simpleMessage("Tools"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"), "trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"),
"tun": MessageLookupByLibrary.simpleMessage("TUN mode"), "tun": MessageLookupByLibrary.simpleMessage("TUN"),
"tunDesc": MessageLookupByLibrary.simpleMessage( "tunDesc": MessageLookupByLibrary.simpleMessage(
"only effective in administrator mode"), "only effective in administrator mode"),
"twoColumns": MessageLookupByLibrary.simpleMessage("Two columns"), "twoColumns": MessageLookupByLibrary.simpleMessage("Two columns"),
@@ -323,7 +419,19 @@ class MessageLookup extends MessageLookupByLibrary {
"url": MessageLookupByLibrary.simpleMessage("URL"), "url": MessageLookupByLibrary.simpleMessage("URL"),
"urlDesc": "urlDesc":
MessageLookupByLibrary.simpleMessage("Obtain profile through URL"), 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"), "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": "webDAVConfiguration":
MessageLookupByLibrary.simpleMessage("WebDAV configuration"), MessageLookupByLibrary.simpleMessage("WebDAV configuration"),
"whitelistMode": MessageLookupByLibrary.simpleMessage("Whitelist mode"), "whitelistMode": MessageLookupByLibrary.simpleMessage("Whitelist mode"),

View File

@@ -31,11 +31,13 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("选中应用将会被排除在VPN之外"), MessageLookupByLibrary.simpleMessage("选中应用将会被排除在VPN之外"),
"account": MessageLookupByLibrary.simpleMessage("账号"), "account": MessageLookupByLibrary.simpleMessage("账号"),
"accountTip": MessageLookupByLibrary.simpleMessage("账号不能为空"), "accountTip": MessageLookupByLibrary.simpleMessage("账号不能为空"),
"action": MessageLookupByLibrary.simpleMessage("操作"),
"add": MessageLookupByLibrary.simpleMessage("添加"), "add": MessageLookupByLibrary.simpleMessage("添加"),
"address": MessageLookupByLibrary.simpleMessage("地址"), "address": MessageLookupByLibrary.simpleMessage("地址"),
"addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"), "addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"),
"addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"), "addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"),
"ago": MessageLookupByLibrary.simpleMessage(""), "ago": MessageLookupByLibrary.simpleMessage(""),
"allApps": MessageLookupByLibrary.simpleMessage("所有应用"),
"allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"), "allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"),
"allowBypassDesc": "allowBypassDesc":
MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"), MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"),
@@ -43,6 +45,7 @@ class MessageLookup extends MessageLookupByLibrary {
"allowLanDesc": MessageLookupByLibrary.simpleMessage("允许通过局域网访问代理"), "allowLanDesc": MessageLookupByLibrary.simpleMessage("允许通过局域网访问代理"),
"app": MessageLookupByLibrary.simpleMessage("应用"), "app": MessageLookupByLibrary.simpleMessage("应用"),
"appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"), "appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"),
"appDesc": MessageLookupByLibrary.simpleMessage("处理应用相关设置"),
"application": MessageLookupByLibrary.simpleMessage("应用程序"), "application": MessageLookupByLibrary.simpleMessage("应用程序"),
"applicationDesc": MessageLookupByLibrary.simpleMessage("修改应用程序相关设置"), "applicationDesc": MessageLookupByLibrary.simpleMessage("修改应用程序相关设置"),
"auto": MessageLookupByLibrary.simpleMessage("自动"), "auto": MessageLookupByLibrary.simpleMessage("自动"),
@@ -62,8 +65,7 @@ class MessageLookup extends MessageLookupByLibrary {
"backup": MessageLookupByLibrary.simpleMessage("备份"), "backup": MessageLookupByLibrary.simpleMessage("备份"),
"backupAndRecovery": MessageLookupByLibrary.simpleMessage("备份与恢复"), "backupAndRecovery": MessageLookupByLibrary.simpleMessage("备份与恢复"),
"backupAndRecoveryDesc": "backupAndRecoveryDesc":
MessageLookupByLibrary.simpleMessage("通过WebDAV同步数据"), MessageLookupByLibrary.simpleMessage("通过WebDAV或者文件同步数据"),
"backupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"),
"backupSuccess": MessageLookupByLibrary.simpleMessage("备份成功"), "backupSuccess": MessageLookupByLibrary.simpleMessage("备份成功"),
"bind": MessageLookupByLibrary.simpleMessage("绑定"), "bind": MessageLookupByLibrary.simpleMessage("绑定"),
"blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"), "blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"),
@@ -74,6 +76,8 @@ class MessageLookup extends MessageLookupByLibrary {
"checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"), "checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"),
"checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"), "checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"),
"checking": MessageLookupByLibrary.simpleMessage("检测中..."), "checking": MessageLookupByLibrary.simpleMessage("检测中..."),
"clipboardExport": MessageLookupByLibrary.simpleMessage("导出剪贴板"),
"clipboardImport": MessageLookupByLibrary.simpleMessage("剪贴板导入"),
"columns": MessageLookupByLibrary.simpleMessage("列数"), "columns": MessageLookupByLibrary.simpleMessage("列数"),
"compatible": MessageLookupByLibrary.simpleMessage("兼容模式"), "compatible": MessageLookupByLibrary.simpleMessage("兼容模式"),
"compatibleDesc": "compatibleDesc":
@@ -91,20 +95,28 @@ class MessageLookup extends MessageLookupByLibrary {
"dark": MessageLookupByLibrary.simpleMessage("深色"), "dark": MessageLookupByLibrary.simpleMessage("深色"),
"dashboard": MessageLookupByLibrary.simpleMessage("仪表盘"), "dashboard": MessageLookupByLibrary.simpleMessage("仪表盘"),
"days": MessageLookupByLibrary.simpleMessage(""), "days": MessageLookupByLibrary.simpleMessage(""),
"defaultNameserver": MessageLookupByLibrary.simpleMessage("默认域名服务器"),
"defaultNameserverDesc":
MessageLookupByLibrary.simpleMessage("用于解析DNS服务器"),
"defaultSort": MessageLookupByLibrary.simpleMessage("按默认排序"), "defaultSort": MessageLookupByLibrary.simpleMessage("按默认排序"),
"defaultText": MessageLookupByLibrary.simpleMessage("默认"), "defaultText": MessageLookupByLibrary.simpleMessage("默认"),
"delay": MessageLookupByLibrary.simpleMessage("延迟"), "delay": MessageLookupByLibrary.simpleMessage("延迟"),
"delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"), "delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"),
"delete": MessageLookupByLibrary.simpleMessage("删除"), "delete": MessageLookupByLibrary.simpleMessage("删除"),
"deleteProfileTip": MessageLookupByLibrary.simpleMessage("确定要删除当前配置吗?"),
"desc": MessageLookupByLibrary.simpleMessage( "desc": MessageLookupByLibrary.simpleMessage(
"基于ClashMeta的多平台代理客户端简单易用开源无广告。"), "基于ClashMeta的多平台代理客户端简单易用开源无广告。"),
"direct": MessageLookupByLibrary.simpleMessage("直连"), "direct": MessageLookupByLibrary.simpleMessage("直连"),
"discoverNewVersion": MessageLookupByLibrary.simpleMessage("发现新版本"), "discoverNewVersion": MessageLookupByLibrary.simpleMessage("发现新版本"),
"discovery": MessageLookupByLibrary.simpleMessage("发现新版本"), "discovery": MessageLookupByLibrary.simpleMessage("发现新版本"),
"dnsDesc": MessageLookupByLibrary.simpleMessage("更新DNS相关设置"),
"dnsMode": MessageLookupByLibrary.simpleMessage("DNS模式"),
"doYouWantToPass": MessageLookupByLibrary.simpleMessage("是否要通过"), "doYouWantToPass": MessageLookupByLibrary.simpleMessage("是否要通过"),
"domain": MessageLookupByLibrary.simpleMessage("域名"),
"download": MessageLookupByLibrary.simpleMessage("下载"), "download": MessageLookupByLibrary.simpleMessage("下载"),
"edit": MessageLookupByLibrary.simpleMessage("编辑"), "edit": MessageLookupByLibrary.simpleMessage("编辑"),
"en": MessageLookupByLibrary.simpleMessage("英语"), "en": MessageLookupByLibrary.simpleMessage("英语"),
"entries": MessageLookupByLibrary.simpleMessage("个条目"),
"exclude": MessageLookupByLibrary.simpleMessage("从最近任务中隐藏"), "exclude": MessageLookupByLibrary.simpleMessage("从最近任务中隐藏"),
"excludeDesc": "excludeDesc":
MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"), MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"),
@@ -116,6 +128,11 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制Clash内核"), MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制Clash内核"),
"externalLink": MessageLookupByLibrary.simpleMessage("外部链接"), "externalLink": MessageLookupByLibrary.simpleMessage("外部链接"),
"externalResources": MessageLookupByLibrary.simpleMessage("外部资源"), "externalResources": MessageLookupByLibrary.simpleMessage("外部资源"),
"fakeipFilter": MessageLookupByLibrary.simpleMessage("Fakeip过滤"),
"fakeipRange": MessageLookupByLibrary.simpleMessage("Fakeip范围"),
"fallback": MessageLookupByLibrary.simpleMessage("Fallback"),
"fallbackDesc": MessageLookupByLibrary.simpleMessage("一般情况下使用境外DNS"),
"fallbackFilter": MessageLookupByLibrary.simpleMessage("Fallback过滤"),
"file": MessageLookupByLibrary.simpleMessage("文件"), "file": MessageLookupByLibrary.simpleMessage("文件"),
"fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"), "fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
"filterSystemApp": MessageLookupByLibrary.simpleMessage("过滤系统应用"), "filterSystemApp": MessageLookupByLibrary.simpleMessage("过滤系统应用"),
@@ -124,37 +141,59 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("开启后存在闪退风险"), MessageLookupByLibrary.simpleMessage("开启后存在闪退风险"),
"fourColumns": MessageLookupByLibrary.simpleMessage("四列"), "fourColumns": MessageLookupByLibrary.simpleMessage("四列"),
"general": MessageLookupByLibrary.simpleMessage("基础"), "general": MessageLookupByLibrary.simpleMessage("基础"),
"generalDesc": MessageLookupByLibrary.simpleMessage("覆写基础设置"),
"geoData": MessageLookupByLibrary.simpleMessage("地理数据"), "geoData": MessageLookupByLibrary.simpleMessage("地理数据"),
"geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"), "geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"),
"geodataLoaderDesc": "geodataLoaderDesc":
MessageLookupByLibrary.simpleMessage("开启将使用Geo低内存加载器"), MessageLookupByLibrary.simpleMessage("开启将使用Geo低内存加载器"),
"geoipCode": MessageLookupByLibrary.simpleMessage("Geoip代码"),
"global": MessageLookupByLibrary.simpleMessage("全局"), "global": MessageLookupByLibrary.simpleMessage("全局"),
"go": MessageLookupByLibrary.simpleMessage("前往"), "go": MessageLookupByLibrary.simpleMessage("前往"),
"goDownload": MessageLookupByLibrary.simpleMessage("前往下载"), "goDownload": MessageLookupByLibrary.simpleMessage("前往下载"),
"hostsDesc": MessageLookupByLibrary.simpleMessage("追加Hosts"),
"hours": MessageLookupByLibrary.simpleMessage("小时"), "hours": MessageLookupByLibrary.simpleMessage("小时"),
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"), "importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
"infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"), "infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"),
"init": MessageLookupByLibrary.simpleMessage("初始化"), "init": MessageLookupByLibrary.simpleMessage("初始化"),
"intelligentSelected": MessageLookupByLibrary.simpleMessage("智能选择"),
"intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"), "intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"),
"ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"), "ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"),
"just": MessageLookupByLibrary.simpleMessage("刚刚"), "just": MessageLookupByLibrary.simpleMessage("刚刚"),
"keepAliveIntervalDesc":
MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"),
"key": MessageLookupByLibrary.simpleMessage(""),
"keyNotEmpty": MessageLookupByLibrary.simpleMessage("键不能为空"),
"language": MessageLookupByLibrary.simpleMessage("语言"), "language": MessageLookupByLibrary.simpleMessage("语言"),
"layout": MessageLookupByLibrary.simpleMessage("布局"),
"light": MessageLookupByLibrary.simpleMessage("浅色"), "light": MessageLookupByLibrary.simpleMessage("浅色"),
"list": MessageLookupByLibrary.simpleMessage("列表"), "list": MessageLookupByLibrary.simpleMessage("列表"),
"local": MessageLookupByLibrary.simpleMessage("本地"),
"localBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到本地"),
"localRecoveryDesc": MessageLookupByLibrary.simpleMessage("通过文件恢复数据"),
"logLevel": MessageLookupByLibrary.simpleMessage("日志等级"), "logLevel": MessageLookupByLibrary.simpleMessage("日志等级"),
"logcat": MessageLookupByLibrary.simpleMessage("日志捕获"), "logcat": MessageLookupByLibrary.simpleMessage("日志捕获"),
"logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"), "logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"),
"logs": MessageLookupByLibrary.simpleMessage("日志"), "logs": MessageLookupByLibrary.simpleMessage("日志"),
"logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"), "logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"),
"loopback": MessageLookupByLibrary.simpleMessage("回环解锁工具"),
"loopbackDesc": MessageLookupByLibrary.simpleMessage("用于UWP回环解锁"),
"loose": MessageLookupByLibrary.simpleMessage("紧凑"),
"min": MessageLookupByLibrary.simpleMessage("最小"), "min": MessageLookupByLibrary.simpleMessage("最小"),
"minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"),
"minimizeOnExitDesc": "minimizeOnExitDesc":
MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"), MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"),
"minutes": MessageLookupByLibrary.simpleMessage("分钟"), "minutes": MessageLookupByLibrary.simpleMessage("分钟"),
"mode": MessageLookupByLibrary.simpleMessage("模式"),
"months": MessageLookupByLibrary.simpleMessage(""), "months": MessageLookupByLibrary.simpleMessage(""),
"more": MessageLookupByLibrary.simpleMessage("更多"), "more": MessageLookupByLibrary.simpleMessage("更多"),
"name": MessageLookupByLibrary.simpleMessage("名称"), "name": MessageLookupByLibrary.simpleMessage("名称"),
"nameSort": MessageLookupByLibrary.simpleMessage("按名称排序"), "nameSort": MessageLookupByLibrary.simpleMessage("按名称排序"),
"nameserver": MessageLookupByLibrary.simpleMessage("域名服务器"),
"nameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析域名"),
"nameserverPolicy": MessageLookupByLibrary.simpleMessage("域名服务器策略"),
"nameserverPolicyDesc":
MessageLookupByLibrary.simpleMessage("指定对应域名服务器策略"),
"networkDetection": MessageLookupByLibrary.simpleMessage("网络检测"), "networkDetection": MessageLookupByLibrary.simpleMessage("网络检测"),
"networkSpeed": MessageLookupByLibrary.simpleMessage("网络速度"), "networkSpeed": MessageLookupByLibrary.simpleMessage("网络速度"),
"noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"), "noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"),
@@ -170,11 +209,19 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"), MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"),
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"), "nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"),
"oneColumn": MessageLookupByLibrary.simpleMessage("一列"), "oneColumn": MessageLookupByLibrary.simpleMessage("一列"),
"onlyOtherApps": MessageLookupByLibrary.simpleMessage("仅第三方应用"),
"onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("仅统计代理"),
"onlyStatisticsProxyDesc":
MessageLookupByLibrary.simpleMessage("开启后,将只统计代理流量"),
"options": MessageLookupByLibrary.simpleMessage("选项"),
"other": MessageLookupByLibrary.simpleMessage("其他"), "other": MessageLookupByLibrary.simpleMessage("其他"),
"otherContributors": MessageLookupByLibrary.simpleMessage("其他贡献者"), "otherContributors": MessageLookupByLibrary.simpleMessage("其他贡献者"),
"outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"), "outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"),
"override": MessageLookupByLibrary.simpleMessage("覆写"), "override": MessageLookupByLibrary.simpleMessage("覆写"),
"overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"), "overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"),
"overrideDns": MessageLookupByLibrary.simpleMessage("覆写DNS"),
"overrideDnsDesc":
MessageLookupByLibrary.simpleMessage("开启后将覆盖配置中的DNS选项"),
"password": MessageLookupByLibrary.simpleMessage("密码"), "password": MessageLookupByLibrary.simpleMessage("密码"),
"passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"), "passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"),
"paste": MessageLookupByLibrary.simpleMessage("粘贴"), "paste": MessageLookupByLibrary.simpleMessage("粘贴"),
@@ -183,6 +230,7 @@ class MessageLookup extends MessageLookupByLibrary {
"pleaseUploadValidQrcode": "pleaseUploadValidQrcode":
MessageLookupByLibrary.simpleMessage("请上传有效的二维码"), MessageLookupByLibrary.simpleMessage("请上传有效的二维码"),
"port": MessageLookupByLibrary.simpleMessage("端口"), "port": MessageLookupByLibrary.simpleMessage("端口"),
"preferH3Desc": MessageLookupByLibrary.simpleMessage("优先使用DOH的http/3"),
"preview": MessageLookupByLibrary.simpleMessage("预览"), "preview": MessageLookupByLibrary.simpleMessage("预览"),
"profile": MessageLookupByLibrary.simpleMessage("配置"), "profile": MessageLookupByLibrary.simpleMessage("配置"),
"profileAutoUpdateIntervalInvalidValidationDesc": "profileAutoUpdateIntervalInvalidValidationDesc":
@@ -198,26 +246,43 @@ class MessageLookup extends MessageLookupByLibrary {
"profileUrlNullValidationDesc": "profileUrlNullValidationDesc":
MessageLookupByLibrary.simpleMessage("请输入配置URL"), MessageLookupByLibrary.simpleMessage("请输入配置URL"),
"profiles": MessageLookupByLibrary.simpleMessage("配置"), "profiles": MessageLookupByLibrary.simpleMessage("配置"),
"profilesSort": MessageLookupByLibrary.simpleMessage("配置排序"),
"project": MessageLookupByLibrary.simpleMessage("项目"), "project": MessageLookupByLibrary.simpleMessage("项目"),
"providers": MessageLookupByLibrary.simpleMessage("提供者"),
"proxies": MessageLookupByLibrary.simpleMessage("代理"), "proxies": MessageLookupByLibrary.simpleMessage("代理"),
"proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"), "proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"),
"proxyGroup": MessageLookupByLibrary.simpleMessage("代理组"), "proxyGroup": MessageLookupByLibrary.simpleMessage("代理组"),
"proxyNameserver": MessageLookupByLibrary.simpleMessage("代理域名服务器"),
"proxyNameserverDesc":
MessageLookupByLibrary.simpleMessage("用于解析代理节点的域名"),
"proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"), "proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"), "proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"),
"proxyProviders": MessageLookupByLibrary.simpleMessage("代理提供者"),
"prueBlackMode": MessageLookupByLibrary.simpleMessage("纯黑模式"),
"qrcode": MessageLookupByLibrary.simpleMessage("二维码"), "qrcode": MessageLookupByLibrary.simpleMessage("二维码"),
"qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"), "qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"),
"recovery": MessageLookupByLibrary.simpleMessage("恢复"), "recovery": MessageLookupByLibrary.simpleMessage("恢复"),
"recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"), "recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"),
"recoveryDesc": MessageLookupByLibrary.simpleMessage("从WebDAV恢复数据"),
"recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"), "recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"),
"recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"), "recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"),
"remote": MessageLookupByLibrary.simpleMessage("远程"),
"remoteBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"),
"remoteRecoveryDesc":
MessageLookupByLibrary.simpleMessage("通过WebDAV恢复数据"),
"requests": MessageLookupByLibrary.simpleMessage("请求"), "requests": MessageLookupByLibrary.simpleMessage("请求"),
"requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求记录"), "requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求记录"),
"reset": MessageLookupByLibrary.simpleMessage("重置"),
"resetDns": MessageLookupByLibrary.simpleMessage("重置DNS"),
"resources": MessageLookupByLibrary.simpleMessage("资源"), "resources": MessageLookupByLibrary.simpleMessage("资源"),
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"), "resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"),
"respectRules": MessageLookupByLibrary.simpleMessage("遵守规则"),
"respectRulesDesc": MessageLookupByLibrary.simpleMessage(
"DNS连接跟随rules,需配置proxy-server-nameserver"),
"rule": MessageLookupByLibrary.simpleMessage("规则"), "rule": MessageLookupByLibrary.simpleMessage("规则"),
"ruleProviders": MessageLookupByLibrary.simpleMessage("规则提供者"),
"save": MessageLookupByLibrary.simpleMessage("保存"), "save": MessageLookupByLibrary.simpleMessage("保存"),
"search": MessageLookupByLibrary.simpleMessage("搜索"), "search": MessageLookupByLibrary.simpleMessage("搜索"),
"seconds": MessageLookupByLibrary.simpleMessage(""),
"selectAll": MessageLookupByLibrary.simpleMessage("全选"), "selectAll": MessageLookupByLibrary.simpleMessage("全选"),
"selected": MessageLookupByLibrary.simpleMessage("已选择"), "selected": MessageLookupByLibrary.simpleMessage("已选择"),
"settings": MessageLookupByLibrary.simpleMessage("设置"), "settings": MessageLookupByLibrary.simpleMessage("设置"),
@@ -227,7 +292,13 @@ class MessageLookup extends MessageLookupByLibrary {
"silentLaunchDesc": MessageLookupByLibrary.simpleMessage("后台启动"), "silentLaunchDesc": MessageLookupByLibrary.simpleMessage("后台启动"),
"size": MessageLookupByLibrary.simpleMessage("尺寸"), "size": MessageLookupByLibrary.simpleMessage("尺寸"),
"sort": MessageLookupByLibrary.simpleMessage("排序"), "sort": MessageLookupByLibrary.simpleMessage("排序"),
"source": MessageLookupByLibrary.simpleMessage("来源"),
"standard": MessageLookupByLibrary.simpleMessage("标准"),
"start": MessageLookupByLibrary.simpleMessage("启动"),
"startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."), "startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."),
"status": MessageLookupByLibrary.simpleMessage("状态"),
"statusDesc": MessageLookupByLibrary.simpleMessage("关闭后将使用系统DNS"),
"stop": MessageLookupByLibrary.simpleMessage("暂停"),
"stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."), "stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."),
"style": MessageLookupByLibrary.simpleMessage("风格"), "style": MessageLookupByLibrary.simpleMessage("风格"),
"submit": MessageLookupByLibrary.simpleMessage("提交"), "submit": MessageLookupByLibrary.simpleMessage("提交"),
@@ -247,10 +318,12 @@ class MessageLookup extends MessageLookupByLibrary {
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"), "themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),
"themeMode": MessageLookupByLibrary.simpleMessage("主题模式"), "themeMode": MessageLookupByLibrary.simpleMessage("主题模式"),
"threeColumns": MessageLookupByLibrary.simpleMessage("三列"), "threeColumns": MessageLookupByLibrary.simpleMessage("三列"),
"tight": MessageLookupByLibrary.simpleMessage("宽松"),
"time": MessageLookupByLibrary.simpleMessage("时间"),
"tip": MessageLookupByLibrary.simpleMessage("提示"), "tip": MessageLookupByLibrary.simpleMessage("提示"),
"tools": MessageLookupByLibrary.simpleMessage("工具"), "tools": MessageLookupByLibrary.simpleMessage("工具"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"), "trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"),
"tun": MessageLookupByLibrary.simpleMessage("TUN模式"), "tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),
"tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"), "tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"),
"twoColumns": MessageLookupByLibrary.simpleMessage("两列"), "twoColumns": MessageLookupByLibrary.simpleMessage("两列"),
"unableToUpdateCurrentProfileDesc": "unableToUpdateCurrentProfileDesc":
@@ -262,7 +335,15 @@ class MessageLookup extends MessageLookupByLibrary {
"upload": MessageLookupByLibrary.simpleMessage("上传"), "upload": MessageLookupByLibrary.simpleMessage("上传"),
"url": MessageLookupByLibrary.simpleMessage("URL"), "url": MessageLookupByLibrary.simpleMessage("URL"),
"urlDesc": MessageLookupByLibrary.simpleMessage("通过URL获取配置文件"), "urlDesc": MessageLookupByLibrary.simpleMessage("通过URL获取配置文件"),
"useHosts": MessageLookupByLibrary.simpleMessage("使用Hosts"),
"useSystemHosts": MessageLookupByLibrary.simpleMessage("使用系统Hosts"),
"value": MessageLookupByLibrary.simpleMessage(""),
"valueNotEmpty": MessageLookupByLibrary.simpleMessage("值不能为空"),
"view": MessageLookupByLibrary.simpleMessage("查看"), "view": MessageLookupByLibrary.simpleMessage("查看"),
"vpnDesc": MessageLookupByLibrary.simpleMessage("修改VPN相关设置"),
"vpnEnableDesc":
MessageLookupByLibrary.simpleMessage("通过VpnService自动路由系统所有流量"),
"vpnTip": MessageLookupByLibrary.simpleMessage("重启VPN后改变生效"),
"webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"), "webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"),
"whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"), "whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"),
"years": MessageLookupByLibrary.simpleMessage(""), "years": MessageLookupByLibrary.simpleMessage(""),

View File

@@ -430,10 +430,10 @@ class AppLocalizations {
); );
} }
/// `TUN mode` /// `TUN`
String get tun { String get tun {
return Intl.message( return Intl.message(
'TUN mode', 'TUN',
name: 'tun', name: 'tun',
desc: '', desc: '',
args: [], args: [],
@@ -720,6 +720,16 @@ class AppLocalizations {
); );
} }
/// `Seconds`
String get seconds {
return Intl.message(
'Seconds',
name: 'seconds',
desc: '',
args: [],
);
}
/// ` Ago` /// ` Ago`
String get ago { String get ago {
return Intl.message( return Intl.message(
@@ -1220,10 +1230,10 @@ class AppLocalizations {
); );
} }
/// `SystemProxy` /// `System proxy`
String get systemProxy { String get systemProxy {
return Intl.message( return Intl.message(
'SystemProxy', 'System proxy',
name: 'systemProxy', name: 'systemProxy',
desc: '', desc: '',
args: [], args: [],
@@ -1360,10 +1370,10 @@ class AppLocalizations {
); );
} }
/// `Sync data by WebDAV` /// `Sync data via WebDAV or file`
String get backupAndRecoveryDesc { String get backupAndRecoveryDesc {
return Intl.message( return Intl.message(
'Sync data by WebDAV', 'Sync data via WebDAV or file',
name: 'backupAndRecoveryDesc', name: 'backupAndRecoveryDesc',
desc: '', desc: '',
args: [], args: [],
@@ -1390,16 +1400,6 @@ class AppLocalizations {
); );
} }
/// `Backup local data to WebDAV`
String get backupDesc {
return Intl.message(
'Backup local data to WebDAV',
name: 'backupDesc',
desc: '',
args: [],
);
}
/// `Recovery` /// `Recovery`
String get recovery { String get recovery {
return Intl.message( return Intl.message(
@@ -1410,16 +1410,6 @@ class AppLocalizations {
); );
} }
/// `Recovery data from WebDAV`
String get recoveryDesc {
return Intl.message(
'Recovery data from WebDAV',
name: 'recoveryDesc',
desc: '',
args: [],
);
}
/// `Only recovery profiles` /// `Only recovery profiles`
String get recoveryProfiles { String get recoveryProfiles {
return Intl.message( return Intl.message(
@@ -2229,6 +2219,736 @@ class AppLocalizations {
args: [], args: [],
); );
} }
/// `Only statistics proxy`
String get onlyStatisticsProxy {
return Intl.message(
'Only statistics proxy',
name: 'onlyStatisticsProxy',
desc: '',
args: [],
);
}
/// `When turned on, only statistics proxy traffic`
String get onlyStatisticsProxyDesc {
return Intl.message(
'When turned on, only statistics proxy traffic',
name: 'onlyStatisticsProxyDesc',
desc: '',
args: [],
);
}
/// `Sure you want to delete the current profile?`
String get deleteProfileTip {
return Intl.message(
'Sure you want to delete the current profile?',
name: 'deleteProfileTip',
desc: '',
args: [],
);
}
/// `Prue black mode`
String get prueBlackMode {
return Intl.message(
'Prue black mode',
name: 'prueBlackMode',
desc: '',
args: [],
);
}
/// `Tcp keep alive interval`
String get keepAliveIntervalDesc {
return Intl.message(
'Tcp keep alive interval',
name: 'keepAliveIntervalDesc',
desc: '',
args: [],
);
}
/// ` entries`
String get entries {
return Intl.message(
' entries',
name: 'entries',
desc: '',
args: [],
);
}
/// `Local`
String get local {
return Intl.message(
'Local',
name: 'local',
desc: '',
args: [],
);
}
/// `Remote`
String get remote {
return Intl.message(
'Remote',
name: 'remote',
desc: '',
args: [],
);
}
/// `Backup local data to WebDAV`
String get remoteBackupDesc {
return Intl.message(
'Backup local data to WebDAV',
name: 'remoteBackupDesc',
desc: '',
args: [],
);
}
/// `Recovery data from WebDAV`
String get remoteRecoveryDesc {
return Intl.message(
'Recovery data from WebDAV',
name: 'remoteRecoveryDesc',
desc: '',
args: [],
);
}
/// `Backup local data to local`
String get localBackupDesc {
return Intl.message(
'Backup local data to local',
name: 'localBackupDesc',
desc: '',
args: [],
);
}
/// `Recovery data from file`
String get localRecoveryDesc {
return Intl.message(
'Recovery data from file',
name: 'localRecoveryDesc',
desc: '',
args: [],
);
}
/// `Mode`
String get mode {
return Intl.message(
'Mode',
name: 'mode',
desc: '',
args: [],
);
}
/// `Time`
String get time {
return Intl.message(
'Time',
name: 'time',
desc: '',
args: [],
);
}
/// `Source`
String get source {
return Intl.message(
'Source',
name: 'source',
desc: '',
args: [],
);
}
/// `All apps`
String get allApps {
return Intl.message(
'All apps',
name: 'allApps',
desc: '',
args: [],
);
}
/// `Only third-party apps`
String get onlyOtherApps {
return Intl.message(
'Only third-party apps',
name: 'onlyOtherApps',
desc: '',
args: [],
);
}
/// `Action`
String get action {
return Intl.message(
'Action',
name: 'action',
desc: '',
args: [],
);
}
/// `Intelligent selection`
String get intelligentSelected {
return Intl.message(
'Intelligent selection',
name: 'intelligentSelected',
desc: '',
args: [],
);
}
/// `Clipboard import`
String get clipboardImport {
return Intl.message(
'Clipboard import',
name: 'clipboardImport',
desc: '',
args: [],
);
}
/// `Export clipboard`
String get clipboardExport {
return Intl.message(
'Export clipboard',
name: 'clipboardExport',
desc: '',
args: [],
);
}
/// `Layout`
String get layout {
return Intl.message(
'Layout',
name: 'layout',
desc: '',
args: [],
);
}
/// `Tight`
String get tight {
return Intl.message(
'Tight',
name: 'tight',
desc: '',
args: [],
);
}
/// `Standard`
String get standard {
return Intl.message(
'Standard',
name: 'standard',
desc: '',
args: [],
);
}
/// `Loose`
String get loose {
return Intl.message(
'Loose',
name: 'loose',
desc: '',
args: [],
);
}
/// `Profiles sort`
String get profilesSort {
return Intl.message(
'Profiles sort',
name: 'profilesSort',
desc: '',
args: [],
);
}
/// `Start`
String get start {
return Intl.message(
'Start',
name: 'start',
desc: '',
args: [],
);
}
/// `Stop`
String get stop {
return Intl.message(
'Stop',
name: 'stop',
desc: '',
args: [],
);
}
/// `Processing app related settings`
String get appDesc {
return Intl.message(
'Processing app related settings',
name: 'appDesc',
desc: '',
args: [],
);
}
/// `Modify VPN related settings`
String get vpnDesc {
return Intl.message(
'Modify VPN related settings',
name: 'vpnDesc',
desc: '',
args: [],
);
}
/// `Overwrite general settings`
String get generalDesc {
return Intl.message(
'Overwrite general settings',
name: 'generalDesc',
desc: '',
args: [],
);
}
/// `Update DNS related settings`
String get dnsDesc {
return Intl.message(
'Update DNS related settings',
name: 'dnsDesc',
desc: '',
args: [],
);
}
/// `Key`
String get key {
return Intl.message(
'Key',
name: 'key',
desc: '',
args: [],
);
}
/// `Value`
String get value {
return Intl.message(
'Value',
name: 'value',
desc: '',
args: [],
);
}
/// `The key cannot be empty`
String get keyNotEmpty {
return Intl.message(
'The key cannot be empty',
name: 'keyNotEmpty',
desc: '',
args: [],
);
}
/// `The value cannot be empty`
String get valueNotEmpty {
return Intl.message(
'The value cannot be empty',
name: 'valueNotEmpty',
desc: '',
args: [],
);
}
/// `Add Hosts`
String get hostsDesc {
return Intl.message(
'Add Hosts',
name: 'hostsDesc',
desc: '',
args: [],
);
}
/// `Changes take effect after restarting the VPN`
String get vpnTip {
return Intl.message(
'Changes take effect after restarting the VPN',
name: 'vpnTip',
desc: '',
args: [],
);
}
/// `Auto routes all system traffic through VpnService`
String get vpnEnableDesc {
return Intl.message(
'Auto routes all system traffic through VpnService',
name: 'vpnEnableDesc',
desc: '',
args: [],
);
}
/// `Options`
String get options {
return Intl.message(
'Options',
name: 'options',
desc: '',
args: [],
);
}
/// `Loopback unlock tool`
String get loopback {
return Intl.message(
'Loopback unlock tool',
name: 'loopback',
desc: '',
args: [],
);
}
/// `Used for UWP loopback unlocking`
String get loopbackDesc {
return Intl.message(
'Used for UWP loopback unlocking',
name: 'loopbackDesc',
desc: '',
args: [],
);
}
/// `Providers`
String get providers {
return Intl.message(
'Providers',
name: 'providers',
desc: '',
args: [],
);
}
/// `Proxy providers`
String get proxyProviders {
return Intl.message(
'Proxy providers',
name: 'proxyProviders',
desc: '',
args: [],
);
}
/// `Rule providers`
String get ruleProviders {
return Intl.message(
'Rule providers',
name: 'ruleProviders',
desc: '',
args: [],
);
}
/// `Override Dns`
String get overrideDns {
return Intl.message(
'Override Dns',
name: 'overrideDns',
desc: '',
args: [],
);
}
/// `Turning it on will override the DNS options in the profile`
String get overrideDnsDesc {
return Intl.message(
'Turning it on will override the DNS options in the profile',
name: 'overrideDnsDesc',
desc: '',
args: [],
);
}
/// `Status`
String get status {
return Intl.message(
'Status',
name: 'status',
desc: '',
args: [],
);
}
/// `System DNS will be used when turned off`
String get statusDesc {
return Intl.message(
'System DNS will be used when turned off',
name: 'statusDesc',
desc: '',
args: [],
);
}
/// `Prioritize the use of DOH's http/3`
String get preferH3Desc {
return Intl.message(
'Prioritize the use of DOH\'s http/3',
name: 'preferH3Desc',
desc: '',
args: [],
);
}
/// `Respect rules`
String get respectRules {
return Intl.message(
'Respect rules',
name: 'respectRules',
desc: '',
args: [],
);
}
/// `DNS connection following rules, need to configure proxy-server-nameserver`
String get respectRulesDesc {
return Intl.message(
'DNS connection following rules, need to configure proxy-server-nameserver',
name: 'respectRulesDesc',
desc: '',
args: [],
);
}
/// `DNS mode`
String get dnsMode {
return Intl.message(
'DNS mode',
name: 'dnsMode',
desc: '',
args: [],
);
}
/// `Fakeip range`
String get fakeipRange {
return Intl.message(
'Fakeip range',
name: 'fakeipRange',
desc: '',
args: [],
);
}
/// `Fakeip filter`
String get fakeipFilter {
return Intl.message(
'Fakeip filter',
name: 'fakeipFilter',
desc: '',
args: [],
);
}
/// `Default nameserver`
String get defaultNameserver {
return Intl.message(
'Default nameserver',
name: 'defaultNameserver',
desc: '',
args: [],
);
}
/// `For resolving DNS server`
String get defaultNameserverDesc {
return Intl.message(
'For resolving DNS server',
name: 'defaultNameserverDesc',
desc: '',
args: [],
);
}
/// `Nameserver`
String get nameserver {
return Intl.message(
'Nameserver',
name: 'nameserver',
desc: '',
args: [],
);
}
/// `For resolving domain`
String get nameserverDesc {
return Intl.message(
'For resolving domain',
name: 'nameserverDesc',
desc: '',
args: [],
);
}
/// `Use hosts`
String get useHosts {
return Intl.message(
'Use hosts',
name: 'useHosts',
desc: '',
args: [],
);
}
/// `Use system hosts`
String get useSystemHosts {
return Intl.message(
'Use system hosts',
name: 'useSystemHosts',
desc: '',
args: [],
);
}
/// `Nameserver policy`
String get nameserverPolicy {
return Intl.message(
'Nameserver policy',
name: 'nameserverPolicy',
desc: '',
args: [],
);
}
/// `Specify the corresponding nameserver policy`
String get nameserverPolicyDesc {
return Intl.message(
'Specify the corresponding nameserver policy',
name: 'nameserverPolicyDesc',
desc: '',
args: [],
);
}
/// `Proxy nameserver`
String get proxyNameserver {
return Intl.message(
'Proxy nameserver',
name: 'proxyNameserver',
desc: '',
args: [],
);
}
/// `Domain for resolving proxy nodes`
String get proxyNameserverDesc {
return Intl.message(
'Domain for resolving proxy nodes',
name: 'proxyNameserverDesc',
desc: '',
args: [],
);
}
/// `Fallback`
String get fallback {
return Intl.message(
'Fallback',
name: 'fallback',
desc: '',
args: [],
);
}
/// `Generally use offshore DNS`
String get fallbackDesc {
return Intl.message(
'Generally use offshore DNS',
name: 'fallbackDesc',
desc: '',
args: [],
);
}
/// `Fallback filter`
String get fallbackFilter {
return Intl.message(
'Fallback filter',
name: 'fallbackFilter',
desc: '',
args: [],
);
}
/// `Geoip code`
String get geoipCode {
return Intl.message(
'Geoip code',
name: 'geoipCode',
desc: '',
args: [],
);
}
/// `Ipcidr`
String get ipcidr {
return Intl.message(
'Ipcidr',
name: 'ipcidr',
desc: '',
args: [],
);
}
/// `Domain`
String get domain {
return Intl.message(
'Domain',
name: 'domain',
desc: '',
args: [],
);
}
/// `Reset Dns`
String get resetDns {
return Intl.message(
'Reset Dns',
name: 'resetDns',
desc: '',
args: [],
);
}
/// `Reset`
String get reset {
return Intl.message(
'Reset',
name: 'reset',
desc: '',
args: [],
);
}
} }
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> { class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -2,9 +2,10 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/http.dart';
import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/plugins/proxy.dart';
import 'package:fl_clash/plugins/tile.dart'; import 'package:fl_clash/plugins/tile.dart';
import 'package:fl_clash/plugins/vpn.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
@@ -18,6 +19,7 @@ Future<void> main() async {
clashCore.initMessage(); clashCore.initMessage();
globalState.packageInfo = await PackageInfo.fromPlatform(); globalState.packageInfo = await PackageInfo.fromPlatform();
final config = await preferences.getConfig() ?? Config(); final config = await preferences.getConfig() ?? Config();
globalState.autoRun = config.autoRun;
final clashConfig = await preferences.getClashConfig() ?? ClashConfig(); final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
await android?.init(); await android?.init();
await window?.init(config.windowProps); await window?.init(config.windowProps);
@@ -30,11 +32,17 @@ Future<void> main() async {
openLogs: config.openLogs, openLogs: config.openLogs,
hasProxies: false, hasProxies: false,
); );
globalState.updateTray(
appState: appState,
config: config,
clashConfig: clashConfig,
);
await globalState.init( await globalState.init(
appState: appState, appState: appState,
config: config, config: config,
clashConfig: clashConfig, clashConfig: clashConfig,
); );
HttpOverrides.global = FlClashHttpOverrides();
runAppWithPreferences( runAppWithPreferences(
const Application(), const Application(),
appState: appState, appState: appState,
@@ -61,14 +69,14 @@ Future<void> vpnService() async {
clashConfig: clashConfig, clashConfig: clashConfig,
); );
proxy?.setServiceMessageHandler( vpn?.setServiceMessageHandler(
ServiceMessageHandler( ServiceMessageHandler(
onProtect: (Fd fd) async { onProtect: (Fd fd) async {
await proxy?.setProtect(fd.value); await vpn?.setProtect(fd.value);
clashCore.setFdMap(fd.id); clashCore.setFdMap(fd.id);
}, },
onProcess: (Process process) async { onProcess: (Process process) async {
var packageName = await app?.resolverProcess(process); final packageName = await app?.resolverProcess(process);
clashCore.setProcessMap( clashCore.setProcessMap(
ProcessMapItem( ProcessMapItem(
id: process.id, id: process.id,
@@ -76,8 +84,8 @@ Future<void> vpnService() async {
), ),
); );
}, },
onStarted: (String runTime) { onStarted: (String runTime) async {
globalState.applyProfile( await globalState.applyProfile(
appState: appState, appState: appState,
config: config, config: config,
clashConfig: clashConfig, clashConfig: clashConfig,
@@ -100,8 +108,7 @@ Future<void> vpnService() async {
WidgetsBinding.instance.platformDispatcher.locale, WidgetsBinding.instance.platformDispatcher.locale,
); );
await app?.tip(appLocalizations.startVpn); await app?.tip(appLocalizations.startVpn);
await globalState.startSystemProxy( await globalState.handleStart(
appState: appState,
config: config, config: config,
clashConfig: clashConfig, clashConfig: clashConfig,
); );
@@ -110,7 +117,7 @@ Future<void> vpnService() async {
TileListenerWithVpn( TileListenerWithVpn(
onStop: () async { onStop: () async {
await app?.tip(appLocalizations.stopVpn); await app?.tip(appLocalizations.stopVpn);
await globalState.stopSystemProxy(); await globalState.handleStop();
clashCore.shutdown(); clashCore.shutdown();
exit(0); exit(0);
}, },
@@ -130,13 +137,13 @@ class ServiceMessageHandler with ServiceMessageListener {
final Function(Fd fd) _onProtect; final Function(Fd fd) _onProtect;
final Function(Process process) _onProcess; final Function(Process process) _onProcess;
final Function(String runTime) _onStarted; final Function(String runTime) _onStarted;
final Function(String groupName) _onLoaded; final Function(String providerName) _onLoaded;
const ServiceMessageHandler({ const ServiceMessageHandler({
required Function(Fd fd) onProtect, required Function(Fd fd) onProtect,
required Function(Process process) onProcess, required Function(Process process) onProcess,
required Function(String runTime) onStarted, required Function(String runTime) onStarted,
required Function(String groupName) onLoaded, required Function(String providerName) onLoaded,
}) : _onProtect = onProtect, }) : _onProtect = onProtect,
_onProcess = onProcess, _onProcess = onProcess,
_onStarted = onStarted, _onStarted = onStarted,
@@ -158,8 +165,8 @@ class ServiceMessageHandler with ServiceMessageListener {
} }
@override @override
onLoaded(String groupName) { onLoaded(String providerName) {
_onLoaded(groupName); _onLoaded(providerName);
} }
} }

View File

@@ -8,6 +8,7 @@ import 'connection.dart';
import 'ffi.dart'; import 'ffi.dart';
import 'log.dart'; import 'log.dart';
import 'navigation.dart'; import 'navigation.dart';
import 'package.dart';
import 'profile.dart'; import 'profile.dart';
import 'proxy.dart'; import 'proxy.dart';
import 'system_color_scheme.dart'; import 'system_color_scheme.dart';
@@ -35,6 +36,8 @@ class AppState with ChangeNotifier {
double _viewWidth; double _viewWidth;
List<Connection> _requests; List<Connection> _requests;
num _checkIpNum; num _checkIpNum;
List<ExternalProvider> _providers;
List<Package> _packages;
AppState({ AppState({
required Mode mode, required Mode mode,
@@ -54,8 +57,10 @@ class AppState with ChangeNotifier {
_totalTraffic = Traffic(), _totalTraffic = Traffic(),
_delayMap = {}, _delayMap = {},
_groups = [], _groups = [],
_providers = [],
_packages = [],
_isCompatible = isCompatible, _isCompatible = isCompatible,
_systemColorSchemes = SystemColorSchemes(); _systemColorSchemes = const SystemColorSchemes();
String get currentLabel => _currentLabel; String get currentLabel => _currentLabel;
@@ -109,7 +114,7 @@ class AppState with ChangeNotifier {
} }
} }
String getDesc(String type, String? proxyName) { String getDesc(String type, String proxyName) {
final groupTypeNamesList = GroupType.values.map((e) => e.name).toList(); final groupTypeNamesList = GroupType.values.map((e) => e.name).toList();
if (!groupTypeNamesList.contains(type)) { if (!groupTypeNamesList.contains(type)) {
return type; return type;
@@ -120,15 +125,17 @@ class AppState with ChangeNotifier {
} }
} }
String? getRealProxyName(String? proxyName) { String getRealProxyName(String proxyName) {
if (proxyName == null) return null; if (proxyName.isEmpty) return proxyName;
final index = groups.indexWhere((element) => element.name == proxyName); final index = groups.indexWhere((element) => element.name == proxyName);
if (index == -1) return proxyName; if (index == -1) return proxyName;
final group = groups[index]; final group = groups[index];
return getRealProxyName((selectedMap.containsKey(proxyName) final currentSelectedName =
? selectedMap[proxyName] group.getCurrentSelectedName(selectedMap[proxyName] ?? '');
: group.now)) ?? if (currentSelectedName.isEmpty) return proxyName;
proxyName; return getRealProxyName(
currentSelectedName,
);
} }
String? get showProxyName { String? get showProxyName {
@@ -140,7 +147,7 @@ class AppState with ChangeNotifier {
return selectedMap[firstGroupName] ?? firstGroup.now; return selectedMap[firstGroupName] ?? firstGroup.now;
} }
int? getDelay(String? proxyName) { int? getDelay(String proxyName) {
return _delayMap[getRealProxyName(proxyName)]; return _delayMap[getRealProxyName(proxyName)];
} }
@@ -293,6 +300,7 @@ class AppState with ChangeNotifier {
.toList(); .toList();
case Mode.rule: case Mode.rule:
return groups return groups
.where((item) => item.hidden == false)
.where((element) => element.name != GroupName.GLOBAL.name) .where((element) => element.name != GroupName.GLOBAL.name)
.toList(); .toList();
} }
@@ -327,6 +335,32 @@ class AppState with ChangeNotifier {
} }
} }
List<Package> get packages => _packages;
set packages(List<Package> value) {
if (!const ListEquality<Package>().equals(_packages, value)) {
_packages = value;
notifyListeners();
}
}
List<ExternalProvider> get providers => _providers;
set providers(List<ExternalProvider> value) {
if (!const ListEquality<ExternalProvider>().equals(_providers, value)) {
_providers = value;
notifyListeners();
}
}
setProvider(ExternalProvider? provider) {
if(provider == null) return;
final index = _providers.indexWhere((item) => item.name == provider.name);
if (index == -1) return;
_providers = List.from(_providers)..[index] = provider;
notifyListeners();
}
Group? getGroupWithName(String groupName) { Group? getGroupWithName(String groupName) {
final index = final index =
currentGroups.indexWhere((element) => element.name == groupName); currentGroups.indexWhere((element) => element.name == groupName);

View File

@@ -26,89 +26,94 @@ class Tun with _$Tun {
factory Tun.fromJson(Map<String, Object?> json) => _$TunFromJson(json); factory Tun.fromJson(Map<String, Object?> json) => _$TunFromJson(json);
} }
@JsonSerializable() @freezed
class Dns { class FallbackFilter with _$FallbackFilter {
bool enable; const factory FallbackFilter({
bool ipv6; @Default(true) bool geoip,
@JsonKey(name: "default-nameserver") @Default("CN") @JsonKey(name: "geoip-code") String geoipCode,
List<String> defaultNameserver; @Default(["gfw"]) List<String> geosite,
@JsonKey(name: "enhanced-mode") @Default(["240.0.0.0/4"]) List<String> ipcidr,
String enhancedMode; @Default([
@JsonKey(name: "fake-ip-range") "+.google.com",
String fakeIpRange; "+.facebook.com",
@JsonKey(name: "use-hosts") "+.youtube.com",
bool useHosts; ])
List<String> nameserver; List<String> domain,
List<String> fallback; }) = _FallbackFilter;
@JsonKey(name: "fake-ip-filter")
List<String> fakeIpFilter;
Dns() factory FallbackFilter.fromJson(Map<String, Object?> json) =>
: enable = true, _$FallbackFilterFromJson(json);
ipv6 = false, }
defaultNameserver = [
"223.5.5.5",
"119.29.29.29",
"8.8.4.4",
"1.0.0.1",
],
enhancedMode = "fake-ip",
fakeIpRange = "198.18.0.1/16",
useHosts = true,
nameserver = [
"8.8.8.8",
"114.114.114.114",
"https://doh.pub/dns-query",
"https://dns.alidns.com/dns-query",
],
fallback = [
'https://doh.dns.sb/dns-query',
'https://dns.cloudflare.com/dns-query',
'https://dns.twnic.tw/dns-query',
'tls://8.8.4.4:853',
],
fakeIpFilter = [
// Stun Services
"+.stun.*.*",
"+.stun.*.*.*",
"+.stun.*.*.*.*",
"+.stun.*.*.*.*.*",
// Google Voices @freezed
"lens.l.google.com", class Dns with _$Dns {
const factory Dns({
@Default(true) bool enable,
@Default(false) @JsonKey(name: "prefer-h3") bool preferH3,
@Default(true) @JsonKey(name: "use-hosts") bool useHosts,
@Default(true) @JsonKey(name: "use-system-hosts") bool useSystemHosts,
@Default(true) @JsonKey(name: "respect-rules") bool respectRules,
@Default(false) bool ipv6,
@Default(["223.5.5.5"])
@JsonKey(name: "default-nameserver")
List<String> defaultNameserver,
@Default(DnsMode.fakeIp)
@JsonKey(name: "enhanced-mode")
DnsMode enhancedMode,
@Default("198.18.0.1/16")
@JsonKey(name: "fake-ip-range")
String fakeIpRange,
@Default([
"*.lan",
"localhost.ptlogin2.qq.com",
])
@JsonKey(name: "fake-ip-filter")
List<String> fakeIpFilter,
@Default({
"www.baidu.com": "114.114.114.114",
"+.internal.crop.com": "10.0.0.1",
"geosite:cn": "https://doh.pub/dns-query"
})
@JsonKey(name: "nameserver-policy")
Map<String, String> nameserverPolicy,
@Default([
"https://doh.pub/dns-query",
"https://dns.alidns.com/dns-query",
])
List<String> nameserver,
@Default([
"tls://8.8.4.4",
"tls://1.1.1.1",
])
List<String> fallback,
@Default([
"https://doh.pub/dns-query",
])
@JsonKey(name: "proxy-server-nameserver")
List<String> proxyServerNameserver,
@Default(FallbackFilter())
@JsonKey(name: "fallback-filter")
FallbackFilter fallbackFilter,
}) = _Dns;
// Nintendo Switch STUN factory Dns.fromJson(Map<String, Object?> json) => _$DnsFromJson(json);
"*.n.n.srv.nintendo.net",
// PlayStation STUN factory Dns.safeDnsFromJson(Map<String, Object?> json) {
"+.stun.playstation.net", try {
return Dns.fromJson(json);
// XBox } catch (_) {
"xbox.*.*.microsoft.com", return const Dns();
"*.*.xboxlive.com", }
// Microsoft Captive Portal
"*.msftncsi.com",
"*.msftconnecttest.com",
// Bilibili CDN
"*.mcdn.bilivideo.cn",
// Windows Default LAN WorkGroup
"WORKGROUP",
];
factory Dns.fromJson(Map<String, dynamic> json) {
return _$DnsFromJson(json);
}
Map<String, dynamic> toJson() {
return _$DnsToJson(this);
} }
} }
typedef GeoXMap = Map<String, String>; typedef GeoXMap = Map<String, String>;
typedef HostsMap = Map<String, String>;
const defaultMixedPort = 7890;
const defaultKeepAliveInterval = 30;
@JsonSerializable() @JsonSerializable()
class ClashConfig extends ChangeNotifier { class ClashConfig extends ChangeNotifier {
int _mixedPort; int _mixedPort;
@@ -119,6 +124,7 @@ class ClashConfig extends ChangeNotifier {
String _externalController; String _externalController;
Mode _mode; Mode _mode;
FindProcessMode _findProcessMode; FindProcessMode _findProcessMode;
int _keepAliveInterval;
bool _unifiedDelay; bool _unifiedDelay;
bool _tcpConcurrent; bool _tcpConcurrent;
Tun _tun; Tun _tun;
@@ -126,9 +132,10 @@ class ClashConfig extends ChangeNotifier {
GeoXMap _geoXUrl; GeoXMap _geoXUrl;
List<String> _rules; List<String> _rules;
String? _globalRealUa; String? _globalRealUa;
HostsMap _hosts;
ClashConfig() ClashConfig()
: _mixedPort = 7890, : _mixedPort = defaultMixedPort,
_mode = Mode.rule, _mode = Mode.rule,
_ipv6 = false, _ipv6 = false,
_findProcessMode = FindProcessMode.off, _findProcessMode = FindProcessMode.off,
@@ -139,11 +146,13 @@ class ClashConfig extends ChangeNotifier {
_unifiedDelay = false, _unifiedDelay = false,
_geodataLoader = geodataLoaderMemconservative, _geodataLoader = geodataLoaderMemconservative,
_externalController = '', _externalController = '',
_dns = Dns(), _keepAliveInterval = defaultKeepAliveInterval,
_dns = const Dns(),
_geoXUrl = defaultGeoXMap, _geoXUrl = defaultGeoXMap,
_rules = []; _rules = [],
_hosts = {};
@JsonKey(name: "mixed-port", defaultValue: 7890) @JsonKey(name: "mixed-port", defaultValue: defaultMixedPort)
int get mixedPort => _mixedPort; int get mixedPort => _mixedPort;
set mixedPort(int value) { set mixedPort(int value) {
@@ -203,6 +212,16 @@ class ClashConfig extends ChangeNotifier {
} }
} }
@JsonKey(name: "keep-alive-interval", defaultValue: defaultKeepAliveInterval)
int get keepAliveInterval => _keepAliveInterval;
set keepAliveInterval(int value) {
if (_keepAliveInterval != value) {
_keepAliveInterval = value;
notifyListeners();
}
}
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool get ipv6 => _ipv6; bool get ipv6 => _ipv6;
@@ -257,6 +276,7 @@ class ClashConfig extends ChangeNotifier {
} }
} }
@JsonKey(fromJson: Dns.safeDnsFromJson)
Dns get dns => _dns; Dns get dns => _dns;
set dns(Dns value) { set dns(Dns value) {
@@ -304,10 +324,21 @@ class ClashConfig extends ChangeNotifier {
} }
} }
@JsonKey(defaultValue: {})
HostsMap get hosts => _hosts;
set hosts(HostsMap value) {
if (!const MapEquality<String, String>().equals(value, _hosts)) {
_hosts = value;
notifyListeners();
}
}
update([ClashConfig? clashConfig]) { update([ClashConfig? clashConfig]) {
if (clashConfig != null) { if (clashConfig != null) {
_mixedPort = clashConfig._mixedPort; _mixedPort = clashConfig._mixedPort;
_allowLan = clashConfig._allowLan; _allowLan = clashConfig._allowLan;
_hosts = clashConfig._hosts;
_mode = clashConfig._mode; _mode = clashConfig._mode;
_logLevel = clashConfig._logLevel; _logLevel = clashConfig._logLevel;
_tun = clashConfig._tun; _tun = clashConfig._tun;

View File

@@ -18,6 +18,7 @@ class AccessControl with _$AccessControl {
@Default(AccessControlMode.rejectSelected) AccessControlMode mode, @Default(AccessControlMode.rejectSelected) AccessControlMode mode,
@Default([]) List<String> acceptList, @Default([]) List<String> acceptList,
@Default([]) List<String> rejectList, @Default([]) List<String> rejectList,
@Default(AccessSortType.none) AccessSortType sort,
@Default(true) bool isFilterSystemApp, @Default(true) bool isFilterSystemApp,
}) = _AccessControl; }) = _AccessControl;
@@ -25,15 +26,38 @@ class AccessControl with _$AccessControl {
_$AccessControlFromJson(json); _$AccessControlFromJson(json);
} }
extension AccessControlExt on AccessControl {
List<String> get currentList => switch (mode) {
AccessControlMode.acceptSelected => acceptList,
AccessControlMode.rejectSelected => rejectList,
};
}
@freezed @freezed
class Props with _$Props { class CoreState with _$CoreState {
const factory Props({ const factory CoreState({
AccessControl? accessControl, AccessControl? accessControl,
required String currentProfileName,
required bool enable,
required bool allowBypass, required bool allowBypass,
required bool systemProxy, required bool systemProxy,
}) = _Props; required int mixedPort,
required bool onlyProxy,
}) = _CoreState;
factory Props.fromJson(Map<String, Object?> json) => _$PropsFromJson(json); factory CoreState.fromJson(Map<String, Object?> json) =>
_$CoreStateFromJson(json);
}
@freezed
class VPNState with _$VPNState {
const factory VPNState({
required AccessControl? accessControl,
required VpnProps vpnProps,
}) = _VPNState;
factory VPNState.fromJson(Map<String, Object?> json) =>
_$VPNStateFromJson(json);
} }
@freezed @freezed
@@ -46,10 +70,45 @@ class WindowProps with _$WindowProps {
}) = _WindowProps; }) = _WindowProps;
factory WindowProps.fromJson(Map<String, Object?>? json) => factory WindowProps.fromJson(Map<String, Object?>? json) =>
json == null ? defaultWindowProps : _$WindowPropsFromJson(json); json == null ? const WindowProps() : _$WindowPropsFromJson(json);
} }
const defaultWindowProps = WindowProps(); @freezed
class VpnProps with _$VpnProps {
const factory VpnProps({
@Default(true) bool enable,
@Default(false) bool systemProxy,
@Default(true) bool allowBypass,
}) = _VpnProps;
factory VpnProps.fromJson(Map<String, Object?>? json) =>
json == null ? const VpnProps() : _$VpnPropsFromJson(json);
}
@freezed
class DesktopProps with _$DesktopProps {
const factory DesktopProps({
@Default(true) bool systemProxy,
}) = _DesktopProps;
factory DesktopProps.fromJson(Map<String, Object?>? json) =>
json == null ? const DesktopProps() : _$DesktopPropsFromJson(json);
}
const defaultCustomFontSizeScale = 1.0;
const defaultScaleProps = ScaleProps();
@freezed
class ScaleProps with _$ScaleProps {
const factory ScaleProps({
@Default(false) bool custom,
@Default(defaultCustomFontSizeScale) double scale,
}) = _ScaleProps;
factory ScaleProps.fromJson(Map<String, Object?>? json) =>
json == null ? defaultScaleProps : _$ScalePropsFromJson(json);
}
@JsonSerializable() @JsonSerializable()
class Config extends ChangeNotifier { class Config extends ChangeNotifier {
@@ -69,16 +128,21 @@ class Config extends ChangeNotifier {
AccessControl _accessControl; AccessControl _accessControl;
bool _isAnimateToPage; bool _isAnimateToPage;
bool _autoCheckUpdate; bool _autoCheckUpdate;
bool _allowBypass;
bool _systemProxy;
bool _isExclude; bool _isExclude;
DAV? _dav; DAV? _dav;
bool _isCloseConnections; bool _isCloseConnections;
ProxiesType _proxiesType; ProxiesType _proxiesType;
ProxyCardType _proxyCardType; ProxyCardType _proxyCardType;
int _proxiesColumns; ProxiesLayout _proxiesLayout;
String _testUrl; String _testUrl;
WindowProps _windowProps; WindowProps _windowProps;
bool _onlyProxy;
bool _prueBlack;
VpnProps _vpnProps;
ScaleProps _scaleProps;
DesktopProps _desktopProps;
bool _showLabel;
bool _overrideDns;
Config() Config()
: _profiles = [], : _profiles = [],
@@ -94,16 +158,21 @@ class Config extends ChangeNotifier {
_isMinimizeOnExit = true, _isMinimizeOnExit = true,
_isAccessControl = false, _isAccessControl = false,
_autoCheckUpdate = true, _autoCheckUpdate = true,
_systemProxy = false,
_testUrl = defaultTestUrl, _testUrl = defaultTestUrl,
_accessControl = const AccessControl(), _accessControl = const AccessControl(),
_isAnimateToPage = true, _isAnimateToPage = true,
_allowBypass = true,
_isExclude = false, _isExclude = false,
_proxyCardType = ProxyCardType.expand, _proxyCardType = ProxyCardType.expand,
_windowProps = defaultWindowProps, _windowProps = const WindowProps(),
_proxiesType = ProxiesType.tab, _proxiesType = ProxiesType.tab,
_proxiesColumns = 2; _prueBlack = false,
_onlyProxy = false,
_proxiesLayout = ProxiesLayout.standard,
_vpnProps = const VpnProps(),
_desktopProps = const DesktopProps(),
_showLabel = false,
_overrideDns = false,
_scaleProps = const ScaleProps();
deleteProfileById(String id) { deleteProfileById(String id) {
_profiles = profiles.where((element) => element.id != id).toList(); _profiles = profiles.where((element) => element.id != id).toList();
@@ -305,6 +374,16 @@ class Config extends ChangeNotifier {
} }
} }
@JsonKey(defaultValue: ProxiesLayout.standard)
ProxiesLayout get proxiesLayout => _proxiesLayout;
set proxiesLayout(ProxiesLayout value) {
if (_proxiesLayout != value) {
_proxiesLayout = value;
notifyListeners();
}
}
@JsonKey(defaultValue: true) @JsonKey(defaultValue: true)
bool get isMinimizeOnExit => _isMinimizeOnExit; bool get isMinimizeOnExit => _isMinimizeOnExit;
@@ -383,26 +462,26 @@ class Config extends ChangeNotifier {
} }
} }
@JsonKey(defaultValue: true) @JsonKey(defaultValue: false)
bool get allowBypass { bool get onlyProxy {
return _allowBypass; return _onlyProxy;
} }
set allowBypass(bool value) { set onlyProxy(bool value) {
if (_allowBypass != value) { if (_onlyProxy != value) {
_allowBypass = value; _onlyProxy = value;
notifyListeners(); notifyListeners();
} }
} }
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool get systemProxy { bool get prueBlack {
return _systemProxy; return _prueBlack;
} }
set systemProxy(bool value) { set prueBlack(bool value) {
if (_systemProxy != value) { if (_prueBlack != value) {
_systemProxy = value; _prueBlack = value;
notifyListeners(); notifyListeners();
} }
} }
@@ -442,16 +521,6 @@ class Config extends ChangeNotifier {
} }
} }
@JsonKey(defaultValue: 2)
int get proxiesColumns => _proxiesColumns;
set proxiesColumns(int value) {
if (_proxiesColumns != value) {
_proxiesColumns = value;
notifyListeners();
}
}
@JsonKey(name: "test-url", defaultValue: defaultTestUrl) @JsonKey(name: "test-url", defaultValue: defaultTestUrl)
String get testUrl => _testUrl; String get testUrl => _testUrl;
@@ -481,6 +550,53 @@ class Config extends ChangeNotifier {
} }
} }
VpnProps get vpnProps => _vpnProps;
set vpnProps(VpnProps value) {
if (_vpnProps != value) {
_vpnProps = value;
notifyListeners();
}
}
DesktopProps get desktopProps => _desktopProps;
set desktopProps(DesktopProps value) {
if (_desktopProps != value) {
_desktopProps = value;
notifyListeners();
}
}
ScaleProps get scaleProps => _scaleProps;
set scaleProps(ScaleProps value) {
if (_scaleProps != value) {
_scaleProps = value;
notifyListeners();
}
}
@JsonKey(defaultValue: false)
bool get showLabel => _showLabel;
set showLabel(bool value) {
if (_showLabel != value) {
_showLabel = value;
notifyListeners();
}
}
@JsonKey(defaultValue: false)
bool get overrideDns => _overrideDns;
set overrideDns(bool value) {
if (_overrideDns != value) {
_overrideDns = value;
notifyListeners();
}
}
update([ update([
Config? config, Config? config,
RecoveryOption recoveryOptions = RecoveryOption.all, RecoveryOption recoveryOptions = RecoveryOption.all,
@@ -499,13 +615,13 @@ class Config extends ChangeNotifier {
_isCloseConnections = config._isCloseConnections; _isCloseConnections = config._isCloseConnections;
_isCompatible = config._isCompatible; _isCompatible = config._isCompatible;
_autoLaunch = config._autoLaunch; _autoLaunch = config._autoLaunch;
_dav = config._dav;
_silentLaunch = config._silentLaunch; _silentLaunch = config._silentLaunch;
_autoRun = config._autoRun; _autoRun = config._autoRun;
_proxiesType = config._proxiesType; _proxiesType = config._proxiesType;
_openLog = config._openLog; _openLog = config._openLog;
_themeMode = config._themeMode; _themeMode = config._themeMode;
_locale = config._locale; _locale = config._locale;
_allowBypass = config._allowBypass;
_primaryColor = config._primaryColor; _primaryColor = config._primaryColor;
_proxiesSortType = config._proxiesSortType; _proxiesSortType = config._proxiesSortType;
_isMinimizeOnExit = config._isMinimizeOnExit; _isMinimizeOnExit = config._isMinimizeOnExit;
@@ -513,9 +629,13 @@ class Config extends ChangeNotifier {
_accessControl = config._accessControl; _accessControl = config._accessControl;
_isAnimateToPage = config._isAnimateToPage; _isAnimateToPage = config._isAnimateToPage;
_autoCheckUpdate = config._autoCheckUpdate; _autoCheckUpdate = config._autoCheckUpdate;
_prueBlack = config._prueBlack;
_testUrl = config._testUrl; _testUrl = config._testUrl;
_isExclude = config._isExclude; _isExclude = config._isExclude;
_windowProps = config._windowProps; _windowProps = config._windowProps;
_vpnProps = config._vpnProps;
_overrideDns = config._overrideDns;
_desktopProps = config._desktopProps;
} }
notifyListeners(); notifyListeners();
} }

View File

@@ -4,14 +4,16 @@ part 'generated/dav.g.dart';
part 'generated/dav.freezed.dart'; part 'generated/dav.freezed.dart';
const defaultDavFileName = "backup.zip";
@freezed @freezed
class DAV with _$DAV{ class DAV with _$DAV {
const factory DAV({ const factory DAV({
required String uri, required String uri,
required String user, required String user,
required String password, required String password,
@Default(defaultDavFileName) String fileName,
}) = _DAV; }) = _DAV;
factory DAV.fromJson(Map<String, Object?> json) => factory DAV.fromJson(Map<String, Object?> json) => _$DAVFromJson(json);
_$DAVFromJson(json); }
}

View File

@@ -14,6 +14,7 @@ class ConfigExtendedParams with _$ConfigExtendedParams {
@JsonKey(name: "is-patch") required bool isPatch, @JsonKey(name: "is-patch") required bool isPatch,
@JsonKey(name: "is-compatible") required bool isCompatible, @JsonKey(name: "is-compatible") required bool isCompatible,
@JsonKey(name: "selected-map") required SelectedMap selectedMap, @JsonKey(name: "selected-map") required SelectedMap selectedMap,
@JsonKey(name: "override-dns") required bool overrideDns,
@JsonKey(name: "test-url") required String testUrl, @JsonKey(name: "test-url") required String testUrl,
}) = _ConfigExtendedParams; }) = _ConfigExtendedParams;
@@ -24,7 +25,7 @@ class ConfigExtendedParams with _$ConfigExtendedParams {
@freezed @freezed
class UpdateConfigParams with _$UpdateConfigParams { class UpdateConfigParams with _$UpdateConfigParams {
const factory UpdateConfigParams({ const factory UpdateConfigParams({
@JsonKey(name: "profile-path") String? profilePath, @JsonKey(name: "profile-id") required String profileId,
required ClashConfig config, required ClashConfig config,
required ConfigExtendedParams params, required ConfigExtendedParams params,
}) = _UpdateConfigParams; }) = _UpdateConfigParams;
@@ -123,6 +124,9 @@ class ExternalProvider with _$ExternalProvider {
const factory ExternalProvider({ const factory ExternalProvider({
required String name, required String name,
required String type, required String type,
required String path,
required int count,
@Default(false) bool isUpdating,
@JsonKey(name: "vehicle-type") required String vehicleType, @JsonKey(name: "vehicle-type") required String vehicleType,
@JsonKey(name: "update-at") required DateTime updateAt, @JsonKey(name: "update-at") required DateTime updateAt,
}) = _ExternalProvider; }) = _ExternalProvider;
@@ -140,7 +144,7 @@ abstract mixin class AppMessageListener {
void onStarted(String runTime) {} void onStarted(String runTime) {}
void onLoaded(String groupName) {} void onLoaded(String providerName) {}
} }
abstract mixin class ServiceMessageListener { abstract mixin class ServiceMessageListener {
@@ -150,7 +154,5 @@ abstract mixin class ServiceMessageListener {
onStarted(String runTime) {} onStarted(String runTime) {}
onLoaded(String groupName) {} onLoaded(String providerName) {}
} }

21
lib/models/file.dart Normal file
View File

@@ -0,0 +1,21 @@
import 'package:fl_clash/common/datetime.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'traffic.dart';
part 'generated/file.freezed.dart';
@freezed
class FileInfo with _$FileInfo {
const factory FileInfo({
required int size,
required DateTime lastModified,
}) = _FileInfo;
}
extension FileInfoExt on FileInfo{
String get desc => "${TrafficValue(value: size).show} · ${lastModified.lastUpdateTimeDesc}";
}

View File

@@ -220,3 +220,818 @@ abstract class _Tun implements Tun {
_$$TunImplCopyWith<_$TunImpl> get copyWith => _$$TunImplCopyWith<_$TunImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
FallbackFilter _$FallbackFilterFromJson(Map<String, dynamic> json) {
return _FallbackFilter.fromJson(json);
}
/// @nodoc
mixin _$FallbackFilter {
bool get geoip => throw _privateConstructorUsedError;
@JsonKey(name: "geoip-code")
String get geoipCode => throw _privateConstructorUsedError;
List<String> get geosite => throw _privateConstructorUsedError;
List<String> get ipcidr => throw _privateConstructorUsedError;
List<String> get domain => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$FallbackFilterCopyWith<FallbackFilter> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $FallbackFilterCopyWith<$Res> {
factory $FallbackFilterCopyWith(
FallbackFilter value, $Res Function(FallbackFilter) then) =
_$FallbackFilterCopyWithImpl<$Res, FallbackFilter>;
@useResult
$Res call(
{bool geoip,
@JsonKey(name: "geoip-code") String geoipCode,
List<String> geosite,
List<String> ipcidr,
List<String> domain});
}
/// @nodoc
class _$FallbackFilterCopyWithImpl<$Res, $Val extends FallbackFilter>
implements $FallbackFilterCopyWith<$Res> {
_$FallbackFilterCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? geoip = null,
Object? geoipCode = null,
Object? geosite = null,
Object? ipcidr = null,
Object? domain = null,
}) {
return _then(_value.copyWith(
geoip: null == geoip
? _value.geoip
: geoip // ignore: cast_nullable_to_non_nullable
as bool,
geoipCode: null == geoipCode
? _value.geoipCode
: geoipCode // ignore: cast_nullable_to_non_nullable
as String,
geosite: null == geosite
? _value.geosite
: geosite // ignore: cast_nullable_to_non_nullable
as List<String>,
ipcidr: null == ipcidr
? _value.ipcidr
: ipcidr // ignore: cast_nullable_to_non_nullable
as List<String>,
domain: null == domain
? _value.domain
: domain // ignore: cast_nullable_to_non_nullable
as List<String>,
) as $Val);
}
}
/// @nodoc
abstract class _$$FallbackFilterImplCopyWith<$Res>
implements $FallbackFilterCopyWith<$Res> {
factory _$$FallbackFilterImplCopyWith(_$FallbackFilterImpl value,
$Res Function(_$FallbackFilterImpl) then) =
__$$FallbackFilterImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{bool geoip,
@JsonKey(name: "geoip-code") String geoipCode,
List<String> geosite,
List<String> ipcidr,
List<String> domain});
}
/// @nodoc
class __$$FallbackFilterImplCopyWithImpl<$Res>
extends _$FallbackFilterCopyWithImpl<$Res, _$FallbackFilterImpl>
implements _$$FallbackFilterImplCopyWith<$Res> {
__$$FallbackFilterImplCopyWithImpl(
_$FallbackFilterImpl _value, $Res Function(_$FallbackFilterImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? geoip = null,
Object? geoipCode = null,
Object? geosite = null,
Object? ipcidr = null,
Object? domain = null,
}) {
return _then(_$FallbackFilterImpl(
geoip: null == geoip
? _value.geoip
: geoip // ignore: cast_nullable_to_non_nullable
as bool,
geoipCode: null == geoipCode
? _value.geoipCode
: geoipCode // ignore: cast_nullable_to_non_nullable
as String,
geosite: null == geosite
? _value._geosite
: geosite // ignore: cast_nullable_to_non_nullable
as List<String>,
ipcidr: null == ipcidr
? _value._ipcidr
: ipcidr // ignore: cast_nullable_to_non_nullable
as List<String>,
domain: null == domain
? _value._domain
: domain // ignore: cast_nullable_to_non_nullable
as List<String>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$FallbackFilterImpl implements _FallbackFilter {
const _$FallbackFilterImpl(
{this.geoip = true,
@JsonKey(name: "geoip-code") this.geoipCode = "CN",
final List<String> geosite = const ["gfw"],
final List<String> ipcidr = const ["240.0.0.0/4"],
final List<String> domain = const [
"+.google.com",
"+.facebook.com",
"+.youtube.com"
]})
: _geosite = geosite,
_ipcidr = ipcidr,
_domain = domain;
factory _$FallbackFilterImpl.fromJson(Map<String, dynamic> json) =>
_$$FallbackFilterImplFromJson(json);
@override
@JsonKey()
final bool geoip;
@override
@JsonKey(name: "geoip-code")
final String geoipCode;
final List<String> _geosite;
@override
@JsonKey()
List<String> get geosite {
if (_geosite is EqualUnmodifiableListView) return _geosite;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_geosite);
}
final List<String> _ipcidr;
@override
@JsonKey()
List<String> get ipcidr {
if (_ipcidr is EqualUnmodifiableListView) return _ipcidr;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_ipcidr);
}
final List<String> _domain;
@override
@JsonKey()
List<String> get domain {
if (_domain is EqualUnmodifiableListView) return _domain;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_domain);
}
@override
String toString() {
return 'FallbackFilter(geoip: $geoip, geoipCode: $geoipCode, geosite: $geosite, ipcidr: $ipcidr, domain: $domain)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$FallbackFilterImpl &&
(identical(other.geoip, geoip) || other.geoip == geoip) &&
(identical(other.geoipCode, geoipCode) ||
other.geoipCode == geoipCode) &&
const DeepCollectionEquality().equals(other._geosite, _geosite) &&
const DeepCollectionEquality().equals(other._ipcidr, _ipcidr) &&
const DeepCollectionEquality().equals(other._domain, _domain));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
geoip,
geoipCode,
const DeepCollectionEquality().hash(_geosite),
const DeepCollectionEquality().hash(_ipcidr),
const DeepCollectionEquality().hash(_domain));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$FallbackFilterImplCopyWith<_$FallbackFilterImpl> get copyWith =>
__$$FallbackFilterImplCopyWithImpl<_$FallbackFilterImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$FallbackFilterImplToJson(
this,
);
}
}
abstract class _FallbackFilter implements FallbackFilter {
const factory _FallbackFilter(
{final bool geoip,
@JsonKey(name: "geoip-code") final String geoipCode,
final List<String> geosite,
final List<String> ipcidr,
final List<String> domain}) = _$FallbackFilterImpl;
factory _FallbackFilter.fromJson(Map<String, dynamic> json) =
_$FallbackFilterImpl.fromJson;
@override
bool get geoip;
@override
@JsonKey(name: "geoip-code")
String get geoipCode;
@override
List<String> get geosite;
@override
List<String> get ipcidr;
@override
List<String> get domain;
@override
@JsonKey(ignore: true)
_$$FallbackFilterImplCopyWith<_$FallbackFilterImpl> get copyWith =>
throw _privateConstructorUsedError;
}
Dns _$DnsFromJson(Map<String, dynamic> json) {
return _Dns.fromJson(json);
}
/// @nodoc
mixin _$Dns {
bool get enable => throw _privateConstructorUsedError;
@JsonKey(name: "prefer-h3")
bool get preferH3 => throw _privateConstructorUsedError;
@JsonKey(name: "use-hosts")
bool get useHosts => throw _privateConstructorUsedError;
@JsonKey(name: "use-system-hosts")
bool get useSystemHosts => throw _privateConstructorUsedError;
@JsonKey(name: "respect-rules")
bool get respectRules => throw _privateConstructorUsedError;
bool get ipv6 => throw _privateConstructorUsedError;
@JsonKey(name: "default-nameserver")
List<String> get defaultNameserver => throw _privateConstructorUsedError;
@JsonKey(name: "enhanced-mode")
DnsMode get enhancedMode => throw _privateConstructorUsedError;
@JsonKey(name: "fake-ip-range")
String get fakeIpRange => throw _privateConstructorUsedError;
@JsonKey(name: "fake-ip-filter")
List<String> get fakeIpFilter => throw _privateConstructorUsedError;
@JsonKey(name: "nameserver-policy")
Map<String, String> get nameserverPolicy =>
throw _privateConstructorUsedError;
List<String> get nameserver => throw _privateConstructorUsedError;
List<String> get fallback => throw _privateConstructorUsedError;
@JsonKey(name: "proxy-server-nameserver")
List<String> get proxyServerNameserver => throw _privateConstructorUsedError;
@JsonKey(name: "fallback-filter")
FallbackFilter get fallbackFilter => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$DnsCopyWith<Dns> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $DnsCopyWith<$Res> {
factory $DnsCopyWith(Dns value, $Res Function(Dns) then) =
_$DnsCopyWithImpl<$Res, Dns>;
@useResult
$Res call(
{bool enable,
@JsonKey(name: "prefer-h3") bool preferH3,
@JsonKey(name: "use-hosts") bool useHosts,
@JsonKey(name: "use-system-hosts") bool useSystemHosts,
@JsonKey(name: "respect-rules") bool respectRules,
bool ipv6,
@JsonKey(name: "default-nameserver") List<String> defaultNameserver,
@JsonKey(name: "enhanced-mode") DnsMode enhancedMode,
@JsonKey(name: "fake-ip-range") String fakeIpRange,
@JsonKey(name: "fake-ip-filter") List<String> fakeIpFilter,
@JsonKey(name: "nameserver-policy") Map<String, String> nameserverPolicy,
List<String> nameserver,
List<String> fallback,
@JsonKey(name: "proxy-server-nameserver")
List<String> proxyServerNameserver,
@JsonKey(name: "fallback-filter") FallbackFilter fallbackFilter});
$FallbackFilterCopyWith<$Res> get fallbackFilter;
}
/// @nodoc
class _$DnsCopyWithImpl<$Res, $Val extends Dns> implements $DnsCopyWith<$Res> {
_$DnsCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? enable = null,
Object? preferH3 = null,
Object? useHosts = null,
Object? useSystemHosts = null,
Object? respectRules = null,
Object? ipv6 = null,
Object? defaultNameserver = null,
Object? enhancedMode = null,
Object? fakeIpRange = null,
Object? fakeIpFilter = null,
Object? nameserverPolicy = null,
Object? nameserver = null,
Object? fallback = null,
Object? proxyServerNameserver = null,
Object? fallbackFilter = null,
}) {
return _then(_value.copyWith(
enable: null == enable
? _value.enable
: enable // ignore: cast_nullable_to_non_nullable
as bool,
preferH3: null == preferH3
? _value.preferH3
: preferH3 // ignore: cast_nullable_to_non_nullable
as bool,
useHosts: null == useHosts
? _value.useHosts
: useHosts // ignore: cast_nullable_to_non_nullable
as bool,
useSystemHosts: null == useSystemHosts
? _value.useSystemHosts
: useSystemHosts // ignore: cast_nullable_to_non_nullable
as bool,
respectRules: null == respectRules
? _value.respectRules
: respectRules // ignore: cast_nullable_to_non_nullable
as bool,
ipv6: null == ipv6
? _value.ipv6
: ipv6 // ignore: cast_nullable_to_non_nullable
as bool,
defaultNameserver: null == defaultNameserver
? _value.defaultNameserver
: defaultNameserver // ignore: cast_nullable_to_non_nullable
as List<String>,
enhancedMode: null == enhancedMode
? _value.enhancedMode
: enhancedMode // ignore: cast_nullable_to_non_nullable
as DnsMode,
fakeIpRange: null == fakeIpRange
? _value.fakeIpRange
: fakeIpRange // ignore: cast_nullable_to_non_nullable
as String,
fakeIpFilter: null == fakeIpFilter
? _value.fakeIpFilter
: fakeIpFilter // ignore: cast_nullable_to_non_nullable
as List<String>,
nameserverPolicy: null == nameserverPolicy
? _value.nameserverPolicy
: nameserverPolicy // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
nameserver: null == nameserver
? _value.nameserver
: nameserver // ignore: cast_nullable_to_non_nullable
as List<String>,
fallback: null == fallback
? _value.fallback
: fallback // ignore: cast_nullable_to_non_nullable
as List<String>,
proxyServerNameserver: null == proxyServerNameserver
? _value.proxyServerNameserver
: proxyServerNameserver // ignore: cast_nullable_to_non_nullable
as List<String>,
fallbackFilter: null == fallbackFilter
? _value.fallbackFilter
: fallbackFilter // ignore: cast_nullable_to_non_nullable
as FallbackFilter,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$FallbackFilterCopyWith<$Res> get fallbackFilter {
return $FallbackFilterCopyWith<$Res>(_value.fallbackFilter, (value) {
return _then(_value.copyWith(fallbackFilter: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$DnsImplCopyWith<$Res> implements $DnsCopyWith<$Res> {
factory _$$DnsImplCopyWith(_$DnsImpl value, $Res Function(_$DnsImpl) then) =
__$$DnsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{bool enable,
@JsonKey(name: "prefer-h3") bool preferH3,
@JsonKey(name: "use-hosts") bool useHosts,
@JsonKey(name: "use-system-hosts") bool useSystemHosts,
@JsonKey(name: "respect-rules") bool respectRules,
bool ipv6,
@JsonKey(name: "default-nameserver") List<String> defaultNameserver,
@JsonKey(name: "enhanced-mode") DnsMode enhancedMode,
@JsonKey(name: "fake-ip-range") String fakeIpRange,
@JsonKey(name: "fake-ip-filter") List<String> fakeIpFilter,
@JsonKey(name: "nameserver-policy") Map<String, String> nameserverPolicy,
List<String> nameserver,
List<String> fallback,
@JsonKey(name: "proxy-server-nameserver")
List<String> proxyServerNameserver,
@JsonKey(name: "fallback-filter") FallbackFilter fallbackFilter});
@override
$FallbackFilterCopyWith<$Res> get fallbackFilter;
}
/// @nodoc
class __$$DnsImplCopyWithImpl<$Res> extends _$DnsCopyWithImpl<$Res, _$DnsImpl>
implements _$$DnsImplCopyWith<$Res> {
__$$DnsImplCopyWithImpl(_$DnsImpl _value, $Res Function(_$DnsImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? enable = null,
Object? preferH3 = null,
Object? useHosts = null,
Object? useSystemHosts = null,
Object? respectRules = null,
Object? ipv6 = null,
Object? defaultNameserver = null,
Object? enhancedMode = null,
Object? fakeIpRange = null,
Object? fakeIpFilter = null,
Object? nameserverPolicy = null,
Object? nameserver = null,
Object? fallback = null,
Object? proxyServerNameserver = null,
Object? fallbackFilter = null,
}) {
return _then(_$DnsImpl(
enable: null == enable
? _value.enable
: enable // ignore: cast_nullable_to_non_nullable
as bool,
preferH3: null == preferH3
? _value.preferH3
: preferH3 // ignore: cast_nullable_to_non_nullable
as bool,
useHosts: null == useHosts
? _value.useHosts
: useHosts // ignore: cast_nullable_to_non_nullable
as bool,
useSystemHosts: null == useSystemHosts
? _value.useSystemHosts
: useSystemHosts // ignore: cast_nullable_to_non_nullable
as bool,
respectRules: null == respectRules
? _value.respectRules
: respectRules // ignore: cast_nullable_to_non_nullable
as bool,
ipv6: null == ipv6
? _value.ipv6
: ipv6 // ignore: cast_nullable_to_non_nullable
as bool,
defaultNameserver: null == defaultNameserver
? _value._defaultNameserver
: defaultNameserver // ignore: cast_nullable_to_non_nullable
as List<String>,
enhancedMode: null == enhancedMode
? _value.enhancedMode
: enhancedMode // ignore: cast_nullable_to_non_nullable
as DnsMode,
fakeIpRange: null == fakeIpRange
? _value.fakeIpRange
: fakeIpRange // ignore: cast_nullable_to_non_nullable
as String,
fakeIpFilter: null == fakeIpFilter
? _value._fakeIpFilter
: fakeIpFilter // ignore: cast_nullable_to_non_nullable
as List<String>,
nameserverPolicy: null == nameserverPolicy
? _value._nameserverPolicy
: nameserverPolicy // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
nameserver: null == nameserver
? _value._nameserver
: nameserver // ignore: cast_nullable_to_non_nullable
as List<String>,
fallback: null == fallback
? _value._fallback
: fallback // ignore: cast_nullable_to_non_nullable
as List<String>,
proxyServerNameserver: null == proxyServerNameserver
? _value._proxyServerNameserver
: proxyServerNameserver // ignore: cast_nullable_to_non_nullable
as List<String>,
fallbackFilter: null == fallbackFilter
? _value.fallbackFilter
: fallbackFilter // ignore: cast_nullable_to_non_nullable
as FallbackFilter,
));
}
}
/// @nodoc
@JsonSerializable()
class _$DnsImpl implements _Dns {
const _$DnsImpl(
{this.enable = true,
@JsonKey(name: "prefer-h3") this.preferH3 = false,
@JsonKey(name: "use-hosts") this.useHosts = true,
@JsonKey(name: "use-system-hosts") this.useSystemHosts = true,
@JsonKey(name: "respect-rules") this.respectRules = true,
this.ipv6 = false,
@JsonKey(name: "default-nameserver")
final List<String> defaultNameserver = const ["223.5.5.5"],
@JsonKey(name: "enhanced-mode") this.enhancedMode = DnsMode.fakeIp,
@JsonKey(name: "fake-ip-range") this.fakeIpRange = "198.18.0.1/16",
@JsonKey(name: "fake-ip-filter") final List<String> fakeIpFilter = const [
"*.lan",
"localhost.ptlogin2.qq.com"
],
@JsonKey(name: "nameserver-policy")
final Map<String, String> nameserverPolicy = const {
"www.baidu.com": "114.114.114.114",
"+.internal.crop.com": "10.0.0.1",
"geosite:cn": "https://doh.pub/dns-query"
},
final List<String> nameserver = const [
"https://doh.pub/dns-query",
"https://dns.alidns.com/dns-query"
],
final List<String> fallback = const ["tls://8.8.4.4", "tls://1.1.1.1"],
@JsonKey(name: "proxy-server-nameserver")
final List<String> proxyServerNameserver = const [
"https://doh.pub/dns-query"
],
@JsonKey(name: "fallback-filter")
this.fallbackFilter = const FallbackFilter()})
: _defaultNameserver = defaultNameserver,
_fakeIpFilter = fakeIpFilter,
_nameserverPolicy = nameserverPolicy,
_nameserver = nameserver,
_fallback = fallback,
_proxyServerNameserver = proxyServerNameserver;
factory _$DnsImpl.fromJson(Map<String, dynamic> json) =>
_$$DnsImplFromJson(json);
@override
@JsonKey()
final bool enable;
@override
@JsonKey(name: "prefer-h3")
final bool preferH3;
@override
@JsonKey(name: "use-hosts")
final bool useHosts;
@override
@JsonKey(name: "use-system-hosts")
final bool useSystemHosts;
@override
@JsonKey(name: "respect-rules")
final bool respectRules;
@override
@JsonKey()
final bool ipv6;
final List<String> _defaultNameserver;
@override
@JsonKey(name: "default-nameserver")
List<String> get defaultNameserver {
if (_defaultNameserver is EqualUnmodifiableListView)
return _defaultNameserver;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_defaultNameserver);
}
@override
@JsonKey(name: "enhanced-mode")
final DnsMode enhancedMode;
@override
@JsonKey(name: "fake-ip-range")
final String fakeIpRange;
final List<String> _fakeIpFilter;
@override
@JsonKey(name: "fake-ip-filter")
List<String> get fakeIpFilter {
if (_fakeIpFilter is EqualUnmodifiableListView) return _fakeIpFilter;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_fakeIpFilter);
}
final Map<String, String> _nameserverPolicy;
@override
@JsonKey(name: "nameserver-policy")
Map<String, String> get nameserverPolicy {
if (_nameserverPolicy is EqualUnmodifiableMapView) return _nameserverPolicy;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_nameserverPolicy);
}
final List<String> _nameserver;
@override
@JsonKey()
List<String> get nameserver {
if (_nameserver is EqualUnmodifiableListView) return _nameserver;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_nameserver);
}
final List<String> _fallback;
@override
@JsonKey()
List<String> get fallback {
if (_fallback is EqualUnmodifiableListView) return _fallback;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_fallback);
}
final List<String> _proxyServerNameserver;
@override
@JsonKey(name: "proxy-server-nameserver")
List<String> get proxyServerNameserver {
if (_proxyServerNameserver is EqualUnmodifiableListView)
return _proxyServerNameserver;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_proxyServerNameserver);
}
@override
@JsonKey(name: "fallback-filter")
final FallbackFilter fallbackFilter;
@override
String toString() {
return 'Dns(enable: $enable, preferH3: $preferH3, useHosts: $useHosts, useSystemHosts: $useSystemHosts, respectRules: $respectRules, ipv6: $ipv6, defaultNameserver: $defaultNameserver, enhancedMode: $enhancedMode, fakeIpRange: $fakeIpRange, fakeIpFilter: $fakeIpFilter, nameserverPolicy: $nameserverPolicy, nameserver: $nameserver, fallback: $fallback, proxyServerNameserver: $proxyServerNameserver, fallbackFilter: $fallbackFilter)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$DnsImpl &&
(identical(other.enable, enable) || other.enable == enable) &&
(identical(other.preferH3, preferH3) ||
other.preferH3 == preferH3) &&
(identical(other.useHosts, useHosts) ||
other.useHosts == useHosts) &&
(identical(other.useSystemHosts, useSystemHosts) ||
other.useSystemHosts == useSystemHosts) &&
(identical(other.respectRules, respectRules) ||
other.respectRules == respectRules) &&
(identical(other.ipv6, ipv6) || other.ipv6 == ipv6) &&
const DeepCollectionEquality()
.equals(other._defaultNameserver, _defaultNameserver) &&
(identical(other.enhancedMode, enhancedMode) ||
other.enhancedMode == enhancedMode) &&
(identical(other.fakeIpRange, fakeIpRange) ||
other.fakeIpRange == fakeIpRange) &&
const DeepCollectionEquality()
.equals(other._fakeIpFilter, _fakeIpFilter) &&
const DeepCollectionEquality()
.equals(other._nameserverPolicy, _nameserverPolicy) &&
const DeepCollectionEquality()
.equals(other._nameserver, _nameserver) &&
const DeepCollectionEquality().equals(other._fallback, _fallback) &&
const DeepCollectionEquality()
.equals(other._proxyServerNameserver, _proxyServerNameserver) &&
(identical(other.fallbackFilter, fallbackFilter) ||
other.fallbackFilter == fallbackFilter));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
enable,
preferH3,
useHosts,
useSystemHosts,
respectRules,
ipv6,
const DeepCollectionEquality().hash(_defaultNameserver),
enhancedMode,
fakeIpRange,
const DeepCollectionEquality().hash(_fakeIpFilter),
const DeepCollectionEquality().hash(_nameserverPolicy),
const DeepCollectionEquality().hash(_nameserver),
const DeepCollectionEquality().hash(_fallback),
const DeepCollectionEquality().hash(_proxyServerNameserver),
fallbackFilter);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$DnsImplCopyWith<_$DnsImpl> get copyWith =>
__$$DnsImplCopyWithImpl<_$DnsImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$DnsImplToJson(
this,
);
}
}
abstract class _Dns implements Dns {
const factory _Dns(
{final bool enable,
@JsonKey(name: "prefer-h3") final bool preferH3,
@JsonKey(name: "use-hosts") final bool useHosts,
@JsonKey(name: "use-system-hosts") final bool useSystemHosts,
@JsonKey(name: "respect-rules") final bool respectRules,
final bool ipv6,
@JsonKey(name: "default-nameserver") final List<String> defaultNameserver,
@JsonKey(name: "enhanced-mode") final DnsMode enhancedMode,
@JsonKey(name: "fake-ip-range") final String fakeIpRange,
@JsonKey(name: "fake-ip-filter") final List<String> fakeIpFilter,
@JsonKey(name: "nameserver-policy")
final Map<String, String> nameserverPolicy,
final List<String> nameserver,
final List<String> fallback,
@JsonKey(name: "proxy-server-nameserver")
final List<String> proxyServerNameserver,
@JsonKey(name: "fallback-filter")
final FallbackFilter fallbackFilter}) = _$DnsImpl;
factory _Dns.fromJson(Map<String, dynamic> json) = _$DnsImpl.fromJson;
@override
bool get enable;
@override
@JsonKey(name: "prefer-h3")
bool get preferH3;
@override
@JsonKey(name: "use-hosts")
bool get useHosts;
@override
@JsonKey(name: "use-system-hosts")
bool get useSystemHosts;
@override
@JsonKey(name: "respect-rules")
bool get respectRules;
@override
bool get ipv6;
@override
@JsonKey(name: "default-nameserver")
List<String> get defaultNameserver;
@override
@JsonKey(name: "enhanced-mode")
DnsMode get enhancedMode;
@override
@JsonKey(name: "fake-ip-range")
String get fakeIpRange;
@override
@JsonKey(name: "fake-ip-filter")
List<String> get fakeIpFilter;
@override
@JsonKey(name: "nameserver-policy")
Map<String, String> get nameserverPolicy;
@override
List<String> get nameserver;
@override
List<String> get fallback;
@override
@JsonKey(name: "proxy-server-nameserver")
List<String> get proxyServerNameserver;
@override
@JsonKey(name: "fallback-filter")
FallbackFilter get fallbackFilter;
@override
@JsonKey(ignore: true)
_$$DnsImplCopyWith<_$DnsImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -6,35 +6,6 @@ part of '../clash_config.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
Dns _$DnsFromJson(Map<String, dynamic> json) => Dns()
..enable = json['enable'] as bool
..ipv6 = json['ipv6'] as bool
..defaultNameserver = (json['default-nameserver'] as List<dynamic>)
.map((e) => e as String)
.toList()
..enhancedMode = json['enhanced-mode'] as String
..fakeIpRange = json['fake-ip-range'] as String
..useHosts = json['use-hosts'] as bool
..nameserver =
(json['nameserver'] as List<dynamic>).map((e) => e as String).toList()
..fallback =
(json['fallback'] as List<dynamic>).map((e) => e as String).toList()
..fakeIpFilter = (json['fake-ip-filter'] as List<dynamic>)
.map((e) => e as String)
.toList();
Map<String, dynamic> _$DnsToJson(Dns instance) => <String, dynamic>{
'enable': instance.enable,
'ipv6': instance.ipv6,
'default-nameserver': instance.defaultNameserver,
'enhanced-mode': instance.enhancedMode,
'fake-ip-range': instance.fakeIpRange,
'use-hosts': instance.useHosts,
'nameserver': instance.nameserver,
'fallback': instance.fallback,
'fake-ip-filter': instance.fakeIpFilter,
};
ClashConfig _$ClashConfigFromJson(Map<String, dynamic> json) => ClashConfig() ClashConfig _$ClashConfigFromJson(Map<String, dynamic> json) => ClashConfig()
..mixedPort = (json['mixed-port'] as num?)?.toInt() ?? 7890 ..mixedPort = (json['mixed-port'] as num?)?.toInt() ?? 7890
..mode = $enumDecodeNullable(_$ModeEnumMap, json['mode']) ?? Mode.rule ..mode = $enumDecodeNullable(_$ModeEnumMap, json['mode']) ?? Mode.rule
@@ -45,12 +16,13 @@ ClashConfig _$ClashConfigFromJson(Map<String, dynamic> json) => ClashConfig()
..logLevel = ..logLevel =
$enumDecodeNullable(_$LogLevelEnumMap, json['log-level']) ?? LogLevel.info $enumDecodeNullable(_$LogLevelEnumMap, json['log-level']) ?? LogLevel.info
..externalController = json['external-controller'] as String? ?? '' ..externalController = json['external-controller'] as String? ?? ''
..keepAliveInterval = (json['keep-alive-interval'] as num?)?.toInt() ?? 30
..ipv6 = json['ipv6'] as bool? ?? false ..ipv6 = json['ipv6'] as bool? ?? false
..geodataLoader = json['geodata-loader'] as String? ?? 'memconservative' ..geodataLoader = json['geodata-loader'] as String? ?? 'memconservative'
..unifiedDelay = json['unified-delay'] as bool? ?? false ..unifiedDelay = json['unified-delay'] as bool? ?? false
..tcpConcurrent = json['tcp-concurrent'] as bool? ?? false ..tcpConcurrent = json['tcp-concurrent'] as bool? ?? false
..tun = Tun.fromJson(json['tun'] as Map<String, dynamic>) ..tun = Tun.fromJson(json['tun'] as Map<String, dynamic>)
..dns = Dns.fromJson(json['dns'] as Map<String, dynamic>) ..dns = Dns.safeDnsFromJson(json['dns'] as Map<String, Object?>)
..rules = (json['rules'] as List<dynamic>).map((e) => e as String).toList() ..rules = (json['rules'] as List<dynamic>).map((e) => e as String).toList()
..globalRealUa = json['global-real-ua'] as String? ..globalRealUa = json['global-real-ua'] as String?
..geoXUrl = (json['geox-url'] as Map<String, dynamic>?)?.map( ..geoXUrl = (json['geox-url'] as Map<String, dynamic>?)?.map(
@@ -65,7 +37,11 @@ ClashConfig _$ClashConfigFromJson(Map<String, dynamic> json) => ClashConfig()
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoIP.dat', 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoIP.dat',
'geosite': 'geosite':
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat' 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat'
}; }
..hosts = (json['hosts'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
) ??
{};
Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) => Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) =>
<String, dynamic>{ <String, dynamic>{
@@ -75,6 +51,7 @@ Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) =>
'allow-lan': instance.allowLan, 'allow-lan': instance.allowLan,
'log-level': _$LogLevelEnumMap[instance.logLevel]!, 'log-level': _$LogLevelEnumMap[instance.logLevel]!,
'external-controller': instance.externalController, 'external-controller': instance.externalController,
'keep-alive-interval': instance.keepAliveInterval,
'ipv6': instance.ipv6, 'ipv6': instance.ipv6,
'geodata-loader': instance.geodataLoader, 'geodata-loader': instance.geodataLoader,
'unified-delay': instance.unifiedDelay, 'unified-delay': instance.unifiedDelay,
@@ -85,6 +62,7 @@ Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) =>
'global-ua': instance.globalUa, 'global-ua': instance.globalUa,
'global-real-ua': instance.globalRealUa, 'global-real-ua': instance.globalRealUa,
'geox-url': instance.geoXUrl, 'geox-url': instance.geoXUrl,
'hosts': instance.hosts,
}; };
const _$ModeEnumMap = { const _$ModeEnumMap = {
@@ -129,3 +107,105 @@ const _$TunStackEnumMap = {
TunStack.system: 'system', TunStack.system: 'system',
TunStack.mixed: 'mixed', TunStack.mixed: 'mixed',
}; };
_$FallbackFilterImpl _$$FallbackFilterImplFromJson(Map<String, dynamic> json) =>
_$FallbackFilterImpl(
geoip: json['geoip'] as bool? ?? true,
geoipCode: json['geoip-code'] as String? ?? "CN",
geosite: (json['geosite'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const ["gfw"],
ipcidr: (json['ipcidr'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const ["240.0.0.0/4"],
domain: (json['domain'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const ["+.google.com", "+.facebook.com", "+.youtube.com"],
);
Map<String, dynamic> _$$FallbackFilterImplToJson(
_$FallbackFilterImpl instance) =>
<String, dynamic>{
'geoip': instance.geoip,
'geoip-code': instance.geoipCode,
'geosite': instance.geosite,
'ipcidr': instance.ipcidr,
'domain': instance.domain,
};
_$DnsImpl _$$DnsImplFromJson(Map<String, dynamic> json) => _$DnsImpl(
enable: json['enable'] as bool? ?? true,
preferH3: json['prefer-h3'] as bool? ?? false,
useHosts: json['use-hosts'] as bool? ?? true,
useSystemHosts: json['use-system-hosts'] as bool? ?? true,
respectRules: json['respect-rules'] as bool? ?? true,
ipv6: json['ipv6'] as bool? ?? false,
defaultNameserver: (json['default-nameserver'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const ["223.5.5.5"],
enhancedMode:
$enumDecodeNullable(_$DnsModeEnumMap, json['enhanced-mode']) ??
DnsMode.fakeIp,
fakeIpRange: json['fake-ip-range'] as String? ?? "198.18.0.1/16",
fakeIpFilter: (json['fake-ip-filter'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const ["*.lan", "localhost.ptlogin2.qq.com"],
nameserverPolicy:
(json['nameserver-policy'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
) ??
const {
"www.baidu.com": "114.114.114.114",
"+.internal.crop.com": "10.0.0.1",
"geosite:cn": "https://doh.pub/dns-query"
},
nameserver: (json['nameserver'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [
"https://doh.pub/dns-query",
"https://dns.alidns.com/dns-query"
],
fallback: (json['fallback'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const ["tls://8.8.4.4", "tls://1.1.1.1"],
proxyServerNameserver: (json['proxy-server-nameserver'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const ["https://doh.pub/dns-query"],
fallbackFilter: json['fallback-filter'] == null
? const FallbackFilter()
: FallbackFilter.fromJson(
json['fallback-filter'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$DnsImplToJson(_$DnsImpl instance) => <String, dynamic>{
'enable': instance.enable,
'prefer-h3': instance.preferH3,
'use-hosts': instance.useHosts,
'use-system-hosts': instance.useSystemHosts,
'respect-rules': instance.respectRules,
'ipv6': instance.ipv6,
'default-nameserver': instance.defaultNameserver,
'enhanced-mode': _$DnsModeEnumMap[instance.enhancedMode]!,
'fake-ip-range': instance.fakeIpRange,
'fake-ip-filter': instance.fakeIpFilter,
'nameserver-policy': instance.nameserverPolicy,
'nameserver': instance.nameserver,
'fallback': instance.fallback,
'proxy-server-nameserver': instance.proxyServerNameserver,
'fallback-filter': instance.fallbackFilter,
};
const _$DnsModeEnumMap = {
DnsMode.normal: 'normal',
DnsMode.fakeIp: 'fake-ip',
DnsMode.redirHost: 'redir-host',
DnsMode.hosts: 'hosts',
};

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