Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6266b7917 | ||
|
|
6c27f2e2f1 | ||
|
|
e04a0094b1 | ||
|
|
683e6a58ea | ||
|
|
b340feeb49 | ||
|
|
6a39b7ef5a | ||
|
|
35f89fea90 | ||
|
|
58acd9c1ab | ||
|
|
ef97ef40a1 | ||
|
|
9cb75f4814 |
4
.github/workflows/build.yaml
vendored
4
.github/workflows/build.yaml
vendored
@@ -67,8 +67,8 @@ jobs:
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.x'
|
||||
channel: 'stable'
|
||||
flutter-version: 3.24.5
|
||||
channel: stable
|
||||
cache: true
|
||||
|
||||
- name: Get Flutter Dependency
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,9 +5,11 @@
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
|
||||
52
CHANGELOG.md
52
CHANGELOG.md
@@ -1,3 +1,55 @@
|
||||
## v0.8.74
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.73
|
||||
|
||||
- Update popup menu
|
||||
|
||||
- Add file editor
|
||||
|
||||
- Fix android service issues
|
||||
|
||||
- Optimize desktop background performance
|
||||
|
||||
- Optimize android main process performance
|
||||
|
||||
- Optimize delay test
|
||||
|
||||
- Optimize vpn protect
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.72
|
||||
|
||||
- Update core
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.71
|
||||
|
||||
- Remake dashboard
|
||||
|
||||
- Optimize theme
|
||||
|
||||
- Optimize more details
|
||||
|
||||
- Update flutter version
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.70
|
||||
|
||||
- Support better window position memory
|
||||
|
||||
- Add windows arm64 and linux arm64 build script
|
||||
|
||||
- Optimize some details
|
||||
|
||||
## v0.8.69
|
||||
|
||||
- Remake desktop
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<application
|
||||
android:name="${applicationName}"
|
||||
android:name=".FlClashApplication"
|
||||
android:hardwareAccelerated="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="FlClash">
|
||||
@@ -64,9 +64,7 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- <meta-data-->
|
||||
<!-- android:name="io.flutter.embedding.android.EnableImpeller"-->
|
||||
<!-- android:value="true" />-->
|
||||
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false" />
|
||||
|
||||
<activity
|
||||
android:name=".TempActivity"
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.follow.clash;
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context;
|
||||
|
||||
class FlClashApplication : Application() {
|
||||
companion object {
|
||||
private lateinit var instance: FlClashApplication
|
||||
fun getAppContext(): Context {
|
||||
return instance.applicationContext
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ object GlobalState {
|
||||
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
|
||||
}
|
||||
|
||||
fun getText(text: String): String {
|
||||
suspend fun getText(text: String): String {
|
||||
return getCurrentAppPlugin()?.getText(text) ?: ""
|
||||
}
|
||||
|
||||
@@ -44,14 +44,14 @@ object GlobalState {
|
||||
return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
|
||||
}
|
||||
|
||||
fun handleToggle(context: Context) {
|
||||
val starting = handleStart(context)
|
||||
fun handleToggle() {
|
||||
val starting = handleStart()
|
||||
if (!starting) {
|
||||
handleStop()
|
||||
}
|
||||
}
|
||||
|
||||
fun handleStart(context: Context): Boolean {
|
||||
fun handleStart(): Boolean {
|
||||
if (runState.value == RunState.STOP) {
|
||||
runState.value = RunState.PENDING
|
||||
runLock.lock()
|
||||
@@ -59,7 +59,7 @@ object GlobalState {
|
||||
if (tilePlugin != null) {
|
||||
tilePlugin.handleStart()
|
||||
} else {
|
||||
initServiceEngine(context)
|
||||
initServiceEngine()
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -74,6 +74,12 @@ object GlobalState {
|
||||
}
|
||||
}
|
||||
|
||||
fun handleTryDestroy() {
|
||||
if (flutterEngine == null) {
|
||||
destroyServiceEngine()
|
||||
}
|
||||
}
|
||||
|
||||
fun destroyServiceEngine() {
|
||||
runLock.withLock {
|
||||
serviceEngine?.destroy()
|
||||
@@ -81,21 +87,21 @@ object GlobalState {
|
||||
}
|
||||
}
|
||||
|
||||
fun initServiceEngine(context: Context) {
|
||||
fun initServiceEngine() {
|
||||
if (serviceEngine != null) return
|
||||
destroyServiceEngine()
|
||||
runLock.withLock {
|
||||
serviceEngine = FlutterEngine(context)
|
||||
serviceEngine?.plugins?.add(VpnPlugin())
|
||||
serviceEngine = FlutterEngine(FlClashApplication.getAppContext())
|
||||
serviceEngine?.plugins?.add(VpnPlugin)
|
||||
serviceEngine?.plugins?.add(AppPlugin())
|
||||
serviceEngine?.plugins?.add(TilePlugin())
|
||||
serviceEngine?.plugins?.add(ServicePlugin())
|
||||
val vpnService = DartExecutor.DartEntrypoint(
|
||||
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
|
||||
"vpnService"
|
||||
"_service"
|
||||
)
|
||||
serviceEngine?.dartExecutor?.executeDartEntrypoint(
|
||||
vpnService,
|
||||
if (flutterEngine == null) listOf("quick") else null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package com.follow.clash
|
||||
|
||||
|
||||
import com.follow.clash.plugins.AppPlugin
|
||||
import com.follow.clash.plugins.ServicePlugin
|
||||
import com.follow.clash.plugins.TilePlugin
|
||||
import com.follow.clash.plugins.VpnPlugin
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
|
||||
@@ -12,8 +10,7 @@ class MainActivity : FlutterActivity() {
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
flutterEngine.plugins.add(AppPlugin())
|
||||
flutterEngine.plugins.add(VpnPlugin())
|
||||
flutterEngine.plugins.add(ServicePlugin())
|
||||
flutterEngine.plugins.add(ServicePlugin)
|
||||
flutterEngine.plugins.add(TilePlugin())
|
||||
GlobalState.flutterEngine = flutterEngine
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ class TempActivity : Activity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
when (intent.action) {
|
||||
wrapAction("START") -> {
|
||||
GlobalState.handleStart(applicationContext)
|
||||
GlobalState.handleStart()
|
||||
}
|
||||
|
||||
wrapAction("STOP") -> {
|
||||
@@ -17,7 +17,7 @@ class TempActivity : Activity() {
|
||||
}
|
||||
|
||||
wrapAction("CHANGE") -> {
|
||||
GlobalState.handleToggle(applicationContext)
|
||||
GlobalState.handleToggle()
|
||||
}
|
||||
}
|
||||
finishAndRemoveTask()
|
||||
|
||||
@@ -97,7 +97,6 @@ fun String.toCIDR(): CIDR {
|
||||
return CIDR(address, prefixLength)
|
||||
}
|
||||
|
||||
|
||||
fun ConnectivityManager.resolveDns(network: Network?): List<String> {
|
||||
val properties = getLinkProperties(network) ?: return listOf()
|
||||
return properties.dnsServers.map { it.asSocketAddressText(53) }
|
||||
@@ -143,7 +142,6 @@ fun Context.getActionPendingIntent(action: String): PendingIntent {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun numericToTextFormat(src: ByteArray): String {
|
||||
val sb = StringBuilder(39)
|
||||
for (i in 0 until 8) {
|
||||
|
||||
@@ -4,5 +4,5 @@ data class Package(
|
||||
val packageName: String,
|
||||
val label: String,
|
||||
val isSystem: Boolean,
|
||||
val firstInstallTime: Long,
|
||||
val lastUpdateTime: Long,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.follow.clash.models
|
||||
|
||||
data class Process(
|
||||
val id: Int,
|
||||
val id: String,
|
||||
val metadata: Metadata,
|
||||
)
|
||||
|
||||
|
||||
@@ -25,4 +25,9 @@ data class VpnOptions(
|
||||
val ipv4Address: String,
|
||||
val ipv6Address: String,
|
||||
val dnsServerAddress: String,
|
||||
)
|
||||
|
||||
data class StartForegroundParams(
|
||||
val title: String,
|
||||
val content: String,
|
||||
)
|
||||
@@ -3,7 +3,6 @@ package com.follow.clash.plugins
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.ComponentInfo
|
||||
@@ -19,6 +18,7 @@ import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
|
||||
import com.follow.clash.FlClashApplication
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.R
|
||||
import com.follow.clash.extensions.awaitResult
|
||||
@@ -37,16 +37,14 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
|
||||
|
||||
private var activity: Activity? = null
|
||||
|
||||
private lateinit var context: Context
|
||||
private var activityRef: WeakReference<Activity>? = null
|
||||
|
||||
private lateinit var channel: MethodChannel
|
||||
|
||||
@@ -121,21 +119,27 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
scope = CoroutineScope(Dispatchers.Default)
|
||||
context = flutterPluginBinding.applicationContext
|
||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
|
||||
channel.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
private fun initShortcuts(label: String) {
|
||||
val shortcut = ShortcutInfoCompat.Builder(context, "toggle")
|
||||
val shortcut = ShortcutInfoCompat.Builder(FlClashApplication.getAppContext(), "toggle")
|
||||
.setShortLabel(label)
|
||||
.setIcon(IconCompat.createWithResource(context, R.mipmap.ic_launcher_round))
|
||||
.setIntent(context.getActionIntent("CHANGE"))
|
||||
.setIcon(
|
||||
IconCompat.createWithResource(
|
||||
FlClashApplication.getAppContext(),
|
||||
R.mipmap.ic_launcher_round
|
||||
)
|
||||
)
|
||||
.setIntent(FlClashApplication.getAppContext().getActionIntent("CHANGE"))
|
||||
.build()
|
||||
ShortcutManagerCompat.setDynamicShortcuts(context, listOf(shortcut))
|
||||
ShortcutManagerCompat.setDynamicShortcuts(
|
||||
FlClashApplication.getAppContext(),
|
||||
listOf(shortcut)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel.setMethodCallHandler(null)
|
||||
scope.cancel()
|
||||
@@ -143,14 +147,14 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
|
||||
private fun tip(message: String?) {
|
||||
if (GlobalState.flutterEngine == null) {
|
||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(FlClashApplication.getAppContext(), message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: Result) {
|
||||
when (call.method) {
|
||||
"moveTaskToBack" -> {
|
||||
activity?.moveTaskToBack(true)
|
||||
activityRef?.get()?.moveTaskToBack(true)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
@@ -192,7 +196,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
if (iconMap["default"] == null) {
|
||||
iconMap["default"] =
|
||||
context.packageManager?.defaultActivityIcon?.getBase64()
|
||||
FlClashApplication.getAppContext().packageManager?.defaultActivityIcon?.getBase64()
|
||||
}
|
||||
result.success(iconMap["default"])
|
||||
return@launch
|
||||
@@ -221,8 +225,8 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
private fun openFile(path: String) {
|
||||
val file = File(path)
|
||||
val uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileProvider",
|
||||
FlClashApplication.getAppContext(),
|
||||
"${FlClashApplication.getAppContext().packageName}.fileProvider",
|
||||
file
|
||||
)
|
||||
|
||||
@@ -234,13 +238,13 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
val flags =
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
|
||||
val resInfoList = context.packageManager.queryIntentActivities(
|
||||
val resInfoList = FlClashApplication.getAppContext().packageManager.queryIntentActivities(
|
||||
intent, PackageManager.MATCH_DEFAULT_ONLY
|
||||
)
|
||||
|
||||
for (resolveInfo in resInfoList) {
|
||||
val packageName = resolveInfo.activityInfo.packageName
|
||||
context.grantUriPermission(
|
||||
FlClashApplication.getAppContext().grantUriPermission(
|
||||
packageName,
|
||||
uri,
|
||||
flags
|
||||
@@ -248,19 +252,19 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
|
||||
try {
|
||||
activity?.startActivity(intent)
|
||||
activityRef?.get()?.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
println(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateExcludeFromRecents(value: Boolean?) {
|
||||
val am = getSystemService(context, ActivityManager::class.java)
|
||||
val am = getSystemService(FlClashApplication.getAppContext(), ActivityManager::class.java)
|
||||
val task = am?.appTasks?.firstOrNull {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
it.taskInfo.taskId == activity?.taskId
|
||||
it.taskInfo.taskId == activityRef?.get()?.taskId
|
||||
} else {
|
||||
it.taskInfo.id == activity?.taskId
|
||||
it.taskInfo.id == activityRef?.get()?.taskId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,7 +276,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
|
||||
private suspend fun getPackageIcon(packageName: String): String? {
|
||||
val packageManager = context.packageManager
|
||||
val packageManager = FlClashApplication.getAppContext().packageManager
|
||||
if (iconMap[packageName] == null) {
|
||||
iconMap[packageName] = try {
|
||||
packageManager?.getApplicationIcon(packageName)?.getBase64()
|
||||
@@ -285,10 +289,10 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
|
||||
private fun getPackages(): List<Package> {
|
||||
val packageManager = context.packageManager
|
||||
val packageManager = FlClashApplication.getAppContext().packageManager
|
||||
if (packages.isNotEmpty()) return packages
|
||||
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
|
||||
it.packageName != context.packageName
|
||||
it.packageName != FlClashApplication.getAppContext().packageName
|
||||
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||
|| it.packageName == "android"
|
||||
|
||||
@@ -297,7 +301,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
packageName = it.packageName,
|
||||
label = it.applicationInfo.loadLabel(packageManager).toString(),
|
||||
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1,
|
||||
firstInstallTime = it.firstInstallTime
|
||||
lastUpdateTime = it.lastUpdateTime
|
||||
)
|
||||
}?.let { packages.addAll(it) }
|
||||
return packages
|
||||
@@ -317,43 +321,45 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
}
|
||||
|
||||
fun requestVpnPermission(context: Context, callBack: () -> Unit) {
|
||||
fun requestVpnPermission(callBack: () -> Unit) {
|
||||
vpnCallBack = callBack
|
||||
val intent = VpnService.prepare(context)
|
||||
val intent = VpnService.prepare(FlClashApplication.getAppContext())
|
||||
if (intent != null) {
|
||||
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
|
||||
activityRef?.get()?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
|
||||
return
|
||||
}
|
||||
vpnCallBack?.invoke()
|
||||
}
|
||||
|
||||
fun requestNotificationsPermission(context: Context) {
|
||||
fun requestNotificationsPermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val permission = ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
FlClashApplication.getAppContext(),
|
||||
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
|
||||
if (activityRef?.get() == null) return
|
||||
activityRef?.get()?.let {
|
||||
ActivityCompat.requestPermissions(
|
||||
it,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
NOTIFICATION_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getText(text: String): String? {
|
||||
return runBlocking {
|
||||
suspend fun getText(text: String): String? {
|
||||
return withContext(Dispatchers.Default){
|
||||
channel.awaitResult<String>("getText", text)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isChinaPackage(packageName: String): Boolean {
|
||||
val packageManager = context.packageManager ?: return false
|
||||
val packageManager = FlClashApplication.getAppContext().packageManager ?: return false
|
||||
skipPrefixList.forEach {
|
||||
if (packageName == it || packageName.startsWith("$it.")) return false
|
||||
}
|
||||
@@ -373,7 +379,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION") packageManager.getPackageInfo(
|
||||
packageManager.getPackageInfo(
|
||||
packageName, packageManagerFlags
|
||||
)
|
||||
}
|
||||
@@ -420,28 +426,28 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
activityRef = WeakReference(binding.activity)
|
||||
binding.addActivityResultListener(::onActivityResult)
|
||||
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
activity = null
|
||||
activityRef = null
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
activityRef = WeakReference(binding.activity)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
channel.invokeMethod("exit", null)
|
||||
activity = null
|
||||
activityRef = 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)
|
||||
GlobalState.initServiceEngine()
|
||||
vpnCallBack?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import android.content.Context
|
||||
import com.follow.clash.FlClashApplication
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import com.google.gson.Gson
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
|
||||
class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
data object 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)
|
||||
}
|
||||
@@ -24,9 +23,22 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
|
||||
"startVpn" -> {
|
||||
val data = call.argument<String>("data")
|
||||
val options = Gson().fromJson(data, VpnOptions::class.java)
|
||||
GlobalState.getCurrentVPNPlugin()?.handleStart(options)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"stopVpn" -> {
|
||||
GlobalState.getCurrentVPNPlugin()?.handleStop()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"init" -> {
|
||||
GlobalState.getCurrentAppPlugin()?.requestNotificationsPermission(context)
|
||||
GlobalState.initServiceEngine(context)
|
||||
GlobalState.getCurrentAppPlugin()
|
||||
?.requestNotificationsPermission()
|
||||
GlobalState.initServiceEngine()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
@@ -41,7 +53,7 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
private fun handleDestroy() {
|
||||
GlobalState.getCurrentVPNPlugin()?.stop()
|
||||
GlobalState.getCurrentVPNPlugin()?.handleStop()
|
||||
GlobalState.destroyServiceEngine()
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class TilePlugin(private val onStart: (() -> Unit)? = null, private val onStop: (() -> Unit)? = null) : FlutterPlugin,
|
||||
MethodChannel.MethodCallHandler {
|
||||
class TilePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
|
||||
private lateinit var channel: MethodChannel
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "tile")
|
||||
channel.setMethodCallHandler(this)
|
||||
@@ -20,13 +19,11 @@ class TilePlugin(private val onStart: (() -> Unit)? = null, private val onStop:
|
||||
}
|
||||
|
||||
fun handleStart() {
|
||||
onStart?.let { it() }
|
||||
channel.invokeMethod("start", null)
|
||||
}
|
||||
|
||||
fun handleStop() {
|
||||
channel.invokeMethod("stop", null)
|
||||
onStop?.let { it() }
|
||||
}
|
||||
|
||||
private fun handleDetached() {
|
||||
|
||||
@@ -11,13 +11,16 @@ import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.content.getSystemService
|
||||
import com.follow.clash.BaseServiceInterface
|
||||
import com.follow.clash.FlClashApplication
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.RunState
|
||||
import com.follow.clash.extensions.awaitResult
|
||||
import com.follow.clash.extensions.getProtocol
|
||||
import com.follow.clash.extensions.resolveDns
|
||||
import com.follow.clash.models.Process
|
||||
import com.follow.clash.models.StartForegroundParams
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import com.follow.clash.services.BaseServiceInterface
|
||||
import com.follow.clash.services.FlClashService
|
||||
import com.follow.clash.services.FlClashVpnService
|
||||
import com.google.gson.Gson
|
||||
@@ -26,21 +29,24 @@ import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.InetSocketAddress
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
|
||||
class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
private lateinit var flutterMethodChannel: MethodChannel
|
||||
private lateinit var context: Context
|
||||
private var flClashService: BaseServiceInterface? = null
|
||||
private lateinit var options: VpnOptions
|
||||
private lateinit var scope: CoroutineScope
|
||||
private var lastStartForegroundParams: StartForegroundParams? = null
|
||||
private var timerJob: Job? = null
|
||||
|
||||
private val connectivity by lazy {
|
||||
context.getSystemService<ConnectivityManager>()
|
||||
FlClashApplication.getAppContext().getSystemService<ConnectivityManager>()
|
||||
}
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
@@ -50,7 +56,7 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
is FlClashService.LocalBinder -> service.getService()
|
||||
else -> throw Exception("invalid binder")
|
||||
}
|
||||
start()
|
||||
handleStartService()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(arg: ComponentName) {
|
||||
@@ -60,7 +66,6 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
scope = CoroutineScope(Dispatchers.Default)
|
||||
context = flutterPluginBinding.applicationContext
|
||||
scope.launch {
|
||||
registerNetworkCallback()
|
||||
}
|
||||
@@ -77,16 +82,11 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
when (call.method) {
|
||||
"start" -> {
|
||||
val data = call.argument<String>("data")
|
||||
options = Gson().fromJson(data, VpnOptions::class.java)
|
||||
when (options.enable) {
|
||||
true -> handleStartVpn()
|
||||
false -> start()
|
||||
}
|
||||
result.success(true)
|
||||
result.success(handleStart(Gson().fromJson(data, VpnOptions::class.java)))
|
||||
}
|
||||
|
||||
"stop" -> {
|
||||
stop()
|
||||
handleStop()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
@@ -102,13 +102,6 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
}
|
||||
|
||||
"startForeground" -> {
|
||||
val title = call.argument<String>("title") as String
|
||||
val content = call.argument<String>("content") as String
|
||||
startForeground(title, content)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"resolverProcess" -> {
|
||||
val data = call.argument<String>("data")
|
||||
val process = if (data != null) Gson().fromJson(
|
||||
@@ -144,7 +137,8 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
result.success(null)
|
||||
return@withContext
|
||||
}
|
||||
val packages = context.packageManager?.getPackagesForUid(uid)
|
||||
val packages =
|
||||
FlClashApplication.getAppContext().packageManager?.getPackagesForUid(uid)
|
||||
result.success(packages?.first())
|
||||
}
|
||||
}
|
||||
@@ -156,10 +150,20 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleStartVpn() {
|
||||
GlobalState.getCurrentAppPlugin()?.requestVpnPermission(context) {
|
||||
start()
|
||||
fun handleStart(options: VpnOptions): Boolean {
|
||||
this.options = options
|
||||
when (options.enable) {
|
||||
true -> handleStartVpn()
|
||||
false -> handleStartService()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun handleStartVpn() {
|
||||
GlobalState.getCurrentAppPlugin()
|
||||
?.requestVpnPermission {
|
||||
handleStartService()
|
||||
}
|
||||
}
|
||||
|
||||
fun requestGc() {
|
||||
@@ -177,16 +181,6 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
flutterMethodChannel.invokeMethod("dnsChanged", dns)
|
||||
}
|
||||
}
|
||||
// if (flClashService is FlClashVpnService) {
|
||||
// val network = networks.maxByOrNull { net ->
|
||||
// connectivity?.getNetworkCapabilities(net)?.let { cap ->
|
||||
// TRANSPORT_PRIORITY.indexOfFirst { cap.hasTransport(it) }
|
||||
// } ?: -1
|
||||
// }
|
||||
// network?.let {
|
||||
// (flClashService as FlClashVpnService).updateUnderlyingNetworks(arrayOf(network))
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private val callback = object : ConnectivityManager.NetworkCallback() {
|
||||
@@ -218,14 +212,41 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
onUpdateNetwork()
|
||||
}
|
||||
|
||||
private fun startForeground(title: String, content: String) {
|
||||
GlobalState.runLock.withLock {
|
||||
private suspend fun startForeground() {
|
||||
GlobalState.runLock.lock()
|
||||
try {
|
||||
if (GlobalState.runState.value != RunState.START) return
|
||||
flClashService?.startForeground(title, content)
|
||||
val data = flutterMethodChannel.awaitResult<String>("getStartForegroundParams")
|
||||
val startForegroundParams = Gson().fromJson(
|
||||
data, StartForegroundParams::class.java
|
||||
)
|
||||
if (lastStartForegroundParams != startForegroundParams) {
|
||||
lastStartForegroundParams = startForegroundParams
|
||||
flClashService?.startForeground(
|
||||
startForegroundParams.title,
|
||||
startForegroundParams.content,
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
GlobalState.runLock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
private fun start() {
|
||||
private fun startForegroundJob() {
|
||||
timerJob = CoroutineScope(Dispatchers.Main).launch {
|
||||
while (isActive) {
|
||||
startForeground()
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopForegroundJob() {
|
||||
timerJob?.cancel()
|
||||
timerJob = null
|
||||
}
|
||||
|
||||
private fun handleStartService() {
|
||||
if (flClashService == null) {
|
||||
bindService()
|
||||
return
|
||||
@@ -237,24 +258,25 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
flutterMethodChannel.invokeMethod(
|
||||
"started", fd
|
||||
)
|
||||
startForegroundJob();
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
fun handleStop() {
|
||||
GlobalState.runLock.withLock {
|
||||
if (GlobalState.runState.value == RunState.STOP) return
|
||||
GlobalState.runState.value = RunState.STOP
|
||||
stopForegroundJob()
|
||||
flClashService?.stop()
|
||||
GlobalState.handleTryDestroy()
|
||||
}
|
||||
GlobalState.destroyServiceEngine()
|
||||
}
|
||||
|
||||
private fun bindService() {
|
||||
val intent = when (options.enable) {
|
||||
true -> Intent(context, FlClashVpnService::class.java)
|
||||
false -> Intent(context, FlClashService::class.java)
|
||||
true -> Intent(FlClashApplication.getAppContext(), FlClashVpnService::class.java)
|
||||
false -> Intent(FlClashApplication.getAppContext(), FlClashService::class.java)
|
||||
}
|
||||
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
FlClashApplication.getAppContext().bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.follow.clash
|
||||
|
||||
package com.follow.clash.services
|
||||
|
||||
import com.follow.clash.models.VpnOptions
|
||||
|
||||
interface BaseServiceInterface {
|
||||
|
||||
fun start(options: VpnOptions): Int
|
||||
|
||||
fun stop()
|
||||
fun startForeground(title: String, content: String)
|
||||
|
||||
suspend fun startForeground(title: String, content: String)
|
||||
}
|
||||
@@ -12,14 +12,14 @@ import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.follow.clash.BaseServiceInterface
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.MainActivity
|
||||
import com.follow.clash.extensions.getActionPendingIntent
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.async
|
||||
|
||||
|
||||
class FlClashService : Service(), BaseServiceInterface {
|
||||
@@ -42,44 +42,54 @@ class FlClashService : Service(), BaseServiceInterface {
|
||||
|
||||
private val notificationId: Int = 1
|
||||
|
||||
private val notificationBuilder: NotificationCompat.Builder by lazy {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy {
|
||||
CoroutineScope(Dispatchers.Main).async {
|
||||
val stopText = GlobalState.getText("stop")
|
||||
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
val intent = Intent(
|
||||
this@FlClashService, MainActivity::class.java
|
||||
)
|
||||
} 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
|
||||
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this@FlClashService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this@FlClashService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
|
||||
with(NotificationCompat.Builder(this@FlClashService, 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
|
||||
}
|
||||
addAction(
|
||||
0,
|
||||
stopText, // 使用 suspend 函数获取的文本
|
||||
getActionPendingIntent("STOP")
|
||||
)
|
||||
setOngoing(true)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
addAction(
|
||||
0,
|
||||
GlobalState.getText("stop"),
|
||||
getActionPendingIntent("STOP")
|
||||
)
|
||||
setOngoing(true)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
}
|
||||
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
|
||||
return notificationBuilderDeferred.await()
|
||||
}
|
||||
|
||||
override fun start(options: VpnOptions) = 0
|
||||
|
||||
@@ -91,24 +101,24 @@ class FlClashService : Service(), BaseServiceInterface {
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
||||
override fun startForeground(title: String, content: String) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
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)
|
||||
override suspend 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 =
|
||||
getNotificationBuilder()
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ class FlClashTileService : TileService() {
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
activityTransfer()
|
||||
GlobalState.handleToggle(applicationContext)
|
||||
GlobalState.handleToggle()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
import android.net.Network
|
||||
import android.net.ProxyInfo
|
||||
import android.net.VpnService
|
||||
import android.os.Binder
|
||||
@@ -17,7 +16,6 @@ import android.os.Parcel
|
||||
import android.os.RemoteException
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.follow.clash.BaseServiceInterface
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.MainActivity
|
||||
import com.follow.clash.R
|
||||
@@ -28,14 +26,16 @@ import com.follow.clash.extensions.toCIDR
|
||||
import com.follow.clash.models.AccessControlMode
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
GlobalState.initServiceEngine(applicationContext)
|
||||
GlobalState.initServiceEngine()
|
||||
}
|
||||
|
||||
override fun start(options: VpnOptions): Int {
|
||||
@@ -105,12 +105,6 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateUnderlyingNetworks(networks: Array<Network>) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
this.setUnderlyingNetworks(networks)
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
stopSelf()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
@@ -122,69 +116,74 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
|
||||
private val notificationId: Int = 1
|
||||
|
||||
private val notificationBuilder: NotificationCompat.Builder by lazy {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy {
|
||||
CoroutineScope(Dispatchers.Main).async {
|
||||
val stopText = GlobalState.getText("stop")
|
||||
val intent = Intent(this@FlClashVpnService, 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(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
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this@FlClashVpnService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this@FlClashVpnService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
|
||||
with(NotificationCompat.Builder(this@FlClashVpnService, CHANNEL)) {
|
||||
setSmallIcon(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)
|
||||
addAction(
|
||||
0,
|
||||
stopText,
|
||||
getActionPendingIntent("STOP")
|
||||
)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
setOngoing(true)
|
||||
addAction(
|
||||
0,
|
||||
GlobalState.getText("stop"),
|
||||
getActionPendingIntent("STOP")
|
||||
)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
|
||||
return notificationBuilderDeferred.await()
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
||||
override fun startForeground(title: String, content: String) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
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)
|
||||
override suspend 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 =
|
||||
getNotificationBuilder()
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
|
||||
Submodule core/Clash.Meta updated: f7c61f885c...0c03d8e4b4
171
core/action.go
171
core/action.go
@@ -1,32 +1,167 @@
|
||||
//go:build !cgo
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
func (action Action) Json() ([]byte, error) {
|
||||
data, err := json.Marshal(action)
|
||||
type Action struct {
|
||||
Id string `json:"id"`
|
||||
Method Method `json:"method"`
|
||||
Data interface{} `json:"data"`
|
||||
DefaultValue interface{} `json:"default-value"`
|
||||
}
|
||||
|
||||
type ActionResult struct {
|
||||
Id string `json:"id"`
|
||||
Method Method `json:"method"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func (result ActionResult) Json() ([]byte, error) {
|
||||
data, err := json.Marshal(result)
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (action Action) callback(data interface{}) bool {
|
||||
if conn == nil {
|
||||
return false
|
||||
}
|
||||
sendAction := Action{
|
||||
func (action Action) wrapMessage(data interface{}) []byte {
|
||||
sendAction := ActionResult{
|
||||
Id: action.Id,
|
||||
Method: action.Method,
|
||||
Data: data,
|
||||
}
|
||||
res, err := sendAction.Json()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, err = conn.Write(append(res, []byte("\n")...))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
res, _ := sendAction.Json()
|
||||
return res
|
||||
}
|
||||
|
||||
func handleAction(action *Action, send func([]byte)) {
|
||||
switch action.Method {
|
||||
case initClashMethod:
|
||||
data := action.Data.(string)
|
||||
send(action.wrapMessage(handleInitClash(data)))
|
||||
return
|
||||
case getIsInitMethod:
|
||||
send(action.wrapMessage(handleGetIsInit()))
|
||||
return
|
||||
case forceGcMethod:
|
||||
handleForceGc()
|
||||
send(action.wrapMessage(true))
|
||||
return
|
||||
case shutdownMethod:
|
||||
send(action.wrapMessage(handleShutdown()))
|
||||
return
|
||||
case validateConfigMethod:
|
||||
data := []byte(action.Data.(string))
|
||||
send(action.wrapMessage(handleValidateConfig(data)))
|
||||
return
|
||||
case updateConfigMethod:
|
||||
data := []byte(action.Data.(string))
|
||||
send(action.wrapMessage(handleUpdateConfig(data)))
|
||||
return
|
||||
case getProxiesMethod:
|
||||
send(action.wrapMessage(handleGetProxies()))
|
||||
return
|
||||
case changeProxyMethod:
|
||||
data := action.Data.(string)
|
||||
handleChangeProxy(data, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
})
|
||||
return
|
||||
case getTrafficMethod:
|
||||
send(action.wrapMessage(handleGetTraffic()))
|
||||
return
|
||||
case getTotalTrafficMethod:
|
||||
send(action.wrapMessage(handleGetTotalTraffic()))
|
||||
return
|
||||
case resetTrafficMethod:
|
||||
handleResetTraffic()
|
||||
send(action.wrapMessage(true))
|
||||
return
|
||||
case asyncTestDelayMethod:
|
||||
data := action.Data.(string)
|
||||
handleAsyncTestDelay(data, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
})
|
||||
return
|
||||
case getConnectionsMethod:
|
||||
send(action.wrapMessage(handleGetConnections()))
|
||||
return
|
||||
case closeConnectionsMethod:
|
||||
send(action.wrapMessage(handleCloseConnections()))
|
||||
return
|
||||
case closeConnectionMethod:
|
||||
id := action.Data.(string)
|
||||
send(action.wrapMessage(handleCloseConnection(id)))
|
||||
return
|
||||
case getExternalProvidersMethod:
|
||||
send(action.wrapMessage(handleGetExternalProviders()))
|
||||
return
|
||||
case getExternalProviderMethod:
|
||||
externalProviderName := action.Data.(string)
|
||||
send(action.wrapMessage(handleGetExternalProvider(externalProviderName)))
|
||||
case updateGeoDataMethod:
|
||||
paramsString := action.Data.(string)
|
||||
var params = map[string]string{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
send(action.wrapMessage(err.Error()))
|
||||
return
|
||||
}
|
||||
geoType := params["geo-type"]
|
||||
geoName := params["geo-name"]
|
||||
handleUpdateGeoData(geoType, geoName, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
})
|
||||
return
|
||||
case updateExternalProviderMethod:
|
||||
providerName := action.Data.(string)
|
||||
handleUpdateExternalProvider(providerName, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
})
|
||||
return
|
||||
case sideLoadExternalProviderMethod:
|
||||
paramsString := action.Data.(string)
|
||||
var params = map[string]string{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
send(action.wrapMessage(err.Error()))
|
||||
return
|
||||
}
|
||||
providerName := params["providerName"]
|
||||
data := params["data"]
|
||||
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
})
|
||||
return
|
||||
case startLogMethod:
|
||||
handleStartLog()
|
||||
send(action.wrapMessage(true))
|
||||
return
|
||||
case stopLogMethod:
|
||||
handleStopLog()
|
||||
send(action.wrapMessage(true))
|
||||
return
|
||||
case startListenerMethod:
|
||||
send(action.wrapMessage(handleStartListener()))
|
||||
return
|
||||
case stopListenerMethod:
|
||||
send(action.wrapMessage(handleStopListener()))
|
||||
return
|
||||
case getCountryCodeMethod:
|
||||
ip := action.Data.(string)
|
||||
handleGetCountryCode(ip, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
})
|
||||
return
|
||||
case getMemoryMethod:
|
||||
handleGetMemory(func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
})
|
||||
return
|
||||
default:
|
||||
handle := nextHandle(action, send)
|
||||
if handle {
|
||||
return
|
||||
} else {
|
||||
send(action.wrapMessage(action.DefaultValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/metacubex/mihomo/adapter"
|
||||
@@ -25,7 +24,6 @@ import (
|
||||
"github.com/samber/lo"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
@@ -33,7 +31,7 @@ import (
|
||||
var (
|
||||
isRunning = false
|
||||
runLock sync.Mutex
|
||||
ips = []string{"ipinfo.io", "ipapi.co", "api.ip.sb", "ipwho.is"}
|
||||
ips = []string{"ipwho.is", "ifconfig.me", "icanhazip.com", "api.ip.sb", "ipinfo.io"}
|
||||
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
|
||||
)
|
||||
|
||||
@@ -43,11 +41,6 @@ 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] }
|
||||
|
||||
func (message *Message) Json() (string, error) {
|
||||
data, err := json.Marshal(message)
|
||||
return string(data), err
|
||||
}
|
||||
|
||||
func readFile(path string) ([]byte, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil, err
|
||||
@@ -86,16 +79,6 @@ func getRawConfigWithId(id string) *config.RawConfig {
|
||||
continue
|
||||
}
|
||||
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
|
||||
if configParams.TestURL != nil {
|
||||
if mapping["health-check"] != nil {
|
||||
hc := mapping["health-check"].(map[string]any)
|
||||
if hc != nil {
|
||||
if hc["url"] != nil {
|
||||
hc["url"] = *configParams.TestURL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, mapping := range prof.RuleProvider {
|
||||
value, exist := mapping["path"].(string)
|
||||
@@ -352,8 +335,6 @@ func applyConfig(rawConfig *config.RawConfig) error {
|
||||
if configParams.IsPatch {
|
||||
patchConfig()
|
||||
} else {
|
||||
handleCloseConnectionsUnLock()
|
||||
runtime.GC()
|
||||
hub.ApplyConfig(currentConfig)
|
||||
patchSelectGroup()
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/metacubex/mihomo/adapter/provider"
|
||||
"github.com/metacubex/mihomo/config"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigExtendedParams struct {
|
||||
IsPatch bool `json:"is-patch"`
|
||||
IsCompatible bool `json:"is-compatible"`
|
||||
SelectedMap map[string]string `json:"selected-map"`
|
||||
TestURL *string `json:"test-url"`
|
||||
OverrideDns bool `json:"override-dns"`
|
||||
IsPatch bool `json:"is-patch"`
|
||||
IsCompatible bool `json:"is-compatible"`
|
||||
SelectedMap map[string]string `json:"selected-map"`
|
||||
TestURL *string `json:"test-url"`
|
||||
OverrideDns bool `json:"override-dns"`
|
||||
OnlyStatisticsProxy bool `json:"only-statistics-proxy"`
|
||||
}
|
||||
|
||||
type GenerateConfigParams struct {
|
||||
@@ -28,14 +29,10 @@ type ChangeProxyParams struct {
|
||||
|
||||
type TestDelayParams struct {
|
||||
ProxyName string `json:"proxy-name"`
|
||||
TestUrl string `json:"test-url"`
|
||||
Timeout int64 `json:"timeout"`
|
||||
}
|
||||
|
||||
type ProcessMapItem struct {
|
||||
Id int64 `json:"id"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type ExternalProvider struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
@@ -65,6 +62,8 @@ const (
|
||||
closeConnectionMethod Method = "closeConnection"
|
||||
getExternalProvidersMethod Method = "getExternalProviders"
|
||||
getExternalProviderMethod Method = "getExternalProvider"
|
||||
getCountryCodeMethod Method = "getCountryCode"
|
||||
getMemoryMethod Method = "getMemory"
|
||||
updateGeoDataMethod Method = "updateGeoData"
|
||||
updateExternalProviderMethod Method = "updateExternalProvider"
|
||||
sideLoadExternalProviderMethod Method = "sideLoadExternalProvider"
|
||||
@@ -72,19 +71,23 @@ const (
|
||||
stopLogMethod Method = "stopLog"
|
||||
startListenerMethod Method = "startListener"
|
||||
stopListenerMethod Method = "stopListener"
|
||||
startTunMethod Method = "startTun"
|
||||
stopTunMethod Method = "stopTun"
|
||||
updateDnsMethod Method = "updateDns"
|
||||
setProcessMapMethod Method = "setProcessMap"
|
||||
setFdMapMethod Method = "setFdMap"
|
||||
setStateMethod Method = "setState"
|
||||
getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions"
|
||||
getRunTimeMethod Method = "getRunTime"
|
||||
getCurrentProfileNameMethod Method = "getCurrentProfileName"
|
||||
)
|
||||
|
||||
type Method string
|
||||
|
||||
type Action struct {
|
||||
Id string `json:"id"`
|
||||
Method Method `json:"method"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type MessageType string
|
||||
|
||||
type Delay struct {
|
||||
Url string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
Value int32 `json:"value"`
|
||||
}
|
||||
@@ -94,17 +97,31 @@ type Message struct {
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type Process struct {
|
||||
Id int64 `json:"id"`
|
||||
Metadata *constant.Metadata `json:"metadata"`
|
||||
}
|
||||
|
||||
const (
|
||||
LogMessage MessageType = "log"
|
||||
ProtectMessage MessageType = "protect"
|
||||
DelayMessage MessageType = "delay"
|
||||
ProcessMessage MessageType = "process"
|
||||
RequestMessage MessageType = "request"
|
||||
StartedMessage MessageType = "started"
|
||||
LoadedMessage MessageType = "loaded"
|
||||
)
|
||||
|
||||
func (message *Message) Json() (string, error) {
|
||||
data, err := json.Marshal(message)
|
||||
return string(data), err
|
||||
}
|
||||
|
||||
type InvokeMessage struct {
|
||||
Type InvokeType `json:"type"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type InvokeType string
|
||||
|
||||
const (
|
||||
ProtectInvoke InvokeType = "protect"
|
||||
ProcessInvoke InvokeType = "process"
|
||||
)
|
||||
|
||||
func (message *InvokeMessage) Json() string {
|
||||
data, _ := json.Marshal(message)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
64
core/go.mod
64
core/go.mod
@@ -1,21 +1,14 @@
|
||||
module core
|
||||
|
||||
go 1.21
|
||||
go 1.20
|
||||
|
||||
replace github.com/metacubex/mihomo => ./Clash.Meta
|
||||
|
||||
require github.com/metacubex/mihomo v1.17.1
|
||||
|
||||
require (
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
|
||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
|
||||
github.com/sagernet/cors v1.2.1 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000
|
||||
github.com/samber/lo v1.47.0
|
||||
)
|
||||
|
||||
replace github.com/sagernet/sing => github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297
|
||||
|
||||
require (
|
||||
github.com/3andne/restls-client-go v0.1.6 // indirect
|
||||
github.com/RyuaNerin/go-krypto v1.2.4 // indirect
|
||||
@@ -27,13 +20,15 @@ require (
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/coreos/go-iptables v0.8.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/ebitengine/purego v0.8.1 // indirect
|
||||
github.com/enfein/mieru/v3 v3.10.0 // indirect
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
|
||||
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gaukas/godicttls v0.0.4 // indirect
|
||||
github.com/go-chi/chi/v5 v5.1.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.0 // indirect
|
||||
github.com/go-chi/render v1.0.3 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
@@ -41,33 +36,35 @@ require (
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.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.3 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475 // indirect
|
||||
github.com/hashicorp/yamux v0.1.2 // indirect
|
||||
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
|
||||
github.com/metacubex/chacha v0.1.0 // indirect
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
|
||||
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec // indirect
|
||||
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4 // indirect
|
||||
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a // indirect
|
||||
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da // indirect
|
||||
github.com/metacubex/randv2 v0.2.0 // indirect
|
||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 // indirect
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1 // indirect
|
||||
github.com/metacubex/sing-tun v0.4.5 // indirect
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 // indirect
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3 // indirect
|
||||
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa // indirect
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect
|
||||
github.com/metacubex/utls v1.6.6 // indirect
|
||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
|
||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
|
||||
@@ -79,16 +76,15 @@ require (
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||
github.com/sagernet/cors v1.2.1 // indirect
|
||||
github.com/sagernet/fswatch v0.1.1 // indirect
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
|
||||
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-shadowtls v0.1.4 // indirect
|
||||
github.com/sagernet/sing v0.5.1 // indirect
|
||||
github.com/sagernet/sing-mux v0.2.1 // indirect
|
||||
github.com/sagernet/sing-shadowtls v0.1.5 // indirect
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
|
||||
github.com/samber/lo v1.47.0
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.24.11 // indirect
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
|
||||
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
|
||||
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
|
||||
@@ -97,22 +93,24 @@ require (
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // 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
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/crypto v0.28.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect; indirect`
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
)
|
||||
|
||||
109
core/go.sum
109
core/go.sum
@@ -26,12 +26,15 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
|
||||
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/enfein/mieru/v3 v3.10.0 h1:KMnAtY4s8MB74sUg4GbvF9R9v3jkXPQTSkxPxl1emxQ=
|
||||
github.com/enfein/mieru/v3 v3.10.0/go.mod h1:jH2nXzJSNUn6UWuzD8E8AsRVa9Ca0CqcTcr9Z+CJO1o=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po=
|
||||
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg=
|
||||
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c/go.mod h1:ETASDWf/FmEb6Ysrtd1QhjNedUU/ZQxBCRLh60bQ/UI=
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY=
|
||||
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=
|
||||
@@ -40,12 +43,11 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
|
||||
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
|
||||
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
@@ -61,29 +63,27 @@ github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk=
|
||||
github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
|
||||
github.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7spovjlY=
|
||||
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.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
|
||||
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475 h1:hxST5pwMBEOWmxpkX20w9oZG+hXdhKmAIPQ3NGGAxas=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d h1:VkCNWh6tuQLgDBc6KrUOz/L1mCUQGnR1Ujj8uTgpwwk=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
|
||||
@@ -102,28 +102,26 @@ github.com/metacubex/chacha v0.1.0 h1:tg9RSJ18NvL38cCWNyYH1eiG6qDCyyXIaTLQthon0s
|
||||
github.com/metacubex/chacha v0.1.0/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
|
||||
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec h1:HxreOiFTUrJXJautEo8rnE1uKTVGY8wtZepY1Tii/Nc=
|
||||
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec/go.mod h1:8BVmQ+3cxjqzWElafm24rb2Ae4jRI6vAXNXWqWjfrXw=
|
||||
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4 h1:CgdUBRxmNlxEGkp35HwvgQ10jwOOUJKWdOxpi8yWi8o=
|
||||
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4/go.mod h1:Y7yRGqFE6UQL/3aKPYmiYdjfVkeujJaStP4+jiZMcN8=
|
||||
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a h1:cZ6oNVrsmsi3SNlnSnRio4zOgtQq+/XidwsaNgKICcg=
|
||||
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a/go.mod h1:xBw/SYJPgUMPQ1tklV/brGn2nxhfr3BnvBzNlyi4Nic=
|
||||
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da h1:Mq6cbHbPTLLTUfA9scrwBmOGkvl6y99E3WmtMIMqo30=
|
||||
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da/go.mod h1:AiZ+UPgrkO1DTnmiAX4b+kRoV1Vfc65UkYD7RbFlIZA=
|
||||
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
||||
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
||||
github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297 h1:YG/JkwGPbca5rUtEMHIu8ZuqzR7BSVm1iqY8hNoMeMA=
|
||||
github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 h1:HobpULaPK6OoxrHMmgcwLkwwIduXVmwdcznwUfH1GQM=
|
||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1 h1:ypfofGDZbP8p3Y4P/m74JYu7sQViesi3c8nbmT6cS0Y=
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1/go.mod h1:olbEx9yVcaw5tHTNlRamRoxmMKcvDvcVS1YLnQGzvWE=
|
||||
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0=
|
||||
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3 h1:xg71VmzLS6ByAzi/57phwDvjE+dLLs+ozH00k4DnOns=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3/go.mod h1:6nitcmzPDL3MXnLdhu6Hm126Zk4S1fBbX3P7jxUxSFw=
|
||||
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa h1:9mcjV+RGZVC3reJBNDjjNPyS8PmFG97zq56X7WNaFO4=
|
||||
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa/go.mod h1:4tLB5c8U0CxpkFM+AJJB77jEaVDbLH5XQvy42vAGsWw=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc=
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
|
||||
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/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
|
||||
@@ -137,7 +135,6 @@ github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:U
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
|
||||
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||
github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0=
|
||||
github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo=
|
||||
github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0=
|
||||
@@ -166,20 +163,19 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
|
||||
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
|
||||
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 h1:5bCAkvDDzSMITiHFjolBwpdqYsvycdTu71FsMEFXQ14=
|
||||
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ=
|
||||
github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
|
||||
github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4=
|
||||
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
|
||||
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
|
||||
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
|
||||
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
|
||||
github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
|
||||
github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4=
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
|
||||
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8=
|
||||
github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8=
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=
|
||||
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk=
|
||||
@@ -189,17 +185,10 @@ github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
@@ -229,8 +218,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
@@ -239,11 +228,11 @@ golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -255,20 +244,18 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.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.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
|
||||
66
core/hub.go
66
core/hub.go
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/metacubex/mihomo/adapter/outboundgroup"
|
||||
"github.com/metacubex/mihomo/common/observable"
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"github.com/metacubex/mihomo/component/mmdb"
|
||||
"github.com/metacubex/mihomo/component/updater"
|
||||
"github.com/metacubex/mihomo/config"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
@@ -17,14 +18,18 @@ import (
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"github.com/metacubex/mihomo/tunnel"
|
||||
"github.com/metacubex/mihomo/tunnel/statistic"
|
||||
"net"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
isInit = false
|
||||
configParams = ConfigExtendedParams{}
|
||||
isInit = false
|
||||
configParams = ConfigExtendedParams{
|
||||
OnlyStatisticsProxy: false,
|
||||
}
|
||||
externalProviders = map[string]cp.Provider{}
|
||||
logSubscriber observable.Subscription[log.Event]
|
||||
currentConfig *config.Config
|
||||
@@ -146,8 +151,8 @@ func handleChangeProxy(data string, fn func(string string)) {
|
||||
}()
|
||||
}
|
||||
|
||||
func handleGetTraffic(onlyProxy bool) string {
|
||||
up, down := statistic.DefaultManager.Current(onlyProxy)
|
||||
func handleGetTraffic() string {
|
||||
up, down := statistic.DefaultManager.Current(configParams.OnlyStatisticsProxy)
|
||||
traffic := map[string]int64{
|
||||
"up": up,
|
||||
"down": down,
|
||||
@@ -160,8 +165,8 @@ func handleGetTraffic(onlyProxy bool) string {
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func handleGetTotalTraffic(onlyProxy bool) string {
|
||||
up, down := statistic.DefaultManager.Total(onlyProxy)
|
||||
func handleGetTotalTraffic() string {
|
||||
up, down := statistic.DefaultManager.Total(configParams.OnlyStatisticsProxy)
|
||||
traffic := map[string]int64{
|
||||
"up": up,
|
||||
"down": down,
|
||||
@@ -210,7 +215,13 @@ func handleAsyncTestDelay(paramsString string, fn func(string)) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus)
|
||||
testUrl := constant.DefaultTestURL
|
||||
|
||||
if params.TestUrl != "" {
|
||||
testUrl = params.TestUrl
|
||||
}
|
||||
|
||||
delay, err := proxy.URLTest(ctx, testUrl, expectedStatus)
|
||||
if err != nil || delay == 0 {
|
||||
delayData.Value = -1
|
||||
data, _ := json.Marshal(delayData)
|
||||
@@ -237,17 +248,6 @@ func handleGetConnections() string {
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func handleCloseConnectionsUnLock() bool {
|
||||
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
|
||||
err := c.Close()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
func handleCloseConnections() bool {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
@@ -392,7 +392,7 @@ func handleStartLog() {
|
||||
Type: LogMessage,
|
||||
Data: logData,
|
||||
}
|
||||
SendMessage(*message)
|
||||
sendMessage(*message)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -404,9 +404,29 @@ func handleStopLog() {
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetCountryCode(ip string, fn func(value string)) {
|
||||
go func() {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
codes := mmdb.IPInstance().LookupCode(net.ParseIP(ip))
|
||||
if len(codes) == 0 {
|
||||
fn("")
|
||||
return
|
||||
}
|
||||
fn(codes[0])
|
||||
}()
|
||||
}
|
||||
|
||||
func handleGetMemory(fn func(value string)) {
|
||||
go func() {
|
||||
fn(strconv.FormatUint(statistic.DefaultManager.Memory(), 10))
|
||||
}()
|
||||
}
|
||||
|
||||
func init() {
|
||||
adapter.UrlTestHook = func(name string, delay uint16) {
|
||||
adapter.UrlTestHook = func(url string, name string, delay uint16) {
|
||||
delayData := &Delay{
|
||||
Url: url,
|
||||
Name: name,
|
||||
}
|
||||
if delay == 0 {
|
||||
@@ -414,19 +434,19 @@ func init() {
|
||||
} else {
|
||||
delayData.Value = int32(delay)
|
||||
}
|
||||
SendMessage(Message{
|
||||
sendMessage(Message{
|
||||
Type: DelayMessage,
|
||||
Data: delayData,
|
||||
})
|
||||
}
|
||||
statistic.DefaultRequestNotify = func(c statistic.Tracker) {
|
||||
SendMessage(Message{
|
||||
sendMessage(Message{
|
||||
Type: RequestMessage,
|
||||
Data: c,
|
||||
})
|
||||
}
|
||||
executor.DefaultProviderLoadedHook = func(providerName string) {
|
||||
SendMessage(Message{
|
||||
sendMessage(Message{
|
||||
Type: LoadedMessage,
|
||||
Data: providerName,
|
||||
})
|
||||
|
||||
188
core/lib.go
188
core/lib.go
@@ -8,18 +8,30 @@ package main
|
||||
import "C"
|
||||
import (
|
||||
bridge "core/dart-bridge"
|
||||
"encoding/json"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var messagePort int64 = -1
|
||||
|
||||
//export initNativeApiBridge
|
||||
func initNativeApiBridge(api unsafe.Pointer) {
|
||||
bridge.InitDartApi(api)
|
||||
}
|
||||
|
||||
//export initMessage
|
||||
func initMessage(port C.longlong) {
|
||||
i := int64(port)
|
||||
Port = i
|
||||
//export attachMessagePort
|
||||
func attachMessagePort(mPort C.longlong) {
|
||||
messagePort = int64(mPort)
|
||||
}
|
||||
|
||||
//export getTraffic
|
||||
func getTraffic() *C.char {
|
||||
return C.CString(handleGetTraffic())
|
||||
}
|
||||
|
||||
//export getTotalTraffic
|
||||
func getTotalTraffic() *C.char {
|
||||
return C.CString(handleGetTotalTraffic())
|
||||
}
|
||||
|
||||
//export freeCString
|
||||
@@ -27,9 +39,32 @@ func freeCString(s *C.char) {
|
||||
C.free(unsafe.Pointer(s))
|
||||
}
|
||||
|
||||
//export initClash
|
||||
func initClash(homeDirStr *C.char) bool {
|
||||
return handleInitClash(C.GoString(homeDirStr))
|
||||
//export invokeAction
|
||||
func invokeAction(paramsChar *C.char, port C.longlong) {
|
||||
params := C.GoString(paramsChar)
|
||||
i := int64(port)
|
||||
var action = &Action{}
|
||||
err := json.Unmarshal([]byte(params), action)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
go handleAction(action, func(bytes []byte) {
|
||||
bridge.SendToPort(i, string(bytes))
|
||||
})
|
||||
}
|
||||
|
||||
func sendMessage(message Message) {
|
||||
if messagePort == -1 {
|
||||
return
|
||||
}
|
||||
res, err := message.Json()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
bridge.SendToPort(messagePort, string(Action{
|
||||
Method: messageMethod,
|
||||
}.wrapMessage(res)))
|
||||
}
|
||||
|
||||
//export startListener
|
||||
@@ -41,142 +76,3 @@ func startListener() {
|
||||
func stopListener() {
|
||||
handleStopListener()
|
||||
}
|
||||
|
||||
//export getIsInit
|
||||
func getIsInit() bool {
|
||||
return handleGetIsInit()
|
||||
}
|
||||
|
||||
//export shutdownClash
|
||||
func shutdownClash() bool {
|
||||
return handleShutdown()
|
||||
}
|
||||
|
||||
//export forceGc
|
||||
func forceGc() {
|
||||
handleForceGc()
|
||||
}
|
||||
|
||||
//export validateConfig
|
||||
func validateConfig(s *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
bytes := []byte(C.GoString(s))
|
||||
go func() {
|
||||
bridge.SendToPort(i, handleValidateConfig(bytes))
|
||||
}()
|
||||
}
|
||||
|
||||
//export updateConfig
|
||||
func updateConfig(s *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
bytes := []byte(C.GoString(s))
|
||||
go func() {
|
||||
bridge.SendToPort(i, handleUpdateConfig(bytes))
|
||||
}()
|
||||
}
|
||||
|
||||
//export getProxies
|
||||
func getProxies() *C.char {
|
||||
return C.CString(handleGetProxies())
|
||||
}
|
||||
|
||||
//export changeProxy
|
||||
func changeProxy(s *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
paramsString := C.GoString(s)
|
||||
handleChangeProxy(paramsString, func(value string) {
|
||||
bridge.SendToPort(i, value)
|
||||
})
|
||||
}
|
||||
|
||||
//export getTraffic
|
||||
func getTraffic(port C.int) *C.char {
|
||||
onlyProxy := int(port) == 1
|
||||
return C.CString(handleGetTraffic(onlyProxy))
|
||||
}
|
||||
|
||||
//export getTotalTraffic
|
||||
func getTotalTraffic(port C.int) *C.char {
|
||||
onlyProxy := int(port) == 1
|
||||
return C.CString(handleGetTotalTraffic(onlyProxy))
|
||||
}
|
||||
|
||||
//export resetTraffic
|
||||
func resetTraffic() {
|
||||
handleResetTraffic()
|
||||
}
|
||||
|
||||
//export asyncTestDelay
|
||||
func asyncTestDelay(s *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
paramsString := C.GoString(s)
|
||||
handleAsyncTestDelay(paramsString, func(value string) {
|
||||
bridge.SendToPort(i, value)
|
||||
})
|
||||
}
|
||||
|
||||
//export getConnections
|
||||
func getConnections() *C.char {
|
||||
return C.CString(handleGetConnections())
|
||||
}
|
||||
|
||||
//export closeConnections
|
||||
func closeConnections() {
|
||||
handleCloseConnections()
|
||||
}
|
||||
|
||||
//export closeConnection
|
||||
func closeConnection(id *C.char) {
|
||||
connectionId := C.GoString(id)
|
||||
handleCloseConnection(connectionId)
|
||||
}
|
||||
|
||||
//export getExternalProviders
|
||||
func getExternalProviders() *C.char {
|
||||
return C.CString(handleGetExternalProviders())
|
||||
}
|
||||
|
||||
//export getExternalProvider
|
||||
func getExternalProvider(externalProviderNameChar *C.char) *C.char {
|
||||
externalProviderName := C.GoString(externalProviderNameChar)
|
||||
return C.CString(handleGetExternalProvider(externalProviderName))
|
||||
}
|
||||
|
||||
//export updateGeoData
|
||||
func updateGeoData(geoTypeChar *C.char, geoNameChar *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
geoType := C.GoString(geoTypeChar)
|
||||
geoName := C.GoString(geoNameChar)
|
||||
handleUpdateGeoData(geoType, geoName, func(value string) {
|
||||
bridge.SendToPort(i, value)
|
||||
})
|
||||
}
|
||||
|
||||
//export updateExternalProvider
|
||||
func updateExternalProvider(providerNameChar *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
providerName := C.GoString(providerNameChar)
|
||||
handleUpdateExternalProvider(providerName, func(value string) {
|
||||
bridge.SendToPort(i, value)
|
||||
})
|
||||
}
|
||||
|
||||
//export sideLoadExternalProvider
|
||||
func sideLoadExternalProvider(providerNameChar *C.char, dataChar *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
providerName := C.GoString(providerNameChar)
|
||||
data := []byte(C.GoString(dataChar))
|
||||
handleSideLoadExternalProvider(providerName, data, func(value string) {
|
||||
bridge.SendToPort(i, value)
|
||||
})
|
||||
}
|
||||
|
||||
//export startLog
|
||||
func startLog() {
|
||||
handleStartLog()
|
||||
}
|
||||
|
||||
//export stopLog
|
||||
func stopLog() {
|
||||
handleStopLog()
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
bridge "core/dart-bridge"
|
||||
"core/platform"
|
||||
"core/state"
|
||||
t "core/tun"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
"github.com/metacubex/mihomo/component/process"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
@@ -19,123 +21,165 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ProcessMap struct {
|
||||
m sync.Map
|
||||
}
|
||||
|
||||
type FdMap struct {
|
||||
m sync.Map
|
||||
}
|
||||
|
||||
type Fd struct {
|
||||
Id int64 `json:"id"`
|
||||
Value int64 `json:"value"`
|
||||
Id string `json:"id"`
|
||||
Value int64 `json:"value"`
|
||||
}
|
||||
|
||||
type Process struct {
|
||||
Id string `json:"id"`
|
||||
Metadata *constant.Metadata `json:"metadata"`
|
||||
}
|
||||
|
||||
type ProcessMapItem struct {
|
||||
Id string `json:"id"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type InvokeManager struct {
|
||||
invokeMap sync.Map
|
||||
chanMap map[string]chan struct{}
|
||||
chanLock sync.Mutex
|
||||
}
|
||||
|
||||
func NewInvokeManager() *InvokeManager {
|
||||
return &InvokeManager{
|
||||
chanMap: make(map[string]chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *InvokeManager) load(id string) string {
|
||||
res, ok := m.invokeMap.Load(id)
|
||||
if ok {
|
||||
return res.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *InvokeManager) delete(id string) {
|
||||
m.invokeMap.Delete(id)
|
||||
}
|
||||
|
||||
func (m *InvokeManager) completer(id string, value string) {
|
||||
m.invokeMap.Store(id, value)
|
||||
m.chanLock.Lock()
|
||||
if ch, ok := m.chanMap[id]; ok {
|
||||
close(ch)
|
||||
delete(m.chanMap, id)
|
||||
}
|
||||
m.chanLock.Unlock()
|
||||
}
|
||||
|
||||
func (m *InvokeManager) await(id string) {
|
||||
m.chanLock.Lock()
|
||||
if _, ok := m.chanMap[id]; !ok {
|
||||
m.chanMap[id] = make(chan struct{})
|
||||
}
|
||||
ch := m.chanMap[id]
|
||||
m.chanLock.Unlock()
|
||||
|
||||
timeout := time.After(500 * time.Millisecond)
|
||||
select {
|
||||
case <-ch:
|
||||
return
|
||||
case <-timeout:
|
||||
m.completer(id, "")
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
tunListener *sing_tun.Listener
|
||||
fdMap FdMap
|
||||
fdCounter int64 = 0
|
||||
counter int64 = 0
|
||||
processMap ProcessMap
|
||||
tunLock sync.Mutex
|
||||
runTime *time.Time
|
||||
errBlocked = errors.New("blocked")
|
||||
invokePort int64 = -1
|
||||
tunListener *sing_tun.Listener
|
||||
fdInvokeMap = NewInvokeManager()
|
||||
processInvokeMap = NewInvokeManager()
|
||||
tunLock sync.Mutex
|
||||
runTime *time.Time
|
||||
errBlocked = errors.New("blocked")
|
||||
)
|
||||
|
||||
func (cm *ProcessMap) Store(key int64, value string) {
|
||||
cm.m.Store(key, value)
|
||||
}
|
||||
|
||||
func (cm *ProcessMap) Load(key int64) (string, bool) {
|
||||
value, ok := cm.m.Load(key)
|
||||
if !ok || value == nil {
|
||||
return "", false
|
||||
}
|
||||
return value.(string), true
|
||||
}
|
||||
|
||||
func (cm *FdMap) Store(key int64) {
|
||||
cm.m.Store(key, struct{}{})
|
||||
}
|
||||
|
||||
func (cm *FdMap) Load(key int64) bool {
|
||||
_, ok := cm.m.Load(key)
|
||||
return ok
|
||||
}
|
||||
|
||||
//export startTUN
|
||||
func startTUN(fd C.int, port C.longlong) {
|
||||
i := int64(port)
|
||||
ServicePort = i
|
||||
func handleStartTun(fd int) string {
|
||||
handleStopTun()
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
if fd == 0 {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
now := time.Now()
|
||||
runTime = &now
|
||||
SendMessage(Message{
|
||||
Type: StartedMessage,
|
||||
Data: strconv.FormatInt(runTime.UnixMilli(), 10),
|
||||
})
|
||||
return
|
||||
}
|
||||
initSocketHook()
|
||||
go func() {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
f := int(fd)
|
||||
tunListener, _ = t.Start(f, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
|
||||
} else {
|
||||
initSocketHook()
|
||||
tunListener, _ = t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
|
||||
if tunListener != nil {
|
||||
log.Infoln("TUN address: %v", tunListener.Address())
|
||||
}
|
||||
now := time.Now()
|
||||
runTime = &now
|
||||
}()
|
||||
}
|
||||
|
||||
//export getRunTime
|
||||
func getRunTime() *C.char {
|
||||
if runTime == nil {
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(strconv.FormatInt(runTime.UnixMilli(), 10))
|
||||
return handleGetRunTime()
|
||||
}
|
||||
|
||||
//export stopTun
|
||||
func stopTun() {
|
||||
func handleStopTun() {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
removeSocketHook()
|
||||
go func() {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
|
||||
runTime = nil
|
||||
|
||||
if tunListener != nil {
|
||||
_ = tunListener.Close()
|
||||
}
|
||||
}()
|
||||
runTime = nil
|
||||
if tunListener != nil {
|
||||
log.Infoln("TUN close")
|
||||
_ = tunListener.Close()
|
||||
}
|
||||
}
|
||||
|
||||
//export setFdMap
|
||||
func setFdMap(fd C.long) {
|
||||
fdInt := int64(fd)
|
||||
go func() {
|
||||
fdMap.Store(fdInt)
|
||||
}()
|
||||
func handleGetRunTime() string {
|
||||
if runTime == nil {
|
||||
return ""
|
||||
}
|
||||
return strconv.FormatInt(runTime.UnixMilli(), 10)
|
||||
}
|
||||
|
||||
func markSocket(fd Fd) {
|
||||
SendMessage(Message{
|
||||
Type: ProtectMessage,
|
||||
func handleSetProcessMap(params string) {
|
||||
var processMapItem = &ProcessMapItem{}
|
||||
err := json.Unmarshal([]byte(params), processMapItem)
|
||||
if err == nil {
|
||||
processInvokeMap.completer(processMapItem.Id, processMapItem.Value)
|
||||
}
|
||||
}
|
||||
|
||||
//export attachInvokePort
|
||||
func attachInvokePort(mPort C.longlong) {
|
||||
invokePort = int64(mPort)
|
||||
}
|
||||
|
||||
func sendInvokeMessage(message InvokeMessage) {
|
||||
if invokePort == -1 {
|
||||
return
|
||||
}
|
||||
bridge.SendToPort(invokePort, message.Json())
|
||||
}
|
||||
|
||||
func handleMarkSocket(fd Fd) {
|
||||
sendInvokeMessage(InvokeMessage{
|
||||
Type: ProtectInvoke,
|
||||
Data: fd,
|
||||
})
|
||||
}
|
||||
|
||||
func handleParseProcess(process Process) {
|
||||
sendInvokeMessage(InvokeMessage{
|
||||
Type: ProcessInvoke,
|
||||
Data: process,
|
||||
})
|
||||
}
|
||||
|
||||
func handleSetFdMap(id string) {
|
||||
go func() {
|
||||
fdInvokeMap.completer(id, "")
|
||||
}()
|
||||
}
|
||||
|
||||
func initSocketHook() {
|
||||
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
|
||||
if platform.ShouldBlockConnection() {
|
||||
@@ -143,26 +187,15 @@ func initSocketHook() {
|
||||
}
|
||||
return conn.Control(func(fd uintptr) {
|
||||
fdInt := int64(fd)
|
||||
timeout := time.After(500 * time.Millisecond)
|
||||
id := atomic.AddInt64(&fdCounter, 1)
|
||||
id := utils.NewUUIDV1().String()
|
||||
|
||||
markSocket(Fd{
|
||||
handleMarkSocket(Fd{
|
||||
Id: id,
|
||||
Value: fdInt,
|
||||
})
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timeout:
|
||||
return
|
||||
default:
|
||||
exists := fdMap.Load(id)
|
||||
if exists {
|
||||
return
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
fdInvokeMap.await(id)
|
||||
fdInvokeMap.delete(id)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -176,58 +209,19 @@ func init() {
|
||||
if metadata == nil {
|
||||
return "", process.ErrInvalidNetwork
|
||||
}
|
||||
id := atomic.AddInt64(&counter, 1)
|
||||
|
||||
timeout := time.After(200 * time.Millisecond)
|
||||
|
||||
SendMessage(Message{
|
||||
Type: ProcessMessage,
|
||||
Data: Process{
|
||||
Id: id,
|
||||
Metadata: metadata,
|
||||
},
|
||||
id := utils.NewUUIDV1().String()
|
||||
handleParseProcess(Process{
|
||||
Id: id,
|
||||
Metadata: metadata,
|
||||
})
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timeout:
|
||||
return "", errors.New("package resolver timeout")
|
||||
default:
|
||||
value, exists := processMap.Load(id)
|
||||
if exists {
|
||||
return value, nil
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
processInvokeMap.await(id)
|
||||
res := processInvokeMap.load(id)
|
||||
processInvokeMap.delete(id)
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
//export setProcessMap
|
||||
func setProcessMap(s *C.char) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
paramsString := C.GoString(s)
|
||||
go func() {
|
||||
var processMapItem = &ProcessMapItem{}
|
||||
err := json.Unmarshal([]byte(paramsString), processMapItem)
|
||||
if err == nil {
|
||||
processMap.Store(processMapItem.Id, processMapItem.Value)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
//export getCurrentProfileName
|
||||
func getCurrentProfileName() *C.char {
|
||||
if state.CurrentState == nil {
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(state.CurrentState.CurrentProfileName)
|
||||
}
|
||||
|
||||
//export getAndroidVpnOptions
|
||||
func getAndroidVpnOptions() *C.char {
|
||||
func handleGetAndroidVpnOptions() string {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
options := state.AndroidVpnOptions{
|
||||
@@ -245,26 +239,140 @@ func getAndroidVpnOptions() *C.char {
|
||||
data, err := json.Marshal(options)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return C.CString("")
|
||||
return ""
|
||||
}
|
||||
return C.CString(string(data))
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func handleSetState(params string) {
|
||||
_ = json.Unmarshal([]byte(params), state.CurrentState)
|
||||
}
|
||||
|
||||
func handleUpdateDns(value string) {
|
||||
go func() {
|
||||
log.Infoln("[DNS] updateDns %s", value)
|
||||
dns.UpdateSystemDNS(strings.Split(value, ","))
|
||||
dns.FlushCacheWithDefaultResolver()
|
||||
}()
|
||||
}
|
||||
|
||||
func handleGetCurrentProfileName() string {
|
||||
if state.CurrentState == nil {
|
||||
return ""
|
||||
}
|
||||
return state.CurrentState.CurrentProfileName
|
||||
}
|
||||
|
||||
func nextHandle(action *Action, send func([]byte)) bool {
|
||||
switch action.Method {
|
||||
case startTunMethod:
|
||||
data := action.Data.(string)
|
||||
var fd int
|
||||
_ = json.Unmarshal([]byte(data), &fd)
|
||||
send(action.wrapMessage(handleStartTun(fd)))
|
||||
return true
|
||||
case stopTunMethod:
|
||||
handleStopTun()
|
||||
send(action.wrapMessage(true))
|
||||
return true
|
||||
case setStateMethod:
|
||||
data := action.Data.(string)
|
||||
handleSetState(data)
|
||||
send(action.wrapMessage(true))
|
||||
return true
|
||||
case getAndroidVpnOptionsMethod:
|
||||
send(action.wrapMessage(handleGetAndroidVpnOptions()))
|
||||
return true
|
||||
case updateDnsMethod:
|
||||
data := action.Data.(string)
|
||||
handleUpdateDns(data)
|
||||
send(action.wrapMessage(true))
|
||||
return true
|
||||
case setFdMapMethod:
|
||||
fdId := action.Data.(string)
|
||||
handleSetFdMap(fdId)
|
||||
send(action.wrapMessage(true))
|
||||
return true
|
||||
case setProcessMapMethod:
|
||||
data := action.Data.(string)
|
||||
handleSetProcessMap(data)
|
||||
send(action.wrapMessage(true))
|
||||
return true
|
||||
case getRunTimeMethod:
|
||||
send(action.wrapMessage(handleGetRunTime()))
|
||||
return true
|
||||
case getCurrentProfileNameMethod:
|
||||
send(action.wrapMessage(handleGetCurrentProfileName()))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//export quickStart
|
||||
func quickStart(dirChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
dir := C.GoString(dirChar)
|
||||
bytes := []byte(C.GoString(paramsChar))
|
||||
stateParams := C.GoString(stateParamsChar)
|
||||
go func() {
|
||||
res := handleInitClash(dir)
|
||||
if res == false {
|
||||
bridge.SendToPort(i, "init error")
|
||||
}
|
||||
handleSetState(stateParams)
|
||||
bridge.SendToPort(i, handleUpdateConfig(bytes))
|
||||
}()
|
||||
}
|
||||
|
||||
//export startTUN
|
||||
func startTUN(fd C.int) *C.char {
|
||||
f := int(fd)
|
||||
return C.CString(handleStartTun(f))
|
||||
}
|
||||
|
||||
//export getRunTime
|
||||
func getRunTime() *C.char {
|
||||
return C.CString(handleGetRunTime())
|
||||
}
|
||||
|
||||
//export stopTun
|
||||
func stopTun() {
|
||||
handleStopTun()
|
||||
}
|
||||
|
||||
//export setFdMap
|
||||
func setFdMap(fdIdChar *C.char) {
|
||||
fdId := C.GoString(fdIdChar)
|
||||
handleSetFdMap(fdId)
|
||||
}
|
||||
|
||||
//export getCurrentProfileName
|
||||
func getCurrentProfileName() *C.char {
|
||||
return C.CString(handleGetCurrentProfileName())
|
||||
}
|
||||
|
||||
//export getAndroidVpnOptions
|
||||
func getAndroidVpnOptions() *C.char {
|
||||
return C.CString(handleGetAndroidVpnOptions())
|
||||
}
|
||||
|
||||
//export setState
|
||||
func setState(s *C.char) {
|
||||
paramsString := C.GoString(s)
|
||||
err := json.Unmarshal([]byte(paramsString), state.CurrentState)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
handleSetState(paramsString)
|
||||
}
|
||||
|
||||
//export updateDns
|
||||
func updateDns(s *C.char) {
|
||||
dnsList := C.GoString(s)
|
||||
go func() {
|
||||
log.Infoln("[DNS] updateDns %s", dnsList)
|
||||
dns.UpdateSystemDNS(strings.Split(dnsList, ","))
|
||||
dns.FlushCacheWithDefaultResolver()
|
||||
}()
|
||||
handleUpdateDns(dnsList)
|
||||
}
|
||||
|
||||
//export setProcessMap
|
||||
func setProcessMap(s *C.char) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
paramsString := C.GoString(s)
|
||||
handleSetProcessMap(paramsString)
|
||||
}
|
||||
|
||||
11
core/lib_no_android.go
Normal file
11
core/lib_no_android.go
Normal file
@@ -0,0 +1,11 @@
|
||||
//go:build !android && cgo
|
||||
|
||||
package main
|
||||
|
||||
func nextHandle(action *Action) {
|
||||
return action
|
||||
}
|
||||
|
||||
func nextHandle(action *Action, send func([]byte)) bool {
|
||||
return false
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
//go:build !cgo
|
||||
|
||||
package main
|
||||
|
||||
func SendMessage(message Message) {
|
||||
s, err := message.Json()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
Action{
|
||||
Method: messageMethod,
|
||||
}.callback(s)
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
//go:build cgo
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
bridge "core/dart-bridge"
|
||||
)
|
||||
|
||||
var (
|
||||
Port int64 = -1
|
||||
ServicePort int64 = -1
|
||||
)
|
||||
|
||||
func SendMessage(message Message) {
|
||||
s, err := message.Json()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if handler, ok := messageHandlers[message.Type]; ok {
|
||||
handler(s)
|
||||
} else {
|
||||
sendToPort(s)
|
||||
}
|
||||
}
|
||||
|
||||
var messageHandlers = map[MessageType]func(string) bool{
|
||||
ProtectMessage: sendToServicePort,
|
||||
ProcessMessage: sendToServicePort,
|
||||
StartedMessage: conditionalSend,
|
||||
LoadedMessage: conditionalSend,
|
||||
}
|
||||
|
||||
func sendToPort(s string) bool {
|
||||
return bridge.SendToPort(Port, s)
|
||||
}
|
||||
|
||||
func sendToServicePort(s string) bool {
|
||||
return bridge.SendToPort(ServicePort, s)
|
||||
}
|
||||
|
||||
func conditionalSend(s string) bool {
|
||||
isSuccess := sendToPort(s)
|
||||
if !isSuccess {
|
||||
return sendToServicePort(s)
|
||||
}
|
||||
return isSuccess
|
||||
}
|
||||
140
core/server.go
140
core/server.go
@@ -10,10 +10,29 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var conn net.Conn = nil
|
||||
var conn net.Conn
|
||||
|
||||
func sendMessage(message Message) {
|
||||
res, err := message.Json()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
send(Action{
|
||||
Method: messageMethod,
|
||||
}.wrapMessage(res))
|
||||
}
|
||||
|
||||
func send(data []byte) {
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
_, _ = conn.Write(append(data, []byte("\n")...))
|
||||
}
|
||||
|
||||
func startServer(arg string) {
|
||||
|
||||
_, err := strconv.Atoi(arg)
|
||||
|
||||
if err != nil {
|
||||
conn, err = net.Dial("unix", arg)
|
||||
} else {
|
||||
@@ -42,121 +61,12 @@ func startServer(arg string) {
|
||||
return
|
||||
}
|
||||
|
||||
go handleAction(action)
|
||||
go handleAction(action, func(bytes []byte) {
|
||||
send(bytes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func handleAction(action *Action) {
|
||||
switch action.Method {
|
||||
case initClashMethod:
|
||||
data := action.Data.(string)
|
||||
action.callback(handleInitClash(data))
|
||||
return
|
||||
case getIsInitMethod:
|
||||
action.callback(handleGetIsInit())
|
||||
return
|
||||
case forceGcMethod:
|
||||
handleForceGc()
|
||||
return
|
||||
case shutdownMethod:
|
||||
action.callback(handleShutdown())
|
||||
return
|
||||
case validateConfigMethod:
|
||||
data := []byte(action.Data.(string))
|
||||
action.callback(handleValidateConfig(data))
|
||||
return
|
||||
case updateConfigMethod:
|
||||
data := []byte(action.Data.(string))
|
||||
action.callback(handleUpdateConfig(data))
|
||||
return
|
||||
case getProxiesMethod:
|
||||
action.callback(handleGetProxies())
|
||||
return
|
||||
case changeProxyMethod:
|
||||
data := action.Data.(string)
|
||||
handleChangeProxy(data, func(value string) {
|
||||
action.callback(value)
|
||||
})
|
||||
return
|
||||
case getTrafficMethod:
|
||||
data := action.Data.(bool)
|
||||
action.callback(handleGetTraffic(data))
|
||||
return
|
||||
case getTotalTrafficMethod:
|
||||
data := action.Data.(bool)
|
||||
action.callback(handleGetTotalTraffic(data))
|
||||
return
|
||||
case resetTrafficMethod:
|
||||
handleResetTraffic()
|
||||
return
|
||||
case asyncTestDelayMethod:
|
||||
data := action.Data.(string)
|
||||
handleAsyncTestDelay(data, func(value string) {
|
||||
action.callback(value)
|
||||
})
|
||||
return
|
||||
case getConnectionsMethod:
|
||||
action.callback(handleGetConnections())
|
||||
return
|
||||
case closeConnectionsMethod:
|
||||
action.callback(handleCloseConnections())
|
||||
return
|
||||
case closeConnectionMethod:
|
||||
id := action.Data.(string)
|
||||
action.callback(handleCloseConnection(id))
|
||||
return
|
||||
case getExternalProvidersMethod:
|
||||
action.callback(handleGetExternalProviders())
|
||||
return
|
||||
case getExternalProviderMethod:
|
||||
externalProviderName := action.Data.(string)
|
||||
action.callback(handleGetExternalProvider(externalProviderName))
|
||||
case updateGeoDataMethod:
|
||||
paramsString := action.Data.(string)
|
||||
var params = map[string]string{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
action.callback(err.Error())
|
||||
return
|
||||
}
|
||||
geoType := params["geoType"]
|
||||
geoName := params["geoName"]
|
||||
handleUpdateGeoData(geoType, geoName, func(value string) {
|
||||
action.callback(value)
|
||||
})
|
||||
return
|
||||
case updateExternalProviderMethod:
|
||||
providerName := action.Data.(string)
|
||||
handleUpdateExternalProvider(providerName, func(value string) {
|
||||
action.callback(value)
|
||||
})
|
||||
return
|
||||
case sideLoadExternalProviderMethod:
|
||||
paramsString := action.Data.(string)
|
||||
var params = map[string]string{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
action.callback(err.Error())
|
||||
return
|
||||
}
|
||||
providerName := params["providerName"]
|
||||
data := params["data"]
|
||||
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
|
||||
action.callback(value)
|
||||
})
|
||||
return
|
||||
case startLogMethod:
|
||||
handleStartLog()
|
||||
return
|
||||
case stopLogMethod:
|
||||
handleStopLog()
|
||||
return
|
||||
case startListenerMethod:
|
||||
action.callback(handleStartListener())
|
||||
return
|
||||
case stopListenerMethod:
|
||||
action.callback(handleStopListener())
|
||||
return
|
||||
}
|
||||
|
||||
func nextHandle(action *Action, send func([]byte)) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
@@ -59,22 +58,15 @@ class Application extends StatefulWidget {
|
||||
|
||||
class ApplicationState extends State<Application> {
|
||||
late SystemColorSchemes systemColorSchemes;
|
||||
Timer? timer;
|
||||
Timer? _autoUpdateGroupTaskTimer;
|
||||
Timer? _autoUpdateProfilesTaskTimer;
|
||||
|
||||
final _pageTransitionsTheme = const PageTransitionsTheme(
|
||||
builders: <TargetPlatform, PageTransitionsBuilder>{
|
||||
TargetPlatform.android: SharedAxisPageTransitionsBuilder(
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
),
|
||||
TargetPlatform.windows: SharedAxisPageTransitionsBuilder(
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
),
|
||||
TargetPlatform.linux: SharedAxisPageTransitionsBuilder(
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
),
|
||||
TargetPlatform.macOS: SharedAxisPageTransitionsBuilder(
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
),
|
||||
TargetPlatform.android: CommonPageTransitionsBuilder(),
|
||||
TargetPlatform.windows: CommonPageTransitionsBuilder(),
|
||||
TargetPlatform.linux: CommonPageTransitionsBuilder(),
|
||||
TargetPlatform.macOS: CommonPageTransitionsBuilder(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -96,7 +88,8 @@ class ApplicationState extends State<Application> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initTimer();
|
||||
_autoUpdateGroupTask();
|
||||
_autoUpdateProfilesTask();
|
||||
globalState.appController = AppController(context);
|
||||
globalState.measure = Measure.of(context);
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
@@ -110,29 +103,29 @@ class ApplicationState extends State<Application> {
|
||||
});
|
||||
}
|
||||
|
||||
_initTimer() {
|
||||
_cancelTimer();
|
||||
timer = Timer.periodic(const Duration(milliseconds: 20000), (_) {
|
||||
_autoUpdateGroupTask() {
|
||||
_autoUpdateGroupTaskTimer = Timer(const Duration(milliseconds: 20000), () {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
globalState.appController.updateGroupDebounce();
|
||||
globalState.appController.updateGroupsDebounce();
|
||||
_autoUpdateGroupTask();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_cancelTimer() {
|
||||
if (timer != null) {
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
}
|
||||
_autoUpdateProfilesTask() {
|
||||
_autoUpdateProfilesTaskTimer = Timer(const Duration(minutes: 20), () async {
|
||||
await globalState.appController.autoUpdateProfiles();
|
||||
_autoUpdateProfilesTask();
|
||||
});
|
||||
}
|
||||
|
||||
_buildApp(Widget app) {
|
||||
_buildPlatformWrap(Widget child) {
|
||||
if (system.isDesktop) {
|
||||
return WindowManager(
|
||||
child: TrayManager(
|
||||
child: HotKeyManager(
|
||||
child: ProxyManager(
|
||||
child: app,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -140,7 +133,7 @@ class ApplicationState extends State<Application> {
|
||||
}
|
||||
return AndroidManager(
|
||||
child: TileManager(
|
||||
child: app,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -156,6 +149,20 @@ class ApplicationState extends State<Application> {
|
||||
);
|
||||
}
|
||||
|
||||
_buildWrap(Widget child) {
|
||||
return AppStateManager(
|
||||
child: ClashManager(
|
||||
child: ConnectivityManager(
|
||||
onConnectivityChanged: () {
|
||||
globalState.appController.updateLocalIp();
|
||||
globalState.appController.addCheckIpNumDebounce();
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_updateSystemColorSchemes(
|
||||
ColorScheme? lightDynamic,
|
||||
ColorScheme? darkDynamic,
|
||||
@@ -171,31 +178,31 @@ class ApplicationState extends State<Application> {
|
||||
|
||||
@override
|
||||
Widget build(context) {
|
||||
return _buildApp(
|
||||
AppStateManager(
|
||||
child: ClashManager(
|
||||
child: Selector2<AppState, Config, ApplicationSelectorState>(
|
||||
selector: (_, appState, config) => ApplicationSelectorState(
|
||||
locale: config.appSetting.locale,
|
||||
themeMode: config.themeProps.themeMode,
|
||||
primaryColor: config.themeProps.primaryColor,
|
||||
prueBlack: config.themeProps.prueBlack,
|
||||
fontFamily: config.themeProps.fontFamily,
|
||||
),
|
||||
builder: (_, state, child) {
|
||||
return DynamicColorBuilder(
|
||||
builder: (lightDynamic, darkDynamic) {
|
||||
_updateSystemColorSchemes(lightDynamic, darkDynamic);
|
||||
return MaterialApp(
|
||||
navigatorKey: globalState.navigatorKey,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate
|
||||
],
|
||||
builder: (_, child) {
|
||||
return LayoutBuilder(
|
||||
return _buildPlatformWrap(
|
||||
_buildWrap(
|
||||
Selector2<AppState, Config, ApplicationSelectorState>(
|
||||
selector: (_, appState, config) => ApplicationSelectorState(
|
||||
locale: config.appSetting.locale,
|
||||
themeMode: config.themeProps.themeMode,
|
||||
primaryColor: config.themeProps.primaryColor,
|
||||
prueBlack: config.themeProps.prueBlack,
|
||||
fontFamily: config.themeProps.fontFamily,
|
||||
),
|
||||
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 MessageManager(
|
||||
child: LayoutBuilder(
|
||||
builder: (_, container) {
|
||||
final appController = globalState.appController;
|
||||
final maxWidth = container.maxWidth;
|
||||
@@ -204,41 +211,40 @@ class ApplicationState extends State<Application> {
|
||||
}
|
||||
return _buildPage(child!);
|
||||
},
|
||||
);
|
||||
},
|
||||
scrollBehavior: BaseScrollBehavior(),
|
||||
title: appName,
|
||||
locale: other.getLocaleForString(state.locale),
|
||||
supportedLocales:
|
||||
AppLocalizations.delegate.supportedLocales,
|
||||
themeMode: state.themeMode,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
fontFamily: state.fontFamily.value,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.light,
|
||||
systemColorSchemes: systemColorSchemes,
|
||||
primaryColor: state.primaryColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
scrollBehavior: BaseScrollBehavior(),
|
||||
title: appName,
|
||||
locale: other.getLocaleForString(state.locale),
|
||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||
themeMode: state.themeMode,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
fontFamily: state.fontFamily.value,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.light,
|
||||
systemColorSchemes: systemColorSchemes,
|
||||
primaryColor: state.primaryColor,
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
fontFamily: state.fontFamily.value,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
systemColorSchemes: systemColorSchemes,
|
||||
primaryColor: state.primaryColor,
|
||||
).toPrueBlack(state.prueBlack),
|
||||
),
|
||||
home: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const HomePage(),
|
||||
),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
fontFamily: state.fontFamily.value,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
systemColorSchemes: systemColorSchemes,
|
||||
primaryColor: state.primaryColor,
|
||||
).toPrueBlack(state.prueBlack),
|
||||
),
|
||||
home: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const HomePage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -247,8 +253,9 @@ class ApplicationState extends State<Application> {
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
linkManager.destroy();
|
||||
_cancelTimer();
|
||||
await clashService?.destroy();
|
||||
_autoUpdateGroupTaskTimer?.cancel();
|
||||
_autoUpdateProfilesTaskTimer?.cancel();
|
||||
await clashCore.destroy();
|
||||
await globalState.appController.savePreferences();
|
||||
await globalState.appController.handleExit();
|
||||
super.dispose();
|
||||
|
||||
@@ -13,7 +13,7 @@ import 'package:path/path.dart';
|
||||
|
||||
class ClashCore {
|
||||
static ClashCore? _instance;
|
||||
late ClashInterface clashInterface;
|
||||
late ClashHandlerInterface clashInterface;
|
||||
|
||||
ClashCore._internal() {
|
||||
if (Platform.isAndroid) {
|
||||
@@ -28,8 +28,12 @@ class ClashCore {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<void> _initGeo() async {
|
||||
final homePath = await appPath.getHomeDirPath();
|
||||
Future<bool> preload() {
|
||||
return clashInterface.preload();
|
||||
}
|
||||
|
||||
static Future<void> initGeo() async {
|
||||
final homePath = await appPath.homeDirPath;
|
||||
final homeDir = Directory(homePath);
|
||||
final isExists = await homeDir.exists();
|
||||
if (!isExists) {
|
||||
@@ -63,8 +67,8 @@ class ClashCore {
|
||||
required ClashConfig clashConfig,
|
||||
required Config config,
|
||||
}) async {
|
||||
await _initGeo();
|
||||
final homeDirPath = await appPath.getHomeDirPath();
|
||||
await initGeo();
|
||||
final homeDirPath = await appPath.homeDirPath;
|
||||
return await clashInterface.init(homeDirPath);
|
||||
}
|
||||
|
||||
@@ -135,6 +139,9 @@ class ClashCore {
|
||||
Future<List<ExternalProvider>> getExternalProviders() async {
|
||||
final externalProvidersRawString =
|
||||
await clashInterface.getExternalProviders();
|
||||
if (externalProvidersRawString.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
return Isolate.run<List<ExternalProvider>>(
|
||||
() {
|
||||
final externalProviders =
|
||||
@@ -152,7 +159,7 @@ class ClashCore {
|
||||
String externalProviderName) async {
|
||||
final externalProvidersRawString =
|
||||
await clashInterface.getExternalProvider(externalProviderName);
|
||||
if (externalProvidersRawString == null) {
|
||||
if (externalProvidersRawString.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (externalProvidersRawString.isEmpty) {
|
||||
@@ -161,11 +168,8 @@ class ClashCore {
|
||||
return ExternalProvider.fromJson(json.decode(externalProvidersRawString));
|
||||
}
|
||||
|
||||
Future<String> updateGeoData({
|
||||
required String geoType,
|
||||
required String geoName,
|
||||
}) {
|
||||
return clashInterface.updateGeoData(geoType: geoType, geoName: geoName);
|
||||
Future<String> updateGeoData(UpdateGeoDataParams params) {
|
||||
return clashInterface.updateGeoData(params);
|
||||
}
|
||||
|
||||
Future<String> sideLoadExternalProvider({
|
||||
@@ -190,21 +194,46 @@ class ClashCore {
|
||||
await clashInterface.stopListener();
|
||||
}
|
||||
|
||||
Future<Delay> getDelay(String proxyName) async {
|
||||
final data = await clashInterface.asyncTestDelay(proxyName);
|
||||
Future<Delay> getDelay(String url, String proxyName) async {
|
||||
final data = await clashInterface.asyncTestDelay(url, proxyName);
|
||||
return Delay.fromJson(json.decode(data));
|
||||
}
|
||||
|
||||
Future<Traffic> getTraffic(bool value) async {
|
||||
final trafficString = await clashInterface.getTraffic(value);
|
||||
Future<Traffic> getTraffic() async {
|
||||
final trafficString = await clashInterface.getTraffic();
|
||||
if (trafficString.isEmpty) {
|
||||
return Traffic();
|
||||
}
|
||||
return Traffic.fromMap(json.decode(trafficString));
|
||||
}
|
||||
|
||||
Future<Traffic> getTotalTraffic(bool value) async {
|
||||
final totalTrafficString = await clashInterface.getTotalTraffic(value);
|
||||
Future<IpInfo?> getCountryCode(String ip) async {
|
||||
final countryCode = await clashInterface.getCountryCode(ip);
|
||||
if (countryCode.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return IpInfo(
|
||||
ip: ip,
|
||||
countryCode: countryCode,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Traffic> getTotalTraffic() async {
|
||||
final totalTrafficString = await clashInterface.getTotalTraffic();
|
||||
if (totalTrafficString.isEmpty) {
|
||||
return Traffic();
|
||||
}
|
||||
return Traffic.fromMap(json.decode(totalTrafficString));
|
||||
}
|
||||
|
||||
Future<int> getMemory() async {
|
||||
final value = await clashInterface.getMemory();
|
||||
if (value.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
return int.parse(value);
|
||||
}
|
||||
|
||||
resetTraffic() {
|
||||
clashInterface.resetTraffic();
|
||||
}
|
||||
@@ -220,6 +249,10 @@ class ClashCore {
|
||||
requestGc() {
|
||||
clashInterface.forceGc();
|
||||
}
|
||||
|
||||
destroy() async {
|
||||
await clashInterface.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
final clashCore = ClashCore();
|
||||
|
||||
@@ -2348,20 +2348,6 @@ class ClashFFI {
|
||||
|
||||
set suboptarg(ffi.Pointer<ffi.Char> value) => _suboptarg.value = value;
|
||||
|
||||
void updateDns(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
) {
|
||||
return _updateDns(
|
||||
s,
|
||||
);
|
||||
}
|
||||
|
||||
late final _updateDnsPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'updateDns');
|
||||
late final _updateDns =
|
||||
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
void initNativeApiBridge(
|
||||
ffi.Pointer<ffi.Void> api,
|
||||
) {
|
||||
@@ -2376,18 +2362,39 @@ class ClashFFI {
|
||||
late final _initNativeApiBridge = _initNativeApiBridgePtr
|
||||
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
|
||||
|
||||
void initMessage(
|
||||
int port,
|
||||
void attachMessagePort(
|
||||
int mPort,
|
||||
) {
|
||||
return _initMessage(
|
||||
port,
|
||||
return _attachMessagePort(
|
||||
mPort,
|
||||
);
|
||||
}
|
||||
|
||||
late final _initMessagePtr =
|
||||
late final _attachMessagePortPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
|
||||
'initMessage');
|
||||
late final _initMessage = _initMessagePtr.asFunction<void Function(int)>();
|
||||
'attachMessagePort');
|
||||
late final _attachMessagePort =
|
||||
_attachMessagePortPtr.asFunction<void Function(int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getTraffic() {
|
||||
return _getTraffic();
|
||||
}
|
||||
|
||||
late final _getTrafficPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
||||
'getTraffic');
|
||||
late final _getTraffic =
|
||||
_getTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getTotalTraffic() {
|
||||
return _getTotalTraffic();
|
||||
}
|
||||
|
||||
late final _getTotalTrafficPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
||||
'getTotalTraffic');
|
||||
late final _getTotalTraffic =
|
||||
_getTotalTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
void freeCString(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
@@ -2403,19 +2410,22 @@ class ClashFFI {
|
||||
late final _freeCString =
|
||||
_freeCStringPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
int initClash(
|
||||
ffi.Pointer<ffi.Char> homeDirStr,
|
||||
void invokeAction(
|
||||
ffi.Pointer<ffi.Char> paramsChar,
|
||||
int port,
|
||||
) {
|
||||
return _initClash(
|
||||
homeDirStr,
|
||||
return _invokeAction(
|
||||
paramsChar,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _initClashPtr =
|
||||
_lookup<ffi.NativeFunction<GoUint8 Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'initClash');
|
||||
late final _initClash =
|
||||
_initClashPtr.asFunction<int Function(ffi.Pointer<ffi.Char>)>();
|
||||
late final _invokeActionPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('invokeAction');
|
||||
late final _invokeAction =
|
||||
_invokeActionPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
void startListener() {
|
||||
return _startListener();
|
||||
@@ -2433,288 +2443,55 @@ class ClashFFI {
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopListener');
|
||||
late final _stopListener = _stopListenerPtr.asFunction<void Function()>();
|
||||
|
||||
int getIsInit() {
|
||||
return _getIsInit();
|
||||
void attachInvokePort(
|
||||
int mPort,
|
||||
) {
|
||||
return _attachInvokePort(
|
||||
mPort,
|
||||
);
|
||||
}
|
||||
|
||||
late final _getIsInitPtr =
|
||||
_lookup<ffi.NativeFunction<GoUint8 Function()>>('getIsInit');
|
||||
late final _getIsInit = _getIsInitPtr.asFunction<int Function()>();
|
||||
late final _attachInvokePortPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
|
||||
'attachInvokePort');
|
||||
late final _attachInvokePort =
|
||||
_attachInvokePortPtr.asFunction<void Function(int)>();
|
||||
|
||||
int shutdownClash() {
|
||||
return _shutdownClash();
|
||||
}
|
||||
|
||||
late final _shutdownClashPtr =
|
||||
_lookup<ffi.NativeFunction<GoUint8 Function()>>('shutdownClash');
|
||||
late final _shutdownClash = _shutdownClashPtr.asFunction<int Function()>();
|
||||
|
||||
void forceGc() {
|
||||
return _forceGc();
|
||||
}
|
||||
|
||||
late final _forceGcPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('forceGc');
|
||||
late final _forceGc = _forceGcPtr.asFunction<void Function()>();
|
||||
|
||||
void validateConfig(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
void quickStart(
|
||||
ffi.Pointer<ffi.Char> dirChar,
|
||||
ffi.Pointer<ffi.Char> paramsChar,
|
||||
ffi.Pointer<ffi.Char> stateParamsChar,
|
||||
int port,
|
||||
) {
|
||||
return _validateConfig(
|
||||
s,
|
||||
return _quickStart(
|
||||
dirChar,
|
||||
paramsChar,
|
||||
stateParamsChar,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _validateConfigPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('validateConfig');
|
||||
late final _validateConfig = _validateConfigPtr
|
||||
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
void updateConfig(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
int port,
|
||||
) {
|
||||
return _updateConfig(
|
||||
s,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _updateConfigPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('updateConfig');
|
||||
late final _updateConfig =
|
||||
_updateConfigPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getProxies() {
|
||||
return _getProxies();
|
||||
}
|
||||
|
||||
late final _getProxiesPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
||||
'getProxies');
|
||||
late final _getProxies =
|
||||
_getProxiesPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
void changeProxy(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
int port,
|
||||
) {
|
||||
return _changeProxy(
|
||||
s,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _changeProxyPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('changeProxy');
|
||||
late final _changeProxy =
|
||||
_changeProxyPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getTraffic(
|
||||
int port,
|
||||
) {
|
||||
return _getTraffic(
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _getTrafficPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
|
||||
'getTraffic');
|
||||
late final _getTraffic =
|
||||
_getTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getTotalTraffic(
|
||||
int port,
|
||||
) {
|
||||
return _getTotalTraffic(
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _getTotalTrafficPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
|
||||
'getTotalTraffic');
|
||||
late final _getTotalTraffic =
|
||||
_getTotalTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
|
||||
|
||||
void resetTraffic() {
|
||||
return _resetTraffic();
|
||||
}
|
||||
|
||||
late final _resetTrafficPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('resetTraffic');
|
||||
late final _resetTraffic = _resetTrafficPtr.asFunction<void Function()>();
|
||||
|
||||
void asyncTestDelay(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
int port,
|
||||
) {
|
||||
return _asyncTestDelay(
|
||||
s,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _asyncTestDelayPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('asyncTestDelay');
|
||||
late final _asyncTestDelay = _asyncTestDelayPtr
|
||||
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getConnections() {
|
||||
return _getConnections();
|
||||
}
|
||||
|
||||
late final _getConnectionsPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
||||
'getConnections');
|
||||
late final _getConnections =
|
||||
_getConnectionsPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
void closeConnections() {
|
||||
return _closeConnections();
|
||||
}
|
||||
|
||||
late final _closeConnectionsPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('closeConnections');
|
||||
late final _closeConnections =
|
||||
_closeConnectionsPtr.asFunction<void Function()>();
|
||||
|
||||
void closeConnection(
|
||||
ffi.Pointer<ffi.Char> id,
|
||||
) {
|
||||
return _closeConnection(
|
||||
id,
|
||||
);
|
||||
}
|
||||
|
||||
late final _closeConnectionPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'closeConnection');
|
||||
late final _closeConnection =
|
||||
_closeConnectionPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getExternalProviders() {
|
||||
return _getExternalProviders();
|
||||
}
|
||||
|
||||
late final _getExternalProvidersPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
||||
'getExternalProviders');
|
||||
late final _getExternalProviders =
|
||||
_getExternalProvidersPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getExternalProvider(
|
||||
ffi.Pointer<ffi.Char> externalProviderNameChar,
|
||||
) {
|
||||
return _getExternalProvider(
|
||||
externalProviderNameChar,
|
||||
);
|
||||
}
|
||||
|
||||
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> geoTypeChar,
|
||||
ffi.Pointer<ffi.Char> geoNameChar,
|
||||
int port,
|
||||
) {
|
||||
return _updateGeoData(
|
||||
geoTypeChar,
|
||||
geoNameChar,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _updateGeoDataPtr = _lookup<
|
||||
late final _quickStartPtr = _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)>();
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('quickStart');
|
||||
late final _quickStart = _quickStartPtr.asFunction<
|
||||
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
|
||||
ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
void updateExternalProvider(
|
||||
ffi.Pointer<ffi.Char> providerNameChar,
|
||||
int port,
|
||||
) {
|
||||
return _updateExternalProvider(
|
||||
providerNameChar,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _updateExternalProviderPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('updateExternalProvider');
|
||||
late final _updateExternalProvider = _updateExternalProviderPtr
|
||||
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
void sideLoadExternalProvider(
|
||||
ffi.Pointer<ffi.Char> providerNameChar,
|
||||
ffi.Pointer<ffi.Char> dataChar,
|
||||
int port,
|
||||
) {
|
||||
return _sideLoadExternalProvider(
|
||||
providerNameChar,
|
||||
dataChar,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _sideLoadExternalProviderPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
|
||||
ffi.LongLong)>>('sideLoadExternalProvider');
|
||||
late final _sideLoadExternalProvider =
|
||||
_sideLoadExternalProviderPtr.asFunction<
|
||||
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
void startLog() {
|
||||
return _startLog();
|
||||
}
|
||||
|
||||
late final _startLogPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('startLog');
|
||||
late final _startLog = _startLogPtr.asFunction<void Function()>();
|
||||
|
||||
void stopLog() {
|
||||
return _stopLog();
|
||||
}
|
||||
|
||||
late final _stopLogPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopLog');
|
||||
late final _stopLog = _stopLogPtr.asFunction<void Function()>();
|
||||
|
||||
void startTUN(
|
||||
ffi.Pointer<ffi.Char> startTUN(
|
||||
int fd,
|
||||
int port,
|
||||
) {
|
||||
return _startTUN(
|
||||
fd,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _startTUNPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int, ffi.LongLong)>>(
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
|
||||
'startTUN');
|
||||
late final _startTUN = _startTUNPtr.asFunction<void Function(int, int)>();
|
||||
late final _startTUN =
|
||||
_startTUNPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getRunTime() {
|
||||
return _getRunTime();
|
||||
@@ -2735,30 +2512,18 @@ class ClashFFI {
|
||||
late final _stopTun = _stopTunPtr.asFunction<void Function()>();
|
||||
|
||||
void setFdMap(
|
||||
int fd,
|
||||
ffi.Pointer<ffi.Char> fdIdChar,
|
||||
) {
|
||||
return _setFdMap(
|
||||
fd,
|
||||
fdIdChar,
|
||||
);
|
||||
}
|
||||
|
||||
late final _setFdMapPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Long)>>('setFdMap');
|
||||
late final _setFdMap = _setFdMapPtr.asFunction<void Function(int)>();
|
||||
|
||||
void setProcessMap(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
) {
|
||||
return _setProcessMap(
|
||||
s,
|
||||
);
|
||||
}
|
||||
|
||||
late final _setProcessMapPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'setProcessMap');
|
||||
late final _setProcessMap =
|
||||
_setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
'setFdMap');
|
||||
late final _setFdMap =
|
||||
_setFdMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getCurrentProfileName() {
|
||||
return _getCurrentProfileName();
|
||||
@@ -2793,6 +2558,34 @@ class ClashFFI {
|
||||
'setState');
|
||||
late final _setState =
|
||||
_setStatePtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
void updateDns(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
) {
|
||||
return _updateDns(
|
||||
s,
|
||||
);
|
||||
}
|
||||
|
||||
late final _updateDnsPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'updateDns');
|
||||
late final _updateDns =
|
||||
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
void setProcessMap(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
) {
|
||||
return _setProcessMap(
|
||||
s,
|
||||
);
|
||||
}
|
||||
|
||||
late final _setProcessMapPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'setProcessMap');
|
||||
late final _setProcessMap =
|
||||
_setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
}
|
||||
|
||||
final class __mbstate_t extends ffi.Union {
|
||||
@@ -3965,8 +3758,6 @@ final class GoSlice extends ffi.Struct {
|
||||
typedef GoInt = GoInt64;
|
||||
typedef GoInt64 = ffi.LongLong;
|
||||
typedef DartGoInt64 = int;
|
||||
typedef GoUint8 = ffi.UnsignedChar;
|
||||
typedef DartGoUint8 = int;
|
||||
|
||||
const int __has_safe_buffers = 1;
|
||||
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_clash/clash/message.dart';
|
||||
import 'package:fl_clash/common/constant.dart';
|
||||
import 'package:fl_clash/common/future.dart';
|
||||
import 'package:fl_clash/common/other.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:flutter/material.dart' hide Action;
|
||||
|
||||
mixin ClashInterface {
|
||||
FutureOr<bool> init(String homeDir);
|
||||
Future<bool> init(String homeDir);
|
||||
|
||||
FutureOr<void> shutdown();
|
||||
Future<bool> preload();
|
||||
|
||||
FutureOr<bool> get isInit;
|
||||
Future<bool> shutdown();
|
||||
|
||||
forceGc();
|
||||
Future<bool> get isInit;
|
||||
|
||||
Future<bool> forceGc();
|
||||
|
||||
FutureOr<String> validateConfig(String data);
|
||||
|
||||
Future<String> asyncTestDelay(String proxyName);
|
||||
Future<String> asyncTestDelay(String url, String proxyName);
|
||||
|
||||
FutureOr<String> updateConfig(UpdateConfigParams updateConfigParams);
|
||||
|
||||
@@ -29,10 +38,7 @@ mixin ClashInterface {
|
||||
|
||||
FutureOr<String>? getExternalProvider(String externalProviderName);
|
||||
|
||||
Future<String> updateGeoData({
|
||||
required String geoType,
|
||||
required String geoName,
|
||||
});
|
||||
Future<String> updateGeoData(UpdateGeoDataParams params);
|
||||
|
||||
Future<String> sideLoadExternalProvider({
|
||||
required String providerName,
|
||||
@@ -41,9 +47,13 @@ mixin ClashInterface {
|
||||
|
||||
Future<String> updateExternalProvider(String providerName);
|
||||
|
||||
FutureOr<String> getTraffic(bool value);
|
||||
FutureOr<String> getTraffic();
|
||||
|
||||
FutureOr<String> getTotalTraffic(bool value);
|
||||
FutureOr<String> getTotalTraffic();
|
||||
|
||||
FutureOr<String> getCountryCode(String ip);
|
||||
|
||||
FutureOr<String> getMemory();
|
||||
|
||||
resetTraffic();
|
||||
|
||||
@@ -57,3 +67,339 @@ mixin ClashInterface {
|
||||
|
||||
FutureOr<bool> closeConnections();
|
||||
}
|
||||
|
||||
mixin AndroidClashInterface {
|
||||
Future<bool> setFdMap(int fd);
|
||||
|
||||
Future<bool> setProcessMap(ProcessMapItem item);
|
||||
|
||||
Future<bool> setState(CoreState state);
|
||||
|
||||
Future<bool> stopTun();
|
||||
|
||||
Future<bool> updateDns(String value);
|
||||
|
||||
Future<DateTime?> startTun(int fd);
|
||||
|
||||
Future<AndroidVpnOptions?> getAndroidVpnOptions();
|
||||
|
||||
Future<String> getCurrentProfileName();
|
||||
|
||||
Future<DateTime?> getRunTime();
|
||||
}
|
||||
|
||||
abstract class ClashHandlerInterface with ClashInterface {
|
||||
Map<String, Completer> callbackCompleterMap = {};
|
||||
|
||||
Future<bool> nextHandleResult(ActionResult result, Completer? completer) =>
|
||||
Future.value(false);
|
||||
|
||||
handleResult(ActionResult result) async {
|
||||
final completer = callbackCompleterMap[result.id];
|
||||
try {
|
||||
switch (result.method) {
|
||||
case ActionMethod.initClash:
|
||||
case ActionMethod.shutdown:
|
||||
case ActionMethod.getIsInit:
|
||||
case ActionMethod.startListener:
|
||||
case ActionMethod.resetTraffic:
|
||||
case ActionMethod.closeConnections:
|
||||
case ActionMethod.closeConnection:
|
||||
case ActionMethod.stopListener:
|
||||
completer?.complete(result.data as bool);
|
||||
return;
|
||||
case ActionMethod.changeProxy:
|
||||
case ActionMethod.getProxies:
|
||||
case ActionMethod.getTraffic:
|
||||
case ActionMethod.getTotalTraffic:
|
||||
case ActionMethod.asyncTestDelay:
|
||||
case ActionMethod.getConnections:
|
||||
case ActionMethod.getExternalProviders:
|
||||
case ActionMethod.getExternalProvider:
|
||||
case ActionMethod.validateConfig:
|
||||
case ActionMethod.updateConfig:
|
||||
case ActionMethod.updateGeoData:
|
||||
case ActionMethod.updateExternalProvider:
|
||||
case ActionMethod.sideLoadExternalProvider:
|
||||
case ActionMethod.getCountryCode:
|
||||
case ActionMethod.getMemory:
|
||||
completer?.complete(result.data as String);
|
||||
return;
|
||||
case ActionMethod.message:
|
||||
clashMessage.controller.add(result.data as String);
|
||||
completer?.complete(true);
|
||||
return;
|
||||
default:
|
||||
final isHandled = await nextHandleResult(result, completer);
|
||||
if (isHandled) {
|
||||
return;
|
||||
}
|
||||
completer?.complete(result.data);
|
||||
}
|
||||
} catch (_) {
|
||||
debugPrint(result.id);
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(String message);
|
||||
|
||||
reStart();
|
||||
|
||||
FutureOr<bool> destroy();
|
||||
|
||||
Future<T> invoke<T>({
|
||||
required ActionMethod method,
|
||||
dynamic data,
|
||||
Duration? timeout,
|
||||
FutureOr<T> Function()? onTimeout,
|
||||
}) async {
|
||||
final id = "${method.name}#${other.id}";
|
||||
|
||||
callbackCompleterMap[id] = Completer<T>();
|
||||
|
||||
dynamic defaultValue;
|
||||
|
||||
if (T == String) {
|
||||
defaultValue = "";
|
||||
}
|
||||
if (T == bool) {
|
||||
defaultValue = false;
|
||||
}
|
||||
|
||||
sendMessage(
|
||||
json.encode(
|
||||
Action(
|
||||
id: id,
|
||||
method: method,
|
||||
data: data,
|
||||
defaultValue: defaultValue,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return (callbackCompleterMap[id] as Completer<T>).safeFuture(
|
||||
timeout: timeout,
|
||||
onLast: () {
|
||||
callbackCompleterMap.remove(id);
|
||||
},
|
||||
onTimeout: onTimeout ??
|
||||
() {
|
||||
return defaultValue;
|
||||
},
|
||||
functionName: id,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> init(String homeDir) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.initClash,
|
||||
data: homeDir,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
shutdown() async {
|
||||
return await invoke<bool>(
|
||||
method: ActionMethod.shutdown,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> get isInit {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.getIsInit,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> forceGc() {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.forceGc,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> validateConfig(String data) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.validateConfig,
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
|
||||
return await invoke<String>(
|
||||
method: ActionMethod.updateConfig,
|
||||
data: json.encode(updateConfigParams),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getProxies() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getProxies,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.changeProxy,
|
||||
data: json.encode(changeProxyParams),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getExternalProviders() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getExternalProviders,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getExternalProvider(String externalProviderName) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getExternalProvider,
|
||||
data: externalProviderName,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> updateGeoData(UpdateGeoDataParams params) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.updateGeoData,
|
||||
data: json.encode(params),
|
||||
timeout: Duration(minutes: 1));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> sideLoadExternalProvider({
|
||||
required String providerName,
|
||||
required String data,
|
||||
}) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.sideLoadExternalProvider,
|
||||
data: json.encode({
|
||||
"providerName": providerName,
|
||||
"data": data,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> updateExternalProvider(String providerName) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.updateExternalProvider,
|
||||
data: providerName,
|
||||
timeout: Duration(minutes: 1),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getConnections() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getConnections,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> closeConnections() {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.closeConnections,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> closeConnection(String id) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.closeConnection,
|
||||
data: id,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getTotalTraffic() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getTotalTraffic,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getTraffic() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getTraffic,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
resetTraffic() {
|
||||
invoke(method: ActionMethod.resetTraffic);
|
||||
}
|
||||
|
||||
@override
|
||||
startLog() {
|
||||
invoke(method: ActionMethod.startLog);
|
||||
}
|
||||
|
||||
@override
|
||||
stopLog() {
|
||||
invoke<bool>(
|
||||
method: ActionMethod.stopLog,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> startListener() {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.startListener,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
stopListener() {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.stopListener,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> asyncTestDelay(String url, String proxyName) {
|
||||
final delayParams = {
|
||||
"proxy-name": proxyName,
|
||||
"timeout": httpTimeoutDuration.inMilliseconds,
|
||||
"test-url": url,
|
||||
};
|
||||
return invoke<String>(
|
||||
method: ActionMethod.asyncTestDelay,
|
||||
data: json.encode(delayParams),
|
||||
timeout: Duration(
|
||||
milliseconds: 6000,
|
||||
),
|
||||
onTimeout: () {
|
||||
return json.encode(
|
||||
Delay(
|
||||
name: proxyName,
|
||||
value: -1,
|
||||
url: url,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getCountryCode(String ip) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getCountryCode,
|
||||
data: ip,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getMemory() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getMemory,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,28 +3,58 @@ import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:fl_clash/common/constant.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/plugins/service.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
|
||||
import 'generated/clash_ffi.dart';
|
||||
import 'interface.dart';
|
||||
|
||||
class ClashLib with ClashInterface {
|
||||
class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
|
||||
static ClashLib? _instance;
|
||||
final receiver = ReceivePort();
|
||||
|
||||
late final ClashFFI clashFFI;
|
||||
|
||||
late final DynamicLibrary lib;
|
||||
Completer<bool> _canSendCompleter = Completer();
|
||||
SendPort? sendPort;
|
||||
final receiverPort = ReceivePort();
|
||||
|
||||
ClashLib._internal() {
|
||||
lib = DynamicLibrary.open("libclash.so");
|
||||
clashFFI = ClashFFI(lib);
|
||||
clashFFI.initNativeApiBridge(
|
||||
NativeApi.initializeApiDLData,
|
||||
);
|
||||
_initService();
|
||||
}
|
||||
|
||||
@override
|
||||
preload() {
|
||||
return _canSendCompleter.future;
|
||||
}
|
||||
|
||||
_initService() async {
|
||||
await service?.destroy();
|
||||
_registerMainPort(receiverPort.sendPort);
|
||||
receiverPort.listen((message) {
|
||||
if (message is SendPort) {
|
||||
if (_canSendCompleter.isCompleted) {
|
||||
sendPort = null;
|
||||
_canSendCompleter = Completer();
|
||||
}
|
||||
sendPort = message;
|
||||
_canSendCompleter.complete(true);
|
||||
} else {
|
||||
handleResult(
|
||||
ActionResult.fromJson(json.decode(
|
||||
message,
|
||||
)),
|
||||
);
|
||||
}
|
||||
});
|
||||
await service?.init();
|
||||
}
|
||||
|
||||
_registerMainPort(SendPort sendPort) {
|
||||
IsolateNameServer.removePortNameMapping(mainIsolate);
|
||||
IsolateNameServer.registerPortWithName(sendPort, mainIsolate);
|
||||
}
|
||||
|
||||
factory ClashLib() {
|
||||
@@ -32,227 +62,154 @@ class ClashLib with ClashInterface {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
initMessage() {
|
||||
clashFFI.initMessage(
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
@override
|
||||
Future<bool> nextHandleResult(result, completer) async {
|
||||
switch (result.method) {
|
||||
case ActionMethod.setFdMap:
|
||||
case ActionMethod.setProcessMap:
|
||||
case ActionMethod.setState:
|
||||
case ActionMethod.stopTun:
|
||||
case ActionMethod.updateDns:
|
||||
completer?.complete(result.data as bool);
|
||||
return true;
|
||||
case ActionMethod.getRunTime:
|
||||
case ActionMethod.startTun:
|
||||
case ActionMethod.getAndroidVpnOptions:
|
||||
case ActionMethod.getCurrentProfileName:
|
||||
completer?.complete(result.data as String);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool init(String homeDir) {
|
||||
final homeDirChar = homeDir.toNativeUtf8().cast<Char>();
|
||||
final isInit = clashFFI.initClash(homeDirChar) == 1;
|
||||
malloc.free(homeDirChar);
|
||||
return isInit;
|
||||
}
|
||||
|
||||
@override
|
||||
shutdown() async {
|
||||
clashFFI.shutdownClash();
|
||||
lib.close();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isInit => clashFFI.getIsInit() == 1;
|
||||
|
||||
@override
|
||||
Future<String> validateConfig(String data) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final dataChar = data.toNativeUtf8().cast<Char>();
|
||||
clashFFI.validateConfig(
|
||||
dataChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(dataChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> updateConfig(UpdateConfigParams updateConfigParams) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final params = json.encode(updateConfigParams);
|
||||
final paramsChar = params.toNativeUtf8().cast<Char>();
|
||||
clashFFI.updateConfig(
|
||||
paramsChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(paramsChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
String getProxies() {
|
||||
final proxiesRaw = clashFFI.getProxies();
|
||||
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(proxiesRaw);
|
||||
return proxiesRawString;
|
||||
}
|
||||
|
||||
@override
|
||||
String getExternalProviders() {
|
||||
final externalProvidersRaw = clashFFI.getExternalProviders();
|
||||
final externalProvidersRawString =
|
||||
externalProvidersRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(externalProvidersRaw);
|
||||
return externalProvidersRawString;
|
||||
}
|
||||
|
||||
@override
|
||||
String 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);
|
||||
return externalProviderRawString;
|
||||
}
|
||||
|
||||
@override
|
||||
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;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> sideLoadExternalProvider({
|
||||
required String providerName,
|
||||
required String data,
|
||||
}) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
|
||||
final dataChar = data.toNativeUtf8().cast<Char>();
|
||||
clashFFI.sideLoadExternalProvider(
|
||||
providerNameChar,
|
||||
dataChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(providerNameChar);
|
||||
malloc.free(dataChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> updateExternalProvider(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,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(providerNameChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> changeProxy(ChangeProxyParams changeProxyParams) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final params = json.encode(changeProxyParams);
|
||||
final paramsChar = params.toNativeUtf8().cast<Char>();
|
||||
clashFFI.changeProxy(
|
||||
paramsChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(paramsChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
String getConnections() {
|
||||
final connectionsDataRaw = clashFFI.getConnections();
|
||||
final connectionsString = connectionsDataRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(connectionsDataRaw);
|
||||
return connectionsString;
|
||||
}
|
||||
|
||||
@override
|
||||
closeConnection(String id) {
|
||||
final idChar = id.toNativeUtf8().cast<Char>();
|
||||
clashFFI.closeConnection(idChar);
|
||||
malloc.free(idChar);
|
||||
destroy() async {
|
||||
await service?.destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
closeConnections() {
|
||||
clashFFI.closeConnections();
|
||||
reStart() {
|
||||
_initService();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> shutdown() async {
|
||||
await super.shutdown();
|
||||
destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
startListener() async {
|
||||
clashFFI.startListener();
|
||||
return true;
|
||||
sendMessage(String message) async {
|
||||
await _canSendCompleter.future;
|
||||
sendPort?.send(message);
|
||||
}
|
||||
|
||||
@override
|
||||
stopListener() async {
|
||||
clashFFI.stopListener();
|
||||
return true;
|
||||
Future<bool> setFdMap(int fd) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.setFdMap,
|
||||
data: json.encode(fd),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> asyncTestDelay(String proxyName) {
|
||||
final delayParams = {
|
||||
"proxy-name": proxyName,
|
||||
"timeout": httpTimeoutDuration.inMilliseconds,
|
||||
};
|
||||
Future<bool> setProcessMap(item) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.setProcessMap,
|
||||
data: item,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> setState(CoreState state) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.setState,
|
||||
data: json.encode(state),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DateTime?> startTun(int fd) async {
|
||||
final res = await invoke<String>(
|
||||
method: ActionMethod.startTun,
|
||||
data: json.encode(fd),
|
||||
);
|
||||
|
||||
if (res.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return DateTime.fromMillisecondsSinceEpoch(int.parse(res));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> stopTun() {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.stopTun,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AndroidVpnOptions?> getAndroidVpnOptions() async {
|
||||
final res = await invoke<String>(
|
||||
method: ActionMethod.getAndroidVpnOptions,
|
||||
);
|
||||
if (res.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return AndroidVpnOptions.fromJson(json.decode(res));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> updateDns(String value) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.updateDns,
|
||||
data: value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DateTime?> getRunTime() async {
|
||||
final runTimeString = await invoke<String>(
|
||||
method: ActionMethod.getRunTime,
|
||||
);
|
||||
if (runTimeString.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getCurrentProfileName() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getCurrentProfileName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ClashLibHandler {
|
||||
static ClashLibHandler? _instance;
|
||||
|
||||
late final ClashFFI clashFFI;
|
||||
|
||||
late final DynamicLibrary lib;
|
||||
|
||||
ClashLibHandler._internal() {
|
||||
lib = DynamicLibrary.open("libclash.so");
|
||||
clashFFI = ClashFFI(lib);
|
||||
clashFFI.initNativeApiBridge(
|
||||
NativeApi.initializeApiDLData,
|
||||
);
|
||||
}
|
||||
|
||||
factory ClashLibHandler() {
|
||||
_instance ??= ClashLibHandler._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<String> invokeAction(String actionParams) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
@@ -261,56 +218,33 @@ class ClashLib with ClashInterface {
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final delayParamsChar =
|
||||
json.encode(delayParams).toNativeUtf8().cast<Char>();
|
||||
clashFFI.asyncTestDelay(
|
||||
delayParamsChar,
|
||||
final actionParamsChar = actionParams.toNativeUtf8().cast<Char>();
|
||||
clashFFI.invokeAction(
|
||||
actionParamsChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(delayParamsChar);
|
||||
malloc.free(actionParamsChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
String getTraffic(bool value) {
|
||||
final trafficRaw = clashFFI.getTraffic(value ? 1 : 0);
|
||||
final trafficString = trafficRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(trafficRaw);
|
||||
return trafficString;
|
||||
attachMessagePort(int messagePort) {
|
||||
clashFFI.attachMessagePort(
|
||||
messagePort,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String getTotalTraffic(bool value) {
|
||||
final trafficRaw = clashFFI.getTotalTraffic(value ? 1 : 0);
|
||||
clashFFI.freeCString(trafficRaw);
|
||||
return trafficRaw.cast<Utf8>().toDartString();
|
||||
attachInvokePort(int invokePort) {
|
||||
clashFFI.attachInvokePort(
|
||||
invokePort,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void resetTraffic() {
|
||||
clashFFI.resetTraffic();
|
||||
}
|
||||
|
||||
@override
|
||||
void startLog() {
|
||||
clashFFI.startLog();
|
||||
}
|
||||
|
||||
@override
|
||||
stopLog() {
|
||||
clashFFI.stopLog();
|
||||
}
|
||||
|
||||
@override
|
||||
forceGc() {
|
||||
clashFFI.forceGc();
|
||||
}
|
||||
|
||||
/// Android
|
||||
|
||||
startTun(int fd, int port) {
|
||||
if (!Platform.isAndroid) return;
|
||||
clashFFI.startTUN(fd, port);
|
||||
DateTime? startTun(int fd) {
|
||||
final runTimeRaw = clashFFI.startTUN(fd);
|
||||
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(runTimeRaw);
|
||||
if (runTimeString.isEmpty) return null;
|
||||
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
|
||||
}
|
||||
|
||||
stopTun() {
|
||||
@@ -318,7 +252,6 @@ class ClashLib with ClashInterface {
|
||||
}
|
||||
|
||||
updateDns(String dns) {
|
||||
if (!Platform.isAndroid) return;
|
||||
final dnsChar = dns.toNativeUtf8().cast<Char>();
|
||||
clashFFI.updateDns(dnsChar);
|
||||
malloc.free(dnsChar);
|
||||
@@ -351,8 +284,70 @@ class ClashLib with ClashInterface {
|
||||
return AndroidVpnOptions.fromJson(vpnOptions);
|
||||
}
|
||||
|
||||
setFdMap(int fd) {
|
||||
clashFFI.setFdMap(fd);
|
||||
Traffic getTraffic() {
|
||||
final trafficRaw = clashFFI.getTraffic();
|
||||
final trafficString = trafficRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(trafficRaw);
|
||||
if (trafficString.isEmpty) {
|
||||
return Traffic();
|
||||
}
|
||||
return Traffic.fromMap(json.decode(trafficString));
|
||||
}
|
||||
|
||||
Traffic getTotalTraffic(bool value) {
|
||||
final trafficRaw = clashFFI.getTotalTraffic();
|
||||
final trafficString = trafficRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(trafficRaw);
|
||||
if (trafficString.isEmpty) {
|
||||
return Traffic();
|
||||
}
|
||||
return Traffic.fromMap(json.decode(trafficString));
|
||||
}
|
||||
|
||||
startListener() async {
|
||||
clashFFI.startListener();
|
||||
return true;
|
||||
}
|
||||
|
||||
stopListener() async {
|
||||
clashFFI.stopListener();
|
||||
return true;
|
||||
}
|
||||
|
||||
setFdMap(String id) {
|
||||
final idChar = id.toNativeUtf8().cast<Char>();
|
||||
clashFFI.setFdMap(idChar);
|
||||
malloc.free(idChar);
|
||||
}
|
||||
|
||||
Future<String> quickStart(
|
||||
String homeDir,
|
||||
UpdateConfigParams updateConfigParams,
|
||||
CoreState state,
|
||||
) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final params = json.encode(updateConfigParams);
|
||||
final stateParams = json.encode(state);
|
||||
final homeChar = homeDir.toNativeUtf8().cast<Char>();
|
||||
final paramsChar = params.toNativeUtf8().cast<Char>();
|
||||
final stateParamsChar = stateParams.toNativeUtf8().cast<Char>();
|
||||
clashFFI.quickStart(
|
||||
homeChar,
|
||||
paramsChar,
|
||||
stateParamsChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(homeChar);
|
||||
malloc.free(paramsChar);
|
||||
malloc.free(stateParamsChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
DateTime? getRunTime() {
|
||||
@@ -364,4 +359,5 @@ class ClashLib with ClashInterface {
|
||||
}
|
||||
}
|
||||
|
||||
final clashLib = Platform.isAndroid ? ClashLib() : null;
|
||||
ClashLib? get clashLib =>
|
||||
Platform.isAndroid && !globalState.isService ? ClashLib() : null;
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class ClashMessage {
|
||||
final controller = StreamController();
|
||||
final controller = StreamController<String>();
|
||||
|
||||
ClashMessage._() {
|
||||
clashLib?.receiver.listen(controller.add);
|
||||
controller.stream.listen(
|
||||
(message) {
|
||||
if(message.isEmpty){
|
||||
return;
|
||||
}
|
||||
final m = AppMessage.fromJson(json.decode(message));
|
||||
for (final AppMessageListener listener in _listeners) {
|
||||
switch (m.type) {
|
||||
@@ -25,9 +26,6 @@ class ClashMessage {
|
||||
case AppMessageType.request:
|
||||
listener.onRequest(Connection.fromJson(m.data));
|
||||
break;
|
||||
case AppMessageType.started:
|
||||
listener.onStarted(m.data);
|
||||
break;
|
||||
case AppMessageType.loaded:
|
||||
listener.onLoaded(m.data);
|
||||
break;
|
||||
|
||||
@@ -3,21 +3,17 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/clash/interface.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/core.dart';
|
||||
|
||||
class ClashService with ClashInterface {
|
||||
class ClashService extends ClashHandlerInterface {
|
||||
static ClashService? _instance;
|
||||
|
||||
Completer<ServerSocket> serverCompleter = Completer();
|
||||
|
||||
Completer<Socket> socketCompleter = Completer();
|
||||
|
||||
Map<String, Completer> callbackCompleterMap = {};
|
||||
|
||||
Process? process;
|
||||
|
||||
factory ClashService() {
|
||||
@@ -26,11 +22,11 @@ class ClashService with ClashInterface {
|
||||
}
|
||||
|
||||
ClashService._internal() {
|
||||
_createServer();
|
||||
startCore();
|
||||
_initServer();
|
||||
reStart();
|
||||
}
|
||||
|
||||
_createServer() async {
|
||||
_initServer() async {
|
||||
final address = !Platform.isWindows
|
||||
? InternetAddress(
|
||||
unixSocketPath,
|
||||
@@ -61,8 +57,8 @@ class ClashService with ClashInterface {
|
||||
.transform(LineSplitter())
|
||||
.listen(
|
||||
(data) {
|
||||
_handleAction(
|
||||
Action.fromJson(
|
||||
handleResult(
|
||||
ActionResult.fromJson(
|
||||
json.decode(data.trim()),
|
||||
),
|
||||
);
|
||||
@@ -71,7 +67,8 @@ class ClashService with ClashInterface {
|
||||
}
|
||||
}
|
||||
|
||||
startCore() async {
|
||||
@override
|
||||
reStart() async {
|
||||
if (process != null) {
|
||||
await shutdown();
|
||||
}
|
||||
@@ -95,6 +92,20 @@ class ClashService with ClashInterface {
|
||||
process!.stdout.listen((_) {});
|
||||
}
|
||||
|
||||
@override
|
||||
destroy() async {
|
||||
final server = await serverCompleter.future;
|
||||
await server.close();
|
||||
await _deleteSocketFile();
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
sendMessage(String message) async {
|
||||
final socket = await socketCompleter.future;
|
||||
socket.writeln(message);
|
||||
}
|
||||
|
||||
_deleteSocketFile() async {
|
||||
if (!Platform.isWindows) {
|
||||
final file = File(unixSocketPath);
|
||||
@@ -112,302 +123,22 @@ class ClashService with ClashInterface {
|
||||
}
|
||||
}
|
||||
|
||||
_handleAction(Action action) {
|
||||
final completer = callbackCompleterMap[action.id];
|
||||
switch (action.method) {
|
||||
case ActionMethod.initClash:
|
||||
case ActionMethod.shutdown:
|
||||
case ActionMethod.getIsInit:
|
||||
case ActionMethod.startListener:
|
||||
case ActionMethod.resetTraffic:
|
||||
case ActionMethod.closeConnections:
|
||||
case ActionMethod.closeConnection:
|
||||
case ActionMethod.stopListener:
|
||||
completer?.complete(action.data as bool);
|
||||
return;
|
||||
case ActionMethod.changeProxy:
|
||||
case ActionMethod.getProxies:
|
||||
case ActionMethod.getTraffic:
|
||||
case ActionMethod.getTotalTraffic:
|
||||
case ActionMethod.asyncTestDelay:
|
||||
case ActionMethod.getConnections:
|
||||
case ActionMethod.getExternalProviders:
|
||||
case ActionMethod.getExternalProvider:
|
||||
case ActionMethod.validateConfig:
|
||||
case ActionMethod.updateConfig:
|
||||
case ActionMethod.updateGeoData:
|
||||
case ActionMethod.updateExternalProvider:
|
||||
case ActionMethod.sideLoadExternalProvider:
|
||||
completer?.complete(action.data as String);
|
||||
return;
|
||||
case ActionMethod.message:
|
||||
clashMessage.controller.add(action.data as String);
|
||||
return;
|
||||
case ActionMethod.forceGc:
|
||||
case ActionMethod.startLog:
|
||||
case ActionMethod.stopLog:
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<T> _invoke<T>({
|
||||
required ActionMethod method,
|
||||
dynamic data,
|
||||
Duration? timeout,
|
||||
FutureOr<T> Function()? onTimeout,
|
||||
}) async {
|
||||
final id = "${method.name}#${other.id}";
|
||||
final socket = await socketCompleter.future;
|
||||
callbackCompleterMap[id] = Completer<T>();
|
||||
socket.writeln(
|
||||
json.encode(
|
||||
Action(
|
||||
id: id,
|
||||
method: method,
|
||||
data: data,
|
||||
),
|
||||
),
|
||||
);
|
||||
return (callbackCompleterMap[id] as Completer<T>).safeFuture(
|
||||
timeout: timeout,
|
||||
onLast: () {
|
||||
callbackCompleterMap.remove(id);
|
||||
},
|
||||
onTimeout: onTimeout,
|
||||
functionName: id,
|
||||
);
|
||||
}
|
||||
|
||||
_prueInvoke({
|
||||
required ActionMethod method,
|
||||
dynamic data,
|
||||
}) async {
|
||||
final id = "${method.name}#${other.id}";
|
||||
final socket = await socketCompleter.future;
|
||||
socket.writeln(
|
||||
json.encode(
|
||||
Action(
|
||||
id: id,
|
||||
method: method,
|
||||
data: data,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> init(String homeDir) {
|
||||
return _invoke<bool>(
|
||||
method: ActionMethod.initClash,
|
||||
data: homeDir,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
shutdown() async {
|
||||
await _invoke<bool>(
|
||||
method: ActionMethod.shutdown,
|
||||
);
|
||||
await super.shutdown();
|
||||
if (Platform.isWindows) {
|
||||
await request.stopCoreByHelper();
|
||||
}
|
||||
await _destroySocket();
|
||||
process?.kill();
|
||||
process = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> get isInit {
|
||||
return _invoke<bool>(
|
||||
method: ActionMethod.getIsInit,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
forceGc() {
|
||||
_prueInvoke(method: ActionMethod.forceGc);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> validateConfig(String data) {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.validateConfig,
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
|
||||
return await _invoke<String>(
|
||||
method: ActionMethod.updateConfig,
|
||||
data: json.encode(updateConfigParams),
|
||||
timeout: const Duration(seconds: 20),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getProxies() {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.getProxies,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.changeProxy,
|
||||
data: json.encode(changeProxyParams),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getExternalProviders() {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.getExternalProviders,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getExternalProvider(String externalProviderName) {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.getExternalProvider,
|
||||
data: externalProviderName,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> updateGeoData({
|
||||
required String geoType,
|
||||
required String geoName,
|
||||
}) {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.updateGeoData,
|
||||
data: json.encode(
|
||||
{
|
||||
"geoType": geoType,
|
||||
"geoName": geoName,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> sideLoadExternalProvider({
|
||||
required String providerName,
|
||||
required String data,
|
||||
}) {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.sideLoadExternalProvider,
|
||||
data: json.encode({
|
||||
"providerName": providerName,
|
||||
"data": data,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> updateExternalProvider(String providerName) {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.updateExternalProvider,
|
||||
data: providerName,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getConnections() {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.getConnections,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> closeConnections() {
|
||||
return _invoke<bool>(
|
||||
method: ActionMethod.closeConnections,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> closeConnection(String id) {
|
||||
return _invoke<bool>(
|
||||
method: ActionMethod.closeConnection,
|
||||
data: id,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getTotalTraffic(bool value) {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.getTotalTraffic,
|
||||
data: value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getTraffic(bool value) {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.getTraffic,
|
||||
data: value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
resetTraffic() {
|
||||
_prueInvoke(method: ActionMethod.resetTraffic);
|
||||
}
|
||||
|
||||
@override
|
||||
startLog() {
|
||||
_prueInvoke(method: ActionMethod.startLog);
|
||||
}
|
||||
|
||||
@override
|
||||
stopLog() {
|
||||
_prueInvoke(method: ActionMethod.stopLog);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> startListener() {
|
||||
return _invoke<bool>(
|
||||
method: ActionMethod.startListener,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
stopListener() {
|
||||
return _invoke<bool>(
|
||||
method: ActionMethod.stopListener,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> asyncTestDelay(String proxyName) {
|
||||
final delayParams = {
|
||||
"proxy-name": proxyName,
|
||||
"timeout": httpTimeoutDuration.inMilliseconds,
|
||||
};
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.asyncTestDelay,
|
||||
data: json.encode(delayParams),
|
||||
timeout: Duration(
|
||||
milliseconds: 6000,
|
||||
),
|
||||
onTimeout: () {
|
||||
return json.encode(
|
||||
Delay(
|
||||
name: proxyName,
|
||||
value: -1,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
destroy() async {
|
||||
final server = await serverCompleter.future;
|
||||
await server.close();
|
||||
await _deleteSocketFile();
|
||||
Future<bool> preload() async {
|
||||
await serverCompleter.future;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension ColorExtension on Color {
|
||||
toLight() {
|
||||
|
||||
Color get toLight {
|
||||
return withOpacity(0.8);
|
||||
}
|
||||
|
||||
Color get toLighter {
|
||||
return withOpacity(0.6);
|
||||
}
|
||||
|
||||
toLighter() {
|
||||
return withOpacity(0.4);
|
||||
}
|
||||
|
||||
toSoft() {
|
||||
Color get toSoft {
|
||||
return withOpacity(0.12);
|
||||
}
|
||||
|
||||
toLittle() {
|
||||
Color get toLittle {
|
||||
return withOpacity(0.03);
|
||||
}
|
||||
|
||||
@@ -23,13 +24,39 @@ extension ColorExtension on Color {
|
||||
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
|
||||
return hslDark.toColor();
|
||||
}
|
||||
|
||||
Color blendDarken(
|
||||
BuildContext context, {
|
||||
double factor = 0.1,
|
||||
}) {
|
||||
final brightness = Theme.of(context).brightness;
|
||||
return Color.lerp(
|
||||
this,
|
||||
brightness == Brightness.dark ? Colors.white : Colors.black,
|
||||
factor,
|
||||
)!;
|
||||
}
|
||||
|
||||
Color blendLighten(
|
||||
BuildContext context, {
|
||||
double factor = 0.1,
|
||||
}) {
|
||||
final brightness = Theme.of(context).brightness;
|
||||
return Color.lerp(
|
||||
this,
|
||||
brightness == Brightness.dark ? Colors.black : Colors.white,
|
||||
factor,
|
||||
)!;
|
||||
}
|
||||
}
|
||||
|
||||
extension ColorSchemeExtension on ColorScheme {
|
||||
ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack
|
||||
? copyWith(
|
||||
surface: Colors.black,
|
||||
surfaceContainer: surfaceContainer.darken(0.05),
|
||||
surfaceContainer: surfaceContainer.darken(
|
||||
0.05,
|
||||
),
|
||||
)
|
||||
: this;
|
||||
}
|
||||
|
||||
@@ -34,3 +34,5 @@ export 'text.dart';
|
||||
export 'tray.dart';
|
||||
export 'window.dart';
|
||||
export 'windows.dart';
|
||||
export 'render.dart';
|
||||
export 'view.dart';
|
||||
@@ -15,9 +15,14 @@ const packageName = "com.follow.clash";
|
||||
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
|
||||
const helperPort = 47890;
|
||||
const helperTag = "2024125";
|
||||
const baseInfoEdgeInsets = EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 16,
|
||||
);
|
||||
const httpTimeoutDuration = Duration(milliseconds: 5000);
|
||||
const moreDuration = Duration(milliseconds: 100);
|
||||
const animateDuration = Duration(milliseconds: 100);
|
||||
const commonDuration = Duration(milliseconds: 300);
|
||||
const defaultUpdateDuration = Duration(days: 1);
|
||||
const mmdbFileName = "geoip.metadb";
|
||||
const asnFileName = "ASN.mmdb";
|
||||
@@ -68,7 +73,7 @@ const hotKeyActionListEquality = ListEquality<HotKeyAction>();
|
||||
const stringAndStringMapEquality = MapEquality<String, String>();
|
||||
const stringAndStringMapEntryIterableEquality =
|
||||
IterableEquality<MapEntry<String, String>>();
|
||||
const stringAndIntQMapEquality = MapEquality<String, int?>();
|
||||
const delayMapEquality = MapEquality<String, Map<String, int?>>();
|
||||
const stringSetEquality = SetEquality<String>();
|
||||
const keyboardModifierListEquality = SetEquality<KeyboardModifier>();
|
||||
|
||||
@@ -79,3 +84,11 @@ const viewModeColumnsMap = {
|
||||
};
|
||||
|
||||
const defaultPrimaryColor = Colors.brown;
|
||||
|
||||
double getWidgetHeight(num lines) {
|
||||
return max(lines * 84 + (lines - 1) * 16, 0);
|
||||
}
|
||||
|
||||
final mainIsolate = "FlClashMainIsolate";
|
||||
|
||||
final serviceIsolate = "FlClashServiceIsolate";
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import 'package:fl_clash/manager/manager.dart';
|
||||
import 'package:fl_clash/widgets/scaffold.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension BuildContextExtension on BuildContext {
|
||||
|
||||
CommonScaffoldState? get commonScaffoldState {
|
||||
return findAncestorStateOfType<CommonScaffoldState>();
|
||||
}
|
||||
|
||||
Size get appSize{
|
||||
showNotifier(String text) {
|
||||
return findAncestorStateOfType<MessageManagerState>()?.message(text);
|
||||
}
|
||||
|
||||
Size get appSize {
|
||||
return MediaQuery.of(this).size;
|
||||
}
|
||||
|
||||
@@ -18,4 +22,23 @@ extension BuildContextExtension on BuildContext {
|
||||
ColorScheme get colorScheme => Theme.of(this).colorScheme;
|
||||
|
||||
TextTheme get textTheme => Theme.of(this).textTheme;
|
||||
|
||||
T? findLastStateOfType<T extends State>() {
|
||||
T? state;
|
||||
|
||||
visitor(Element element) {
|
||||
if(!element.mounted){
|
||||
return;
|
||||
}
|
||||
if(element is StatefulElement){
|
||||
if (element.state is T) {
|
||||
state = element.state as T;
|
||||
}
|
||||
}
|
||||
element.visitChildren(visitor);
|
||||
}
|
||||
|
||||
visitor(this as Element);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,69 @@
|
||||
import 'dart:async';
|
||||
|
||||
class Debouncer {
|
||||
final Duration delay;
|
||||
Timer? _timer;
|
||||
final Map<dynamic, Timer> _operations = {};
|
||||
|
||||
Debouncer({required this.delay});
|
||||
call(
|
||||
dynamic tag,
|
||||
Function func, {
|
||||
List<dynamic>? args,
|
||||
Duration duration = const Duration(milliseconds: 600),
|
||||
}) {
|
||||
final timer = _operations[tag];
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
}
|
||||
_operations[tag] = Timer(
|
||||
duration,
|
||||
() {
|
||||
_operations[tag]?.cancel();
|
||||
_operations.remove(tag);
|
||||
Function.apply(
|
||||
func,
|
||||
args,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void call(Function action, List<dynamic> positionalArguments, [Map<Symbol, dynamic>? namedArguments]) {
|
||||
_timer?.cancel();
|
||||
_timer = Timer(delay, () => Function.apply(action, positionalArguments, namedArguments));
|
||||
cancel(dynamic tag) {
|
||||
_operations[tag]?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
Function debounce<F extends Function>(F func,{int milliseconds = 600}) {
|
||||
Timer? timer;
|
||||
class Throttler {
|
||||
final Map<dynamic, Timer> _operations = {};
|
||||
|
||||
return ([List<dynamic>? args, Map<Symbol, dynamic>? namedArgs]) {
|
||||
call(
|
||||
String tag,
|
||||
Function func, {
|
||||
List<dynamic>? args,
|
||||
Duration duration = const Duration(milliseconds: 600),
|
||||
}) {
|
||||
final timer = _operations[tag];
|
||||
if (timer != null) {
|
||||
timer!.cancel();
|
||||
return true;
|
||||
}
|
||||
timer = Timer(Duration(milliseconds: milliseconds), () async {
|
||||
await Function.apply(func, args ?? [], namedArgs);
|
||||
});
|
||||
};
|
||||
}
|
||||
_operations[tag] = Timer(
|
||||
duration,
|
||||
() {
|
||||
_operations[tag]?.cancel();
|
||||
_operations.remove(tag);
|
||||
Function.apply(
|
||||
func,
|
||||
args,
|
||||
);
|
||||
},
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
cancel(dynamic tag) {
|
||||
_operations[tag]?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final debouncer = Debouncer();
|
||||
|
||||
final throttler = Throttler();
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
|
||||
extension CompleterExt<T> on Completer<T> {
|
||||
safeFuture({
|
||||
Duration? timeout,
|
||||
@@ -8,8 +10,8 @@ extension CompleterExt<T> on Completer<T> {
|
||||
FutureOr<T> Function()? onTimeout,
|
||||
required String functionName,
|
||||
}) {
|
||||
final realTimeout = timeout ?? const Duration(seconds: 6);
|
||||
Timer(realTimeout + Duration(milliseconds: 1000), () {
|
||||
final realTimeout = timeout ?? const Duration(seconds: 1);
|
||||
Timer(realTimeout + commonDuration, () {
|
||||
if (onLast != null) {
|
||||
onLast();
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ class FlClashHttpOverrides extends HttpOverrides {
|
||||
if ([localhost].contains(url.host)) {
|
||||
return "DIRECT";
|
||||
}
|
||||
debugPrint("find $url");
|
||||
final appController = globalState.appController;
|
||||
final port = appController.clashConfig.mixedPort;
|
||||
final isStart = appController.appFlowingState.isStart;
|
||||
debugPrint("find $url proxy:$isStart");
|
||||
if (!isStart) return "DIRECT";
|
||||
return "PROXY localhost:$port";
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:launch_at_startup/launch_at_startup.dart';
|
||||
|
||||
import 'constant.dart';
|
||||
@@ -34,8 +33,7 @@ class AutoLaunch {
|
||||
return await launchAtStartup.disable();
|
||||
}
|
||||
|
||||
updateStatus(AutoLaunchState state) async {
|
||||
final isAutoLaunch = state.isAutoLaunch;
|
||||
updateStatus(bool isAutoLaunch) async {
|
||||
if (await isEnable == isAutoLaunch) return;
|
||||
if (isAutoLaunch == true) {
|
||||
enable();
|
||||
|
||||
@@ -1,3 +1,55 @@
|
||||
import 'dart:collection';
|
||||
|
||||
class FixedList<T> {
|
||||
final int maxLength;
|
||||
final List<T> _list = [];
|
||||
|
||||
FixedList(this.maxLength);
|
||||
|
||||
add(T item) {
|
||||
if (_list.length == maxLength) {
|
||||
_list.removeAt(0);
|
||||
}
|
||||
_list.add(item);
|
||||
}
|
||||
|
||||
List<T> get list => List.unmodifiable(_list);
|
||||
|
||||
int get length => _list.length;
|
||||
|
||||
T operator [](int index) => _list[index];
|
||||
}
|
||||
|
||||
class FixedMap<K, V> {
|
||||
final int maxSize;
|
||||
final Map<K, V> _map = {};
|
||||
final Queue<K> _queue = Queue<K>();
|
||||
|
||||
FixedMap(this.maxSize);
|
||||
|
||||
put(K key, V value) {
|
||||
if (_map.length == maxSize) {
|
||||
final oldestKey = _queue.removeFirst();
|
||||
_map.remove(oldestKey);
|
||||
}
|
||||
_map[key] = value;
|
||||
_queue.add(key);
|
||||
}
|
||||
|
||||
clear(){
|
||||
_map.clear();
|
||||
_queue.clear();
|
||||
}
|
||||
|
||||
V? get(K key) => _map[key];
|
||||
|
||||
bool containsKey(K key) => _map.containsKey(key);
|
||||
|
||||
int get length => _map.length;
|
||||
|
||||
Map<K, V> get map => Map.unmodifiable(_map);
|
||||
}
|
||||
|
||||
extension ListExtension<T> on List<T> {
|
||||
List<T> intersection(List<T> list) {
|
||||
return where((item) => list.contains(item)).toList();
|
||||
@@ -17,8 +69,8 @@ extension ListExtension<T> on List<T> {
|
||||
}
|
||||
|
||||
List<T> safeSublist(int start) {
|
||||
if(start <= 0) return this;
|
||||
if(start > length) return [];
|
||||
if (start <= 0) return this;
|
||||
if (start > length) return [];
|
||||
return sublist(start);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class SingleInstanceLock {
|
||||
|
||||
Future<bool> acquire() async {
|
||||
try {
|
||||
final lockFilePath = await appPath.getLockFilePath();
|
||||
final lockFilePath = await appPath.lockFilePath;
|
||||
final lockFile = File(lockFilePath);
|
||||
await lockFile.create();
|
||||
_accessFile = await lockFile.open(mode: FileMode.write);
|
||||
|
||||
@@ -11,13 +11,18 @@ class Measure {
|
||||
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
|
||||
);
|
||||
|
||||
Size computeTextSize(Text text) {
|
||||
Size computeTextSize(
|
||||
Text text, {
|
||||
double maxWidth = double.infinity,
|
||||
}) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(text: text.data, style: text.style),
|
||||
maxLines: text.maxLines,
|
||||
textScaler: _textScale,
|
||||
textDirection: text.textDirection ?? TextDirection.ltr,
|
||||
)..layout();
|
||||
)..layout(
|
||||
maxWidth: maxWidth,
|
||||
);
|
||||
return textPainter.size;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,16 +30,16 @@ class Navigation {
|
||||
fragment: ProfilesFragment(),
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.view_timeline),
|
||||
icon: Icon(Icons.view_timeline),
|
||||
label: "requests",
|
||||
fragment: RequestsFragment(),
|
||||
fragment: RequestsFragment(),
|
||||
description: "requestsDesc",
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.ballot),
|
||||
icon: Icon(Icons.ballot),
|
||||
label: "connections",
|
||||
fragment: ConnectionsFragment(),
|
||||
fragment: ConnectionsFragment(),
|
||||
description: "connectionsDesc",
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||
),
|
||||
@@ -49,7 +49,7 @@ class Navigation {
|
||||
description: "resourcesDesc",
|
||||
keep: false,
|
||||
fragment: Resources(),
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||
modes: [NavigationItemMode.more],
|
||||
),
|
||||
NavigationItem(
|
||||
icon: const Icon(Icons.adb),
|
||||
|
||||
@@ -1,11 +1,299 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BaseNavigator {
|
||||
static Future<T?> push<T>(BuildContext context, Widget child) async {
|
||||
if (!globalState.appController.isMobileView) {
|
||||
return await Navigator.of(context).push<T>(
|
||||
CommonDesktopRoute(
|
||||
builder: (context) => child,
|
||||
),
|
||||
);
|
||||
}
|
||||
return await Navigator.of(context).push<T>(
|
||||
MaterialPageRoute(
|
||||
CommonRoute(
|
||||
builder: (context) => child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CommonDesktopRoute<T> extends PageRoute<T> {
|
||||
final Widget Function(BuildContext context) builder;
|
||||
|
||||
CommonDesktopRoute({
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
@override
|
||||
Color? get barrierColor => null;
|
||||
|
||||
@override
|
||||
String? get barrierLabel => null;
|
||||
|
||||
@override
|
||||
Widget buildPage(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
final Widget result = builder(context);
|
||||
return Semantics(
|
||||
scopesRoute: true,
|
||||
explicitChildNodes: true,
|
||||
child: FadeTransition(
|
||||
opacity: animation,
|
||||
child: result,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get maintainState => true;
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => Duration(milliseconds: 200);
|
||||
|
||||
@override
|
||||
Duration get reverseTransitionDuration => Duration(milliseconds: 200);
|
||||
}
|
||||
|
||||
class CommonRoute<T> extends MaterialPageRoute<T> {
|
||||
CommonRoute({
|
||||
required super.builder,
|
||||
});
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => const Duration(milliseconds: 500);
|
||||
|
||||
@override
|
||||
Duration get reverseTransitionDuration => const Duration(milliseconds: 300);
|
||||
}
|
||||
|
||||
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
|
||||
begin: const Offset(1.0, 0.0),
|
||||
end: Offset.zero,
|
||||
);
|
||||
final Animatable<Offset> _kMiddleLeftTween = Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: const Offset(-1.0 / 3.0, 0.0),
|
||||
);
|
||||
|
||||
class CommonPageTransitionsBuilder extends PageTransitionsBuilder {
|
||||
const CommonPageTransitionsBuilder();
|
||||
|
||||
@override
|
||||
Widget buildTransitions<T>(
|
||||
PageRoute<T> route,
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child,
|
||||
) {
|
||||
return CommonPageTransition(
|
||||
context: context,
|
||||
primaryRouteAnimation: animation,
|
||||
secondaryRouteAnimation: secondaryAnimation,
|
||||
linearTransition: false,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CommonPageTransition extends StatefulWidget {
|
||||
const CommonPageTransition({
|
||||
super.key,
|
||||
required this.context,
|
||||
required this.primaryRouteAnimation,
|
||||
required this.secondaryRouteAnimation,
|
||||
required this.child,
|
||||
required this.linearTransition,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
|
||||
final Animation<double> primaryRouteAnimation;
|
||||
|
||||
final Animation<double> secondaryRouteAnimation;
|
||||
|
||||
final BuildContext context;
|
||||
|
||||
final bool linearTransition;
|
||||
|
||||
static Widget? delegatedTransition(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
bool allowSnapshotting,
|
||||
Widget? child) {
|
||||
final Animation<Offset> delegatedPositionAnimation = CurvedAnimation(
|
||||
parent: secondaryAnimation,
|
||||
curve: Curves.linearToEaseOut,
|
||||
reverseCurve: Curves.easeInToLinear,
|
||||
).drive(_kMiddleLeftTween);
|
||||
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
final TextDirection textDirection = Directionality.of(context);
|
||||
return SlideTransition(
|
||||
position: delegatedPositionAnimation,
|
||||
textDirection: textDirection,
|
||||
transformHitTests: false,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<CommonPageTransition> createState() => _CommonPageTransitionState();
|
||||
}
|
||||
|
||||
class _CommonPageTransitionState extends State<CommonPageTransition> {
|
||||
late Animation<Offset> _primaryPositionAnimation;
|
||||
late Animation<Offset> _secondaryPositionAnimation;
|
||||
late Animation<Decoration> _primaryShadowAnimation;
|
||||
CurvedAnimation? _primaryPositionCurve;
|
||||
CurvedAnimation? _secondaryPositionCurve;
|
||||
CurvedAnimation? _primaryShadowCurve;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setupAnimation();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant CommonPageTransition oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.primaryRouteAnimation != widget.primaryRouteAnimation ||
|
||||
oldWidget.secondaryRouteAnimation != widget.secondaryRouteAnimation ||
|
||||
oldWidget.linearTransition != widget.linearTransition) {
|
||||
_disposeCurve();
|
||||
_setupAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposeCurve();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _disposeCurve() {
|
||||
_primaryPositionCurve?.dispose();
|
||||
_secondaryPositionCurve?.dispose();
|
||||
_primaryShadowCurve?.dispose();
|
||||
_primaryPositionCurve = null;
|
||||
_secondaryPositionCurve = null;
|
||||
_primaryShadowCurve = null;
|
||||
}
|
||||
|
||||
void _setupAnimation() {
|
||||
if (!widget.linearTransition) {
|
||||
_primaryPositionCurve = CurvedAnimation(
|
||||
parent: widget.primaryRouteAnimation,
|
||||
curve: Curves.fastEaseInToSlowEaseOut,
|
||||
reverseCurve: Curves.easeInOut,
|
||||
);
|
||||
_secondaryPositionCurve = CurvedAnimation(
|
||||
parent: widget.secondaryRouteAnimation,
|
||||
curve: Curves.linearToEaseOut,
|
||||
reverseCurve: Curves.easeInToLinear,
|
||||
);
|
||||
_primaryShadowCurve = CurvedAnimation(
|
||||
parent: widget.primaryRouteAnimation,
|
||||
curve: Curves.linearToEaseOut,
|
||||
);
|
||||
}
|
||||
_primaryPositionAnimation =
|
||||
(_primaryPositionCurve ?? widget.primaryRouteAnimation)
|
||||
.drive(_kRightMiddleTween);
|
||||
_secondaryPositionAnimation =
|
||||
(_secondaryPositionCurve ?? widget.secondaryRouteAnimation)
|
||||
.drive(_kMiddleLeftTween);
|
||||
_primaryShadowAnimation =
|
||||
(_primaryShadowCurve ?? widget.primaryRouteAnimation).drive(
|
||||
DecorationTween(
|
||||
begin: const _CommonEdgeShadowDecoration(),
|
||||
end: _CommonEdgeShadowDecoration(
|
||||
<Color>[
|
||||
widget.context.colorScheme.inverseSurface.withOpacity(
|
||||
0.06,
|
||||
),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
final TextDirection textDirection = Directionality.of(context);
|
||||
return SlideTransition(
|
||||
position: _secondaryPositionAnimation,
|
||||
textDirection: textDirection,
|
||||
transformHitTests: false,
|
||||
child: SlideTransition(
|
||||
position: _primaryPositionAnimation,
|
||||
textDirection: textDirection,
|
||||
child: DecoratedBoxTransition(
|
||||
decoration: _primaryShadowAnimation,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CommonEdgeShadowDecoration extends Decoration {
|
||||
final List<Color>? _colors;
|
||||
|
||||
const _CommonEdgeShadowDecoration([this._colors]);
|
||||
|
||||
@override
|
||||
BoxPainter createBoxPainter([VoidCallback? onChanged]) {
|
||||
return _CommonEdgeShadowPainter(this, onChanged);
|
||||
}
|
||||
}
|
||||
|
||||
class _CommonEdgeShadowPainter extends BoxPainter {
|
||||
_CommonEdgeShadowPainter(
|
||||
this._decoration,
|
||||
super.onChanged,
|
||||
) : assert(_decoration._colors == null || _decoration._colors!.length > 1);
|
||||
|
||||
final _CommonEdgeShadowDecoration _decoration;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
|
||||
final List<Color>? colors = _decoration._colors;
|
||||
if (colors == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final double shadowWidth = 0.05 * configuration.size!.width;
|
||||
final double shadowHeight = configuration.size!.height;
|
||||
final double bandWidth = shadowWidth / (colors.length - 1);
|
||||
|
||||
final TextDirection? textDirection = configuration.textDirection;
|
||||
assert(textDirection != null);
|
||||
final (double shadowDirection, double start) = switch (textDirection!) {
|
||||
TextDirection.rtl => (1, offset.dx + configuration.size!.width),
|
||||
TextDirection.ltr => (-1, offset.dx),
|
||||
};
|
||||
|
||||
int bandColorIndex = 0;
|
||||
for (int dx = 0; dx < shadowWidth; dx += 1) {
|
||||
if (dx ~/ bandWidth != bandColorIndex) {
|
||||
bandColorIndex += 1;
|
||||
}
|
||||
final Paint paint = Paint()
|
||||
..color = Color.lerp(colors[bandColorIndex], colors[bandColorIndex + 1],
|
||||
(dx % bandWidth) / bandWidth)!;
|
||||
final double x = start + shadowDirection * dx;
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(x - 1.0, offset.dy, 1.0, shadowHeight), paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
extension NumExtension on num {
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension NumExt on num {
|
||||
String fixed({digit = 2}) {
|
||||
return toStringAsFixed(truncateToDouble() == this ? 0 : digit);
|
||||
}
|
||||
}
|
||||
|
||||
extension DoubleExt on double {
|
||||
moreOrEqual(double value) {
|
||||
return this > value || (value - this).abs() < precisionErrorTolerance + 1;
|
||||
}
|
||||
}
|
||||
|
||||
extension OffsetExt on Offset {
|
||||
double getCrossAxisOffset(Axis direction) {
|
||||
return direction == Axis.vertical ? dx : dy;
|
||||
}
|
||||
|
||||
double getMainAxisOffset(Axis direction) {
|
||||
return direction == Axis.vertical ? dy : dx;
|
||||
}
|
||||
|
||||
bool less(Offset offset) {
|
||||
if (dy < offset.dy) {
|
||||
return true;
|
||||
}
|
||||
if (dy == offset.dy && dx < offset.dx) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
extension RectExt on Rect {
|
||||
doRectIntersect(Rect rect) {
|
||||
return left < rect.right &&
|
||||
right > rect.left &&
|
||||
top < rect.bottom &&
|
||||
bottom > rect.top;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:lpinyin/lpinyin.dart';
|
||||
import 'package:zxing2/qrcode.dart';
|
||||
|
||||
class Other {
|
||||
Color? getDelayColor(int? delay) {
|
||||
@@ -34,6 +30,39 @@ class Other {
|
||||
);
|
||||
}
|
||||
|
||||
String generateRandomString({int minLength = 10, int maxLength = 100}) {
|
||||
const latinChars =
|
||||
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
final random = Random();
|
||||
|
||||
int length = minLength + random.nextInt(maxLength - minLength + 1);
|
||||
|
||||
String result = '';
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (random.nextBool()) {
|
||||
result +=
|
||||
String.fromCharCode(0x4E00 + random.nextInt(0x9FA5 - 0x4E00 + 1));
|
||||
} else {
|
||||
result += latinChars[random.nextInt(latinChars.length)];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
String get uuidV4 {
|
||||
final Random random = Random();
|
||||
final bytes = List.generate(16, (_) => random.nextInt(256));
|
||||
|
||||
bytes[6] = (bytes[6] & 0x0F) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3F) | 0x80;
|
||||
|
||||
final hex =
|
||||
bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
|
||||
|
||||
return '${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20, 32)}';
|
||||
}
|
||||
|
||||
String getTimeDifference(DateTime dateTime) {
|
||||
var currentDateTime = DateTime.now();
|
||||
var difference = currentDateTime.difference(dateTime);
|
||||
@@ -152,30 +181,6 @@ class Other {
|
||||
: "";
|
||||
}
|
||||
|
||||
Future<String?> parseQRCode(Uint8List? bytes) {
|
||||
return Isolate.run<String?>(() {
|
||||
if (bytes == null) return null;
|
||||
img.Image? image = img.decodeImage(bytes);
|
||||
LuminanceSource source = RGBLuminanceSource(
|
||||
image!.width,
|
||||
image.height,
|
||||
image
|
||||
.convert(numChannels: 4)
|
||||
.getBytes(order: img.ChannelOrder.abgr)
|
||||
.buffer
|
||||
.asInt32List(),
|
||||
);
|
||||
final bitmap = BinaryBitmap(GlobalHistogramBinarizer(source));
|
||||
final reader = QRCodeReader();
|
||||
try {
|
||||
final result = reader.decode(bitmap);
|
||||
return result.text;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String? getFileNameForDisposition(String? disposition) {
|
||||
if (disposition == null) return null;
|
||||
final parseValue = HeaderValue.parse(disposition);
|
||||
@@ -225,7 +230,7 @@ class Other {
|
||||
}
|
||||
|
||||
int getProfilesColumns(double viewWidth) {
|
||||
return max((viewWidth / 400).floor(), 1);
|
||||
return max((viewWidth / 350).floor(), 1);
|
||||
}
|
||||
|
||||
String getBackupFileName() {
|
||||
@@ -240,6 +245,32 @@ class Other {
|
||||
final view = WidgetsBinding.instance.platformDispatcher.views.first;
|
||||
return view.physicalSize / view.devicePixelRatio;
|
||||
}
|
||||
|
||||
Future<String?> getLocalIpAddress() async {
|
||||
List<NetworkInterface> interfaces = await NetworkInterface.list(
|
||||
includeLoopback: false,
|
||||
)
|
||||
..sort((a, b) {
|
||||
if (a.isWifi && !b.isWifi) return -1;
|
||||
if (!a.isWifi && b.isWifi) return 1;
|
||||
if (a.includesIPv4 && !b.includesIPv4) return -1;
|
||||
if (!a.includesIPv4 && b.includesIPv4) return 1;
|
||||
return 0;
|
||||
});
|
||||
for (final interface in interfaces) {
|
||||
final addresses = interface.addresses;
|
||||
if (addresses.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
addresses.sort((a, b) {
|
||||
if (a.isIPv4 && !b.isIPv4) return -1;
|
||||
if (!a.isIPv4 && b.isIPv4) return 1;
|
||||
return 0;
|
||||
});
|
||||
return addresses.first.address;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
final other = Other();
|
||||
|
||||
@@ -48,35 +48,40 @@ class AppPath {
|
||||
return join(executableDirPath, "$appHelperService$executableExtension");
|
||||
}
|
||||
|
||||
Future<String> getDownloadDirPath() async {
|
||||
Future<String> get downloadDirPath async {
|
||||
final directory = await downloadDir.future;
|
||||
return directory.path;
|
||||
}
|
||||
|
||||
Future<String> getHomeDirPath() async {
|
||||
Future<String> get homeDirPath async {
|
||||
final directory = await dataDir.future;
|
||||
return directory.path;
|
||||
}
|
||||
|
||||
Future<String> getLockFilePath() async {
|
||||
Future<String> get lockFilePath async {
|
||||
final directory = await dataDir.future;
|
||||
return join(directory.path, "FlClash.lock");
|
||||
}
|
||||
|
||||
Future<String> getProfilesPath() async {
|
||||
Future<String> get sharedPreferencesPath async {
|
||||
final directory = await dataDir.future;
|
||||
return join(directory.path, "shared_preferences.json");
|
||||
}
|
||||
|
||||
Future<String> get profilesPath async {
|
||||
final directory = await dataDir.future;
|
||||
return join(directory.path, profilesDirectoryName);
|
||||
}
|
||||
|
||||
Future<String?> getProfilePath(String? id) async {
|
||||
if (id == null) return null;
|
||||
final directory = await getProfilesPath();
|
||||
final directory = await profilesPath;
|
||||
return join(directory, "$id.yaml");
|
||||
}
|
||||
|
||||
Future<String?> getProvidersPath(String? id) async {
|
||||
if (id == null) return null;
|
||||
final directory = await getProfilesPath();
|
||||
final directory = await profilesPath;
|
||||
return join(directory, "providers", id);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@ import 'dart:typed_data';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
|
||||
class Picker {
|
||||
Future<PlatformFile?> pickerFile() async {
|
||||
final filePickerResult = await FilePicker.platform.pickFiles(
|
||||
withData: true,
|
||||
allowMultiple: false,
|
||||
initialDirectory: await appPath.getDownloadDirPath(),
|
||||
initialDirectory: await appPath.downloadDirPath,
|
||||
);
|
||||
return filePickerResult?.files.first;
|
||||
}
|
||||
@@ -18,7 +19,7 @@ class Picker {
|
||||
Future<String?> saveFile(String fileName, Uint8List bytes) async {
|
||||
final path = await FilePicker.platform.saveFile(
|
||||
fileName: fileName,
|
||||
initialDirectory: await appPath.getDownloadDirPath(),
|
||||
initialDirectory: await appPath.downloadDirPath,
|
||||
bytes: Platform.isAndroid ? bytes : null,
|
||||
);
|
||||
if (!Platform.isAndroid && path != null) {
|
||||
@@ -30,9 +31,14 @@ class Picker {
|
||||
|
||||
Future<String?> pickerConfigQRCode() async {
|
||||
final xFile = await ImagePicker().pickImage(source: ImageSource.gallery);
|
||||
final bytes = await xFile?.readAsBytes();
|
||||
if (bytes == null) return null;
|
||||
final result = await other.parseQRCode(bytes);
|
||||
if (xFile == null) {
|
||||
return null;
|
||||
}
|
||||
final controller = MobileScannerController();
|
||||
final capture = await controller.analyzeImage(xFile.path, formats: [
|
||||
BarcodeFormat.qrCode,
|
||||
]);
|
||||
final result = capture?.barcodes.first.rawValue;
|
||||
if (result == null || !result.isUrl) {
|
||||
throw appLocalizations.pleaseUploadValidQrcode;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/models.dart';
|
||||
|
||||
import 'constant.dart';
|
||||
|
||||
class Preferences {
|
||||
static Preferences? _instance;
|
||||
Completer<SharedPreferences> sharedPreferencesCompleter = Completer();
|
||||
Completer<SharedPreferences?> sharedPreferencesCompleter = Completer();
|
||||
|
||||
Future<bool> get isInit async => await sharedPreferencesCompleter.future != null;
|
||||
|
||||
Preferences._internal() {
|
||||
SharedPreferences.getInstance()
|
||||
.then((value) => sharedPreferencesCompleter.complete(value));
|
||||
SharedPreferences.getInstance().then((value) => sharedPreferencesCompleter.complete(value))
|
||||
.onError((_,__)=>sharedPreferencesCompleter.complete(null));
|
||||
}
|
||||
|
||||
factory Preferences() {
|
||||
@@ -21,52 +23,44 @@ class Preferences {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
|
||||
Future<ClashConfig?> getClashConfig() async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
final clashConfigString = preferences.getString(clashConfigKey);
|
||||
final clashConfigString = preferences?.getString(clashConfigKey);
|
||||
if (clashConfigString == null) return null;
|
||||
final clashConfigMap = json.decode(clashConfigString);
|
||||
try {
|
||||
return ClashConfig.fromJson(clashConfigMap);
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
return null;
|
||||
}
|
||||
return ClashConfig.fromJson(clashConfigMap);
|
||||
}
|
||||
|
||||
Future<bool> saveClashConfig(ClashConfig clashConfig) async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
return preferences.setString(
|
||||
preferences?.setString(
|
||||
clashConfigKey,
|
||||
json.encode(clashConfig),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<Config?> getConfig() async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
final configString = preferences.getString(configKey);
|
||||
final configString = preferences?.getString(configKey);
|
||||
if (configString == null) return null;
|
||||
final configMap = json.decode(configString);
|
||||
try {
|
||||
return Config.fromJson(configMap);
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
return null;
|
||||
}
|
||||
return Config.fromJson(configMap);
|
||||
}
|
||||
|
||||
Future<bool> saveConfig(Config config) async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
return preferences.setString(
|
||||
return await preferences?.setString(
|
||||
configKey,
|
||||
json.encode(config),
|
||||
);
|
||||
) ?? false;
|
||||
}
|
||||
|
||||
clearPreferences() async {
|
||||
final sharedPreferencesIns = await sharedPreferencesCompleter.future;
|
||||
sharedPreferencesIns.clear();
|
||||
sharedPreferencesIns?.clear();
|
||||
}
|
||||
}
|
||||
|
||||
final preferences = Preferences();
|
||||
final preferences = Preferences();
|
||||
|
||||
57
lib/common/render.dart
Normal file
57
lib/common/render.dart
Normal file
@@ -0,0 +1,57 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
class Render {
|
||||
static Render? _instance;
|
||||
bool _isPaused = false;
|
||||
final _dispatcher = SchedulerBinding.instance.platformDispatcher;
|
||||
FrameCallback? _beginFrame;
|
||||
VoidCallback? _drawFrame;
|
||||
|
||||
Render._internal();
|
||||
|
||||
factory Render() {
|
||||
_instance ??= Render._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
active() {
|
||||
resume();
|
||||
pause();
|
||||
}
|
||||
|
||||
pause() {
|
||||
debouncer.call(
|
||||
"render_pause",
|
||||
_pause,
|
||||
duration: Duration(seconds: 5),
|
||||
);
|
||||
}
|
||||
|
||||
resume() {
|
||||
debouncer.cancel("render_pause");
|
||||
_resume();
|
||||
}
|
||||
|
||||
void _pause() {
|
||||
if (_isPaused) return;
|
||||
_isPaused = true;
|
||||
_beginFrame = _dispatcher.onBeginFrame;
|
||||
_drawFrame = _dispatcher.onDrawFrame;
|
||||
_dispatcher.onBeginFrame = null;
|
||||
_dispatcher.onDrawFrame = null;
|
||||
debugPrint("[App] pause");
|
||||
}
|
||||
|
||||
void _resume() {
|
||||
if (!_isPaused) return;
|
||||
_isPaused = false;
|
||||
_dispatcher.onBeginFrame = _beginFrame;
|
||||
_dispatcher.onDrawFrame = _drawFrame;
|
||||
_dispatcher.scheduleFrame();
|
||||
debugPrint("[App] resume");
|
||||
}
|
||||
}
|
||||
|
||||
final render = system.isDesktop ? Render() : null;
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
@@ -70,28 +71,34 @@ class Request {
|
||||
return data;
|
||||
}
|
||||
|
||||
final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
|
||||
"https://ipwho.is/": IpInfo.fromIpwhoIsJson,
|
||||
"https://api.ip.sb/geoip/": IpInfo.fromIpSbJson,
|
||||
"https://ipapi.co/json/": IpInfo.fromIpApiCoJson,
|
||||
"https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
|
||||
};
|
||||
final List<String> _ipInfoSources = [
|
||||
"https://ipwho.is/?fields=ip&output=csv",
|
||||
"https://ipinfo.io/ip",
|
||||
"https://ifconfig.me/ip/",
|
||||
];
|
||||
|
||||
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
|
||||
for (final source in _ipInfoSources.entries) {
|
||||
for (final source in _ipInfoSources) {
|
||||
try {
|
||||
final response = await _dio
|
||||
.get<Map<String, dynamic>>(source.key, cancelToken: cancelToken)
|
||||
.get<String>(
|
||||
source,
|
||||
cancelToken: cancelToken,
|
||||
)
|
||||
.timeout(httpTimeoutDuration);
|
||||
if (response.statusCode != 200 || response.data == null) {
|
||||
continue;
|
||||
}
|
||||
return source.value(response.data!);
|
||||
final ipInfo = await clashCore.getCountryCode(response.data!);
|
||||
if (ipInfo == null && source != _ipInfoSources.last) {
|
||||
continue;
|
||||
}
|
||||
return ipInfo;
|
||||
} catch (e) {
|
||||
debugPrint("checkIp error ===> $e");
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) {
|
||||
throw "cancelled";
|
||||
}
|
||||
debugPrint("checkIp error ===> $e");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
@@ -15,6 +16,8 @@ class BaseScrollBehavior extends MaterialScrollBehavior {
|
||||
};
|
||||
}
|
||||
|
||||
class BaseScrollBehavior2 extends ScrollBehavior {}
|
||||
|
||||
class HiddenBarScrollBehavior extends BaseScrollBehavior {
|
||||
@override
|
||||
Widget buildScrollbar(
|
||||
@@ -40,3 +43,90 @@ class ShowBarScrollBehavior extends BaseScrollBehavior {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NextClampingScrollPhysics extends ClampingScrollPhysics {
|
||||
@override
|
||||
Simulation? createBallisticSimulation(
|
||||
ScrollMetrics position, double velocity) {
|
||||
final Tolerance tolerance = toleranceFor(position);
|
||||
if (position.outOfRange) {
|
||||
double? end;
|
||||
if (position.pixels > position.maxScrollExtent) {
|
||||
end = position.maxScrollExtent;
|
||||
}
|
||||
if (position.pixels < position.minScrollExtent) {
|
||||
end = position.minScrollExtent;
|
||||
}
|
||||
assert(end != null);
|
||||
return ScrollSpringSimulation(
|
||||
spring,
|
||||
end!,
|
||||
end,
|
||||
min(0.0, velocity),
|
||||
tolerance: tolerance,
|
||||
);
|
||||
}
|
||||
if (velocity.abs() < tolerance.velocity) {
|
||||
return null;
|
||||
}
|
||||
if (velocity > 0.0 && position.pixels >= position.maxScrollExtent) {
|
||||
return null;
|
||||
}
|
||||
if (velocity < 0.0 && position.pixels <= position.minScrollExtent) {
|
||||
return null;
|
||||
}
|
||||
return ClampingScrollSimulation(
|
||||
position: position.pixels,
|
||||
velocity: velocity,
|
||||
tolerance: tolerance,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ReverseScrollController extends ScrollController {
|
||||
ReverseScrollController({
|
||||
super.initialScrollOffset,
|
||||
super.keepScrollOffset,
|
||||
super.debugLabel,
|
||||
});
|
||||
|
||||
@override
|
||||
ScrollPosition createScrollPosition(
|
||||
ScrollPhysics physics,
|
||||
ScrollContext context,
|
||||
ScrollPosition? oldPosition,
|
||||
) {
|
||||
return ReverseScrollPosition(
|
||||
physics: physics,
|
||||
context: context,
|
||||
initialPixels: initialScrollOffset,
|
||||
keepScrollOffset: keepScrollOffset,
|
||||
oldPosition: oldPosition,
|
||||
debugLabel: debugLabel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ReverseScrollPosition extends ScrollPositionWithSingleContext {
|
||||
ReverseScrollPosition({
|
||||
required super.physics,
|
||||
required super.context,
|
||||
super.initialPixels = 0.0,
|
||||
super.keepScrollOffset,
|
||||
super.oldPosition,
|
||||
super.debugLabel,
|
||||
}) : _initialPixels = initialPixels ?? 0;
|
||||
|
||||
final double _initialPixels;
|
||||
|
||||
bool _isInit = false;
|
||||
|
||||
@override
|
||||
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
|
||||
if (!_isInit) {
|
||||
correctPixels(maxScrollExtent);
|
||||
_isInit = true;
|
||||
}
|
||||
return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@ import 'package:flutter/material.dart';
|
||||
import 'color.dart';
|
||||
|
||||
extension TextStyleExtension on TextStyle {
|
||||
TextStyle get toLight => copyWith(color: color?.toLight());
|
||||
TextStyle get toLight => copyWith(color: color?.toLight);
|
||||
|
||||
TextStyle get toLighter => copyWith(color: color?.toLighter());
|
||||
TextStyle get toLighter => copyWith(color: color?.toLighter);
|
||||
|
||||
TextStyle get toSoftBold => copyWith(fontWeight: FontWeight.w500);
|
||||
|
||||
TextStyle get toBold => copyWith(fontWeight: FontWeight.bold);
|
||||
|
||||
TextStyle get toMinus => copyWith(fontSize: fontSize! - 2);
|
||||
TextStyle adjustSize(int size) => copyWith(
|
||||
fontSize: fontSize! + size,
|
||||
);
|
||||
}
|
||||
|
||||
20
lib/common/view.dart
Normal file
20
lib/common/view.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'context.dart';
|
||||
|
||||
mixin ViewMixin<T extends StatefulWidget> on State<T> {
|
||||
List<Widget> get actions => [];
|
||||
|
||||
Widget? get floatingActionButton => null;
|
||||
|
||||
initViewState() {
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
commonScaffoldState?.actions = actions;
|
||||
commonScaffoldState?.floatingActionButton = floatingActionButton;
|
||||
commonScaffoldState?.onSearch = onSearch;
|
||||
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
|
||||
}
|
||||
|
||||
Function(String)? get onSearch => null;
|
||||
|
||||
Function(List<String>)? get onKeywordsUpdate => null;
|
||||
}
|
||||
@@ -63,6 +63,7 @@ class Window {
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
await windowManager.setSkipTaskbar(false);
|
||||
render?.resume();
|
||||
}
|
||||
|
||||
Future<bool> isVisible() async {
|
||||
@@ -76,6 +77,7 @@ class Window {
|
||||
hide() async {
|
||||
await windowManager.hide();
|
||||
await windowManager.setSkipTaskbar(true);
|
||||
render?.pause();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,40 +23,47 @@ class AppController {
|
||||
late AppFlowingState appFlowingState;
|
||||
late Config config;
|
||||
late ClashConfig clashConfig;
|
||||
late Function updateClashConfigDebounce;
|
||||
late Function updateGroupDebounce;
|
||||
late Function addCheckIpNumDebounce;
|
||||
late Function applyProfileDebounce;
|
||||
late Function savePreferencesDebounce;
|
||||
late Function changeProxyDebounce;
|
||||
|
||||
AppController(this.context) {
|
||||
appState = context.read<AppState>();
|
||||
config = context.read<Config>();
|
||||
clashConfig = context.read<ClashConfig>();
|
||||
appFlowingState = context.read<AppFlowingState>();
|
||||
updateClashConfigDebounce = debounce<Function()>(() async {
|
||||
await updateClashConfig();
|
||||
}
|
||||
|
||||
updateClashConfigDebounce() {
|
||||
debouncer.call(DebounceTag.updateClashConfig, updateClashConfig);
|
||||
}
|
||||
|
||||
updateGroupsDebounce() {
|
||||
debouncer.call(DebounceTag.updateGroups, updateGroups);
|
||||
}
|
||||
|
||||
addCheckIpNumDebounce() {
|
||||
debouncer.call(DebounceTag.addCheckIpNum, () {
|
||||
appState.checkIpNum++;
|
||||
});
|
||||
savePreferencesDebounce = debounce<Function()>(() async {
|
||||
await savePreferences();
|
||||
}
|
||||
|
||||
applyProfileDebounce() {
|
||||
debouncer.call(DebounceTag.addCheckIpNum, () {
|
||||
applyProfile(isPrue: true);
|
||||
});
|
||||
applyProfileDebounce = debounce<Function()>(() async {
|
||||
await applyProfile(isPrue: true);
|
||||
});
|
||||
changeProxyDebounce = debounce((String groupName, String proxyName) async {
|
||||
}
|
||||
|
||||
savePreferencesDebounce() {
|
||||
debouncer.call(DebounceTag.savePreferences, savePreferences);
|
||||
}
|
||||
|
||||
changeProxyDebounce(String groupName, String proxyName) {
|
||||
debouncer.call(DebounceTag.changeProxy,
|
||||
(String groupName, String proxyName) async {
|
||||
await changeProxy(
|
||||
groupName: groupName,
|
||||
proxyName: proxyName,
|
||||
);
|
||||
await updateGroups();
|
||||
});
|
||||
addCheckIpNumDebounce = debounce(() {
|
||||
appState.checkIpNum++;
|
||||
});
|
||||
updateGroupDebounce = debounce(() async {
|
||||
await updateGroups();
|
||||
});
|
||||
}, args: [groupName, proxyName]);
|
||||
}
|
||||
|
||||
restartCore() async {
|
||||
@@ -69,13 +76,10 @@ class AppController {
|
||||
|
||||
updateStatus(bool isStart) async {
|
||||
if (isStart) {
|
||||
await globalState.handleStart();
|
||||
updateRunTime();
|
||||
updateTraffic();
|
||||
globalState.updateFunctionLists = [
|
||||
await globalState.handleStart([
|
||||
updateRunTime,
|
||||
updateTraffic,
|
||||
];
|
||||
]);
|
||||
final currentLastModified =
|
||||
await config.getCurrentProfile()?.profileLastModified;
|
||||
if (currentLastModified == null ||
|
||||
@@ -94,9 +98,6 @@ class AppController {
|
||||
appFlowingState.traffics = [];
|
||||
appFlowingState.totalTraffic = Traffic();
|
||||
appFlowingState.runTime = null;
|
||||
await Future.delayed(
|
||||
Duration(milliseconds: 300),
|
||||
);
|
||||
addCheckIpNumDebounce();
|
||||
}
|
||||
}
|
||||
@@ -139,8 +140,14 @@ class AppController {
|
||||
}
|
||||
}
|
||||
|
||||
updateProviders() {
|
||||
globalState.updateProviders(appState);
|
||||
updateProviders() async {
|
||||
await globalState.updateProviders(appState);
|
||||
}
|
||||
|
||||
updateLocalIp() async {
|
||||
appFlowingState.localIp = null;
|
||||
await Future.delayed(commonDuration);
|
||||
appFlowingState.localIp = await other.getLocalIpAddress();
|
||||
}
|
||||
|
||||
Future<void> updateProfile(Profile profile) async {
|
||||
@@ -148,6 +155,16 @@ class AppController {
|
||||
config.setProfile(
|
||||
newProfile.copyWith(isUpdating: false),
|
||||
);
|
||||
if (profile.id == config.currentProfile?.id) {
|
||||
applyProfileDebounce();
|
||||
}
|
||||
}
|
||||
|
||||
setProfile(Profile profile) {
|
||||
config.setProfile(profile);
|
||||
if (profile.id == config.currentProfile?.id) {
|
||||
applyProfileDebounce();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateClashConfig({bool isPatch = true}) async {
|
||||
@@ -266,8 +283,9 @@ class AppController {
|
||||
await clashService?.destroy();
|
||||
await proxy?.stopProxy();
|
||||
await savePreferences();
|
||||
} catch (_) {}
|
||||
system.exit();
|
||||
} finally {
|
||||
system.exit();
|
||||
}
|
||||
}
|
||||
|
||||
autoCheckUpdate() async {
|
||||
@@ -285,7 +303,7 @@ class AppController {
|
||||
final body = data['body'];
|
||||
final submits = other.parseReleaseBody(body);
|
||||
final textTheme = context.textTheme;
|
||||
globalState.showMessage(
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.discoverNewVersion,
|
||||
message: TextSpan(
|
||||
text: "$tagName \n",
|
||||
@@ -302,13 +320,14 @@ class AppController {
|
||||
),
|
||||
],
|
||||
),
|
||||
onTab: () {
|
||||
launchUrl(
|
||||
Uri.parse("https://github.com/$repository/releases/latest"),
|
||||
);
|
||||
},
|
||||
confirmText: appLocalizations.goDownload,
|
||||
);
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
launchUrl(
|
||||
Uri.parse("https://github.com/$repository/releases/latest"),
|
||||
);
|
||||
} else if (handleError) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.checkUpdate,
|
||||
@@ -319,32 +338,55 @@ class AppController {
|
||||
}
|
||||
}
|
||||
|
||||
_handlePreference() async {
|
||||
if (await preferences.isInit) {
|
||||
return;
|
||||
}
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(text: appLocalizations.cacheCorrupt),
|
||||
);
|
||||
if (res) {
|
||||
final file = File(await appPath.sharedPreferencesPath);
|
||||
final isExists = await file.exists();
|
||||
if (isExists) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
await handleExit();
|
||||
}
|
||||
|
||||
init() async {
|
||||
final isDisclaimerAccepted = await handlerDisclaimer();
|
||||
if (!isDisclaimerAccepted) {
|
||||
handleExit();
|
||||
}
|
||||
if (!config.appSetting.silentLaunch) {
|
||||
window?.show();
|
||||
}
|
||||
await _handlePreference();
|
||||
await _handlerDisclaimer();
|
||||
await globalState.initCore(
|
||||
appState: appState,
|
||||
clashConfig: clashConfig,
|
||||
config: config,
|
||||
);
|
||||
await _initStatus();
|
||||
autoLaunch?.updateStatus(
|
||||
config.appSetting.autoLaunch,
|
||||
);
|
||||
autoUpdateProfiles();
|
||||
autoCheckUpdate();
|
||||
if (!config.appSetting.silentLaunch) {
|
||||
window?.show();
|
||||
} else {
|
||||
window?.hide();
|
||||
}
|
||||
}
|
||||
|
||||
_initStatus() async {
|
||||
if (Platform.isAndroid) {
|
||||
globalState.updateStartTime();
|
||||
await globalState.updateStartTime();
|
||||
}
|
||||
if (globalState.isStart) {
|
||||
await updateStatus(true);
|
||||
} else {
|
||||
await updateStatus(config.appSetting.autoRun);
|
||||
final status =
|
||||
globalState.isStart == true ? true : config.appSetting.autoRun;
|
||||
|
||||
await updateStatus(status);
|
||||
if (!status) {
|
||||
addCheckIpNumDebounce();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,7 +394,10 @@ class AppController {
|
||||
appState.setDelay(delay);
|
||||
}
|
||||
|
||||
toPage(int index, {bool hasAnimate = false}) {
|
||||
toPage(
|
||||
int index, {
|
||||
bool hasAnimate = false,
|
||||
}) {
|
||||
if (index > appState.currentNavigationItems.length - 1) {
|
||||
return;
|
||||
}
|
||||
@@ -379,8 +424,8 @@ class AppController {
|
||||
|
||||
initLink() {
|
||||
linkManager.initAppLinksListen(
|
||||
(url) {
|
||||
globalState.showMessage(
|
||||
(url) async {
|
||||
final res = await globalState.showMessage(
|
||||
title: "${appLocalizations.add}${appLocalizations.profile}",
|
||||
message: TextSpan(
|
||||
children: [
|
||||
@@ -398,18 +443,16 @@ class AppController {
|
||||
"${appLocalizations.create}${appLocalizations.profile}"),
|
||||
],
|
||||
),
|
||||
onTab: () {
|
||||
addProfileFormURL(url);
|
||||
},
|
||||
);
|
||||
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
addProfileFormURL(url);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
showSnackBar(String message) {
|
||||
globalState.showSnackBar(context, message: message);
|
||||
}
|
||||
|
||||
Future<bool> showDisclaimer() async {
|
||||
return await globalState.showCommonDialog<bool>(
|
||||
dismissible: false,
|
||||
@@ -446,11 +489,15 @@ class AppController {
|
||||
false;
|
||||
}
|
||||
|
||||
Future<bool> handlerDisclaimer() async {
|
||||
_handlerDisclaimer() async {
|
||||
if (config.appSetting.disclaimerAccepted) {
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
return showDisclaimer();
|
||||
final isDisclaimerAccepted = await showDisclaimer();
|
||||
if (!isDisclaimerAccepted) {
|
||||
await handleExit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
addProfileFormURL(String url) async {
|
||||
@@ -508,6 +555,18 @@ class AppController {
|
||||
});
|
||||
}
|
||||
|
||||
int? getDelay(String proxyName, [String? url]) {
|
||||
final currentDelayMap = appState.delayMap[getRealTestUrl(url)];
|
||||
return currentDelayMap?[appState.getRealProxyName(proxyName)];
|
||||
}
|
||||
|
||||
String getRealTestUrl(String? url) {
|
||||
if (url == null || url.isEmpty) {
|
||||
return config.appSetting.testUrl;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
List<Proxy> _sortOfName(List<Proxy> proxies) {
|
||||
return List.of(proxies)
|
||||
..sort(
|
||||
@@ -518,12 +577,12 @@ class AppController {
|
||||
);
|
||||
}
|
||||
|
||||
List<Proxy> _sortOfDelay(List<Proxy> proxies) {
|
||||
return proxies = List.of(proxies)
|
||||
List<Proxy> _sortOfDelay(String url, List<Proxy> proxies) {
|
||||
return List.of(proxies)
|
||||
..sort(
|
||||
(a, b) {
|
||||
final aDelay = appState.getDelay(a.name);
|
||||
final bDelay = appState.getDelay(b.name);
|
||||
final aDelay = getDelay(a.name, url);
|
||||
final bDelay = getDelay(b.name, url);
|
||||
if (aDelay == null && bDelay == null) {
|
||||
return 0;
|
||||
}
|
||||
@@ -538,10 +597,10 @@ class AppController {
|
||||
);
|
||||
}
|
||||
|
||||
List<Proxy> getSortProxies(List<Proxy> proxies) {
|
||||
List<Proxy> getSortProxies(List<Proxy> proxies, [String? url]) {
|
||||
return switch (config.proxiesStyle.sortType) {
|
||||
ProxiesSortType.none => proxies,
|
||||
ProxiesSortType.delay => _sortOfDelay(proxies),
|
||||
ProxiesSortType.delay => _sortOfDelay(getRealTestUrl(url), proxies),
|
||||
ProxiesSortType.name => _sortOfName(proxies),
|
||||
};
|
||||
}
|
||||
@@ -566,6 +625,10 @@ class AppController {
|
||||
});
|
||||
}
|
||||
|
||||
bool get isMobileView {
|
||||
return appState.viewMode == ViewMode.mobile;
|
||||
}
|
||||
|
||||
updateTun() {
|
||||
clashConfig.tun = clashConfig.tun.copyWith(
|
||||
enable: !clashConfig.tun.enable,
|
||||
@@ -630,8 +693,8 @@ class AppController {
|
||||
}
|
||||
|
||||
Future<List<int>> backupData() async {
|
||||
final homeDirPath = await appPath.getHomeDirPath();
|
||||
final profilesPath = await appPath.getProfilesPath();
|
||||
final homeDirPath = await appPath.homeDirPath;
|
||||
final profilesPath = await appPath.profilesPath;
|
||||
final configJson = config.toJson();
|
||||
final clashConfigJson = clashConfig.toJson();
|
||||
return Isolate.run<List<int>>(() async {
|
||||
@@ -662,7 +725,7 @@ class AppController {
|
||||
final zipDecoder = ZipDecoder();
|
||||
return zipDecoder.decodeBytes(data);
|
||||
});
|
||||
final homeDirPath = await appPath.getHomeDirPath();
|
||||
final homeDirPath = await appPath.homeDirPath;
|
||||
final configs =
|
||||
archive.files.where((item) => item.name.endsWith(".json")).toList();
|
||||
final profiles =
|
||||
|
||||
@@ -1,9 +1,39 @@
|
||||
// ignore_for_file: constant_identifier_names
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/fragments/dashboard/widgets/widgets.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
|
||||
enum SupportPlatform {
|
||||
Windows,
|
||||
MacOS,
|
||||
Linux,
|
||||
Android;
|
||||
|
||||
static SupportPlatform get currentPlatform {
|
||||
if (Platform.isWindows) {
|
||||
return SupportPlatform.Windows;
|
||||
} else if (Platform.isMacOS) {
|
||||
return SupportPlatform.MacOS;
|
||||
} else if (Platform.isLinux) {
|
||||
return SupportPlatform.Linux;
|
||||
} else if (Platform.isAndroid) {
|
||||
return SupportPlatform.Android;
|
||||
}
|
||||
throw "invalid platform";
|
||||
}
|
||||
}
|
||||
|
||||
const desktopPlatforms = [
|
||||
SupportPlatform.Linux,
|
||||
SupportPlatform.MacOS,
|
||||
SupportPlatform.Windows,
|
||||
];
|
||||
|
||||
enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay }
|
||||
|
||||
enum GroupName { GLOBAL, Proxy, Auto, Fallback }
|
||||
@@ -70,15 +100,12 @@ enum AppMessageType {
|
||||
log,
|
||||
delay,
|
||||
request,
|
||||
started,
|
||||
loaded,
|
||||
}
|
||||
|
||||
enum ServiceMessageType {
|
||||
enum InvokeMessageType {
|
||||
protect,
|
||||
process,
|
||||
started,
|
||||
loaded,
|
||||
}
|
||||
|
||||
enum FindProcessMode { always, off }
|
||||
@@ -91,6 +118,10 @@ enum RecoveryOption {
|
||||
enum ChipType { action, delete }
|
||||
|
||||
enum CommonCardType { plain, filled }
|
||||
//
|
||||
// extension CommonCardTypeExt on CommonCardType {
|
||||
// CommonCardType get variant => CommonCardType.plain;
|
||||
// }
|
||||
|
||||
enum ProxiesType { tab, list }
|
||||
|
||||
@@ -205,6 +236,19 @@ enum ActionMethod {
|
||||
stopLog,
|
||||
startListener,
|
||||
stopListener,
|
||||
getCountryCode,
|
||||
getMemory,
|
||||
|
||||
///Android,
|
||||
setFdMap,
|
||||
setProcessMap,
|
||||
setState,
|
||||
startTun,
|
||||
stopTun,
|
||||
getRunTime,
|
||||
updateDns,
|
||||
getAndroidVpnOptions,
|
||||
getCurrentProfileName,
|
||||
}
|
||||
|
||||
enum AuthorizeCode { none, success, error }
|
||||
@@ -214,3 +258,86 @@ enum WindowsHelperServiceStatus {
|
||||
presence,
|
||||
running,
|
||||
}
|
||||
|
||||
enum DebounceTag {
|
||||
updateClashConfig,
|
||||
updateGroups,
|
||||
addCheckIpNum,
|
||||
applyProfile,
|
||||
savePreferences,
|
||||
changeProxy,
|
||||
checkIp,
|
||||
handleWill,
|
||||
updateDelay,
|
||||
vpnTip,
|
||||
autoLaunch
|
||||
}
|
||||
|
||||
enum DashboardWidget {
|
||||
networkSpeed(
|
||||
GridItem(
|
||||
crossAxisCellCount: 8,
|
||||
child: NetworkSpeed(),
|
||||
),
|
||||
),
|
||||
outboundMode(
|
||||
GridItem(
|
||||
crossAxisCellCount: 4,
|
||||
child: OutboundMode(),
|
||||
),
|
||||
),
|
||||
trafficUsage(
|
||||
GridItem(
|
||||
crossAxisCellCount: 4,
|
||||
child: TrafficUsage(),
|
||||
),
|
||||
),
|
||||
networkDetection(
|
||||
GridItem(
|
||||
crossAxisCellCount: 4,
|
||||
child: NetworkDetection(),
|
||||
),
|
||||
),
|
||||
tunButton(
|
||||
GridItem(
|
||||
crossAxisCellCount: 4,
|
||||
child: TUNButton(),
|
||||
),
|
||||
platforms: desktopPlatforms,
|
||||
),
|
||||
systemProxyButton(
|
||||
GridItem(
|
||||
crossAxisCellCount: 4,
|
||||
child: SystemProxyButton(),
|
||||
),
|
||||
platforms: desktopPlatforms,
|
||||
),
|
||||
intranetIp(
|
||||
GridItem(
|
||||
crossAxisCellCount: 4,
|
||||
child: IntranetIP(),
|
||||
),
|
||||
),
|
||||
memoryInfo(
|
||||
GridItem(
|
||||
crossAxisCellCount: 4,
|
||||
child: MemoryInfo(),
|
||||
),
|
||||
);
|
||||
|
||||
final GridItem widget;
|
||||
final List<SupportPlatform> platforms;
|
||||
|
||||
const DashboardWidget(
|
||||
this.widget, {
|
||||
this.platforms = SupportPlatform.values,
|
||||
});
|
||||
|
||||
static DashboardWidget getDashboardWidget(GridItem gridItem) {
|
||||
final dashboardWidgets = DashboardWidget.values;
|
||||
final index = dashboardWidgets.indexWhere(
|
||||
(item) => item.widget == gridItem,
|
||||
);
|
||||
return dashboardWidgets[index];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,13 @@ class AccessFragment extends StatefulWidget {
|
||||
class _AccessFragmentState extends State<AccessFragment> {
|
||||
List<String> acceptList = [];
|
||||
List<String> rejectList = [];
|
||||
late ScrollController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateInitList();
|
||||
_controller = ScrollController();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final appState = globalState.appController.appState;
|
||||
if (appState.packages.isEmpty) {
|
||||
@@ -35,6 +37,12 @@ class _AccessFragmentState extends State<AccessFragment> {
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_updateInitList() {
|
||||
final accessControl = globalState.appController.config.accessControl;
|
||||
acceptList = accessControl.acceptList;
|
||||
@@ -52,8 +60,8 @@ class _AccessFragmentState extends State<AccessFragment> {
|
||||
rejectList: rejectList,
|
||||
),
|
||||
).then((_) => setState(() {
|
||||
_updateInitList();
|
||||
}));
|
||||
_updateInitList();
|
||||
}));
|
||||
},
|
||||
icon: const Icon(Icons.search),
|
||||
);
|
||||
@@ -104,11 +112,9 @@ class _AccessFragmentState extends State<AccessFragment> {
|
||||
showSheet(
|
||||
title: appLocalizations.proxiesSetting,
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return AccessControlWidget(
|
||||
context: context,
|
||||
);
|
||||
},
|
||||
body: AccessControlWidget(
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.tune),
|
||||
@@ -178,8 +184,8 @@ class _AccessFragmentState extends State<AccessFragment> {
|
||||
status: !isAccessControl,
|
||||
child: Column(
|
||||
children: [
|
||||
AbsorbPointer(
|
||||
absorbing: !isAccessControl,
|
||||
ActivateBox(
|
||||
active: isAccessControl,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4,
|
||||
@@ -270,39 +276,44 @@ class _AccessFragmentState extends State<AccessFragment> {
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: packages.length,
|
||||
itemBuilder: (_, index) {
|
||||
final package = packages[index];
|
||||
return PackageListItem(
|
||||
key: Key(package.packageName),
|
||||
package: package,
|
||||
value:
|
||||
valueList.contains(package.packageName),
|
||||
isActive: isAccessControl,
|
||||
onChanged: (value) {
|
||||
if (value == true) {
|
||||
valueList.add(package.packageName);
|
||||
} else {
|
||||
valueList.remove(package.packageName);
|
||||
}
|
||||
final config =
|
||||
globalState.appController.config;
|
||||
if (accessControlMode ==
|
||||
AccessControlMode.acceptSelected) {
|
||||
config.accessControl =
|
||||
config.accessControl.copyWith(
|
||||
acceptList: valueList,
|
||||
);
|
||||
} else {
|
||||
config.accessControl =
|
||||
config.accessControl.copyWith(
|
||||
rejectList: valueList,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
: CommonScrollBar(
|
||||
controller: _controller,
|
||||
child: ListView.builder(
|
||||
controller: _controller,
|
||||
itemCount: packages.length,
|
||||
itemExtent: 72,
|
||||
itemBuilder: (_, index) {
|
||||
final package = packages[index];
|
||||
return PackageListItem(
|
||||
key: Key(package.packageName),
|
||||
package: package,
|
||||
value:
|
||||
valueList.contains(package.packageName),
|
||||
isActive: isAccessControl,
|
||||
onChanged: (value) {
|
||||
if (value == true) {
|
||||
valueList.add(package.packageName);
|
||||
} else {
|
||||
valueList.remove(package.packageName);
|
||||
}
|
||||
final config =
|
||||
globalState.appController.config;
|
||||
if (accessControlMode ==
|
||||
AccessControlMode.acceptSelected) {
|
||||
config.accessControl =
|
||||
config.accessControl.copyWith(
|
||||
acceptList: valueList,
|
||||
);
|
||||
} else {
|
||||
config.accessControl =
|
||||
config.accessControl.copyWith(
|
||||
rejectList: valueList,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -332,8 +343,8 @@ class PackageListItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AbsorbPointer(
|
||||
absorbing: !isActive,
|
||||
return ActivateBox(
|
||||
active: isActive,
|
||||
child: ListItem.checkbox(
|
||||
leading: SizedBox(
|
||||
width: 48,
|
||||
|
||||
@@ -40,7 +40,7 @@ class UsageSwitch extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.onlyProxy,
|
||||
selector: (_, config) => config.appSetting.onlyStatisticsProxy,
|
||||
builder: (_, onlyProxy, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.onlyStatisticsProxy),
|
||||
@@ -50,7 +50,7 @@ class UsageSwitch extends StatelessWidget {
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
onlyProxy: value,
|
||||
onlyStatisticsProxy: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -343,8 +343,8 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_obscureController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -225,7 +225,7 @@ class FakeIpFilterItem extends StatelessWidget {
|
||||
title: appLocalizations.fakeipFilter,
|
||||
items: fakeIpFilter,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -260,7 +260,7 @@ class DefaultNameserverItem extends StatelessWidget {
|
||||
title: appLocalizations.defaultNameserver,
|
||||
items: defaultNameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -295,7 +295,7 @@ class NameserverItem extends StatelessWidget {
|
||||
title: "域名服务器",
|
||||
items: nameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -384,7 +384,7 @@ class NameserverPolicyItem extends StatelessWidget {
|
||||
items: nameserverPolicy.entries,
|
||||
titleBuilder: (item) => Text(item.key),
|
||||
subtitleBuilder: (item) => Text(item.value),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -419,7 +419,7 @@ class ProxyServerNameserverItem extends StatelessWidget {
|
||||
title: appLocalizations.proxyNameserver,
|
||||
items: proxyServerNameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -454,7 +454,7 @@ class FallbackItem extends StatelessWidget {
|
||||
title: appLocalizations.fallback,
|
||||
items: fallback,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -555,7 +555,7 @@ class GeositeItem extends StatelessWidget {
|
||||
title: "Geosite",
|
||||
items: geosite,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -591,7 +591,7 @@ class IpcidrItem extends StatelessWidget {
|
||||
title: appLocalizations.ipcidr,
|
||||
items: ipcidr,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -627,7 +627,7 @@ class DomainItem extends StatelessWidget {
|
||||
title: appLocalizations.domain,
|
||||
items: domain,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -705,20 +705,19 @@ class DnsListView extends StatelessWidget {
|
||||
|
||||
_initActions(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final commonScaffoldState =
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
commonScaffoldState?.actions = [
|
||||
context.commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.reset,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.resetTip,
|
||||
),
|
||||
onTab: () {
|
||||
globalState.appController.clashConfig.dns = defaultDns;
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
onPressed: () async {
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.reset,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.resetTip,
|
||||
),
|
||||
);
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
globalState.appController.clashConfig.dns = defaultDns;
|
||||
},
|
||||
tooltip: appLocalizations.reset,
|
||||
icon: const Icon(
|
||||
|
||||
@@ -206,23 +206,21 @@ class BypassDomainItem extends StatelessWidget {
|
||||
|
||||
_initActions(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final commonScaffoldState =
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
commonScaffoldState?.actions = [
|
||||
context.commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
globalState.showMessage(
|
||||
onPressed: () async {
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.reset,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.resetTip,
|
||||
),
|
||||
onTab: () {
|
||||
final config = globalState.appController.config;
|
||||
config.networkProps = config.networkProps.copyWith(
|
||||
bypassDomain: defaultBypassDomain,
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
final config = globalState.appController.config;
|
||||
config.networkProps = config.networkProps.copyWith(
|
||||
bypassDomain: defaultBypassDomain,
|
||||
);
|
||||
},
|
||||
tooltip: appLocalizations.reset,
|
||||
@@ -378,23 +376,21 @@ class NetworkListView extends StatelessWidget {
|
||||
|
||||
_initActions(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final commonScaffoldState =
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
commonScaffoldState?.actions = [
|
||||
context.commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
globalState.showMessage(
|
||||
onPressed: () async {
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.reset,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.resetTip,
|
||||
),
|
||||
onTab: () {
|
||||
final appController = globalState.appController;
|
||||
appController.config.vpnProps = defaultVpnProps;
|
||||
appController.clashConfig.tun = defaultTun;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
appController.config.vpnProps = defaultVpnProps;
|
||||
appController.clashConfig.tun = defaultTun;
|
||||
},
|
||||
tooltip: appLocalizations.reset,
|
||||
icon: const Icon(
|
||||
|
||||
155
lib/fragments/connection/connections.dart
Normal file
155
lib/fragments/connection/connections.dart
Normal file
@@ -0,0 +1,155 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'item.dart';
|
||||
|
||||
class ConnectionsFragment extends StatefulWidget {
|
||||
const ConnectionsFragment({super.key});
|
||||
|
||||
@override
|
||||
State<ConnectionsFragment> createState() => _ConnectionsFragmentState();
|
||||
}
|
||||
|
||||
class _ConnectionsFragmentState extends State<ConnectionsFragment>
|
||||
with ViewMixin {
|
||||
final _connectionsStateNotifier = ValueNotifier<ConnectionsState>(
|
||||
const ConnectionsState(),
|
||||
);
|
||||
final ScrollController _scrollController = ScrollController(
|
||||
keepScrollOffset: false,
|
||||
);
|
||||
|
||||
Timer? timer;
|
||||
|
||||
@override
|
||||
List<Widget> get actions => [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
clashCore.closeConnections();
|
||||
_connectionsStateNotifier.value =
|
||||
_connectionsStateNotifier.value.copyWith(
|
||||
connections: await clashCore.getConnections(),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.delete_sweep_outlined),
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
get onSearch => (value) {
|
||||
_connectionsStateNotifier.value =
|
||||
_connectionsStateNotifier.value.copyWith(
|
||||
query: value,
|
||||
);
|
||||
};
|
||||
|
||||
@override
|
||||
get onKeywordsUpdate => (keywords) {
|
||||
_connectionsStateNotifier.value =
|
||||
_connectionsStateNotifier.value.copyWith(keywords: keywords);
|
||||
};
|
||||
|
||||
_updateConnections() async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
_connectionsStateNotifier.value =
|
||||
_connectionsStateNotifier.value.copyWith(
|
||||
connections: await clashCore.getConnections(),
|
||||
);
|
||||
timer = Timer(Duration(seconds: 1), () async {
|
||||
_updateConnections();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateConnections();
|
||||
}
|
||||
|
||||
_initActions() {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) {
|
||||
initViewState();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_handleBlockConnection(String id) async {
|
||||
clashCore.closeConnection(id);
|
||||
_connectionsStateNotifier.value = _connectionsStateNotifier.value.copyWith(
|
||||
connections: await clashCore.getConnections(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
timer?.cancel();
|
||||
_connectionsStateNotifier.dispose();
|
||||
_scrollController.dispose();
|
||||
timer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<AppState, bool?>(
|
||||
selector: (_, appState) =>
|
||||
appState.currentLabel == 'connections' ||
|
||||
appState.viewMode == ViewMode.mobile &&
|
||||
appState.currentLabel == "tools",
|
||||
builder: (_, isCurrent, child) {
|
||||
if (isCurrent == null || isCurrent) {
|
||||
_initActions();
|
||||
}
|
||||
return child!;
|
||||
},
|
||||
child: ValueListenableBuilder<ConnectionsState>(
|
||||
valueListenable: _connectionsStateNotifier,
|
||||
builder: (_, state, __) {
|
||||
final connections = state.list;
|
||||
if (connections.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullConnectionsDesc,
|
||||
);
|
||||
}
|
||||
return CommonScrollBar(
|
||||
controller: _scrollController,
|
||||
child: ListView.separated(
|
||||
controller: _scrollController,
|
||||
itemBuilder: (_, index) {
|
||||
final connection = connections[index];
|
||||
return ConnectionItem(
|
||||
key: Key(connection.id),
|
||||
connection: connection,
|
||||
onClick: (value) {
|
||||
context.commonScaffoldState?.addKeyword(value);
|
||||
},
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.block),
|
||||
onPressed: () {
|
||||
_handleBlockConnection(connection.id);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(
|
||||
height: 0,
|
||||
);
|
||||
},
|
||||
itemCount: connections.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
150
lib/fragments/connection/item.dart
Normal file
150
lib/fragments/connection/item.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
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/plugins/app.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class FindProcessBuilder extends StatelessWidget {
|
||||
final Widget Function(bool value) builder;
|
||||
|
||||
const FindProcessBuilder({
|
||||
super.key,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) =>
|
||||
clashConfig.findProcessMode == FindProcessMode.always &&
|
||||
Platform.isAndroid,
|
||||
builder: (_, value, __) {
|
||||
return builder(value);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectionItem extends StatelessWidget {
|
||||
final Connection connection;
|
||||
final Function(String)? onClick;
|
||||
final Widget? trailing;
|
||||
|
||||
const ConnectionItem({
|
||||
super.key,
|
||||
required this.connection,
|
||||
this.onClick,
|
||||
this.trailing,
|
||||
});
|
||||
|
||||
Future<ImageProvider?> _getPackageIcon(Connection connection) async {
|
||||
return await app?.getPackageIcon(connection.metadata.process);
|
||||
}
|
||||
|
||||
String _getSourceText(Connection connection) {
|
||||
final metadata = connection.metadata;
|
||||
if (metadata.process.isEmpty) {
|
||||
return connection.start.lastUpdateTimeDesc;
|
||||
}
|
||||
return "${metadata.process} · ${connection.start.lastUpdateTimeDesc}";
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final title = Text(
|
||||
connection.desc,
|
||||
style: context.textTheme.bodyLarge,
|
||||
);
|
||||
final subTitle = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Text(
|
||||
_getSourceText(connection),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Wrap(
|
||||
runSpacing: 6,
|
||||
spacing: 6,
|
||||
children: [
|
||||
for (final chain in connection.chains)
|
||||
CommonChip(
|
||||
label: chain,
|
||||
onPressed: () {
|
||||
if (onClick == null) return;
|
||||
onClick!(chain);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
if (!Platform.isAndroid) {
|
||||
return ListItem(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
|
||||
title: title,
|
||||
subtitle: subTitle,
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
return FindProcessBuilder(
|
||||
builder: (bool value) {
|
||||
final leading = value
|
||||
? GestureDetector(
|
||||
onTap: () {
|
||||
if (onClick == null) return;
|
||||
final process = connection.metadata.process;
|
||||
if (process.isEmpty) return;
|
||||
onClick!(process);
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 4),
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: FutureBuilder<ImageProvider?>(
|
||||
future: _getPackageIcon(connection),
|
||||
builder: (_, snapshot) {
|
||||
if (!snapshot.hasData && snapshot.data == null) {
|
||||
return Container();
|
||||
} else {
|
||||
return Image(
|
||||
image: snapshot.data!,
|
||||
gaplessPlayback: true,
|
||||
width: 48,
|
||||
height: 48,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
return ListItem(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
|
||||
leading: leading,
|
||||
title: title,
|
||||
subtitle: subTitle,
|
||||
trailing: trailing,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
239
lib/fragments/connection/requests.dart
Normal file
239
lib/fragments/connection/requests.dart
Normal file
@@ -0,0 +1,239 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/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';
|
||||
|
||||
import 'item.dart';
|
||||
|
||||
double _preOffset = 0;
|
||||
|
||||
class RequestsFragment extends StatefulWidget {
|
||||
const RequestsFragment({super.key});
|
||||
|
||||
@override
|
||||
State<RequestsFragment> createState() => _RequestsFragmentState();
|
||||
}
|
||||
|
||||
class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
|
||||
final _requestsStateNotifier =
|
||||
ValueNotifier<ConnectionsState>(const ConnectionsState());
|
||||
List<Connection> _requests = [];
|
||||
|
||||
final ScrollController _scrollController = ScrollController(
|
||||
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
|
||||
);
|
||||
|
||||
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
|
||||
|
||||
double _currentMaxWidth = 0;
|
||||
|
||||
@override
|
||||
get onSearch => (value) {
|
||||
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
|
||||
query: value,
|
||||
);
|
||||
};
|
||||
|
||||
@override
|
||||
get onKeywordsUpdate => (keywords) {
|
||||
_requestsStateNotifier.value =
|
||||
_requestsStateNotifier.value.copyWith(keywords: keywords);
|
||||
};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final appController = globalState.appController;
|
||||
final appState = appController.appState;
|
||||
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
|
||||
connections: appState.requests,
|
||||
);
|
||||
}
|
||||
|
||||
_initActions() {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) {
|
||||
initViewState();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
double _calcCacheHeight(Connection item) {
|
||||
final cacheHeight = _cacheDynamicHeightMap.get(item.id);
|
||||
if (cacheHeight != null) {
|
||||
return cacheHeight;
|
||||
}
|
||||
final size = globalState.measure.computeTextSize(
|
||||
Text(
|
||||
item.desc,
|
||||
style: context.textTheme.bodyLarge,
|
||||
),
|
||||
maxWidth: _currentMaxWidth,
|
||||
);
|
||||
final chainsText = item.chains.join("");
|
||||
final length = item.chains.length;
|
||||
final chainSize = globalState.measure.computeTextSize(
|
||||
Text(
|
||||
chainsText,
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
maxWidth: (_currentMaxWidth - (length - 1) * 6 - length * 24),
|
||||
);
|
||||
final baseHeight = globalState.measure.bodyMediumHeight;
|
||||
final lines = (chainSize.height / baseHeight).round();
|
||||
final computerHeight =
|
||||
size.height + chainSize.height + 24 + 24 * (lines - 1);
|
||||
_cacheDynamicHeightMap.put(item.id, computerHeight);
|
||||
return computerHeight;
|
||||
}
|
||||
|
||||
_handleTryClearCache(double maxWidth) {
|
||||
if (_currentMaxWidth != maxWidth) {
|
||||
_currentMaxWidth = maxWidth;
|
||||
_cacheDynamicHeightMap.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_requestsStateNotifier.dispose();
|
||||
_scrollController.dispose();
|
||||
_currentMaxWidth = 0;
|
||||
_cacheDynamicHeightMap.clear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _wrapPage(Widget child) {
|
||||
return Selector<AppState, bool?>(
|
||||
selector: (_, appState) =>
|
||||
appState.currentLabel == 'requests' ||
|
||||
appState.viewMode == ViewMode.mobile &&
|
||||
appState.currentLabel == "tools",
|
||||
builder: (_, isCurrent, child) {
|
||||
if (isCurrent == null || isCurrent) {
|
||||
_initActions();
|
||||
}
|
||||
return child!;
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
updateRequestsThrottler() {
|
||||
throttler.call("request", () {
|
||||
final isEquality = connectionListEquality.equals(
|
||||
_requests,
|
||||
_requestsStateNotifier.value.connections,
|
||||
);
|
||||
if (isEquality) {
|
||||
return;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
|
||||
connections: _requests,
|
||||
);
|
||||
});
|
||||
}, duration: commonDuration);
|
||||
}
|
||||
|
||||
Widget _wrapRequestsUpdate(Widget child) {
|
||||
return Selector<AppState, List<Connection>>(
|
||||
selector: (_, appState) => appState.requests,
|
||||
shouldRebuild: (prev, next) {
|
||||
final isEquality = connectionListEquality.equals(prev, next);
|
||||
if (!isEquality) {
|
||||
_requests = next;
|
||||
updateRequestsThrottler();
|
||||
}
|
||||
return !isEquality;
|
||||
},
|
||||
builder: (_, next, child) {
|
||||
return child!;
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (_, constraints) {
|
||||
return FindProcessBuilder(builder: (value) {
|
||||
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0));
|
||||
return _wrapPage(
|
||||
_wrapRequestsUpdate(
|
||||
ValueListenableBuilder<ConnectionsState>(
|
||||
valueListenable: _requestsStateNotifier,
|
||||
builder: (_, state, __) {
|
||||
final connections = state.list;
|
||||
if (connections.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullRequestsDesc,
|
||||
);
|
||||
}
|
||||
final items = connections
|
||||
.map<Widget>(
|
||||
(connection) => ConnectionItem(
|
||||
key: Key(connection.id),
|
||||
connection: connection,
|
||||
onClick: (value) {
|
||||
context.commonScaffoldState?.addKeyword(value);
|
||||
},
|
||||
),
|
||||
)
|
||||
.separated(
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: NotificationListener<ScrollEndNotification>(
|
||||
onNotification: (details) {
|
||||
_preOffset = details.metrics.pixels;
|
||||
return false;
|
||||
},
|
||||
child: CommonScrollBar(
|
||||
controller: _scrollController,
|
||||
child: ListView.builder(
|
||||
reverse: true,
|
||||
shrinkWrap: true,
|
||||
physics: NextClampingScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
itemExtentBuilder: (index, __) {
|
||||
final widget = items[index];
|
||||
if (widget.runtimeType == Divider) {
|
||||
return 0;
|
||||
}
|
||||
final measure = globalState.measure;
|
||||
final bodyMediumHeight = measure.bodyMediumHeight;
|
||||
final connection = connections[(index / 2).floor()];
|
||||
final height = _calcCacheHeight(connection);
|
||||
return height + bodyMediumHeight + 32;
|
||||
},
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
itemCount: items.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,352 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ConnectionsFragment extends StatefulWidget {
|
||||
const ConnectionsFragment({super.key});
|
||||
|
||||
@override
|
||||
State<ConnectionsFragment> createState() => _ConnectionsFragmentState();
|
||||
}
|
||||
|
||||
class _ConnectionsFragmentState extends State<ConnectionsFragment> {
|
||||
final connectionsNotifier =
|
||||
ValueNotifier<ConnectionsAndKeywords>(const ConnectionsAndKeywords());
|
||||
final ScrollController _scrollController = ScrollController(
|
||||
keepScrollOffset: false,
|
||||
);
|
||||
|
||||
Timer? timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
||||
connections: await clashCore.getConnections(),
|
||||
);
|
||||
if (timer != null) {
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
}
|
||||
timer = Timer.periodic(
|
||||
const Duration(seconds: 1),
|
||||
(timer) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
||||
connections: await clashCore.getConnections(),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_initActions() {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) {
|
||||
final commonScaffoldState =
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showSearch(
|
||||
context: context,
|
||||
delegate: ConnectionsSearchDelegate(
|
||||
state: connectionsNotifier.value,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.search),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
clashCore.closeConnections();
|
||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
||||
connections: await clashCore.getConnections(),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.delete_sweep_outlined),
|
||||
),
|
||||
];
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_addKeyword(String keyword) {
|
||||
final isContains = connectionsNotifier.value.keywords.contains(keyword);
|
||||
if (isContains) return;
|
||||
final keywords = List<String>.from(connectionsNotifier.value.keywords)
|
||||
..add(keyword);
|
||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
||||
keywords: keywords,
|
||||
);
|
||||
}
|
||||
|
||||
_deleteKeyword(String keyword) {
|
||||
final isContains = connectionsNotifier.value.keywords.contains(keyword);
|
||||
if (!isContains) return;
|
||||
final keywords = List<String>.from(connectionsNotifier.value.keywords)
|
||||
..remove(keyword);
|
||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
||||
keywords: keywords,
|
||||
);
|
||||
}
|
||||
|
||||
_handleBlockConnection(String id) async {
|
||||
clashCore.closeConnection(id);
|
||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
||||
connections: await clashCore.getConnections(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
timer?.cancel();
|
||||
connectionsNotifier.dispose();
|
||||
_scrollController.dispose();
|
||||
timer = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<AppState, bool?>(
|
||||
selector: (_, appState) =>
|
||||
appState.currentLabel == 'connections' ||
|
||||
appState.viewMode == ViewMode.mobile &&
|
||||
appState.currentLabel == "tools",
|
||||
builder: (_, isCurrent, child) {
|
||||
if (isCurrent == null || isCurrent) {
|
||||
_initActions();
|
||||
}
|
||||
return child!;
|
||||
},
|
||||
child: ValueListenableBuilder<ConnectionsAndKeywords>(
|
||||
valueListenable: connectionsNotifier,
|
||||
builder: (_, state, __) {
|
||||
var connections = state.filteredConnections;
|
||||
if (connections.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullConnectionsDesc,
|
||||
);
|
||||
}
|
||||
connections = connections.reversed.toList();
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (state.keywords.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
child: Wrap(
|
||||
runSpacing: 6,
|
||||
spacing: 6,
|
||||
children: [
|
||||
for (final keyword in state.keywords)
|
||||
CommonChip(
|
||||
label: keyword,
|
||||
type: ChipType.delete,
|
||||
onPressed: () {
|
||||
_deleteKeyword(keyword);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
controller: _scrollController,
|
||||
itemBuilder: (_, index) {
|
||||
final connection = connections[index];
|
||||
return ConnectionItem(
|
||||
key: Key(connection.id),
|
||||
connection: connection,
|
||||
onClick: _addKeyword,
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.block),
|
||||
onPressed: () {
|
||||
_handleBlockConnection(connection.id);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(
|
||||
height: 0,
|
||||
);
|
||||
},
|
||||
itemCount: connections.length,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectionsSearchDelegate extends SearchDelegate {
|
||||
ValueNotifier<ConnectionsAndKeywords> connectionsNotifier;
|
||||
|
||||
ConnectionsSearchDelegate({
|
||||
required ConnectionsAndKeywords state,
|
||||
}) : connectionsNotifier = ValueNotifier<ConnectionsAndKeywords>(state);
|
||||
|
||||
get state => connectionsNotifier.value;
|
||||
|
||||
List<Connection> get _results {
|
||||
final lowerQuery = query.toLowerCase().trim();
|
||||
return connectionsNotifier.value.filteredConnections.where((request) {
|
||||
final lowerNetwork = request.metadata.network.toLowerCase();
|
||||
final lowerHost = request.metadata.host.toLowerCase();
|
||||
final lowerDestinationIP = request.metadata.destinationIP.toLowerCase();
|
||||
final lowerProcess = request.metadata.process.toLowerCase();
|
||||
final lowerChains = request.chains.join("").toLowerCase();
|
||||
return lowerNetwork.contains(lowerQuery) ||
|
||||
lowerHost.contains(lowerQuery) ||
|
||||
lowerDestinationIP.contains(lowerQuery) ||
|
||||
lowerProcess.contains(lowerQuery) ||
|
||||
lowerChains.contains(lowerQuery);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
_addKeyword(String keyword) {
|
||||
final isContains = connectionsNotifier.value.keywords.contains(keyword);
|
||||
if (isContains) return;
|
||||
final keywords = List<String>.from(connectionsNotifier.value.keywords)
|
||||
..add(keyword);
|
||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
||||
keywords: keywords,
|
||||
);
|
||||
}
|
||||
|
||||
_deleteKeyword(String keyword) {
|
||||
final isContains = connectionsNotifier.value.keywords.contains(keyword);
|
||||
if (!isContains) return;
|
||||
final keywords = List<String>.from(connectionsNotifier.value.keywords)
|
||||
..remove(keyword);
|
||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
||||
keywords: keywords,
|
||||
);
|
||||
}
|
||||
|
||||
_handleBlockConnection(String id) async {
|
||||
clashCore.closeConnection(id);
|
||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
||||
connections: await clashCore.getConnections(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Widget>? buildActions(BuildContext context) {
|
||||
return [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (query.isEmpty) {
|
||||
close(context, null);
|
||||
return;
|
||||
}
|
||||
query = '';
|
||||
},
|
||||
icon: const Icon(Icons.clear),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? buildLeading(BuildContext context) {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
close(context, null);
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildResults(BuildContext context) {
|
||||
return buildSuggestions(context);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
connectionsNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildSuggestions(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: connectionsNotifier,
|
||||
builder: (_, __, ___) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (state.keywords.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
child: Wrap(
|
||||
runSpacing: 6,
|
||||
spacing: 6,
|
||||
children: [
|
||||
for (final keyword in state.keywords)
|
||||
CommonChip(
|
||||
label: keyword,
|
||||
type: ChipType.delete,
|
||||
onPressed: () {
|
||||
_deleteKeyword(keyword);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemBuilder: (_, index) {
|
||||
final connection = _results[index];
|
||||
return ConnectionItem(
|
||||
key: Key(connection.id),
|
||||
connection: connection,
|
||||
onClick: _addKeyword,
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.block),
|
||||
onPressed: () {
|
||||
_handleBlockConnection(connection.id);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(
|
||||
height: 0,
|
||||
);
|
||||
},
|
||||
itemCount: _results.length,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,13 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/fragments/dashboard/intranet_ip.dart';
|
||||
import 'package:fl_clash/fragments/dashboard/status_button.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';
|
||||
|
||||
import 'network_detection.dart';
|
||||
import 'network_speed.dart';
|
||||
import 'outbound_mode.dart';
|
||||
import 'start_button.dart';
|
||||
import 'traffic_usage.dart';
|
||||
import 'widgets/start_button.dart';
|
||||
|
||||
class DashboardFragment extends StatefulWidget {
|
||||
const DashboardFragment({super.key});
|
||||
@@ -22,14 +17,56 @@ class DashboardFragment extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DashboardFragmentState extends State<DashboardFragment> {
|
||||
_initFab(bool isCurrent) {
|
||||
final key = GlobalKey<SuperGridState>();
|
||||
|
||||
_initScaffold(bool isCurrent) {
|
||||
if (!isCurrent) {
|
||||
return;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final commonScaffoldState =
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
commonScaffoldState?.floatingActionButton = const StartButton();
|
||||
commonScaffoldState?.actions = [
|
||||
ValueListenableBuilder(
|
||||
valueListenable: key.currentState!.addedChildrenNotifier,
|
||||
builder: (_, addedChildren, child) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: key.currentState!.isEditNotifier,
|
||||
builder: (_, isEdit, child) {
|
||||
if (!isEdit || addedChildren.isEmpty) {
|
||||
return Container();
|
||||
}
|
||||
return child!;
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
key.currentState!.showAddModal();
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.add_circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: ValueListenableBuilder(
|
||||
valueListenable: key.currentState!.isEditNotifier,
|
||||
builder: (_, isEdit, ___) {
|
||||
return isEdit
|
||||
? Icon(Icons.save)
|
||||
: Icon(
|
||||
Icons.edit,
|
||||
);
|
||||
},
|
||||
),
|
||||
onPressed: () {
|
||||
key.currentState!.isEditNotifier.value =
|
||||
!key.currentState!.isEditNotifier.value;
|
||||
},
|
||||
),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -38,7 +75,7 @@ class _DashboardFragmentState extends State<DashboardFragment> {
|
||||
return ActiveBuilder(
|
||||
label: "dashboard",
|
||||
builder: (isCurrent, child) {
|
||||
_initFab(isCurrent);
|
||||
_initScaffold(isCurrent);
|
||||
return child!;
|
||||
},
|
||||
child: Align(
|
||||
@@ -47,52 +84,52 @@ class _DashboardFragmentState extends State<DashboardFragment> {
|
||||
padding: const EdgeInsets.all(16).copyWith(
|
||||
bottom: 88,
|
||||
),
|
||||
child: Selector<AppState, double>(
|
||||
selector: (_, appState) => appState.viewWidth,
|
||||
builder: (_, viewWidth, ___) {
|
||||
final columns = max(4 * ((viewWidth / 350).ceil()), 8);
|
||||
final int switchCount = (4 / columns) * viewWidth < 200 ? 8 : 4;
|
||||
return Grid(
|
||||
child: Selector2<AppState, Config, DashboardState>(
|
||||
selector: (_, appState, config) => DashboardState(
|
||||
dashboardWidgets: config.appSetting.dashboardWidgets,
|
||||
viewWidth: appState.viewWidth,
|
||||
),
|
||||
builder: (_, state, ___) {
|
||||
final columns = max(4 * ((state.viewWidth / 350).ceil()), 8);
|
||||
return SuperGrid(
|
||||
key: key,
|
||||
crossAxisCount: columns,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
children: [
|
||||
const GridItem(
|
||||
crossAxisCellCount: 8,
|
||||
child: NetworkSpeed(),
|
||||
),
|
||||
// if (Platform.isAndroid)
|
||||
// GridItem(
|
||||
// crossAxisCellCount: switchCount,
|
||||
// child: const VPNSwitch(),
|
||||
// ),
|
||||
if (system.isDesktop) ...[
|
||||
GridItem(
|
||||
crossAxisCellCount: switchCount,
|
||||
child: const TUNButton(),
|
||||
),
|
||||
GridItem(
|
||||
crossAxisCellCount: switchCount,
|
||||
child: const SystemProxyButton(),
|
||||
),
|
||||
],
|
||||
const GridItem(
|
||||
crossAxisCellCount: 4,
|
||||
child: OutboundMode(),
|
||||
),
|
||||
const GridItem(
|
||||
crossAxisCellCount: 4,
|
||||
child: NetworkDetection(),
|
||||
),
|
||||
const GridItem(
|
||||
crossAxisCellCount: 4,
|
||||
child: TrafficUsage(),
|
||||
),
|
||||
const GridItem(
|
||||
crossAxisCellCount: 4,
|
||||
child: IntranetIP(),
|
||||
),
|
||||
...state.dashboardWidgets
|
||||
.where(
|
||||
(item) => item.platforms.contains(
|
||||
SupportPlatform.currentPlatform,
|
||||
),
|
||||
)
|
||||
.map(
|
||||
(item) => item.widget,
|
||||
),
|
||||
],
|
||||
onSave: (girdItems) {
|
||||
final dashboardWidgets = girdItems
|
||||
.map(
|
||||
(item) => DashboardWidget.getDashboardWidget(item),
|
||||
)
|
||||
.toList();
|
||||
final config = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
dashboardWidgets: dashboardWidgets,
|
||||
);
|
||||
},
|
||||
addedItemsBuilder: (girdItems) {
|
||||
return DashboardWidget.values
|
||||
.where(
|
||||
(item) =>
|
||||
!girdItems.contains(item.widget) &&
|
||||
item.platforms.contains(
|
||||
SupportPlatform.currentPlatform,
|
||||
),
|
||||
)
|
||||
.map((item) => item.widget)
|
||||
.toList();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class IntranetIP extends StatefulWidget {
|
||||
const IntranetIP({super.key});
|
||||
|
||||
@override
|
||||
State<IntranetIP> createState() => _IntranetIPState();
|
||||
}
|
||||
|
||||
class _IntranetIPState extends State<IntranetIP> {
|
||||
final ipNotifier = ValueNotifier<String?>("");
|
||||
late StreamSubscription subscription;
|
||||
|
||||
Future<String> getNetworkType() async {
|
||||
try {
|
||||
final interfaces = await NetworkInterface.list(
|
||||
includeLoopback: false,
|
||||
type: InternetAddressType.any,
|
||||
);
|
||||
for (var interface in interfaces) {
|
||||
if (interface.name.toLowerCase().contains('wlan') ||
|
||||
interface.name.toLowerCase().contains('wi-fi')) {
|
||||
return 'WiFi';
|
||||
}
|
||||
if (interface.name.toLowerCase().contains('rmnet') ||
|
||||
interface.name.toLowerCase().contains('ccmni') ||
|
||||
interface.name.toLowerCase().contains('cellular')) {
|
||||
return 'Mobile Data';
|
||||
}
|
||||
}
|
||||
return 'Unknown';
|
||||
} catch (e) {
|
||||
return 'Error';
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> getLocalIpAddress() async {
|
||||
await Future.delayed(animateDuration);
|
||||
List<NetworkInterface> interfaces = await NetworkInterface.list(
|
||||
includeLoopback: false,
|
||||
)
|
||||
..sort((a, b) {
|
||||
if (a.isWifi && !b.isWifi) return -1;
|
||||
if (!a.isWifi && b.isWifi) return 1;
|
||||
if (a.includesIPv4 && !b.includesIPv4) return -1;
|
||||
if (!a.includesIPv4 && b.includesIPv4) return 1;
|
||||
return 0;
|
||||
});
|
||||
for (final interface in interfaces) {
|
||||
final addresses = interface.addresses;
|
||||
if (addresses.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
addresses.sort((a, b) {
|
||||
if (a.isIPv4 && !b.isIPv4) return -1;
|
||||
if (!a.isIPv4 && b.isIPv4) return 1;
|
||||
return 0;
|
||||
});
|
||||
return addresses.first.address;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
subscription = Connectivity().onConnectivityChanged.listen((_) async {
|
||||
ipNotifier.value = null;
|
||||
debugPrint("[App] Connection change");
|
||||
ipNotifier.value = await getLocalIpAddress() ?? "";
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
ipNotifier.value = await getLocalIpAddress() ?? "";
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonCard(
|
||||
info: Info(
|
||||
label: appLocalizations.intranetIP,
|
||||
iconData: Icons.devices,
|
||||
),
|
||||
onPressed: () {},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16).copyWith(top: 0),
|
||||
height: globalState.measure.titleMediumHeight + 24 - 2,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: ipNotifier,
|
||||
builder: (_, value, __) {
|
||||
return FadeBox(
|
||||
child: value != null
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
value.isNotEmpty
|
||||
? value
|
||||
: appLocalizations.noNetwork,
|
||||
style: context
|
||||
.textTheme.titleLarge?.toSoftBold.toMinus,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const Padding(
|
||||
padding: EdgeInsets.all(2),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
subscription.cancel();
|
||||
ipNotifier.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.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';
|
||||
|
||||
final networkDetectionState = ValueNotifier<NetworkDetectionState>(
|
||||
const NetworkDetectionState(
|
||||
isTesting: true,
|
||||
ipInfo: null,
|
||||
),
|
||||
);
|
||||
|
||||
class NetworkDetection extends StatefulWidget {
|
||||
const NetworkDetection({super.key});
|
||||
|
||||
@override
|
||||
State<NetworkDetection> createState() => _NetworkDetectionState();
|
||||
}
|
||||
|
||||
class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
bool? _preIsStart;
|
||||
Function? _checkIpDebounce;
|
||||
Timer? _setTimeoutTimer;
|
||||
CancelToken? cancelToken;
|
||||
|
||||
_checkIp() async {
|
||||
final appState = globalState.appController.appState;
|
||||
final appFlowingState = globalState.appController.appFlowingState;
|
||||
final isInit = appState.isInit;
|
||||
if (!isInit) return;
|
||||
final isStart = appFlowingState.isStart;
|
||||
if (_preIsStart == false && _preIsStart == isStart) return;
|
||||
_clearSetTimeoutTimer();
|
||||
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||
isTesting: true,
|
||||
ipInfo: null,
|
||||
);
|
||||
_preIsStart = isStart;
|
||||
if (cancelToken != null) {
|
||||
cancelToken!.cancel();
|
||||
cancelToken = null;
|
||||
}
|
||||
cancelToken = CancelToken();
|
||||
try {
|
||||
final ipInfo = await request.checkIp(cancelToken: cancelToken);
|
||||
if (ipInfo != null) {
|
||||
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||
isTesting: false,
|
||||
ipInfo: ipInfo,
|
||||
);
|
||||
return;
|
||||
}
|
||||
_clearSetTimeoutTimer();
|
||||
_setTimeoutTimer = Timer(const Duration(milliseconds: 300), () {
|
||||
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||
isTesting: false,
|
||||
ipInfo: null,
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.toString() == "cancelled") {
|
||||
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||
isTesting: true,
|
||||
ipInfo: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_clearSetTimeoutTimer() {
|
||||
if (_setTimeoutTimer != null) {
|
||||
_setTimeoutTimer?.cancel();
|
||||
_setTimeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
_checkIpContainer(Widget child) {
|
||||
return Selector<AppState, num>(
|
||||
selector: (_, appState) {
|
||||
return appState.checkIpNum;
|
||||
},
|
||||
builder: (_, checkIpNum, child) {
|
||||
if (_checkIpDebounce != null) {
|
||||
_checkIpDebounce!();
|
||||
}
|
||||
return child!;
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String countryCodeToEmoji(String countryCode) {
|
||||
final String code = countryCode.toUpperCase();
|
||||
if (code.length != 2) {
|
||||
return countryCode;
|
||||
}
|
||||
final int firstLetter = code.codeUnitAt(0) - 0x41 + 0x1F1E6;
|
||||
final int secondLetter = code.codeUnitAt(1) - 0x41 + 0x1F1E6;
|
||||
return String.fromCharCode(firstLetter) + String.fromCharCode(secondLetter);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_checkIpDebounce ??= debounce(_checkIp);
|
||||
return _checkIpContainer(
|
||||
ValueListenableBuilder<NetworkDetectionState>(
|
||||
valueListenable: networkDetectionState,
|
||||
builder: (_, state, __) {
|
||||
final ipInfo = state.ipInfo;
|
||||
final isTesting = state.isTesting;
|
||||
return CommonCard(
|
||||
onPressed: () {},
|
||||
child: Column(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.network_check,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: FadeBox(
|
||||
child: isTesting
|
||||
? Text(
|
||||
appLocalizations.checking,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium,
|
||||
)
|
||||
: ipInfo != null
|
||||
? Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
height: globalState
|
||||
.measure.titleMediumHeight,
|
||||
child: Text(
|
||||
countryCodeToEmoji(
|
||||
ipInfo.countryCode),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(
|
||||
fontFamily:
|
||||
FontFamily.twEmoji.value,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
appLocalizations.checkError,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: globalState.measure.titleLargeHeight + 24 - 2,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.all(16).copyWith(top: 0),
|
||||
child: FadeBox(
|
||||
child: ipInfo != null
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
ipInfo.ip,
|
||||
style: context.textTheme.titleLarge
|
||||
?.toSoftBold.toMinus,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: FadeBox(
|
||||
child: isTesting == false && ipInfo == null
|
||||
? Text(
|
||||
"timeout",
|
||||
style: context.textTheme.titleLarge
|
||||
?.copyWith(color: Colors.red)
|
||||
.toSoftBold
|
||||
.toMinus,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: const AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
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 NetworkSpeed extends StatefulWidget {
|
||||
const NetworkSpeed({super.key});
|
||||
|
||||
@override
|
||||
State<NetworkSpeed> createState() => _NetworkSpeedState();
|
||||
}
|
||||
|
||||
class _NetworkSpeedState extends State<NetworkSpeed> {
|
||||
List<Point> initPoints = const [Point(0, 0), Point(1, 0)];
|
||||
|
||||
List<Point> _getPoints(List<Traffic> traffics) {
|
||||
List<Point> trafficPoints = traffics
|
||||
.toList()
|
||||
.asMap()
|
||||
.map(
|
||||
(index, e) => MapEntry(
|
||||
index,
|
||||
Point(
|
||||
(index + initPoints.length).toDouble(),
|
||||
e.speed.toDouble(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.values
|
||||
.toList();
|
||||
|
||||
return [...initPoints, ...trafficPoints];
|
||||
}
|
||||
|
||||
Traffic _getLastTraffic(List<Traffic> traffics) {
|
||||
if (traffics.isEmpty) return Traffic();
|
||||
return traffics.last;
|
||||
}
|
||||
|
||||
Widget _getLabel({
|
||||
required String label,
|
||||
required IconData iconData,
|
||||
required TrafficValue value,
|
||||
}) {
|
||||
final showValue = value.showValue;
|
||||
final showUnit = "${value.showUnit}/s";
|
||||
final titleLargeSoftBold =
|
||||
Theme.of(context).textTheme.titleLarge?.toSoftBold;
|
||||
final bodyMedium = Theme.of(context).textTheme.bodySmall?.toLight;
|
||||
final valueText = Text(
|
||||
showValue,
|
||||
style: titleLargeSoftBold,
|
||||
maxLines: 1,
|
||||
);
|
||||
final unitText = Text(
|
||||
showUnit,
|
||||
style: bodyMedium,
|
||||
maxLines: 1,
|
||||
);
|
||||
final size = globalState.measure.computeTextSize(valueText);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Icon(iconData),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.titleSmall?.toSoftBold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
child: OverflowBox(
|
||||
maxWidth: 156,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: valueText,
|
||||
),
|
||||
const Flexible(
|
||||
flex: 0,
|
||||
child: SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: unitText,
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonCard(
|
||||
onPressed: () {},
|
||||
info: Info(
|
||||
label: appLocalizations.networkSpeed,
|
||||
iconData: Icons.speed_sharp,
|
||||
),
|
||||
child: Selector<AppFlowingState, List<Traffic>>(
|
||||
selector: (_, appFlowingState) => appFlowingState.traffics,
|
||||
builder: (_, traffics, __) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: LineChart(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
points: _getPoints(traffics),
|
||||
height: 100,
|
||||
),
|
||||
),
|
||||
const Flexible(child: SizedBox(height: 16)),
|
||||
Flexible(
|
||||
flex: 0,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _getLabel(
|
||||
iconData: Icons.upload,
|
||||
label: appLocalizations.upload,
|
||||
value: _getLastTraffic(traffics).up,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _getLabel(
|
||||
iconData: Icons.download,
|
||||
label: appLocalizations.download,
|
||||
value: _getLastTraffic(traffics).down,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
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:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class OutboundMode extends StatelessWidget {
|
||||
const OutboundMode({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, Mode>(
|
||||
selector: (_, clashConfig) => clashConfig.mode,
|
||||
builder: (_, mode, __) {
|
||||
return CommonCard(
|
||||
onPressed: () {},
|
||||
info: Info(
|
||||
label: appLocalizations.outboundMode,
|
||||
iconData: Icons.call_split_sharp,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
for (final item in Mode.values)
|
||||
ListItem.radio(
|
||||
horizontalTitleGap: 4,
|
||||
prue: true,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12,
|
||||
right: 16,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
delegate: RadioDelegate(
|
||||
value: item,
|
||||
groupValue: mode,
|
||||
onChanged: (value) async {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
globalState.appController.changeMode(value);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
Intl.message(item.name),
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium?.toSoftBold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import 'package:fl_clash/common/app_localizations.dart';
|
||||
import 'package:fl_clash/common/system.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';
|
||||
|
||||
import '../config/network.dart';
|
||||
|
||||
class TUNButton extends StatelessWidget {
|
||||
const TUNButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ButtonContainer(
|
||||
onPressed: () {
|
||||
showSheet(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return generateListView(generateSection(
|
||||
items: [
|
||||
if (system.isDesktop) const TUNItem(),
|
||||
const TunStackItem(),
|
||||
],
|
||||
));
|
||||
},
|
||||
title: appLocalizations.tun,
|
||||
);
|
||||
},
|
||||
info: Info(
|
||||
label: appLocalizations.tun,
|
||||
iconData: Icons.stacked_line_chart,
|
||||
),
|
||||
child: Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.tun.enable,
|
||||
builder: (_, enable, __) {
|
||||
return LocaleBuilder(
|
||||
builder: (_) => Switch(
|
||||
value: enable,
|
||||
onChanged: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
clashConfig.tun = clashConfig.tun.copyWith(
|
||||
enable: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SystemProxyButton extends StatelessWidget {
|
||||
const SystemProxyButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ButtonContainer(
|
||||
onPressed: () {
|
||||
showSheet(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return generateListView(
|
||||
generateSection(
|
||||
items: [
|
||||
SystemProxyItem(),
|
||||
BypassDomainItem(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
title: appLocalizations.systemProxy,
|
||||
);
|
||||
},
|
||||
info: Info(
|
||||
label: appLocalizations.systemProxy,
|
||||
iconData: Icons.shuffle,
|
||||
),
|
||||
child: Selector<Config, bool>(
|
||||
selector: (_, config) => config.networkProps.systemProxy,
|
||||
builder: (_, systemProxy, __) {
|
||||
return LocaleBuilder(
|
||||
builder: (_) => Switch(
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
value: systemProxy,
|
||||
onChanged: (value) {
|
||||
final config = globalState.appController.config;
|
||||
config.networkProps =
|
||||
config.networkProps.copyWith(systemProxy: value);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ButtonContainer extends StatelessWidget {
|
||||
final Info info;
|
||||
final Widget child;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const ButtonContainer({
|
||||
super.key,
|
||||
required this.info,
|
||||
required this.child,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonCard(
|
||||
onPressed: onPressed,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InfoHeader(
|
||||
info: info,
|
||||
actions: [
|
||||
child,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class TrafficUsage extends StatelessWidget {
|
||||
const TrafficUsage({super.key});
|
||||
|
||||
Widget getTrafficDataItem(
|
||||
BuildContext context,
|
||||
IconData iconData,
|
||||
TrafficValue trafficValue,
|
||||
) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
iconData,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
trafficValue.showValue,
|
||||
style: context.textTheme.labelLarge?.copyWith(fontSize: 18),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
trafficValue.showUnit,
|
||||
style: context.textTheme.labelMedium?.toLight,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonCard(
|
||||
onPressed: () {},
|
||||
info: Info(
|
||||
label: appLocalizations.trafficUsage,
|
||||
iconData: Icons.data_saver_off,
|
||||
),
|
||||
child: Selector<AppFlowingState, Traffic>(
|
||||
selector: (_, appFlowingState) => appFlowingState.totalTraffic,
|
||||
builder: (_, totalTraffic, __) {
|
||||
final upTotalTrafficValue = totalTraffic.up;
|
||||
final downTotalTrafficValue = totalTraffic.down;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16).copyWith(top: 0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: getTrafficDataItem(
|
||||
context,
|
||||
Icons.arrow_upward,
|
||||
upTotalTrafficValue,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: getTrafficDataItem(
|
||||
context,
|
||||
Icons.arrow_downward,
|
||||
downTotalTrafficValue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
64
lib/fragments/dashboard/widgets/intranet_ip.dart
Normal file
64
lib/fragments/dashboard/widgets/intranet_ip.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/app.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 IntranetIP extends StatelessWidget {
|
||||
const IntranetIP({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: getWidgetHeight(1),
|
||||
child: CommonCard(
|
||||
info: Info(
|
||||
label: appLocalizations.intranetIP,
|
||||
iconData: Icons.devices,
|
||||
),
|
||||
onPressed: () {},
|
||||
child: Container(
|
||||
padding: baseInfoEdgeInsets.copyWith(
|
||||
top: 0,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: globalState.measure.bodyMediumHeight + 2,
|
||||
child: Selector<AppFlowingState, String?>(
|
||||
selector: (_, appFlowingState) => appFlowingState.localIp,
|
||||
builder: (_, value, __) {
|
||||
return FadeBox(
|
||||
child: value != null
|
||||
? TooltipText(
|
||||
text: Text(
|
||||
value.isNotEmpty
|
||||
? value
|
||||
: appLocalizations.noNetwork,
|
||||
style: context.textTheme.bodyMedium?.toLight
|
||||
.adjustSize(1),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
padding: EdgeInsets.all(2),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
116
lib/fragments/dashboard/widgets/memory_info.dart
Normal file
116
lib/fragments/dashboard/widgets/memory_info.dart
Normal file
@@ -0,0 +1,116 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/common.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final _memoryInfoStateNotifier = ValueNotifier<TrafficValue>(
|
||||
TrafficValue(value: 0),
|
||||
);
|
||||
|
||||
class MemoryInfo extends StatefulWidget {
|
||||
const MemoryInfo({super.key});
|
||||
|
||||
@override
|
||||
State<MemoryInfo> createState() => _MemoryInfoState();
|
||||
}
|
||||
|
||||
class _MemoryInfoState extends State<MemoryInfo> {
|
||||
Timer? timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateMemory();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_updateMemory() async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final rss = ProcessInfo.currentRss;
|
||||
_memoryInfoStateNotifier.value = TrafficValue(
|
||||
value: clashLib != null ? rss : await clashCore.getMemory() + rss,
|
||||
);
|
||||
timer = Timer(Duration(seconds: 5), () async {
|
||||
_updateMemory();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: getWidgetHeight(2),
|
||||
child: CommonCard(
|
||||
info: Info(
|
||||
iconData: Icons.memory,
|
||||
label: appLocalizations.memoryInfo,
|
||||
),
|
||||
onPressed: () {
|
||||
clashCore.requestGc();
|
||||
},
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _memoryInfoStateNotifier,
|
||||
builder: (_, trafficValue, __) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: baseInfoEdgeInsets.copyWith(
|
||||
bottom: 0,
|
||||
top: 12,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
trafficValue.showValue,
|
||||
style: context.textTheme.titleLarge?.toLight,
|
||||
),
|
||||
SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Text(
|
||||
trafficValue.showUnit,
|
||||
style: context.textTheme.titleLarge?.toLight,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: WaveView(
|
||||
waveAmplitude: 12.0,
|
||||
waveFrequency: 0.35,
|
||||
waveColor: context.colorScheme.secondaryContainer
|
||||
.blendDarken(context, factor: 0.1)
|
||||
.toLighter,
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: WaveView(
|
||||
waveAmplitude: 12.0,
|
||||
waveFrequency: 0.9,
|
||||
waveColor: context.colorScheme.secondaryContainer
|
||||
.blendDarken(context, factor: 0.1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
250
lib/fragments/dashboard/widgets/network_detection.dart
Normal file
250
lib/fragments/dashboard/widgets/network_detection.dart
Normal file
@@ -0,0 +1,250 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.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';
|
||||
|
||||
final _networkDetectionState = ValueNotifier<NetworkDetectionState>(
|
||||
const NetworkDetectionState(
|
||||
isTesting: true,
|
||||
ipInfo: null,
|
||||
),
|
||||
);
|
||||
|
||||
class NetworkDetection extends StatefulWidget {
|
||||
const NetworkDetection({super.key});
|
||||
|
||||
@override
|
||||
State<NetworkDetection> createState() => _NetworkDetectionState();
|
||||
}
|
||||
|
||||
class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
bool? _preIsStart;
|
||||
Timer? _setTimeoutTimer;
|
||||
CancelToken? cancelToken;
|
||||
Completer? checkedCompleter;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
_startCheck() async {
|
||||
await checkedCompleter?.future;
|
||||
if (cancelToken != null) {
|
||||
cancelToken!.cancel();
|
||||
cancelToken = null;
|
||||
}
|
||||
debouncer.call(
|
||||
DebounceTag.checkIp,
|
||||
_checkIp,
|
||||
);
|
||||
}
|
||||
|
||||
_checkIp() async {
|
||||
final appState = globalState.appController.appState;
|
||||
final appFlowingState = globalState.appController.appFlowingState;
|
||||
final isInit = appState.isInit;
|
||||
if (!isInit) return;
|
||||
final isStart = appFlowingState.isStart;
|
||||
if (_preIsStart == false && _preIsStart == isStart) return;
|
||||
_clearSetTimeoutTimer();
|
||||
_networkDetectionState.value = _networkDetectionState.value.copyWith(
|
||||
isTesting: true,
|
||||
ipInfo: null,
|
||||
);
|
||||
_preIsStart = isStart;
|
||||
if (cancelToken != null) {
|
||||
cancelToken!.cancel();
|
||||
cancelToken = null;
|
||||
}
|
||||
cancelToken = CancelToken();
|
||||
try {
|
||||
final ipInfo = await request.checkIp(cancelToken: cancelToken);
|
||||
if (ipInfo != null) {
|
||||
checkedCompleter = Completer();
|
||||
checkedCompleter?.complete(
|
||||
Future.delayed(
|
||||
Duration(milliseconds: 3000),
|
||||
),
|
||||
);
|
||||
_networkDetectionState.value = _networkDetectionState.value.copyWith(
|
||||
isTesting: false,
|
||||
ipInfo: ipInfo,
|
||||
);
|
||||
return;
|
||||
}
|
||||
_clearSetTimeoutTimer();
|
||||
_setTimeoutTimer = Timer(const Duration(milliseconds: 300), () {
|
||||
_networkDetectionState.value = _networkDetectionState.value.copyWith(
|
||||
isTesting: false,
|
||||
ipInfo: null,
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.toString() == "cancelled") {
|
||||
_networkDetectionState.value = _networkDetectionState.value.copyWith(
|
||||
isTesting: true,
|
||||
ipInfo: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_clearSetTimeoutTimer();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_clearSetTimeoutTimer() {
|
||||
if (_setTimeoutTimer != null) {
|
||||
_setTimeoutTimer?.cancel();
|
||||
_setTimeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
_checkIpContainer(Widget child) {
|
||||
return Selector<AppState, num>(
|
||||
selector: (_, appState) {
|
||||
return appState.checkIpNum;
|
||||
},
|
||||
shouldRebuild: (prev, next) {
|
||||
if (prev != next) {
|
||||
_startCheck();
|
||||
}
|
||||
return prev != next;
|
||||
},
|
||||
builder: (_, checkIpNum, child) {
|
||||
return child!;
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
_countryCodeToEmoji(String countryCode) {
|
||||
final String code = countryCode.toUpperCase();
|
||||
if (code.length != 2) {
|
||||
return countryCode;
|
||||
}
|
||||
final int firstLetter = code.codeUnitAt(0) - 0x41 + 0x1F1E6;
|
||||
final int secondLetter = code.codeUnitAt(1) - 0x41 + 0x1F1E6;
|
||||
return String.fromCharCode(firstLetter) + String.fromCharCode(secondLetter);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: getWidgetHeight(1),
|
||||
child: _checkIpContainer(
|
||||
ValueListenableBuilder<NetworkDetectionState>(
|
||||
valueListenable: _networkDetectionState,
|
||||
builder: (_, state, __) {
|
||||
final ipInfo = state.ipInfo;
|
||||
final isTesting = state.isTesting;
|
||||
return CommonCard(
|
||||
onPressed: () {},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
height: globalState.measure.titleMediumHeight + 16,
|
||||
padding: baseInfoEdgeInsets.copyWith(
|
||||
bottom: 0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
ipInfo != null
|
||||
? Text(
|
||||
_countryCodeToEmoji(
|
||||
ipInfo.countryCode,
|
||||
),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.toLight
|
||||
.copyWith(
|
||||
fontFamily: FontFamily.twEmoji.value,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.network_check,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
appLocalizations.networkDetection,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleSmall
|
||||
?.copyWith(
|
||||
color: context.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: baseInfoEdgeInsets.copyWith(
|
||||
top: 0,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: globalState.measure.bodyMediumHeight + 2,
|
||||
child: FadeBox(
|
||||
child: ipInfo != null
|
||||
? TooltipText(
|
||||
text: Text(
|
||||
ipInfo.ip,
|
||||
style: context.textTheme.bodyMedium?.toLight
|
||||
.adjustSize(1),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
: FadeBox(
|
||||
child: isTesting == false && ipInfo == null
|
||||
? Text(
|
||||
"timeout",
|
||||
style: context.textTheme.bodyMedium
|
||||
?.copyWith(color: Colors.red)
|
||||
.adjustSize(1),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: const AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
94
lib/fragments/dashboard/widgets/network_speed.dart
Normal file
94
lib/fragments/dashboard/widgets/network_speed.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class NetworkSpeed extends StatefulWidget {
|
||||
const NetworkSpeed({super.key});
|
||||
|
||||
@override
|
||||
State<NetworkSpeed> createState() => _NetworkSpeedState();
|
||||
}
|
||||
|
||||
class _NetworkSpeedState extends State<NetworkSpeed> {
|
||||
List<Point> initPoints = const [Point(0, 0), Point(1, 0)];
|
||||
|
||||
List<Point> _getPoints(List<Traffic> traffics) {
|
||||
List<Point> trafficPoints = traffics
|
||||
.toList()
|
||||
.asMap()
|
||||
.map(
|
||||
(index, e) => MapEntry(
|
||||
index,
|
||||
Point(
|
||||
(index + initPoints.length).toDouble(),
|
||||
e.speed.toDouble(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.values
|
||||
.toList();
|
||||
|
||||
return [...initPoints, ...trafficPoints];
|
||||
}
|
||||
|
||||
Traffic _getLastTraffic(List<Traffic> traffics) {
|
||||
if (traffics.isEmpty) return Traffic();
|
||||
return traffics.last;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = context.colorScheme.onSurfaceVariant.toLight;
|
||||
return SizedBox(
|
||||
height: getWidgetHeight(2),
|
||||
child: CommonCard(
|
||||
onPressed: () {},
|
||||
info: Info(
|
||||
label: appLocalizations.networkSpeed,
|
||||
iconData: Icons.speed_sharp,
|
||||
),
|
||||
child: Selector<AppFlowingState, List<Traffic>>(
|
||||
selector: (_, appFlowingState) => appFlowingState.traffics,
|
||||
builder: (_, traffics, __) {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16).copyWith(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
),
|
||||
child: LineChart(
|
||||
gradient: true,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
points: _getPoints(traffics),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Transform.translate(
|
||||
offset: Offset(
|
||||
-16,
|
||||
-20,
|
||||
),
|
||||
child: Text(
|
||||
"${_getLastTraffic(traffics).up}↑ ${_getLastTraffic(traffics).down}↓",
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
73
lib/fragments/dashboard/widgets/outbound_mode.dart
Normal file
73
lib/fragments/dashboard/widgets/outbound_mode.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
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:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class OutboundMode extends StatelessWidget {
|
||||
const OutboundMode({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final height = getWidgetHeight(2);
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: Selector<ClashConfig, Mode>(
|
||||
selector: (_, clashConfig) => clashConfig.mode,
|
||||
builder: (_, mode, __) {
|
||||
return CommonCard(
|
||||
onPressed: () {},
|
||||
info: Info(
|
||||
label: appLocalizations.outboundMode,
|
||||
iconData: Icons.call_split_sharp,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 12,
|
||||
bottom: 16,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
for (final item in Mode.values)
|
||||
Flexible(
|
||||
child: ListItem.radio(
|
||||
prue: true,
|
||||
horizontalTitleGap: 4,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12,
|
||||
right: 16,
|
||||
),
|
||||
delegate: RadioDelegate(
|
||||
value: item,
|
||||
groupValue: mode,
|
||||
onChanged: (value) async {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
globalState.appController.changeMode(value);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
Intl.message(item.name),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.toSoftBold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
156
lib/fragments/dashboard/widgets/quick_options.dart
Normal file
156
lib/fragments/dashboard/widgets/quick_options.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/fragments/config/network.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 TUNButton extends StatelessWidget {
|
||||
const TUNButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LocaleBuilder(
|
||||
builder: (_) => SizedBox(
|
||||
height: getWidgetHeight(1),
|
||||
child: CommonCard(
|
||||
onPressed: () {
|
||||
showSheet(
|
||||
context: context,
|
||||
body: generateListView(generateSection(
|
||||
items: [
|
||||
if (system.isDesktop) const TUNItem(),
|
||||
const TunStackItem(),
|
||||
],
|
||||
)),
|
||||
title: appLocalizations.tun,
|
||||
);
|
||||
},
|
||||
info: Info(
|
||||
label: appLocalizations.tun,
|
||||
iconData: Icons.stacked_line_chart,
|
||||
),
|
||||
child: Container(
|
||||
padding: baseInfoEdgeInsets.copyWith(
|
||||
top: 4,
|
||||
bottom: 8,
|
||||
right: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
appLocalizations.options,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleSmall
|
||||
?.adjustSize(-2)
|
||||
.toLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
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 SystemProxyButton extends StatelessWidget {
|
||||
const SystemProxyButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: getWidgetHeight(1),
|
||||
child: LocaleBuilder(
|
||||
builder: (_) => CommonCard(
|
||||
onPressed: () {
|
||||
showSheet(
|
||||
context: context,
|
||||
body: generateListView(
|
||||
generateSection(
|
||||
items: [
|
||||
SystemProxyItem(),
|
||||
BypassDomainItem(),
|
||||
],
|
||||
),
|
||||
),
|
||||
title: appLocalizations.systemProxy,
|
||||
);
|
||||
},
|
||||
info: Info(
|
||||
label: appLocalizations.systemProxy,
|
||||
iconData: Icons.shuffle,
|
||||
),
|
||||
child: Container(
|
||||
padding: baseInfoEdgeInsets.copyWith(
|
||||
top: 4,
|
||||
bottom: 8,
|
||||
right: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
appLocalizations.options,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleSmall
|
||||
?.adjustSize(-2)
|
||||
.toLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.networkProps.systemProxy,
|
||||
builder: (_, systemProxy, __) {
|
||||
return Switch(
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
value: systemProxy,
|
||||
onChanged: (value) {
|
||||
final config = globalState.appController.config;
|
||||
config.networkProps =
|
||||
config.networkProps.copyWith(systemProxy: value);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
220
lib/fragments/dashboard/widgets/traffic_usage.dart
Normal file
220
lib/fragments/dashboard/widgets/traffic_usage.dart
Normal file
@@ -0,0 +1,220 @@
|
||||
import 'dart:math';
|
||||
|
||||
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 TrafficUsage extends StatelessWidget {
|
||||
const TrafficUsage({super.key});
|
||||
|
||||
Widget getTrafficDataItem(
|
||||
BuildContext context,
|
||||
Icon icon,
|
||||
TrafficValue trafficValue,
|
||||
) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
icon,
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
trafficValue.showValue,
|
||||
style: context.textTheme.bodySmall,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
trafficValue.showUnit,
|
||||
style: context.textTheme.bodySmall?.toLighter,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final primaryColor =
|
||||
context.colorScheme.surfaceContainer.blendDarken(context, factor: 0.2);
|
||||
final secondaryColor =
|
||||
context.colorScheme.primaryContainer.blendDarken(context, factor: 0.3);
|
||||
return SizedBox(
|
||||
height: getWidgetHeight(2),
|
||||
child: CommonCard(
|
||||
info: Info(
|
||||
label: appLocalizations.trafficUsage,
|
||||
iconData: Icons.data_saver_off,
|
||||
),
|
||||
onPressed: () {},
|
||||
child: Selector<AppFlowingState, Traffic>(
|
||||
selector: (_, appFlowingState) => appFlowingState.totalTraffic,
|
||||
builder: (_, totalTraffic, __) {
|
||||
final upTotalTrafficValue = totalTraffic.up;
|
||||
final downTotalTrafficValue = totalTraffic.down;
|
||||
return Padding(
|
||||
padding: baseInfoEdgeInsets.copyWith(
|
||||
top: 0,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: DonutChart(
|
||||
data: [
|
||||
DonutChartData(
|
||||
value: upTotalTrafficValue.value.toDouble(),
|
||||
color: primaryColor,
|
||||
),
|
||||
DonutChartData(
|
||||
value: downTotalTrafficValue.value.toDouble(),
|
||||
color: secondaryColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Flexible(
|
||||
child: LayoutBuilder(
|
||||
builder: (_, container) {
|
||||
final uploadText = Text(
|
||||
maxLines: 1,
|
||||
appLocalizations.upload,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodySmall,
|
||||
);
|
||||
final downloadText = Text(
|
||||
maxLines: 1,
|
||||
appLocalizations.download,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodySmall,
|
||||
);
|
||||
final uploadTextSize = globalState.measure
|
||||
.computeTextSize(uploadText);
|
||||
final downloadTextSize = globalState.measure
|
||||
.computeTextSize(downloadText);
|
||||
final maxTextWidth = max(uploadTextSize.width,
|
||||
downloadTextSize.width);
|
||||
if (maxTextWidth + 24 > container.maxWidth) {
|
||||
return Container();
|
||||
}
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 20,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor,
|
||||
borderRadius:
|
||||
BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
Text(
|
||||
maxLines: 1,
|
||||
appLocalizations.upload,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 20,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: secondaryColor,
|
||||
borderRadius:
|
||||
BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
Text(
|
||||
maxLines: 1,
|
||||
appLocalizations.download,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
getTrafficDataItem(
|
||||
context,
|
||||
Icon(
|
||||
Icons.arrow_upward,
|
||||
color: primaryColor,
|
||||
size: 14,
|
||||
),
|
||||
upTotalTrafficValue,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
getTrafficDataItem(
|
||||
context,
|
||||
Icon(
|
||||
Icons.arrow_downward,
|
||||
color: secondaryColor,
|
||||
size: 14,
|
||||
),
|
||||
downTotalTrafficValue,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
7
lib/fragments/dashboard/widgets/widgets.dart
Normal file
7
lib/fragments/dashboard/widgets/widgets.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
export 'intranet_ip.dart';
|
||||
export 'network_detection.dart';
|
||||
export 'network_speed.dart';
|
||||
export 'outbound_mode.dart';
|
||||
export 'quick_options.dart';
|
||||
export 'traffic_usage.dart';
|
||||
export 'memory_info.dart';
|
||||
@@ -3,11 +3,11 @@ export 'dashboard/dashboard.dart';
|
||||
export 'tools.dart';
|
||||
export 'profiles/profiles.dart';
|
||||
export 'logs.dart';
|
||||
export 'connections.dart';
|
||||
export 'access.dart';
|
||||
export 'config/config.dart';
|
||||
export 'application_setting.dart';
|
||||
export 'about.dart';
|
||||
export 'backup_and_recovery.dart';
|
||||
export 'resources.dart';
|
||||
export 'requests.dart';
|
||||
export 'connection/requests.dart';
|
||||
export 'connection/connections.dart';
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../models/models.dart';
|
||||
import '../widgets/widgets.dart';
|
||||
|
||||
double _preOffset = 0;
|
||||
|
||||
class LogsFragment extends StatefulWidget {
|
||||
const LogsFragment({super.key});
|
||||
|
||||
@@ -14,46 +18,64 @@ class LogsFragment extends StatefulWidget {
|
||||
State<LogsFragment> createState() => _LogsFragmentState();
|
||||
}
|
||||
|
||||
class _LogsFragmentState extends State<LogsFragment> {
|
||||
final logsNotifier = ValueNotifier<LogsAndKeywords>(const LogsAndKeywords());
|
||||
final scrollController = ScrollController(
|
||||
keepScrollOffset: false,
|
||||
class _LogsFragmentState extends State<LogsFragment> with ViewMixin {
|
||||
final _logsStateNotifier = ValueNotifier<LogsState>(LogsState());
|
||||
final _scrollController = ScrollController(
|
||||
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
|
||||
);
|
||||
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
|
||||
double _currentMaxWidth = 0;
|
||||
|
||||
Timer? timer;
|
||||
List<Log> _logs = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final appFlowingState = globalState.appController.appFlowingState;
|
||||
logsNotifier.value =
|
||||
logsNotifier.value.copyWith(logs: appFlowingState.logs);
|
||||
if (timer != null) {
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
}
|
||||
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
|
||||
final logs = appFlowingState.logs;
|
||||
if (!logListEquality.equals(
|
||||
logsNotifier.value.logs,
|
||||
logs,
|
||||
)) {
|
||||
logsNotifier.value = logsNotifier.value.copyWith(
|
||||
logs: logs,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
final appController = globalState.appController;
|
||||
final appFlowingState = appController.appFlowingState;
|
||||
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
|
||||
logs: appFlowingState.logs,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Widget> get actions => [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_handleExport();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.file_download_outlined,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
get onSearch => (value) {
|
||||
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
|
||||
query: value,
|
||||
);
|
||||
};
|
||||
|
||||
@override
|
||||
get onKeywordsUpdate => (keywords) {
|
||||
_logsStateNotifier.value =
|
||||
_logsStateNotifier.value.copyWith(keywords: keywords);
|
||||
};
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_logsStateNotifier.dispose();
|
||||
_scrollController.dispose();
|
||||
_cacheDynamicHeightMap.clear();
|
||||
super.dispose();
|
||||
timer?.cancel();
|
||||
logsNotifier.dispose();
|
||||
scrollController.dispose();
|
||||
timer = null;
|
||||
}
|
||||
|
||||
_handleTryClearCache(double maxWidth) {
|
||||
if (_currentMaxWidth != maxWidth) {
|
||||
_currentMaxWidth = maxWidth;
|
||||
_cacheDynamicHeightMap.clear();
|
||||
}
|
||||
}
|
||||
|
||||
_handleExport() async {
|
||||
@@ -73,296 +95,151 @@ class _LogsFragmentState extends State<LogsFragment> {
|
||||
|
||||
_initActions() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final commonScaffoldState =
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showSearch(
|
||||
context: context,
|
||||
delegate: LogsSearchDelegate(
|
||||
logs: logsNotifier.value,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.search),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_handleExport();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.file_download_outlined,
|
||||
),
|
||||
),
|
||||
];
|
||||
super.initViewState();
|
||||
});
|
||||
}
|
||||
|
||||
_addKeyword(String keyword) {
|
||||
final isContains = logsNotifier.value.keywords.contains(keyword);
|
||||
if (isContains) return;
|
||||
final keywords = List<String>.from(logsNotifier.value.keywords)
|
||||
..add(keyword);
|
||||
logsNotifier.value = logsNotifier.value.copyWith(
|
||||
keywords: keywords,
|
||||
double _calcCacheHeight(String text) {
|
||||
final cacheHeight = _cacheDynamicHeightMap.get(text);
|
||||
if (cacheHeight != null) {
|
||||
return cacheHeight;
|
||||
}
|
||||
final size = globalState.measure.computeTextSize(
|
||||
Text(
|
||||
text,
|
||||
style: globalState.appController.context.textTheme.bodyLarge,
|
||||
),
|
||||
maxWidth: _currentMaxWidth,
|
||||
);
|
||||
_cacheDynamicHeightMap.put(text, size.height);
|
||||
return size.height;
|
||||
}
|
||||
|
||||
_deleteKeyword(String keyword) {
|
||||
final isContains = logsNotifier.value.keywords.contains(keyword);
|
||||
if (!isContains) return;
|
||||
final keywords = List<String>.from(logsNotifier.value.keywords)
|
||||
..remove(keyword);
|
||||
logsNotifier.value = logsNotifier.value.copyWith(
|
||||
keywords: keywords,
|
||||
double _getItemHeight(Log log) {
|
||||
final measure = globalState.measure;
|
||||
final bodySmallHeight = measure.bodySmallHeight;
|
||||
final bodyMediumHeight = measure.bodyMediumHeight;
|
||||
final height = _calcCacheHeight(log.payload ?? "");
|
||||
return height + bodySmallHeight + 8 + bodyMediumHeight + 40;
|
||||
}
|
||||
|
||||
updateLogsThrottler() {
|
||||
throttler.call("logs", () {
|
||||
final isEquality = logListEquality.equals(
|
||||
_logs,
|
||||
_logsStateNotifier.value.logs,
|
||||
);
|
||||
if (isEquality) {
|
||||
return;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
|
||||
logs: _logs,
|
||||
);
|
||||
});
|
||||
}, duration: commonDuration);
|
||||
}
|
||||
|
||||
Widget _wrapLogsUpdate(Widget child) {
|
||||
return Selector<AppFlowingState, List<Log>>(
|
||||
selector: (_, appFlowingState) => appFlowingState.logs,
|
||||
shouldRebuild: (prev, next) {
|
||||
final isEquality = logListEquality.equals(prev, next);
|
||||
if (!isEquality) {
|
||||
_logs = next;
|
||||
updateLogsThrottler();
|
||||
}
|
||||
return !isEquality;
|
||||
},
|
||||
builder: (_, next, child) {
|
||||
return child!;
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<AppState, bool?>(
|
||||
selector: (_, appState) =>
|
||||
appState.currentLabel == 'logs' ||
|
||||
appState.viewMode == ViewMode.mobile &&
|
||||
appState.currentLabel == "tools",
|
||||
builder: (_, isCurrent, child) {
|
||||
if (isCurrent == null || isCurrent) {
|
||||
_initActions();
|
||||
}
|
||||
return child!;
|
||||
},
|
||||
child: ValueListenableBuilder<LogsAndKeywords>(
|
||||
valueListenable: logsNotifier,
|
||||
builder: (_, state, __) {
|
||||
final logs = state.filteredLogs;
|
||||
if (logs.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullLogsDesc,
|
||||
);
|
||||
}
|
||||
final reversedLogs = logs.reversed.toList();
|
||||
final logWidgets = reversedLogs
|
||||
.map<Widget>(
|
||||
(log) => LogItem(
|
||||
key: Key(log.dateTime.toString()),
|
||||
log: log,
|
||||
onClick: _addKeyword,
|
||||
),
|
||||
)
|
||||
.separated(
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (state.keywords.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
child: Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (final keyword in state.keywords)
|
||||
CommonChip(
|
||||
label: keyword,
|
||||
type: ChipType.delete,
|
||||
onPressed: () {
|
||||
_deleteKeyword(keyword);
|
||||
return LayoutBuilder(
|
||||
builder: (_, constraints) {
|
||||
_handleTryClearCache(constraints.maxWidth - 40);
|
||||
return Selector<AppState, bool?>(
|
||||
selector: (_, appState) =>
|
||||
appState.currentLabel == 'logs' ||
|
||||
appState.viewMode == ViewMode.mobile &&
|
||||
appState.currentLabel == "tools",
|
||||
builder: (_, isCurrent, child) {
|
||||
if (isCurrent == null || isCurrent) {
|
||||
_initActions();
|
||||
}
|
||||
return child!;
|
||||
},
|
||||
child: _wrapLogsUpdate(
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ValueListenableBuilder<LogsState>(
|
||||
valueListenable: _logsStateNotifier,
|
||||
builder: (_, state, __) {
|
||||
final logs = state.list;
|
||||
if (logs.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullLogsDesc,
|
||||
);
|
||||
}
|
||||
final items = logs
|
||||
.map<Widget>(
|
||||
(log) => LogItem(
|
||||
key: Key(log.dateTime.toString()),
|
||||
log: log,
|
||||
onClick: (value) {
|
||||
context.commonScaffoldState?.addKeyword(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (_, constraints) {
|
||||
return ScrollConfiguration(
|
||||
behavior: ShowBarScrollBehavior(),
|
||||
child: ListView.builder(
|
||||
controller: scrollController,
|
||||
itemExtentBuilder: (index, __) {
|
||||
final widget = logWidgets[index];
|
||||
if (widget.runtimeType == Divider) {
|
||||
return 0;
|
||||
}
|
||||
final measure = globalState.measure;
|
||||
final bodyLargeSize = measure.bodyLargeSize;
|
||||
final bodySmallHeight = measure.bodySmallHeight;
|
||||
final bodyMediumHeight = measure.bodyMediumHeight;
|
||||
final log = reversedLogs[(index / 2).floor()];
|
||||
final width = (log.payload?.length ?? 0) *
|
||||
bodyLargeSize.width +
|
||||
200;
|
||||
final lines = (width / constraints.maxWidth).ceil();
|
||||
return lines * bodyLargeSize.height +
|
||||
bodySmallHeight +
|
||||
8 +
|
||||
bodyMediumHeight +
|
||||
40;
|
||||
},
|
||||
itemBuilder: (_, index) {
|
||||
return logWidgets[index];
|
||||
},
|
||||
itemCount: logWidgets.length,
|
||||
));
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LogsSearchDelegate extends SearchDelegate {
|
||||
ValueNotifier<LogsAndKeywords> logsNotifier;
|
||||
|
||||
LogsSearchDelegate({
|
||||
required LogsAndKeywords logs,
|
||||
}) : logsNotifier = ValueNotifier(logs);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
logsNotifier.dispose();
|
||||
}
|
||||
|
||||
get state => logsNotifier.value;
|
||||
|
||||
List<Log> get _results {
|
||||
final lowQuery = query.toLowerCase();
|
||||
return logsNotifier.value.filteredLogs
|
||||
.where(
|
||||
(log) =>
|
||||
(log.payload?.toLowerCase().contains(lowQuery) ?? false) ||
|
||||
log.logLevel.name.contains(lowQuery),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
List<Widget>? buildActions(BuildContext context) {
|
||||
return [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (query.isEmpty) {
|
||||
close(context, null);
|
||||
return;
|
||||
}
|
||||
query = '';
|
||||
},
|
||||
icon: const Icon(Icons.clear),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? buildLeading(BuildContext context) {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
close(context, null);
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildResults(BuildContext context) {
|
||||
return buildSuggestions(context);
|
||||
}
|
||||
|
||||
_addKeyword(String keyword) {
|
||||
final isContains = logsNotifier.value.keywords.contains(keyword);
|
||||
if (isContains) return;
|
||||
final keywords = List<String>.from(logsNotifier.value.keywords)
|
||||
..add(keyword);
|
||||
logsNotifier.value = logsNotifier.value.copyWith(
|
||||
keywords: keywords,
|
||||
);
|
||||
}
|
||||
|
||||
_deleteKeyword(String keyword) {
|
||||
final isContains = logsNotifier.value.keywords.contains(keyword);
|
||||
if (!isContains) return;
|
||||
final keywords = List<String>.from(logsNotifier.value.keywords)
|
||||
..remove(keyword);
|
||||
logsNotifier.value = logsNotifier.value.copyWith(
|
||||
keywords: keywords,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildSuggestions(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: logsNotifier,
|
||||
builder: (_, __, ___) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (state.keywords.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
child: Wrap(
|
||||
runSpacing: 6,
|
||||
spacing: 6,
|
||||
children: [
|
||||
for (final keyword in state.keywords)
|
||||
CommonChip(
|
||||
label: keyword,
|
||||
type: ChipType.delete,
|
||||
onPressed: () {
|
||||
_deleteKeyword(keyword);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemBuilder: (_, index) {
|
||||
final log = _results[index];
|
||||
return LogItem(
|
||||
key: Key(log.dateTime.toString()),
|
||||
log: log,
|
||||
onClick: (value) {
|
||||
_addKeyword(value);
|
||||
)
|
||||
.separated(
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
return NotificationListener<ScrollEndNotification>(
|
||||
onNotification: (details) {
|
||||
_preOffset = details.metrics.pixels;
|
||||
return false;
|
||||
},
|
||||
child: CommonScrollBar(
|
||||
controller: _scrollController,
|
||||
child: ListView.builder(
|
||||
reverse: true,
|
||||
shrinkWrap: true,
|
||||
physics: NextClampingScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
itemExtentBuilder: (index, __) {
|
||||
final item = items[index];
|
||||
if (item.runtimeType == Divider) {
|
||||
return 0;
|
||||
}
|
||||
final log = logs[(index / 2).floor()];
|
||||
return _getItemHeight(log);
|
||||
},
|
||||
itemCount: items.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(
|
||||
height: 0,
|
||||
);
|
||||
},
|
||||
itemCount: _results.length,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LogItem extends StatefulWidget {
|
||||
class LogItem extends StatelessWidget {
|
||||
final Log log;
|
||||
final Function(String)? onClick;
|
||||
|
||||
@@ -372,14 +249,8 @@ class LogItem extends StatefulWidget {
|
||||
this.onClick,
|
||||
});
|
||||
|
||||
@override
|
||||
State<LogItem> createState() => _LogItemState();
|
||||
}
|
||||
|
||||
class _LogItemState extends State<LogItem> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final log = widget.log;
|
||||
return ListItem(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
@@ -387,14 +258,16 @@ class _LogItemState extends State<LogItem> {
|
||||
),
|
||||
title: SelectableText(
|
||||
log.payload ?? '',
|
||||
style: context.textTheme.bodyLarge,
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SelectableText(
|
||||
"${log.dateTime}",
|
||||
style: context.textTheme.bodySmall
|
||||
?.copyWith(color: context.colorScheme.primary),
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
@@ -403,8 +276,8 @@ class _LogItemState extends State<LogItem> {
|
||||
alignment: Alignment.centerLeft,
|
||||
child: CommonChip(
|
||||
onPressed: () {
|
||||
if (widget.onClick == null) return;
|
||||
widget.onClick!(log.logLevel.name);
|
||||
if (onClick == null) return;
|
||||
onClick!(log.logLevel.name);
|
||||
},
|
||||
label: log.logLevel.name,
|
||||
),
|
||||
@@ -414,3 +287,11 @@ class _LogItemState extends State<LogItem> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NoGlowScrollBehavior extends ScrollBehavior {
|
||||
@override
|
||||
Widget buildOverscrollIndicator(
|
||||
BuildContext context, Widget child, ScrollableDetails details) {
|
||||
return child; // 禁用过度滚动效果
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/pages/editor.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class EditProfile extends StatefulWidget {
|
||||
final Profile profile;
|
||||
@@ -30,10 +30,13 @@ class _EditProfileState extends State<EditProfile> {
|
||||
late TextEditingController urlController;
|
||||
late TextEditingController autoUpdateDurationController;
|
||||
late bool autoUpdate;
|
||||
String? rawText;
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
final fileInfoNotifier = ValueNotifier<FileInfo?>(null);
|
||||
Uint8List? fileData;
|
||||
|
||||
Profile get profile => widget.profile;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -51,28 +54,43 @@ class _EditProfileState extends State<EditProfile> {
|
||||
|
||||
_handleConfirm() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
final config = widget.context.read<Config>();
|
||||
var profile = widget.profile.copyWith(
|
||||
url: urlController.text,
|
||||
label: labelController.text,
|
||||
autoUpdate: autoUpdate,
|
||||
autoUpdateDuration: Duration(
|
||||
minutes: int.parse(
|
||||
autoUpdateDurationController.text,
|
||||
),
|
||||
),
|
||||
);
|
||||
final appController = globalState.appController;
|
||||
Profile profile = this.profile.copyWith(
|
||||
url: urlController.text,
|
||||
label: labelController.text,
|
||||
autoUpdate: autoUpdate,
|
||||
autoUpdateDuration: Duration(
|
||||
minutes: int.parse(
|
||||
autoUpdateDurationController.text,
|
||||
),
|
||||
),
|
||||
);
|
||||
final hasUpdate = widget.profile.url != profile.url;
|
||||
if (fileData != null) {
|
||||
config.setProfile(await profile.saveFile(fileData!));
|
||||
if (profile.type == ProfileType.url && autoUpdate) {
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.profileHasUpdate,
|
||||
),
|
||||
);
|
||||
if (res == true) {
|
||||
profile = profile.copyWith(
|
||||
autoUpdate: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
appController.setProfile(await profile.saveFile(fileData!));
|
||||
} else if (!hasUpdate) {
|
||||
appController.setProfile(profile);
|
||||
} else {
|
||||
config.setProfile(profile);
|
||||
}
|
||||
if (hasUpdate) {
|
||||
globalState.homeScaffoldKey.currentState?.loadingRun(
|
||||
() async {
|
||||
await Future.delayed(
|
||||
commonDuration,
|
||||
);
|
||||
if (hasUpdate) {
|
||||
await globalState.appController.updateProfile(profile);
|
||||
await appController.updateProfile(profile);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -102,22 +120,69 @@ class _EditProfileState extends State<EditProfile> {
|
||||
);
|
||||
}
|
||||
|
||||
_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,
|
||||
),
|
||||
_handleSaveEdit(BuildContext context, String data) async {
|
||||
final message = await globalState.safeRun<String>(
|
||||
() async {
|
||||
final message = await clashCore.validateConfig(data);
|
||||
return message;
|
||||
},
|
||||
silence: false,
|
||||
);
|
||||
if (message?.isNotEmpty == true) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(text: message),
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop(data);
|
||||
}
|
||||
}
|
||||
|
||||
_editProfileFile() async {
|
||||
if (rawText == null) {
|
||||
final profilePath = await appPath.getProfilePath(widget.profile.id);
|
||||
if (profilePath == null) return;
|
||||
final file = File(profilePath);
|
||||
rawText = await file.readAsString();
|
||||
}
|
||||
if (!mounted) return;
|
||||
final title = widget.profile.label ?? widget.profile.id;
|
||||
final data = await BaseNavigator.push<String>(
|
||||
globalState.homeScaffoldKey.currentContext!,
|
||||
EditorPage(
|
||||
title: title,
|
||||
content: rawText!,
|
||||
onSave: _handleSaveEdit,
|
||||
onPop: (context, data) async {
|
||||
if (data == rawText) {
|
||||
return true;
|
||||
}
|
||||
final res = await globalState.showMessage(
|
||||
title: title,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.hasCacheChange,
|
||||
),
|
||||
);
|
||||
if (res == true && context.mounted) {
|
||||
_handleSaveEdit(context, data);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
),
|
||||
);
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
rawText = data;
|
||||
fileData = Uint8List.fromList(utf8.encode(data));
|
||||
fileInfoNotifier.value = fileInfoNotifier.value?.copyWith(
|
||||
size: fileData?.length ?? 0,
|
||||
lastModified: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
_uploadProfileFile() async {
|
||||
@@ -130,6 +195,20 @@ class _EditProfileState extends State<EditProfile> {
|
||||
);
|
||||
}
|
||||
|
||||
_handleBack() async {
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(text: appLocalizations.fileIsUpdate),
|
||||
);
|
||||
if (res == true) {
|
||||
_handleConfirm();
|
||||
} else {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = [
|
||||
@@ -245,34 +324,45 @@ class _EditProfileState extends State<EditProfile> {
|
||||
},
|
||||
),
|
||||
];
|
||||
return FloatLayout(
|
||||
floatingWidget: FloatWrapper(
|
||||
child: FloatingActionButton.extended(
|
||||
heroTag: null,
|
||||
onPressed: _handleConfirm,
|
||||
label: Text(appLocalizations.save),
|
||||
icon: const Icon(Icons.save),
|
||||
),
|
||||
),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, __) {
|
||||
if (didPop) return;
|
||||
if (fileData == null) {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
_handleBack();
|
||||
},
|
||||
child: FloatLayout(
|
||||
floatingWidget: FloatWrapper(
|
||||
child: FloatingActionButton.extended(
|
||||
heroTag: null,
|
||||
onPressed: _handleConfirm,
|
||||
label: Text(appLocalizations.save),
|
||||
icon: const Icon(Icons.save),
|
||||
),
|
||||
child: ListView.separated(
|
||||
padding: kMaterialListPadding.copyWith(
|
||||
bottom: 72,
|
||||
),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
),
|
||||
child: ListView.separated(
|
||||
padding: kMaterialListPadding.copyWith(
|
||||
bottom: 72,
|
||||
),
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
separatorBuilder: (_, __) {
|
||||
return const SizedBox(
|
||||
height: 24,
|
||||
);
|
||||
},
|
||||
itemCount: items.length,
|
||||
),
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
separatorBuilder: (_, __) {
|
||||
return const SizedBox(
|
||||
height: 24,
|
||||
);
|
||||
},
|
||||
itemCount: items.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -7,18 +7,11 @@ 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:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'add_profile.dart';
|
||||
|
||||
enum PopupMenuItemEnum { delete, edit }
|
||||
|
||||
enum ProfileActions {
|
||||
edit,
|
||||
update,
|
||||
delete,
|
||||
}
|
||||
|
||||
class ProfilesFragment extends StatefulWidget {
|
||||
const ProfilesFragment({super.key});
|
||||
|
||||
@@ -52,9 +45,6 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
|
||||
);
|
||||
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(
|
||||
@@ -84,8 +74,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) {
|
||||
if (!mounted) return;
|
||||
final commonScaffoldState =
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
@@ -93,16 +82,13 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
|
||||
},
|
||||
icon: const Icon(Icons.sync),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
final profiles = globalState.appController.config.profiles;
|
||||
showSheet(
|
||||
title: appLocalizations.profilesSort,
|
||||
context: context,
|
||||
builder: (_) => SizedBox(
|
||||
body: SizedBox(
|
||||
height: 400,
|
||||
child: ReorderableProfiles(profiles: profiles),
|
||||
),
|
||||
@@ -191,18 +177,16 @@ class ProfileItem extends StatelessWidget {
|
||||
});
|
||||
|
||||
_handleDeleteProfile(BuildContext context) async {
|
||||
globalState.showMessage(
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.deleteProfileTip,
|
||||
),
|
||||
onTab: () async {
|
||||
await globalState.appController.deleteProfile(profile.id);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
);
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
await globalState.appController.deleteProfile(profile.id);
|
||||
}
|
||||
|
||||
_handleUpdateProfile() async {
|
||||
@@ -213,7 +197,7 @@ class ProfileItem extends StatelessWidget {
|
||||
final appController = globalState.appController;
|
||||
final config = appController.config;
|
||||
if (profile.type == ProfileType.file) return;
|
||||
await globalState.safeRun(() async {
|
||||
await globalState.safeRun(silence: false, () async {
|
||||
try {
|
||||
config.setProfile(
|
||||
profile.copyWith(
|
||||
@@ -221,9 +205,6 @@ class ProfileItem extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
await appController.updateProfile(profile);
|
||||
if (profile.id == appController.config.currentProfile?.id) {
|
||||
appController.applyProfileDebounce();
|
||||
}
|
||||
} catch (e) {
|
||||
config.setProfile(
|
||||
profile.copyWith(
|
||||
@@ -275,8 +256,39 @@ class ProfileItem extends StatelessWidget {
|
||||
];
|
||||
}
|
||||
|
||||
_handleCopyLink(BuildContext context) async {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: profile.url,
|
||||
),
|
||||
);
|
||||
if (context.mounted) {
|
||||
context.showNotifier(appLocalizations.copySuccess);
|
||||
}
|
||||
}
|
||||
|
||||
_handleExportFile(BuildContext context) async {
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
final res = await commonScaffoldState?.loadingRun<bool>(
|
||||
() async {
|
||||
final file = await profile.getFile();
|
||||
final value = await picker.saveFile(
|
||||
profile.label ?? profile.id,
|
||||
file.readAsBytesSync(),
|
||||
);
|
||||
if (value == null) return false;
|
||||
return true;
|
||||
},
|
||||
title: appLocalizations.tip,
|
||||
);
|
||||
if (res == true && context.mounted) {
|
||||
context.showNotifier(appLocalizations.exportSuccess);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final key = GlobalKey<CommonPopupBoxState>();
|
||||
return CommonCard(
|
||||
isSelected: profile.id == groupValue,
|
||||
onPressed: () {
|
||||
@@ -295,45 +307,62 @@ class ProfileItem extends StatelessWidget {
|
||||
padding: EdgeInsets.all(8),
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: CommonPopupMenu<ProfileActions>(
|
||||
items: [
|
||||
CommonPopupMenuItem(
|
||||
action: ProfileActions.edit,
|
||||
label: appLocalizations.edit,
|
||||
iconData: Icons.edit,
|
||||
),
|
||||
if (profile.type == ProfileType.url)
|
||||
CommonPopupMenuItem(
|
||||
action: ProfileActions.update,
|
||||
label: appLocalizations.update,
|
||||
iconData: Icons.sync,
|
||||
: CommonPopupBox(
|
||||
key: key,
|
||||
popup: CommonPopupMenu(
|
||||
items: [
|
||||
ActionItemData(
|
||||
icon: Icons.edit_outlined,
|
||||
label: appLocalizations.edit,
|
||||
onPressed: () {
|
||||
_handleShowEditExtendPage(context);
|
||||
},
|
||||
),
|
||||
CommonPopupMenuItem(
|
||||
action: ProfileActions.delete,
|
||||
label: appLocalizations.delete,
|
||||
iconData: Icons.delete,
|
||||
),
|
||||
],
|
||||
onSelected: (ProfileActions? action) async {
|
||||
switch (action) {
|
||||
case ProfileActions.edit:
|
||||
_handleShowEditExtendPage(context);
|
||||
break;
|
||||
case ProfileActions.delete:
|
||||
_handleDeleteProfile(context);
|
||||
break;
|
||||
case ProfileActions.update:
|
||||
_handleUpdateProfile();
|
||||
break;
|
||||
case null:
|
||||
break;
|
||||
}
|
||||
},
|
||||
if (profile.type == ProfileType.url) ...[
|
||||
ActionItemData(
|
||||
icon: Icons.sync_alt_sharp,
|
||||
label: appLocalizations.sync,
|
||||
onPressed: () {
|
||||
_handleUpdateProfile();
|
||||
},
|
||||
),
|
||||
ActionItemData(
|
||||
icon: Icons.copy,
|
||||
label: appLocalizations.copyLink,
|
||||
onPressed: () {
|
||||
_handleCopyLink(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
ActionItemData(
|
||||
icon: Icons.file_copy_outlined,
|
||||
label: appLocalizations.exportFile,
|
||||
onPressed: () {
|
||||
_handleExportFile(context);
|
||||
},
|
||||
),
|
||||
ActionItemData(
|
||||
icon: Icons.delete_outlined,
|
||||
iconSize: 20,
|
||||
label: appLocalizations.delete,
|
||||
onPressed: () {
|
||||
_handleDeleteProfile(context);
|
||||
},
|
||||
type: ActionType.danger,
|
||||
),
|
||||
],
|
||||
),
|
||||
target: IconButton(
|
||||
onPressed: () {
|
||||
key.currentState?.pop();
|
||||
},
|
||||
icon: Icon(Icons.more_vert),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
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/scaffold.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:re_editor/re_editor.dart';
|
||||
import 'package:re_highlight/languages/yaml.dart';
|
||||
import 'package:re_highlight/styles/atom-one-light.dart';
|
||||
|
||||
class ViewProfile extends StatefulWidget {
|
||||
final Profile profile;
|
||||
|
||||
const ViewProfile({
|
||||
super.key,
|
||||
required this.profile,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ViewProfile> createState() => _ViewProfileState();
|
||||
}
|
||||
|
||||
class _ViewProfileState extends State<ViewProfile> {
|
||||
bool readOnly = true;
|
||||
final CodeLineEditingController _controller = CodeLineEditingController();
|
||||
final key = GlobalKey<CommonScaffoldState>();
|
||||
final _focusNode = FocusNode();
|
||||
String? rawText;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
appPath.getProfilePath(widget.profile.id).then((path) async {
|
||||
if (path == null) return;
|
||||
final file = File(path);
|
||||
rawText = await file.readAsString();
|
||||
_controller.text = rawText ?? "";
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
}
|
||||
|
||||
Profile get profile => widget.profile;
|
||||
|
||||
_handleChangeReadOnly() async {
|
||||
if (readOnly == true) {
|
||||
setState(() {
|
||||
readOnly = false;
|
||||
});
|
||||
} else {
|
||||
if (_controller.text == rawText) return;
|
||||
final newProfile = await key.currentState?.loadingRun<Profile>(() async {
|
||||
return await profile.saveFileWithString(_controller.text);
|
||||
});
|
||||
if (newProfile == null) return;
|
||||
globalState.appController.config.setProfile(newProfile);
|
||||
setState(() {
|
||||
readOnly = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonScaffold(
|
||||
key: key,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: _controller.undo,
|
||||
icon: const Icon(Icons.undo),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _controller.redo,
|
||||
icon: const Icon(Icons.redo),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _handleChangeReadOnly,
|
||||
icon: readOnly ? const Icon(Icons.edit) : const Icon(Icons.save),
|
||||
),
|
||||
],
|
||||
body: CodeEditor(
|
||||
readOnly: readOnly,
|
||||
focusNode: _focusNode,
|
||||
scrollbarBuilder: (context, child, details) {
|
||||
return Scrollbar(
|
||||
controller: details.controller,
|
||||
thickness: 8,
|
||||
radius: const Radius.circular(2),
|
||||
interactive: true,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ContextMenuItemWidget extends PopupMenuItem<void> {
|
||||
ContextMenuItemWidget({
|
||||
super.key,
|
||||
required String text,
|
||||
required VoidCallback super.onTap,
|
||||
}) : super(child: Text(text));
|
||||
}
|
||||
|
||||
class ContextMenuControllerImpl implements SelectionToolbarController {
|
||||
OverlayEntry? _overlayEntry;
|
||||
|
||||
final FocusNode focusNode;
|
||||
|
||||
ContextMenuControllerImpl(
|
||||
this.focusNode,
|
||||
);
|
||||
|
||||
_removeOverLayEntry() {
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
}
|
||||
|
||||
@override
|
||||
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
|
||||
void show({
|
||||
required BuildContext context,
|
||||
required CodeLineEditingController controller,
|
||||
required TextSelectionToolbarAnchors anchors,
|
||||
Rect? renderRect,
|
||||
required LayerLink layerLink,
|
||||
required ValueNotifier<bool> visibility,
|
||||
}) {
|
||||
if (controller.selectedText.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_removeOverLayEntry();
|
||||
final relativeRect = RelativeRect.fromSize(
|
||||
(anchors.primaryAnchor) &
|
||||
const Size(150, double.infinity),
|
||||
MediaQuery.of(context).size,
|
||||
);
|
||||
_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!);
|
||||
}
|
||||
}
|
||||
@@ -11,31 +11,33 @@ class ProxyCard extends StatelessWidget {
|
||||
final String groupName;
|
||||
final Proxy proxy;
|
||||
final GroupType groupType;
|
||||
final CommonCardType style;
|
||||
final ProxyCardType type;
|
||||
final String? testUrl;
|
||||
|
||||
const ProxyCard({
|
||||
super.key,
|
||||
required this.groupName,
|
||||
required this.testUrl,
|
||||
required this.proxy,
|
||||
required this.groupType,
|
||||
this.style = CommonCardType.plain,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
Measure get measure => globalState.measure;
|
||||
|
||||
_handleTestCurrentDelay() {
|
||||
proxyDelayTest(proxy);
|
||||
proxyDelayTest(
|
||||
proxy,
|
||||
testUrl,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDelayText() {
|
||||
return SizedBox(
|
||||
height: measure.labelSmallHeight,
|
||||
child: Selector<AppState, int?>(
|
||||
selector: (context, appState) => appState.getDelay(
|
||||
proxy.name,
|
||||
),
|
||||
selector: (context, appState) =>
|
||||
globalState.appController.getDelay(proxy.name,testUrl),
|
||||
builder: (context, delay, __) {
|
||||
return FadeBox(
|
||||
child: Builder(
|
||||
@@ -115,15 +117,11 @@ class ProxyCard extends StatelessWidget {
|
||||
groupName,
|
||||
nextProxyName,
|
||||
);
|
||||
await appController.changeProxyDebounce([
|
||||
groupName,
|
||||
nextProxyName,
|
||||
]);
|
||||
await appController.changeProxyDebounce(groupName, nextProxyName);
|
||||
return;
|
||||
}
|
||||
globalState.showSnackBar(
|
||||
context,
|
||||
message: appLocalizations.notSelectedTip,
|
||||
globalState.showNotifier(
|
||||
appLocalizations.notSelectedTip,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -138,7 +136,6 @@ class ProxyCard extends StatelessWidget {
|
||||
return Stack(
|
||||
children: [
|
||||
CommonCard(
|
||||
type: style,
|
||||
key: key,
|
||||
onPressed: () {
|
||||
_changeProxy(context);
|
||||
@@ -167,8 +164,8 @@ class ProxyCard extends StatelessWidget {
|
||||
desc,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.textTheme.bodySmall?.color
|
||||
?.toLight(),
|
||||
color:
|
||||
context.textTheme.bodySmall?.color?.toLight,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -192,8 +189,8 @@ class ProxyCard extends StatelessWidget {
|
||||
proxy.type,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: context.textTheme.bodySmall?.color
|
||||
?.toLight(),
|
||||
color: context
|
||||
.textTheme.bodySmall?.color?.toLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -30,7 +30,7 @@ double get listHeaderHeight {
|
||||
double getItemHeight(ProxyCardType proxyCardType) {
|
||||
final measure = globalState.measure;
|
||||
final baseHeight =
|
||||
12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8;
|
||||
12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8 + 4;
|
||||
return switch (proxyCardType) {
|
||||
ProxyCardType.expand => baseHeight + measure.labelSmallHeight + 8,
|
||||
ProxyCardType.shrink => baseHeight,
|
||||
@@ -38,33 +38,48 @@ double getItemHeight(ProxyCardType proxyCardType) {
|
||||
};
|
||||
}
|
||||
|
||||
proxyDelayTest(Proxy proxy) async {
|
||||
proxyDelayTest(Proxy proxy, [String? testUrl]) async {
|
||||
final appController = globalState.appController;
|
||||
final proxyName = appController.appState.getRealProxyName(proxy.name);
|
||||
final url = appController.getRealTestUrl(testUrl);
|
||||
globalState.appController.setDelay(
|
||||
Delay(
|
||||
url: url,
|
||||
name: proxyName,
|
||||
value: 0,
|
||||
),
|
||||
);
|
||||
globalState.appController.setDelay(await clashCore.getDelay(proxyName));
|
||||
globalState.appController.setDelay(
|
||||
await clashCore.getDelay(
|
||||
url,
|
||||
proxyName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
delayTest(List<Proxy> proxies) async {
|
||||
delayTest(List<Proxy> proxies, [String? testUrl]) async {
|
||||
final appController = globalState.appController;
|
||||
final proxyNames = proxies
|
||||
.map((proxy) => appController.appState.getRealProxyName(proxy.name))
|
||||
.toSet()
|
||||
.toList();
|
||||
|
||||
final url = appController.getRealTestUrl(testUrl);
|
||||
|
||||
final delayProxies = proxyNames.map<Future>((proxyName) async {
|
||||
globalState.appController.setDelay(
|
||||
Delay(
|
||||
url: url,
|
||||
name: proxyName,
|
||||
value: 0,
|
||||
),
|
||||
);
|
||||
globalState.appController.setDelay(await clashCore.getDelay(proxyName));
|
||||
globalState.appController.setDelay(
|
||||
await clashCore.getDelay(
|
||||
url,
|
||||
proxyName,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final batchesDelayProxies = delayProxies.batch(100);
|
||||
@@ -86,7 +101,7 @@ double getScrollToSelectedOffset({
|
||||
final proxyCardType = appController.config.proxiesStyle.cardType;
|
||||
final selectedName = appController.getCurrentSelectedName(groupName);
|
||||
final findSelectedIndex = proxies.indexWhere(
|
||||
(proxy) => proxy.name == selectedName,
|
||||
(proxy) => proxy.name == selectedName,
|
||||
);
|
||||
final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0;
|
||||
final rows = (selectedIndex / columns).floor();
|
||||
|
||||
@@ -65,10 +65,10 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_headerStateNotifier.dispose();
|
||||
_controller.removeListener(_adjustHeader);
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_handleChange(Set<String> currentUnfoldSet, String groupName) {
|
||||
@@ -134,6 +134,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
if (isExpand) {
|
||||
final sortedProxies = globalState.appController.getSortProxies(
|
||||
group.all,
|
||||
group.testUrl,
|
||||
);
|
||||
groupNameProxiesMap[groupName] = sortedProxies;
|
||||
final chunks = sortedProxies.chunks(columns);
|
||||
@@ -142,6 +143,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
.map<Widget>(
|
||||
(proxy) => Flexible(
|
||||
child: ProxyCard(
|
||||
testUrl: group.testUrl,
|
||||
type: type,
|
||||
groupType: group.type,
|
||||
key: ValueKey('$groupName.${proxy.name}'),
|
||||
@@ -259,6 +261,11 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
return prev != next;
|
||||
},
|
||||
builder: (_, state, __) {
|
||||
if (state.groupNames.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullProxies,
|
||||
);
|
||||
}
|
||||
final items = _buildItems(
|
||||
groupNames: state.groupNames,
|
||||
currentUnfoldSet: state.currentUnfoldSet,
|
||||
@@ -266,13 +273,8 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
type: state.proxyCardType,
|
||||
);
|
||||
final itemsOffset = _getItemHeightList(items, state.proxyCardType);
|
||||
return Scrollbar(
|
||||
return CommonScrollBar(
|
||||
controller: _controller,
|
||||
thumbVisibility: true,
|
||||
trackVisibility: true,
|
||||
thickness: 8,
|
||||
radius: const Radius.circular(8),
|
||||
interactive: true,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
@@ -367,10 +369,13 @@ class _ListHeaderState extends State<ListHeader>
|
||||
|
||||
bool get isExpand => widget.isExpand;
|
||||
|
||||
_delayTest(List<Proxy> proxies) async {
|
||||
_delayTest() async {
|
||||
if (isLock) return;
|
||||
isLock = true;
|
||||
await delayTest(proxies);
|
||||
await delayTest(
|
||||
widget.group.all,
|
||||
widget.group.testUrl,
|
||||
);
|
||||
isLock = false;
|
||||
}
|
||||
|
||||
@@ -442,10 +447,10 @@ class _ListHeaderState extends State<ListHeader>
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: CommonIcon(
|
||||
child: CommonTargetIcon(
|
||||
src: icon,
|
||||
size: 32,
|
||||
),
|
||||
@@ -454,7 +459,7 @@ class _ListHeaderState extends State<ListHeader>
|
||||
margin: const EdgeInsets.only(
|
||||
right: 16,
|
||||
),
|
||||
child: CommonIcon(
|
||||
child: CommonTargetIcon(
|
||||
src: icon,
|
||||
size: 42,
|
||||
),
|
||||
@@ -471,7 +476,10 @@ class _ListHeaderState extends State<ListHeader>
|
||||
Widget build(BuildContext context) {
|
||||
return CommonCard(
|
||||
key: widget.key,
|
||||
radius: 18,
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
context.colorScheme.surfaceContainer,
|
||||
),
|
||||
radius: 14,
|
||||
type: CommonCardType.filled,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -560,9 +568,7 @@ class _ListHeaderState extends State<ListHeader>
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_delayTest(widget.group.all);
|
||||
},
|
||||
onPressed: _delayTest,
|
||||
icon: const Icon(
|
||||
Icons.network_ping,
|
||||
),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user