Compare commits

...

11 Commits

Author SHA1 Message Date
chen08209
5dda2854be Fix list form input view issues
Fix traffic view issues
2025-03-05 15:11:19 +08:00
chen08209
5184ed6fc7 Update changelog 2025-03-05 02:36:31 +00:00
chen08209
4e679f776e Optimize performance
Update core

Optimize core stability

Fix linux tun authority check error

Fix some issues
2025-03-05 10:21:51 +08:00
chen08209
96328f66e9 Fix scroll physics error 2025-02-09 16:51:57 +08:00
chen08209
3eb14ab8a1 Update changelog 2025-02-09 08:36:14 +00:00
chen08209
c6266b7917 Add windows storage corruption detection
Fix core crash caused by windows resource manager restart

Optimize logs, requests, access to pages

Fix macos bypass domain issues
2025-02-09 16:23:40 +08:00
chen08209
6c27f2e2f1 Update changelog 2025-02-03 13:28:20 +00:00
chen08209
e04a0094b1 Fix some issues 2025-02-03 21:15:26 +08:00
chen08209
683e6a58ea Update changelog 2025-02-02 11:48:19 +00:00
chen08209
b340feeb49 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
2025-02-02 19:34:42 +08:00
chen08209
6a39b7ef5a Update changelog 2025-01-10 11:22:18 +00:00
183 changed files with 19456 additions and 14646 deletions

6
.gitmodules vendored
View File

@@ -6,3 +6,9 @@
path = plugins/flutter_distributor path = plugins/flutter_distributor
url = git@github.com:chen08209/flutter_distributor.git url = git@github.com:chen08209/flutter_distributor.git
branch = FlClash branch = FlClash
[submodule "plugins/tray_manager"]
path = plugins/tray_manager
url = git@github.com:chen08209/tray_manager.git
branch = main

View File

@@ -1,3 +1,63 @@
## v0.8.77
- Optimize performance
- Update core
- Optimize core stability
- Fix linux tun authority check error
- Fix some issues
- Fix scroll physics error
- Update changelog
## v0.8.75
- Add windows storage corruption detection
- Fix core crash caused by windows resource manager restart
- Optimize logs, requests, access to pages
- Fix macos bypass domain issues
- Update changelog
## 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 ## v0.8.71
- Remake dashboard - Remake dashboard

View File

@@ -1,29 +1,8 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml include: package:flutter_lints/flutter.yaml
linter: linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at analyzer:
# https://dart.dev/guides/language/analysis-options plugins:
- custom_lint

View File

@@ -23,7 +23,7 @@
tools:ignore="QueryAllPackagesPermission" /> tools:ignore="QueryAllPackagesPermission" />
<application <application
android:name="${applicationName}" android:name=".FlClashApplication"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="FlClash"> android:label="FlClash">

View File

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

View File

@@ -31,7 +31,7 @@ object GlobalState {
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin? return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
} }
fun getText(text: String): String { suspend fun getText(text: String): String {
return getCurrentAppPlugin()?.getText(text) ?: "" return getCurrentAppPlugin()?.getText(text) ?: ""
} }
@@ -44,14 +44,14 @@ object GlobalState {
return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin? return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
} }
fun handleToggle(context: Context) { fun handleToggle() {
val starting = handleStart(context) val starting = handleStart()
if (!starting) { if (!starting) {
handleStop() handleStop()
} }
} }
fun handleStart(context: Context): Boolean { fun handleStart(): Boolean {
if (runState.value == RunState.STOP) { if (runState.value == RunState.STOP) {
runState.value = RunState.PENDING runState.value = RunState.PENDING
runLock.lock() runLock.lock()
@@ -59,7 +59,7 @@ object GlobalState {
if (tilePlugin != null) { if (tilePlugin != null) {
tilePlugin.handleStart() tilePlugin.handleStart()
} else { } else {
initServiceEngine(context) initServiceEngine()
} }
return true return true
} }
@@ -74,6 +74,12 @@ object GlobalState {
} }
} }
fun handleTryDestroy() {
if (flutterEngine == null) {
destroyServiceEngine()
}
}
fun destroyServiceEngine() { fun destroyServiceEngine() {
runLock.withLock { runLock.withLock {
serviceEngine?.destroy() serviceEngine?.destroy()
@@ -81,21 +87,21 @@ object GlobalState {
} }
} }
fun initServiceEngine(context: Context) { fun initServiceEngine() {
if (serviceEngine != null) return if (serviceEngine != null) return
destroyServiceEngine() destroyServiceEngine()
runLock.withLock { runLock.withLock {
serviceEngine = FlutterEngine(context) serviceEngine = FlutterEngine(FlClashApplication.getAppContext())
serviceEngine?.plugins?.add(VpnPlugin()) serviceEngine?.plugins?.add(VpnPlugin)
serviceEngine?.plugins?.add(AppPlugin()) serviceEngine?.plugins?.add(AppPlugin())
serviceEngine?.plugins?.add(TilePlugin()) serviceEngine?.plugins?.add(TilePlugin())
serviceEngine?.plugins?.add(ServicePlugin())
val vpnService = DartExecutor.DartEntrypoint( val vpnService = DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(), FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"vpnService" "_service"
) )
serviceEngine?.dartExecutor?.executeDartEntrypoint( serviceEngine?.dartExecutor?.executeDartEntrypoint(
vpnService, vpnService,
if (flutterEngine == null) listOf("quick") else null
) )
} }
} }

View File

@@ -1,10 +1,8 @@
package com.follow.clash package com.follow.clash
import com.follow.clash.plugins.AppPlugin import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ServicePlugin import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.TilePlugin import com.follow.clash.plugins.TilePlugin
import com.follow.clash.plugins.VpnPlugin
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
@@ -12,8 +10,7 @@ class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
flutterEngine.plugins.add(AppPlugin()) flutterEngine.plugins.add(AppPlugin())
flutterEngine.plugins.add(VpnPlugin()) flutterEngine.plugins.add(ServicePlugin)
flutterEngine.plugins.add(ServicePlugin())
flutterEngine.plugins.add(TilePlugin()) flutterEngine.plugins.add(TilePlugin())
GlobalState.flutterEngine = flutterEngine GlobalState.flutterEngine = flutterEngine
} }

View File

@@ -9,7 +9,7 @@ class TempActivity : Activity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
when (intent.action) { when (intent.action) {
wrapAction("START") -> { wrapAction("START") -> {
GlobalState.handleStart(applicationContext) GlobalState.handleStart()
} }
wrapAction("STOP") -> { wrapAction("STOP") -> {
@@ -17,7 +17,7 @@ class TempActivity : Activity() {
} }
wrapAction("CHANGE") -> { wrapAction("CHANGE") -> {
GlobalState.handleToggle(applicationContext) GlobalState.handleToggle()
} }
} }
finishAndRemoveTask() finishAndRemoveTask()

View File

@@ -97,7 +97,6 @@ fun String.toCIDR(): CIDR {
return CIDR(address, prefixLength) return CIDR(address, prefixLength)
} }
fun ConnectivityManager.resolveDns(network: Network?): List<String> { fun ConnectivityManager.resolveDns(network: Network?): List<String> {
val properties = getLinkProperties(network) ?: return listOf() val properties = getLinkProperties(network) ?: return listOf()
return properties.dnsServers.map { it.asSocketAddressText(53) } return properties.dnsServers.map { it.asSocketAddressText(53) }
@@ -143,7 +142,6 @@ fun Context.getActionPendingIntent(action: String): PendingIntent {
} }
} }
private fun numericToTextFormat(src: ByteArray): String { private fun numericToTextFormat(src: ByteArray): String {
val sb = StringBuilder(39) val sb = StringBuilder(39)
for (i in 0 until 8) { for (i in 0 until 8) {

View File

@@ -4,5 +4,5 @@ data class Package(
val packageName: String, val packageName: String,
val label: String, val label: String,
val isSystem: Boolean, val isSystem: Boolean,
val firstInstallTime: Long, val lastUpdateTime: Long,
) )

View File

@@ -1,7 +1,7 @@
package com.follow.clash.models package com.follow.clash.models
data class Process( data class Process(
val id: Int, val id: String,
val metadata: Metadata, val metadata: Metadata,
) )

View File

@@ -7,6 +7,7 @@ enum class AccessControlMode {
} }
data class AccessControl( data class AccessControl(
val enable: Boolean,
val mode: AccessControlMode, val mode: AccessControlMode,
val acceptList: List<String>, val acceptList: List<String>,
val rejectList: List<String>, val rejectList: List<String>,
@@ -17,7 +18,7 @@ data class CIDR(val address: InetAddress, val prefixLength: Int)
data class VpnOptions( data class VpnOptions(
val enable: Boolean, val enable: Boolean,
val port: Int, val port: Int,
val accessControl: AccessControl?, val accessControl: AccessControl,
val allowBypass: Boolean, val allowBypass: Boolean,
val systemProxy: Boolean, val systemProxy: Boolean,
val bypassDomain: List<String>, val bypassDomain: List<String>,
@@ -25,4 +26,9 @@ data class VpnOptions(
val ipv4Address: String, val ipv4Address: String,
val ipv6Address: String, val ipv6Address: String,
val dnsServerAddress: String, val dnsServerAddress: String,
)
data class StartForegroundParams(
val title: String,
val content: String,
) )

View File

@@ -3,7 +3,6 @@ package com.follow.clash.plugins
import android.Manifest import android.Manifest
import android.app.Activity import android.app.Activity
import android.app.ActivityManager import android.app.ActivityManager
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo import android.content.pm.ComponentInfo
@@ -19,6 +18,7 @@ import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
import com.follow.clash.FlClashApplication
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.R import com.follow.clash.R
import com.follow.clash.extensions.awaitResult import com.follow.clash.extensions.awaitResult
@@ -37,16 +37,14 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.lang.ref.WeakReference
import java.util.zip.ZipFile import java.util.zip.ZipFile
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware { class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
private var activity: Activity? = null private var activityRef: WeakReference<Activity>? = null
private lateinit var context: Context
private lateinit var channel: MethodChannel private lateinit var channel: MethodChannel
@@ -121,21 +119,27 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
scope = CoroutineScope(Dispatchers.Default) scope = CoroutineScope(Dispatchers.Default)
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app") channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
channel.setMethodCallHandler(this) channel.setMethodCallHandler(this)
} }
private fun initShortcuts(label: String) { private fun initShortcuts(label: String) {
val shortcut = ShortcutInfoCompat.Builder(context, "toggle") val shortcut = ShortcutInfoCompat.Builder(FlClashApplication.getAppContext(), "toggle")
.setShortLabel(label) .setShortLabel(label)
.setIcon(IconCompat.createWithResource(context, R.mipmap.ic_launcher_round)) .setIcon(
.setIntent(context.getActionIntent("CHANGE")) IconCompat.createWithResource(
FlClashApplication.getAppContext(),
R.mipmap.ic_launcher_round
)
)
.setIntent(FlClashApplication.getAppContext().getActionIntent("CHANGE"))
.build() .build()
ShortcutManagerCompat.setDynamicShortcuts(context, listOf(shortcut)) ShortcutManagerCompat.setDynamicShortcuts(
FlClashApplication.getAppContext(),
listOf(shortcut)
)
} }
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null) channel.setMethodCallHandler(null)
scope.cancel() scope.cancel()
@@ -143,14 +147,14 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private fun tip(message: String?) { private fun tip(message: String?) {
if (GlobalState.flutterEngine == null) { 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) { override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) { when (call.method) {
"moveTaskToBack" -> { "moveTaskToBack" -> {
activity?.moveTaskToBack(true) activityRef?.get()?.moveTaskToBack(true)
result.success(true) result.success(true)
} }
@@ -192,7 +196,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
if (iconMap["default"] == null) { if (iconMap["default"] == null) {
iconMap["default"] = iconMap["default"] =
context.packageManager?.defaultActivityIcon?.getBase64() FlClashApplication.getAppContext().packageManager?.defaultActivityIcon?.getBase64()
} }
result.success(iconMap["default"]) result.success(iconMap["default"])
return@launch return@launch
@@ -221,8 +225,8 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private fun openFile(path: String) { private fun openFile(path: String) {
val file = File(path) val file = File(path)
val uri = FileProvider.getUriForFile( val uri = FileProvider.getUriForFile(
context, FlClashApplication.getAppContext(),
"${context.packageName}.fileProvider", "${FlClashApplication.getAppContext().packageName}.fileProvider",
file file
) )
@@ -234,13 +238,13 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
val flags = val flags =
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION 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 intent, PackageManager.MATCH_DEFAULT_ONLY
) )
for (resolveInfo in resInfoList) { for (resolveInfo in resInfoList) {
val packageName = resolveInfo.activityInfo.packageName val packageName = resolveInfo.activityInfo.packageName
context.grantUriPermission( FlClashApplication.getAppContext().grantUriPermission(
packageName, packageName,
uri, uri,
flags flags
@@ -248,19 +252,19 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
try { try {
activity?.startActivity(intent) activityRef?.get()?.startActivity(intent)
} catch (e: Exception) { } catch (e: Exception) {
println(e) println(e)
} }
} }
private fun updateExcludeFromRecents(value: Boolean?) { 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 { val task = am?.appTasks?.firstOrNull {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
it.taskInfo.taskId == activity?.taskId it.taskInfo.taskId == activityRef?.get()?.taskId
} else { } 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? { private suspend fun getPackageIcon(packageName: String): String? {
val packageManager = context.packageManager val packageManager = FlClashApplication.getAppContext().packageManager
if (iconMap[packageName] == null) { if (iconMap[packageName] == null) {
iconMap[packageName] = try { iconMap[packageName] = try {
packageManager?.getApplicationIcon(packageName)?.getBase64() packageManager?.getApplicationIcon(packageName)?.getBase64()
@@ -285,10 +289,10 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
private fun getPackages(): List<Package> { private fun getPackages(): List<Package> {
val packageManager = context.packageManager val packageManager = FlClashApplication.getAppContext().packageManager
if (packages.isNotEmpty()) return packages if (packages.isNotEmpty()) return packages
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter { packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
it.packageName != context.packageName it.packageName != FlClashApplication.getAppContext().packageName
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true || it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|| it.packageName == "android" || it.packageName == "android"
@@ -297,7 +301,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
packageName = it.packageName, packageName = it.packageName,
label = it.applicationInfo.loadLabel(packageManager).toString(), label = it.applicationInfo.loadLabel(packageManager).toString(),
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1, isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1,
firstInstallTime = it.firstInstallTime lastUpdateTime = it.lastUpdateTime
) )
}?.let { packages.addAll(it) } }?.let { packages.addAll(it) }
return packages return packages
@@ -317,43 +321,45 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
} }
fun requestVpnPermission(context: Context, callBack: () -> Unit) { fun requestVpnPermission(callBack: () -> Unit) {
vpnCallBack = callBack vpnCallBack = callBack
val intent = VpnService.prepare(context) val intent = VpnService.prepare(FlClashApplication.getAppContext())
if (intent != null) { if (intent != null) {
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE) activityRef?.get()?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
return return
} }
vpnCallBack?.invoke() vpnCallBack?.invoke()
} }
fun requestNotificationsPermission(context: Context) { fun requestNotificationsPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permission = ContextCompat.checkSelfPermission( val permission = ContextCompat.checkSelfPermission(
context, FlClashApplication.getAppContext(),
Manifest.permission.POST_NOTIFICATIONS Manifest.permission.POST_NOTIFICATIONS
) )
if (permission != PackageManager.PERMISSION_GRANTED) { if (permission != PackageManager.PERMISSION_GRANTED) {
if (isBlockNotification) return if (isBlockNotification) return
if (activity == null) return if (activityRef?.get() == null) return
ActivityCompat.requestPermissions( activityRef?.get()?.let {
activity!!, ActivityCompat.requestPermissions(
arrayOf(Manifest.permission.POST_NOTIFICATIONS), it,
NOTIFICATION_PERMISSION_REQUEST_CODE arrayOf(Manifest.permission.POST_NOTIFICATIONS),
) NOTIFICATION_PERMISSION_REQUEST_CODE
return )
return
}
} }
} }
} }
fun getText(text: String): String? { suspend fun getText(text: String): String? {
return runBlocking { return withContext(Dispatchers.Default){
channel.awaitResult<String>("getText", text) channel.awaitResult<String>("getText", text)
} }
} }
private fun isChinaPackage(packageName: String): Boolean { private fun isChinaPackage(packageName: String): Boolean {
val packageManager = context.packageManager ?: return false val packageManager = FlClashApplication.getAppContext().packageManager ?: return false
skipPrefixList.forEach { skipPrefixList.forEach {
if (packageName == it || packageName.startsWith("$it.")) return false if (packageName == it || packageName.startsWith("$it.")) return false
} }
@@ -373,7 +379,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong()) PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
) )
} else { } else {
@Suppress("DEPRECATION") packageManager.getPackageInfo( packageManager.getPackageInfo(
packageName, packageManagerFlags packageName, packageManagerFlags
) )
} }
@@ -420,28 +426,28 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
override fun onAttachedToActivity(binding: ActivityPluginBinding) { override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity activityRef = WeakReference(binding.activity)
binding.addActivityResultListener(::onActivityResult) binding.addActivityResultListener(::onActivityResult)
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener) binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
} }
override fun onDetachedFromActivityForConfigChanges() { override fun onDetachedFromActivityForConfigChanges() {
activity = null activityRef = null
} }
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity activityRef = WeakReference(binding.activity)
} }
override fun onDetachedFromActivity() { override fun onDetachedFromActivity() {
channel.invokeMethod("exit", null) channel.invokeMethod("exit", null)
activity = null activityRef = null
} }
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == VPN_PERMISSION_REQUEST_CODE) { if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
if (resultCode == FlutterActivity.RESULT_OK) { if (resultCode == FlutterActivity.RESULT_OK) {
GlobalState.initServiceEngine(context) GlobalState.initServiceEngine()
vpnCallBack?.invoke() vpnCallBack?.invoke()
} }
} }

View File

@@ -1,20 +1,19 @@
package com.follow.clash.plugins package com.follow.clash.plugins
import android.content.Context import com.follow.clash.FlClashApplication
import com.follow.clash.GlobalState 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.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel 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 flutterMethodChannel: MethodChannel
private lateinit var context: Context
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "service") flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "service")
flutterMethodChannel.setMethodCallHandler(this) flutterMethodChannel.setMethodCallHandler(this)
} }
@@ -24,9 +23,22 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
} }
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) { 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" -> { "init" -> {
GlobalState.getCurrentAppPlugin()?.requestNotificationsPermission(context) GlobalState.getCurrentAppPlugin()
GlobalState.initServiceEngine(context) ?.requestNotificationsPermission()
GlobalState.initServiceEngine()
result.success(true) result.success(true)
} }
@@ -41,7 +53,7 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
} }
private fun handleDestroy() { private fun handleDestroy() {
GlobalState.getCurrentVPNPlugin()?.stop() GlobalState.getCurrentVPNPlugin()?.handleStop()
GlobalState.destroyServiceEngine() GlobalState.destroyServiceEngine()
} }
} }

View File

@@ -1,14 +1,13 @@
package com.follow.clash.plugins package com.follow.clash.plugins
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
class TilePlugin(private val onStart: (() -> Unit)? = null, private val onStop: (() -> Unit)? = null) : FlutterPlugin, class TilePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
MethodChannel.MethodCallHandler {
private lateinit var channel: MethodChannel private lateinit var channel: MethodChannel
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "tile") channel = MethodChannel(flutterPluginBinding.binaryMessenger, "tile")
channel.setMethodCallHandler(this) channel.setMethodCallHandler(this)
@@ -20,13 +19,11 @@ class TilePlugin(private val onStart: (() -> Unit)? = null, private val onStop:
} }
fun handleStart() { fun handleStart() {
onStart?.let { it() }
channel.invokeMethod("start", null) channel.invokeMethod("start", null)
} }
fun handleStop() { fun handleStop() {
channel.invokeMethod("stop", null) channel.invokeMethod("stop", null)
onStop?.let { it() }
} }
private fun handleDetached() { private fun handleDetached() {

View File

@@ -10,14 +10,18 @@ import android.net.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.util.Log
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import com.follow.clash.BaseServiceInterface import com.follow.clash.FlClashApplication
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.RunState import com.follow.clash.RunState
import com.follow.clash.extensions.awaitResult
import com.follow.clash.extensions.getProtocol import com.follow.clash.extensions.getProtocol
import com.follow.clash.extensions.resolveDns import com.follow.clash.extensions.resolveDns
import com.follow.clash.models.Process import com.follow.clash.models.Process
import com.follow.clash.models.StartForegroundParams
import com.follow.clash.models.VpnOptions import com.follow.clash.models.VpnOptions
import com.follow.clash.services.BaseServiceInterface
import com.follow.clash.services.FlClashService import com.follow.clash.services.FlClashService
import com.follow.clash.services.FlClashVpnService import com.follow.clash.services.FlClashVpnService
import com.google.gson.Gson import com.google.gson.Gson
@@ -26,21 +30,24 @@ import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.net.InetSocketAddress import java.net.InetSocketAddress
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var flutterMethodChannel: MethodChannel private lateinit var flutterMethodChannel: MethodChannel
private lateinit var context: Context
private var flClashService: BaseServiceInterface? = null private var flClashService: BaseServiceInterface? = null
private lateinit var options: VpnOptions private lateinit var options: VpnOptions
private lateinit var scope: CoroutineScope private lateinit var scope: CoroutineScope
private var lastStartForegroundParams: StartForegroundParams? = null
private var timerJob: Job? = null
private val connectivity by lazy { private val connectivity by lazy {
context.getSystemService<ConnectivityManager>() FlClashApplication.getAppContext().getSystemService<ConnectivityManager>()
} }
private val connection = object : ServiceConnection { private val connection = object : ServiceConnection {
@@ -50,7 +57,7 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
is FlClashService.LocalBinder -> service.getService() is FlClashService.LocalBinder -> service.getService()
else -> throw Exception("invalid binder") else -> throw Exception("invalid binder")
} }
start() handleStartService()
} }
override fun onServiceDisconnected(arg: ComponentName) { override fun onServiceDisconnected(arg: ComponentName) {
@@ -60,7 +67,6 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
scope = CoroutineScope(Dispatchers.Default) scope = CoroutineScope(Dispatchers.Default)
context = flutterPluginBinding.applicationContext
scope.launch { scope.launch {
registerNetworkCallback() registerNetworkCallback()
} }
@@ -77,38 +83,28 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
when (call.method) { when (call.method) {
"start" -> { "start" -> {
val data = call.argument<String>("data") val data = call.argument<String>("data")
options = Gson().fromJson(data, VpnOptions::class.java) result.success(handleStart(Gson().fromJson(data, VpnOptions::class.java)))
when (options.enable) {
true -> handleStartVpn()
false -> start()
}
result.success(true)
} }
"stop" -> { "stop" -> {
stop() handleStop()
result.success(true) result.success(true)
} }
"setProtect" -> { "setProtect" -> {
val fd = call.argument<Int>("fd") val fd = call.argument<Int>("fd")
if (fd != null) { if (fd != null && flClashService is FlClashVpnService) {
if (flClashService is FlClashVpnService) { try {
(flClashService as FlClashVpnService).protect(fd) (flClashService as FlClashVpnService).protect(fd)
result.success(true)
} catch (e: RuntimeException) {
result.success(false)
} }
result.success(true)
} else { } else {
result.success(false) result.success(false)
} }
} }
"startForeground" -> {
val title = call.argument<String>("title") as String
val content = call.argument<String>("content") as String
startForeground(title, content)
result.success(true)
}
"resolverProcess" -> { "resolverProcess" -> {
val data = call.argument<String>("data") val data = call.argument<String>("data")
val process = if (data != null) Gson().fromJson( val process = if (data != null) Gson().fromJson(
@@ -144,7 +140,8 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
result.success(null) result.success(null)
return@withContext return@withContext
} }
val packages = context.packageManager?.getPackagesForUid(uid) val packages =
FlClashApplication.getAppContext().packageManager?.getPackagesForUid(uid)
result.success(packages?.first()) result.success(packages?.first())
} }
} }
@@ -156,10 +153,20 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
} }
} }
private fun handleStartVpn() { fun handleStart(options: VpnOptions): Boolean {
GlobalState.getCurrentAppPlugin()?.requestVpnPermission(context) { this.options = options
start() when (options.enable) {
true -> handleStartVpn()
false -> handleStartService()
} }
return true
}
private fun handleStartVpn() {
GlobalState.getCurrentAppPlugin()
?.requestVpnPermission {
handleStartService()
}
} }
fun requestGc() { fun requestGc() {
@@ -177,16 +184,6 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
flutterMethodChannel.invokeMethod("dnsChanged", dns) 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() { private val callback = object : ConnectivityManager.NetworkCallback() {
@@ -218,14 +215,41 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
onUpdateNetwork() onUpdateNetwork()
} }
private fun startForeground(title: String, content: String) { private suspend fun startForeground() {
GlobalState.runLock.withLock { GlobalState.runLock.lock()
try {
if (GlobalState.runState.value != RunState.START) return 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) { if (flClashService == null) {
bindService() bindService()
return return
@@ -237,24 +261,25 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
flutterMethodChannel.invokeMethod( flutterMethodChannel.invokeMethod(
"started", fd "started", fd
) )
startForegroundJob();
} }
} }
fun stop() { fun handleStop() {
GlobalState.runLock.withLock { GlobalState.runLock.withLock {
if (GlobalState.runState.value == RunState.STOP) return if (GlobalState.runState.value == RunState.STOP) return
GlobalState.runState.value = RunState.STOP GlobalState.runState.value = RunState.STOP
stopForegroundJob()
flClashService?.stop() flClashService?.stop()
GlobalState.handleTryDestroy()
} }
GlobalState.destroyServiceEngine()
} }
private fun bindService() { private fun bindService() {
val intent = when (options.enable) { val intent = when (options.enable) {
true -> Intent(context, FlClashVpnService::class.java) true -> Intent(FlClashApplication.getAppContext(), FlClashVpnService::class.java)
false -> Intent(context, FlClashService::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)
} }
} }

View File

@@ -1,10 +1,12 @@
package com.follow.clash package com.follow.clash.services
import com.follow.clash.models.VpnOptions import com.follow.clash.models.VpnOptions
interface BaseServiceInterface { interface BaseServiceInterface {
fun start(options: VpnOptions): Int fun start(options: VpnOptions): Int
fun stop() fun stop()
fun startForeground(title: String, content: String)
suspend fun startForeground(title: String, content: String)
} }

View File

@@ -12,14 +12,14 @@ import android.os.Binder
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.follow.clash.BaseServiceInterface
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.MainActivity import com.follow.clash.MainActivity
import com.follow.clash.extensions.getActionPendingIntent import com.follow.clash.extensions.getActionPendingIntent
import com.follow.clash.models.VpnOptions import com.follow.clash.models.VpnOptions
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.async
class FlClashService : Service(), BaseServiceInterface { class FlClashService : Service(), BaseServiceInterface {
@@ -42,44 +42,54 @@ class FlClashService : Service(), BaseServiceInterface {
private val notificationId: Int = 1 private val notificationId: Int = 1
private val notificationBuilder: NotificationCompat.Builder by lazy { private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy {
val intent = Intent(this, MainActivity::class.java) CoroutineScope(Dispatchers.Main).async {
val stopText = GlobalState.getText("stop")
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) { val intent = Intent(
PendingIntent.getActivity( this@FlClashService, MainActivity::class.java
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
) )
} else {
PendingIntent.getActivity( val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
this, PendingIntent.getActivity(
0, this@FlClashService,
intent, 0,
PendingIntent.FLAG_UPDATE_CURRENT intent,
) PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} )
with(NotificationCompat.Builder(this, CHANNEL)) { } else {
setSmallIcon(com.follow.clash.R.drawable.ic_stat_name) PendingIntent.getActivity(
setContentTitle("FlClash") this@FlClashService,
setContentIntent(pendingIntent) 0,
setCategory(NotificationCompat.CATEGORY_SERVICE) intent,
priority = NotificationCompat.PRIORITY_MIN PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { )
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE }
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 override fun start(options: VpnOptions) = 0
@@ -91,24 +101,24 @@ class FlClashService : Service(), BaseServiceInterface {
} }
@SuppressLint("ForegroundServiceType", "WrongConstant") @SuppressLint("ForegroundServiceType", "WrongConstant")
override fun startForeground(title: String, content: String) { override suspend fun startForeground(title: String, content: String) {
CoroutineScope(Dispatchers.Default).launch { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val manager = getSystemService(NotificationManager::class.java)
val manager = getSystemService(NotificationManager::class.java) var channel = manager?.getNotificationChannel(CHANNEL)
var channel = manager?.getNotificationChannel(CHANNEL) if (channel == null) {
if (channel == null) { channel =
channel = NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW) manager?.createNotificationChannel(channel)
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)
} }
} }
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)
}
} }
} }

View File

@@ -66,7 +66,7 @@ class FlClashTileService : TileService() {
override fun onClick() { override fun onClick() {
super.onClick() super.onClick()
activityTransfer() activityTransfer()
GlobalState.handleToggle(applicationContext) GlobalState.handleToggle()
} }
override fun onDestroy() { override fun onDestroy() {

View File

@@ -7,7 +7,6 @@ import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.net.Network
import android.net.ProxyInfo import android.net.ProxyInfo
import android.net.VpnService import android.net.VpnService
import android.os.Binder import android.os.Binder
@@ -17,7 +16,6 @@ import android.os.Parcel
import android.os.RemoteException import android.os.RemoteException
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.follow.clash.BaseServiceInterface
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.MainActivity import com.follow.clash.MainActivity
import com.follow.clash.R import com.follow.clash.R
@@ -28,14 +26,16 @@ import com.follow.clash.extensions.toCIDR
import com.follow.clash.models.AccessControlMode import com.follow.clash.models.AccessControlMode
import com.follow.clash.models.VpnOptions import com.follow.clash.models.VpnOptions
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class FlClashVpnService : VpnService(), BaseServiceInterface { class FlClashVpnService : VpnService(), BaseServiceInterface {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
GlobalState.initServiceEngine(applicationContext) GlobalState.initServiceEngine()
} }
override fun start(options: VpnOptions): Int { override fun start(options: VpnOptions): Int {
@@ -68,17 +68,19 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
} }
addDnsServer(options.dnsServerAddress) addDnsServer(options.dnsServerAddress)
setMtu(9000) setMtu(9000)
options.accessControl?.let { accessControl -> options.accessControl.let { accessControl ->
when (accessControl.mode) { if (accessControl.enable) {
AccessControlMode.acceptSelected -> { when (accessControl.mode) {
(accessControl.acceptList + packageName).forEach { AccessControlMode.acceptSelected -> {
addAllowedApplication(it) (accessControl.acceptList + packageName).forEach {
addAllowedApplication(it)
}
} }
}
AccessControlMode.rejectSelected -> { AccessControlMode.rejectSelected -> {
(accessControl.rejectList - packageName).forEach { (accessControl.rejectList - packageName).forEach {
addDisallowedApplication(it) addDisallowedApplication(it)
}
} }
} }
} }
@@ -105,12 +107,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() { override fun stop() {
stopSelf() stopSelf()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@@ -122,69 +118,74 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
private val notificationId: Int = 1 private val notificationId: Int = 1
private val notificationBuilder: NotificationCompat.Builder by lazy { private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy {
val intent = Intent(this, MainActivity::class.java) 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) { val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity( PendingIntent.getActivity(
this, this@FlClashVpnService,
0, 0,
intent, intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
) )
} else { } else {
PendingIntent.getActivity( PendingIntent.getActivity(
this, this@FlClashVpnService,
0, 0,
intent, intent,
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
) )
} }
with(NotificationCompat.Builder(this, CHANNEL)) { with(NotificationCompat.Builder(this@FlClashVpnService, CHANNEL)) {
setSmallIcon(R.drawable.ic_stat_name) setSmallIcon(R.drawable.ic_stat_name)
setContentTitle("FlClash") setContentTitle("FlClash")
setContentIntent(pendingIntent) setContentIntent(pendingIntent)
setCategory(NotificationCompat.CATEGORY_SERVICE) setCategory(NotificationCompat.CATEGORY_SERVICE)
priority = NotificationCompat.PRIORITY_MIN priority = NotificationCompat.PRIORITY_MIN
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE 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") @SuppressLint("ForegroundServiceType", "WrongConstant")
override fun startForeground(title: String, content: String) { override suspend fun startForeground(title: String, content: String) {
CoroutineScope(Dispatchers.Default).launch { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val manager = getSystemService(NotificationManager::class.java)
val manager = getSystemService(NotificationManager::class.java) var channel = manager?.getNotificationChannel(CHANNEL)
var channel = manager?.getNotificationChannel(CHANNEL) if (channel == null) {
if (channel == null) { channel =
channel = NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW) manager?.createNotificationChannel(channel)
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)
} }
} }
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) { override fun onTrimMemory(level: Int) {

View File

@@ -5,6 +5,7 @@ targets:
options: options:
build_extensions: build_extensions:
'^lib/models/{{}}.dart': 'lib/models/generated/{{}}.g.dart' '^lib/models/{{}}.dart': 'lib/models/generated/{{}}.g.dart'
'^lib/providers/{{}}.dart': 'lib/providers/generated/{{}}.g.dart'
freezed: freezed:
options: options:
build_extensions: build_extensions:

View File

@@ -1,32 +1,177 @@
//go:build !cgo
package main package main
import ( import (
"encoding/json" "encoding/json"
) )
func (action Action) Json() ([]byte, error) { type Action struct {
data, err := json.Marshal(action) 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 return data, err
} }
func (action Action) callback(data interface{}) bool { func (action Action) getResult(data interface{}) []byte {
if conn == nil { resultAction := ActionResult{
return false
}
sendAction := Action{
Id: action.Id, Id: action.Id,
Method: action.Method, Method: action.Method,
Data: data, Data: data,
} }
res, err := sendAction.Json() res, _ := resultAction.Json()
if err != nil { return res
return false }
}
_, err = conn.Write(append(res, []byte("\n")...)) func handleAction(action *Action, result func(data interface{})) {
if err != nil { switch action.Method {
return false case initClashMethod:
} data := action.Data.(string)
return true result(handleInitClash(data))
return
case getIsInitMethod:
result(handleGetIsInit())
return
case forceGcMethod:
handleForceGc()
result(true)
return
case shutdownMethod:
result(handleShutdown())
return
case validateConfigMethod:
data := []byte(action.Data.(string))
result(handleValidateConfig(data))
return
case updateConfigMethod:
data := []byte(action.Data.(string))
result(handleUpdateConfig(data))
return
case getProxiesMethod:
result(handleGetProxies())
return
case changeProxyMethod:
data := action.Data.(string)
handleChangeProxy(data, func(value string) {
result(value)
})
return
case getTrafficMethod:
result(handleGetTraffic())
return
case getTotalTrafficMethod:
result(handleGetTotalTraffic())
return
case resetTrafficMethod:
handleResetTraffic()
result(true)
return
case asyncTestDelayMethod:
data := action.Data.(string)
handleAsyncTestDelay(data, func(value string) {
result(value)
})
return
case getConnectionsMethod:
result(handleGetConnections())
return
case closeConnectionsMethod:
result(handleCloseConnections())
return
case closeConnectionMethod:
id := action.Data.(string)
result(handleCloseConnection(id))
return
case getExternalProvidersMethod:
result(handleGetExternalProviders())
return
case getExternalProviderMethod:
externalProviderName := action.Data.(string)
result(handleGetExternalProvider(externalProviderName))
case updateGeoDataMethod:
paramsString := action.Data.(string)
var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
result(err.Error())
return
}
geoType := params["geo-type"]
geoName := params["geo-name"]
handleUpdateGeoData(geoType, geoName, func(value string) {
result(value)
})
return
case updateExternalProviderMethod:
providerName := action.Data.(string)
handleUpdateExternalProvider(providerName, func(value string) {
result(value)
})
return
case sideLoadExternalProviderMethod:
paramsString := action.Data.(string)
var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
result(err.Error())
return
}
providerName := params["providerName"]
data := params["data"]
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
result(value)
})
return
case startLogMethod:
handleStartLog()
result(true)
return
case stopLogMethod:
handleStopLog()
result(true)
return
case startListenerMethod:
result(handleStartListener())
return
case stopListenerMethod:
result(handleStopListener())
return
case getCountryCodeMethod:
ip := action.Data.(string)
handleGetCountryCode(ip, func(value string) {
result(value)
})
return
case getMemoryMethod:
handleGetMemory(func(value string) {
result(value)
})
return
case getProfileMethod:
profileId := action.Data.(string)
handleGetMemory(func(value string) {
result(handleGetProfile(profileId))
})
return
case setStateMethod:
data := action.Data.(string)
handleSetState(data)
result(true)
default:
handle := nextHandle(action, result)
if handle {
return
} else {
result(action.DefaultValue)
}
}
} }

View File

@@ -2,7 +2,6 @@ package main
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter"
@@ -32,7 +31,7 @@ import (
var ( var (
isRunning = false isRunning = false
runLock sync.Mutex runLock sync.Mutex
ips = []string{"ipwho.is", "ifconfig.me", "icanhazip.com", "api.ip.sb", "ipinfo.io"} ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"}
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50)) b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
) )
@@ -42,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) 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 (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) { func readFile(path string) ([]byte, error) {
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err return nil, err
@@ -85,16 +79,6 @@ func getRawConfigWithId(id string) *config.RawConfig {
continue continue
} }
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value) 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 { for _, mapping := range prof.RuleProvider {
value, exist := mapping["path"].(string) value, exist := mapping["path"].(string)
@@ -231,6 +215,7 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.Tun.Device = patchConfig.Tun.Device targetConfig.Tun.Device = patchConfig.Tun.Device
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
targetConfig.Tun.Stack = patchConfig.Tun.Stack targetConfig.Tun.Stack = patchConfig.Tun.Stack
targetConfig.Tun.RouteAddress = patchConfig.Tun.RouteAddress
targetConfig.GeodataLoader = patchConfig.GeodataLoader targetConfig.GeodataLoader = patchConfig.GeodataLoader
targetConfig.Profile.StoreSelected = false targetConfig.Profile.StoreSelected = false
targetConfig.GeoXUrl = patchConfig.GeoXUrl targetConfig.GeoXUrl = patchConfig.GeoXUrl

View File

@@ -1,9 +1,9 @@
package main package main
import ( import (
"encoding/json"
"github.com/metacubex/mihomo/adapter/provider" "github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
"time" "time"
) )
@@ -28,14 +28,10 @@ type ChangeProxyParams struct {
type TestDelayParams struct { type TestDelayParams struct {
ProxyName string `json:"proxy-name"` ProxyName string `json:"proxy-name"`
TestUrl string `json:"test-url"`
Timeout int64 `json:"timeout"` Timeout int64 `json:"timeout"`
} }
type ProcessMapItem struct {
Id int64 `json:"id"`
Value string `json:"value"`
}
type ExternalProvider struct { type ExternalProvider struct {
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
@@ -74,19 +70,24 @@ const (
stopLogMethod Method = "stopLog" stopLogMethod Method = "stopLog"
startListenerMethod Method = "startListener" startListenerMethod Method = "startListener"
stopListenerMethod Method = "stopListener" 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"
getProfileMethod Method = "getProfile"
) )
type Method string type Method string
type Action struct {
Id string `json:"id"`
Method Method `json:"method"`
Data interface{} `json:"data"`
}
type MessageType string type MessageType string
type Delay struct { type Delay struct {
Url string `json:"url"`
Name string `json:"name"` Name string `json:"name"`
Value int32 `json:"value"` Value int32 `json:"value"`
} }
@@ -96,17 +97,31 @@ type Message struct {
Data interface{} `json:"data"` Data interface{} `json:"data"`
} }
type Process struct {
Id int64 `json:"id"`
Metadata *constant.Metadata `json:"metadata"`
}
const ( const (
LogMessage MessageType = "log" LogMessage MessageType = "log"
ProtectMessage MessageType = "protect"
DelayMessage MessageType = "delay" DelayMessage MessageType = "delay"
ProcessMessage MessageType = "process"
RequestMessage MessageType = "request" RequestMessage MessageType = "request"
StartedMessage MessageType = "started"
LoadedMessage MessageType = "loaded" 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)
}

View File

@@ -6,7 +6,7 @@ replace github.com/metacubex/mihomo => ./Clash.Meta
require ( require (
github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000 github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000
github.com/samber/lo v1.47.0 github.com/samber/lo v1.49.1
) )
require ( require (
@@ -19,28 +19,28 @@ require (
github.com/buger/jsonparser v1.1.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect
github.com/cloudflare/circl v1.3.7 // indirect github.com/cloudflare/circl v1.3.7 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect github.com/coreos/go-iptables v0.8.0 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/ebitengine/purego v0.8.1 // indirect github.com/ebitengine/purego v0.8.2 // indirect
github.com/enfein/mieru/v3 v3.10.0 // indirect github.com/enfein/mieru/v3 v3.11.2 // indirect
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect github.com/gaukas/godicttls v0.0.4 // indirect
github.com/go-chi/chi/v5 v5.2.0 // indirect github.com/go-chi/chi/v5 v5.2.1 // indirect
github.com/go-chi/render v1.0.3 // indirect github.com/go-chi/render v1.0.3 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect github.com/gobwas/ws v1.4.0 // indirect
github.com/gofrs/uuid/v5 v5.3.0 // indirect github.com/gofrs/uuid/v5 v5.3.1 // indirect
github.com/google/btree v1.1.3 // indirect github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect github.com/hashicorp/yamux v0.1.2 // indirect
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d // indirect github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 // indirect
github.com/josharian/native v1.1.0 // indirect github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect
@@ -51,21 +51,22 @@ require (
github.com/mdlayher/socket v0.4.1 // indirect github.com/mdlayher/socket v0.4.1 // indirect
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // 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/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
github.com/metacubex/chacha v0.1.0 // indirect github.com/metacubex/chacha v0.1.1 // indirect
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a // 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/quic-go v0.49.1-0.20250212162123-c135a4412996 // indirect
github.com/metacubex/randv2 v0.2.0 // indirect github.com/metacubex/randv2 v0.2.0 // indirect
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 // indirect github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 // indirect
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 // indirect
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
github.com/metacubex/sing-tun v0.4.5 // 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-vmess v0.1.14-0.20250228002636-abc39e113b82 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // 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/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect
github.com/metacubex/utls v1.6.6 // indirect github.com/metacubex/utls v1.6.6 // indirect
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
github.com/miekg/dns v1.1.62 // indirect github.com/miekg/dns v1.1.63 // indirect
github.com/mroth/weightedrand/v2 v2.1.0 // indirect github.com/mroth/weightedrand/v2 v2.1.0 // indirect
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect
@@ -73,18 +74,18 @@ require (
github.com/oschwald/maxminddb-golang v1.12.0 // indirect github.com/oschwald/maxminddb-golang v1.12.0 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/sagernet/cors v1.2.1 // indirect github.com/sagernet/cors v1.2.1 // indirect
github.com/sagernet/fswatch v0.1.1 // indirect github.com/sagernet/fswatch v0.1.1 // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/sagernet/sing v0.5.1 // indirect github.com/sagernet/sing v0.5.2 // indirect
github.com/sagernet/sing-mux v0.2.1 // indirect github.com/sagernet/sing-mux v0.2.1 // indirect
github.com/sagernet/sing-shadowtls v0.1.5 // indirect github.com/sagernet/sing-shadowtls v0.1.5 // indirect
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
github.com/shirou/gopsutil/v4 v4.24.11 // indirect github.com/shirou/gopsutil/v4 v4.25.1 // indirect
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // 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/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
@@ -101,13 +102,13 @@ require (
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
go.uber.org/mock v0.4.0 // indirect go.uber.org/mock v0.4.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.31.0 // indirect golang.org/x/crypto v0.33.0 // indirect
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
golang.org/x/mod v0.20.0 // indirect golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.33.0 // indirect golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.10.0 // indirect golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.28.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.7.0 // indirect golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.24.0 // indirect golang.org/x/tools v0.24.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect google.golang.org/protobuf v1.34.2 // indirect

View File

@@ -24,12 +24,12 @@ github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFE
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE= github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.10.0 h1:KMnAtY4s8MB74sUg4GbvF9R9v3jkXPQTSkxPxl1emxQ= github.com/enfein/mieru/v3 v3.11.2 h1:06KyGbXiiGz2nSHLJDOOkztAVY3cRr3wBMOpYxPotTo=
github.com/enfein/mieru/v3 v3.10.0/go.mod h1:jH2nXzJSNUn6UWuzD8E8AsRVa9Ca0CqcTcr9Z+CJO1o= github.com/enfein/mieru/v3 v3.11.2/go.mod h1:XvVfNsM78lUMSlJJKXJZ0Hn3lAB2o/ETXTbb84x5egw=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
@@ -43,8 +43,8 @@ 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/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= 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-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 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
@@ -59,8 +59,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk= github.com/gofrs/uuid/v5 v5.3.1 h1:aPx49MwJbekCzOyhZDjJVb0hx3A0KLjlbLx6p2gY0p0=
github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gofrs/uuid/v5 v5.3.1/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
@@ -74,8 +74,8 @@ github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= 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/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/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d h1:VkCNWh6tuQLgDBc6KrUOz/L1mCUQGnR1Ujj8uTgpwwk= github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k= github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
@@ -84,6 +84,7 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= 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/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
@@ -98,26 +99,28 @@ github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI= github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig= github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro= github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
github.com/metacubex/chacha v0.1.0 h1:tg9RSJ18NvL38cCWNyYH1eiG6qDCyyXIaTLQthon0sc= github.com/metacubex/chacha v0.1.1 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c=
github.com/metacubex/chacha v0.1.0/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8= github.com/metacubex/chacha v0.1.1/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a h1:cZ6oNVrsmsi3SNlnSnRio4zOgtQq+/XidwsaNgKICcg= 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/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.49.1-0.20250212162123-c135a4412996 h1:B+AP/Pj2/jBDS/kCYjz/x+0BCOKfd2VODYevyeIt+Ds=
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da/go.mod h1:AiZ+UPgrkO1DTnmiAX4b+kRoV1Vfc65UkYD7RbFlIZA= github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996/go.mod h1:ExVjGyEwTUjCFqx+5uxgV7MOoA3fZI+th4D40H35xmY=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 h1:HobpULaPK6OoxrHMmgcwLkwwIduXVmwdcznwUfH1GQM= github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 h1:aHsYiTvubfgMa3JMTDY//hDXVvFWrHg6ARckR52ttZs=
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8= github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629/go.mod h1:TTeIOZLdGmzc07Oedn++vWUUfkZoXLF4sEMxWuhBFr8=
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 h1:UkPoRAnoBQMn7IK5qpoIV3OejU15q+rqel3NrbSCFKA=
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4= 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-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 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q= github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0= 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-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.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY= github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg= 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/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 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
@@ -126,10 +129,11 @@ github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8=
github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo= github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ= github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y= github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU= github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU= github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
@@ -149,8 +153,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
@@ -164,18 +168,18 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJ
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y= github.com/sagernet/sing v0.5.2 h1:2OZQJNKGtji/66QLxbf/T/dqtK/3+fF/zuHH9tsGK7M=
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.5.2/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 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE= 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 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4= 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 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8= github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8= github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8= 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/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= github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk=
@@ -218,8 +222,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= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= 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/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= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
@@ -228,11 +232,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/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -248,12 +252,12 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= 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/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.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
@@ -263,8 +267,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"core/state"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter"
@@ -149,8 +150,8 @@ func handleChangeProxy(data string, fn func(string string)) {
}() }()
} }
func handleGetTraffic(onlyProxy bool) string { func handleGetTraffic() string {
up, down := statistic.DefaultManager.Current(onlyProxy) up, down := statistic.DefaultManager.Current(state.CurrentState.OnlyStatisticsProxy)
traffic := map[string]int64{ traffic := map[string]int64{
"up": up, "up": up,
"down": down, "down": down,
@@ -163,8 +164,8 @@ func handleGetTraffic(onlyProxy bool) string {
return string(data) return string(data)
} }
func handleGetTotalTraffic(onlyProxy bool) string { func handleGetTotalTraffic() string {
up, down := statistic.DefaultManager.Total(onlyProxy) up, down := statistic.DefaultManager.Total(state.CurrentState.OnlyStatisticsProxy)
traffic := map[string]int64{ traffic := map[string]int64{
"up": up, "up": up,
"down": down, "down": down,
@@ -177,6 +178,15 @@ func handleGetTotalTraffic(onlyProxy bool) string {
return string(data) return string(data)
} }
func handleGetProfile(profileId string) string {
prof := getRawConfigWithId(profileId)
data, err := json.Marshal(prof)
if err != nil {
return ""
}
return string(data)
}
func handleResetTraffic() { func handleResetTraffic() {
statistic.DefaultManager.ResetStatistic() statistic.DefaultManager.ResetStatistic()
} }
@@ -213,7 +223,14 @@ func handleAsyncTestDelay(paramsString string, fn func(string)) {
return false, nil return false, nil
} }
delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus) testUrl := constant.DefaultTestURL
if params.TestUrl != "" {
testUrl = params.TestUrl
}
delayData.Url = testUrl
delay, err := proxy.URLTest(ctx, testUrl, expectedStatus)
if err != nil || delay == 0 { if err != nil || delay == 0 {
delayData.Value = -1 delayData.Value = -1
data, _ := json.Marshal(delayData) data, _ := json.Marshal(delayData)
@@ -240,17 +257,6 @@ func handleGetConnections() string {
return string(data) 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 { func handleCloseConnections() bool {
runLock.Lock() runLock.Lock()
defer runLock.Unlock() defer runLock.Unlock()
@@ -395,7 +401,7 @@ func handleStartLog() {
Type: LogMessage, Type: LogMessage,
Data: logData, Data: logData,
} }
SendMessage(*message) sendMessage(*message)
} }
}() }()
} }
@@ -426,9 +432,14 @@ func handleGetMemory(fn func(value string)) {
}() }()
} }
func handleSetState(params string) {
_ = json.Unmarshal([]byte(params), state.CurrentState)
}
func init() { func init() {
adapter.UrlTestHook = func(name string, delay uint16) { adapter.UrlTestHook = func(url string, name string, delay uint16) {
delayData := &Delay{ delayData := &Delay{
Url: url,
Name: name, Name: name,
} }
if delay == 0 { if delay == 0 {
@@ -436,19 +447,19 @@ func init() {
} else { } else {
delayData.Value = int32(delay) delayData.Value = int32(delay)
} }
SendMessage(Message{ sendMessage(Message{
Type: DelayMessage, Type: DelayMessage,
Data: delayData, Data: delayData,
}) })
} }
statistic.DefaultRequestNotify = func(c statistic.Tracker) { statistic.DefaultRequestNotify = func(c statistic.Tracker) {
SendMessage(Message{ sendMessage(Message{
Type: RequestMessage, Type: RequestMessage,
Data: c, Data: c,
}) })
} }
executor.DefaultProviderLoadedHook = func(providerName string) { executor.DefaultProviderLoadedHook = func(providerName string) {
SendMessage(Message{ sendMessage(Message{
Type: LoadedMessage, Type: LoadedMessage,
Data: providerName, Data: providerName,
}) })

View File

@@ -8,18 +8,30 @@ package main
import "C" import "C"
import ( import (
bridge "core/dart-bridge" bridge "core/dart-bridge"
"encoding/json"
"unsafe" "unsafe"
) )
var messagePort int64 = -1
//export initNativeApiBridge //export initNativeApiBridge
func initNativeApiBridge(api unsafe.Pointer) { func initNativeApiBridge(api unsafe.Pointer) {
bridge.InitDartApi(api) bridge.InitDartApi(api)
} }
//export initMessage //export attachMessagePort
func initMessage(port C.longlong) { func attachMessagePort(mPort C.longlong) {
i := int64(port) messagePort = int64(mPort)
Port = i }
//export getTraffic
func getTraffic() *C.char {
return C.CString(handleGetTraffic())
}
//export getTotalTraffic
func getTotalTraffic() *C.char {
return C.CString(handleGetTotalTraffic())
} }
//export freeCString //export freeCString
@@ -27,9 +39,32 @@ func freeCString(s *C.char) {
C.free(unsafe.Pointer(s)) C.free(unsafe.Pointer(s))
} }
//export initClash //export invokeAction
func initClash(homeDirStr *C.char) bool { func invokeAction(paramsChar *C.char, port C.longlong) {
return handleInitClash(C.GoString(homeDirStr)) 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(data interface{}) {
bridge.SendToPort(i, string(action.getResult(data)))
})
}
func sendMessage(message Message) {
if messagePort == -1 {
return
}
res, err := message.Json()
if err != nil {
return
}
bridge.SendToPort(messagePort, string(Action{
Method: messageMethod,
}.getResult(res)))
} }
//export startListener //export startListener
@@ -41,159 +76,3 @@ func startListener() {
func stopListener() { func stopListener() {
handleStopListener() 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 getMemory
func getMemory(port C.longlong) {
i := int64(port)
handleGetMemory(func(value string) {
bridge.SendToPort(i, value)
})
}
//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 getCountryCode
func getCountryCode(ipChar *C.char, port C.longlong) {
ip := C.GoString(ipChar)
i := int64(port)
handleGetCountryCode(ip, 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()
}

View File

@@ -4,12 +4,14 @@ package main
import "C" import "C"
import ( import (
bridge "core/dart-bridge"
"core/platform" "core/platform"
"core/state" "core/state"
t "core/tun" t "core/tun"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/process" "github.com/metacubex/mihomo/component/process"
"github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant"
@@ -19,123 +21,158 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"syscall" "syscall"
"time" "time"
) )
type ProcessMap struct {
m sync.Map
}
type FdMap struct {
m sync.Map
}
type Fd struct { type Fd struct {
Id int64 `json:"id"` Id string `json:"id"`
Value int64 `json:"value"` 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) 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) 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:
res, ok := m.invokeMap.Load(id)
m.invokeMap.Delete(id)
if ok {
return res.(string)
} else {
return ""
}
case <-timeout:
m.completer(id, "")
return ""
}
} }
var ( var (
tunListener *sing_tun.Listener invokePort int64 = -1
fdMap FdMap tunListener *sing_tun.Listener
fdCounter int64 = 0 fdInvokeMap = NewInvokeManager()
counter int64 = 0 processInvokeMap = NewInvokeManager()
processMap ProcessMap tunLock sync.Mutex
tunLock sync.Mutex runTime *time.Time
runTime *time.Time errBlocked = errors.New("blocked")
errBlocked = errors.New("blocked")
) )
func (cm *ProcessMap) Store(key int64, value string) { func handleStartTun(fd int) string {
cm.m.Store(key, value) handleStopTun()
} tunLock.Lock()
defer tunLock.Unlock()
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
if fd == 0 { if fd == 0 {
tunLock.Lock()
defer tunLock.Unlock()
now := time.Now() now := time.Now()
runTime = &now runTime = &now
SendMessage(Message{ } else {
Type: StartedMessage, initSocketHook()
Data: strconv.FormatInt(runTime.UnixMilli(), 10), tunListener, _ = t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
})
return
}
initSocketHook()
go func() {
tunLock.Lock()
defer tunLock.Unlock()
f := int(fd)
tunListener, _ = t.Start(f, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
if tunListener != nil { if tunListener != nil {
log.Infoln("TUN address: %v", tunListener.Address()) log.Infoln("TUN address: %v", tunListener.Address())
} }
now := time.Now() now := time.Now()
runTime = &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 handleStopTun() {
func stopTun() { tunLock.Lock()
defer tunLock.Unlock()
removeSocketHook() removeSocketHook()
go func() { runTime = nil
tunLock.Lock() if tunListener != nil {
defer tunLock.Unlock() log.Infoln("TUN close")
_ = tunListener.Close()
runTime = nil }
if tunListener != nil {
_ = tunListener.Close()
}
}()
} }
//export setFdMap func handleGetRunTime() string {
func setFdMap(fd C.long) { if runTime == nil {
fdInt := int64(fd) return ""
go func() { }
fdMap.Store(fdInt) return strconv.FormatInt(runTime.UnixMilli(), 10)
}()
} }
func markSocket(fd Fd) { func handleSetProcessMap(params string) {
SendMessage(Message{ var processMapItem = &ProcessMapItem{}
Type: ProtectMessage, 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, Data: fd,
}) })
} }
func handleParseProcess(process Process) {
sendInvokeMessage(InvokeMessage{
Type: ProcessInvoke,
Data: process,
})
}
func handleSetFdMap(id string) {
go func() {
fdInvokeMap.completer(id, "")
}()
}
func initSocketHook() { func initSocketHook() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error { dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() { if platform.ShouldBlockConnection() {
@@ -143,26 +180,14 @@ func initSocketHook() {
} }
return conn.Control(func(fd uintptr) { return conn.Control(func(fd uintptr) {
fdInt := int64(fd) fdInt := int64(fd)
timeout := time.After(500 * time.Millisecond) id := utils.NewUUIDV1().String()
id := atomic.AddInt64(&fdCounter, 1)
markSocket(Fd{ handleMarkSocket(Fd{
Id: id, Id: id,
Value: fdInt, Value: fdInt,
}) })
for { fdInvokeMap.await(id)
select {
case <-timeout:
return
default:
exists := fdMap.Load(id)
if exists {
return
}
time.Sleep(20 * time.Millisecond)
}
}
}) })
} }
} }
@@ -176,95 +201,158 @@ func init() {
if metadata == nil { if metadata == nil {
return "", process.ErrInvalidNetwork return "", process.ErrInvalidNetwork
} }
id := atomic.AddInt64(&counter, 1) id := utils.NewUUIDV1().String()
handleParseProcess(Process{
timeout := time.After(200 * time.Millisecond) Id: id,
Metadata: metadata,
SendMessage(Message{
Type: ProcessMessage,
Data: Process{
Id: id,
Metadata: metadata,
},
}) })
return processInvokeMap.await(id), nil
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)
}
}
} }
} }
func handleGetAndroidVpnOptions() string {
tunLock.Lock()
defer tunLock.Unlock()
options := state.AndroidVpnOptions{
Enable: state.CurrentState.VpnProps.Enable,
Port: currentConfig.General.MixedPort,
Ipv4Address: state.DefaultIpv4Address,
Ipv6Address: state.GetIpv6Address(),
AccessControl: state.CurrentState.VpnProps.AccessControl,
SystemProxy: state.CurrentState.VpnProps.SystemProxy,
AllowBypass: state.CurrentState.VpnProps.AllowBypass,
RouteAddress: currentConfig.General.Tun.RouteAddress,
BypassDomain: state.CurrentState.BypassDomain,
DnsServerAddress: state.GetDnsServerAddress(),
}
data, err := json.Marshal(options)
if err != nil {
fmt.Println("Error:", err)
return ""
}
return string(data)
}
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, result func(data interface{})) bool {
switch action.Method {
case startTunMethod:
data := action.Data.(string)
var fd int
_ = json.Unmarshal([]byte(data), &fd)
result(handleStartTun(fd))
return true
case stopTunMethod:
handleStopTun()
result(true)
return true
case getAndroidVpnOptionsMethod:
result(handleGetAndroidVpnOptions())
return true
case updateDnsMethod:
data := action.Data.(string)
handleUpdateDns(data)
result(true)
return true
case setFdMapMethod:
fdId := action.Data.(string)
handleSetFdMap(fdId)
result(true)
return true
case setProcessMapMethod:
data := action.Data.(string)
handleSetProcessMap(data)
result(true)
return true
case getRunTimeMethod:
result(handleGetRunTime())
return true
case getCurrentProfileNameMethod:
result(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)
handleSetState(paramsString)
}
//export updateDns
func updateDns(s *C.char) {
dnsList := C.GoString(s)
handleUpdateDns(dnsList)
}
//export setProcessMap //export setProcessMap
func setProcessMap(s *C.char) { func setProcessMap(s *C.char) {
if s == nil { if s == nil {
return return
} }
paramsString := C.GoString(s) paramsString := C.GoString(s)
go func() { handleSetProcessMap(paramsString)
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 {
tunLock.Lock()
defer tunLock.Unlock()
options := state.AndroidVpnOptions{
Enable: state.CurrentState.Enable,
Port: currentConfig.General.MixedPort,
Ipv4Address: state.DefaultIpv4Address,
Ipv6Address: state.GetIpv6Address(),
AccessControl: state.CurrentState.AccessControl,
SystemProxy: state.CurrentState.SystemProxy,
AllowBypass: state.CurrentState.AllowBypass,
RouteAddress: state.CurrentState.RouteAddress,
BypassDomain: state.CurrentState.BypassDomain,
DnsServerAddress: state.GetDnsServerAddress(),
}
data, err := json.Marshal(options)
if err != nil {
fmt.Println("Error:", err)
return C.CString("")
}
return C.CString(string(data))
}
//export setState
func setState(s *C.char) {
paramsString := C.GoString(s)
err := json.Unmarshal([]byte(paramsString), state.CurrentState)
if err != nil {
return
}
}
//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()
}()
} }

7
core/lib_no_android.go Normal file
View File

@@ -0,0 +1,7 @@
//go:build !android && cgo
package main
func nextHandle(action *Action, result func(data interface{})) bool {
return false
}

View File

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

View File

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

View File

@@ -10,10 +10,29 @@ import (
"strconv" "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,
}.getResult(res))
}
func send(data []byte) {
if conn == nil {
return
}
_, _ = conn.Write(append(data, []byte("\n")...))
}
func startServer(arg string) { func startServer(arg string) {
_, err := strconv.Atoi(arg) _, err := strconv.Atoi(arg)
if err != nil { if err != nil {
conn, err = net.Dial("unix", arg) conn, err = net.Dial("unix", arg)
} else { } else {
@@ -42,132 +61,12 @@ func startServer(arg string) {
return return
} }
go handleAction(action) go handleAction(action, func(data interface{}) {
send(action.getResult(data))
})
} }
} }
func handleAction(action *Action) { func nextHandle(action *Action, result func(data interface{})) bool {
switch action.Method { return false
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), &params)
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), &params)
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
case getCountryCodeMethod:
ip := action.Data.(string)
handleGetCountryCode(ip, func(value string) {
action.callback(value)
})
return
case getMemoryMethod:
handleGetMemory(func(value string) {
action.callback(value)
})
return
}
} }

View File

@@ -1,7 +1,7 @@
//go:build android && cgo
package state package state
import "net/netip"
var DefaultIpv4Address = "172.19.0.1/30" var DefaultIpv4Address = "172.19.0.1/30"
var DefaultDnsAddress = "172.19.0.2" var DefaultDnsAddress = "172.19.0.2"
var DefaultIpv6Address = "fdfe:dcba:9876::1/126" var DefaultIpv6Address = "fdfe:dcba:9876::1/126"
@@ -13,13 +13,14 @@ type AndroidVpnOptions struct {
AllowBypass bool `json:"allowBypass"` AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"` SystemProxy bool `json:"systemProxy"`
BypassDomain []string `json:"bypassDomain"` BypassDomain []string `json:"bypassDomain"`
RouteAddress []string `json:"routeAddress"` RouteAddress []netip.Prefix `json:"routeAddress"`
Ipv4Address string `json:"ipv4Address"` Ipv4Address string `json:"ipv4Address"`
Ipv6Address string `json:"ipv6Address"` Ipv6Address string `json:"ipv6Address"`
DnsServerAddress string `json:"dnsServerAddress"` DnsServerAddress string `json:"dnsServerAddress"`
} }
type AccessControl struct { type AccessControl struct {
Enable bool `json:"enable"`
Mode string `json:"mode"` Mode string `json:"mode"`
AcceptList []string `json:"acceptList"` AcceptList []string `json:"acceptList"`
RejectList []string `json:"rejectList"` RejectList []string `json:"rejectList"`
@@ -31,20 +32,23 @@ type AndroidVpnRawOptions struct {
AccessControl *AccessControl `json:"accessControl"` AccessControl *AccessControl `json:"accessControl"`
AllowBypass bool `json:"allowBypass"` AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"` SystemProxy bool `json:"systemProxy"`
RouteAddress []string `json:"routeAddress"`
Ipv6 bool `json:"ipv6"` Ipv6 bool `json:"ipv6"`
BypassDomain []string `json:"bypassDomain"`
} }
type State struct { type State struct {
AndroidVpnRawOptions VpnProps AndroidVpnRawOptions `json:"vpn-props"`
CurrentProfileName string `json:"currentProfileName"` CurrentProfileName string `json:"current-profile-name"`
OnlyStatisticsProxy bool `json:"only-statistics-proxy"`
BypassDomain []string `json:"bypass-domain"`
} }
var CurrentState = &State{} var CurrentState = &State{
OnlyStatisticsProxy: false,
CurrentProfileName: "",
}
func GetIpv6Address() string { func GetIpv6Address() string {
if CurrentState.Ipv6 { if CurrentState.VpnProps.Ipv6 {
return DefaultIpv6Address return DefaultIpv6Address
} else { } else {
return "" return ""

View File

@@ -33,7 +33,7 @@ func Start(fd int, device string, stack constant.TUNStack) (*sing_tun.Listener,
} }
prefix4 = append(prefix4, tempPrefix4) prefix4 = append(prefix4, tempPrefix4)
var prefix6 []netip.Prefix var prefix6 []netip.Prefix
if state.CurrentState.Ipv6 { if state.CurrentState.VpnProps.Ipv6 {
tempPrefix6, err := netip.ParsePrefix(state.DefaultIpv6Address) tempPrefix6, err := netip.ParsePrefix(state.DefaultIpv6Address)
if err != nil { if err != nil {
log.Errorln("startTUN error:", err) log.Errorln("startTUN error:", err)

View File

@@ -7,57 +7,27 @@ import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/manager/hotkey_manager.dart'; import 'package:fl_clash/manager/hotkey_manager.dart';
import 'package:fl_clash/manager/manager.dart'; import 'package:fl_clash/manager/manager.dart';
import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'controller.dart'; import 'controller.dart';
import 'models/models.dart'; import 'models/models.dart';
import 'pages/pages.dart'; import 'pages/pages.dart';
runAppWithPreferences( class Application extends ConsumerStatefulWidget {
Widget child, {
required AppState appState,
required Config config,
required AppFlowingState appFlowingState,
required ClashConfig clashConfig,
}) {
runApp(MultiProvider(
providers: [
ChangeNotifierProvider<ClashConfig>(
create: (_) => clashConfig,
),
ChangeNotifierProvider<Config>(
create: (_) => config,
),
ChangeNotifierProvider<AppFlowingState>(
create: (_) => appFlowingState,
),
ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>(
create: (_) => appState,
update: (_, config, clashConfig, appState) {
appState?.mode = clashConfig.mode;
appState?.selectedMap = config.currentSelectedMap;
return appState!;
},
)
],
child: child,
));
}
class Application extends StatefulWidget {
const Application({ const Application({
super.key, super.key,
}); });
@override @override
State<Application> createState() => ApplicationState(); ConsumerState<Application> createState() => ApplicationState();
} }
class ApplicationState extends State<Application> { class ApplicationState extends ConsumerState<Application> {
late SystemColorSchemes systemColorSchemes; late ColorSchemes systemColorSchemes;
Timer? _autoUpdateGroupTaskTimer; Timer? _autoUpdateGroupTaskTimer;
Timer? _autoUpdateProfilesTaskTimer; Timer? _autoUpdateProfilesTaskTimer;
@@ -73,7 +43,7 @@ class ApplicationState extends State<Application> {
ColorScheme _getAppColorScheme({ ColorScheme _getAppColorScheme({
required Brightness brightness, required Brightness brightness,
int? primaryColor, int? primaryColor,
required SystemColorSchemes systemColorSchemes, required ColorSchemes systemColorSchemes,
}) { }) {
if (primaryColor != null) { if (primaryColor != null) {
return ColorScheme.fromSeed( return ColorScheme.fromSeed(
@@ -81,7 +51,7 @@ class ApplicationState extends State<Application> {
brightness: brightness, brightness: brightness,
); );
} else { } else {
return systemColorSchemes.getSystemColorSchemeForBrightness(brightness); return systemColorSchemes.getColorSchemeForBrightness(brightness);
} }
} }
@@ -90,12 +60,21 @@ class ApplicationState extends State<Application> {
super.initState(); super.initState();
_autoUpdateGroupTask(); _autoUpdateGroupTask();
_autoUpdateProfilesTask(); _autoUpdateProfilesTask();
globalState.appController = AppController(context); globalState.appController = AppController(context, ref);
globalState.measure = Measure.of(context); globalState.measure = Measure.of(context);
ref.listenManual(themeSettingProvider.select((state) => state.fontFamily),
(prev, next) {
if (prev != next) {
globalState.measure = Measure.of(
context,
fontFamily: next.value,
);
}
}, fireImmediately: true);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final currentContext = globalState.navigatorKey.currentContext; final currentContext = globalState.navigatorKey.currentContext;
if (currentContext != null) { if (currentContext != null) {
globalState.appController = AppController(currentContext); globalState.appController = AppController(currentContext, ref);
} }
await globalState.appController.init(); await globalState.appController.init();
globalState.appController.initLink(); globalState.appController.initLink();
@@ -153,7 +132,10 @@ class ApplicationState extends State<Application> {
return AppStateManager( return AppStateManager(
child: ClashManager( child: ClashManager(
child: ConnectivityManager( child: ConnectivityManager(
onConnectivityChanged: globalState.appController.updateLocalIp, onConnectivityChanged: () {
globalState.appController.updateLocalIp();
globalState.appController.addCheckIpNumDebounce();
},
child: child, child: child,
), ),
), ),
@@ -164,7 +146,7 @@ class ApplicationState extends State<Application> {
ColorScheme? lightDynamic, ColorScheme? lightDynamic,
ColorScheme? darkDynamic, ColorScheme? darkDynamic,
) { ) {
systemColorSchemes = SystemColorSchemes( systemColorSchemes = ColorSchemes(
lightColorScheme: lightDynamic, lightColorScheme: lightDynamic,
darkColorScheme: darkDynamic, darkColorScheme: darkDynamic,
); );
@@ -175,17 +157,13 @@ class ApplicationState extends State<Application> {
@override @override
Widget build(context) { Widget build(context) {
return _buildWrap( return _buildPlatformWrap(
_buildPlatformWrap( _buildWrap(
Selector2<AppState, Config, ApplicationSelectorState>( Consumer(
selector: (_, appState, config) => ApplicationSelectorState( builder: (_, ref, child) {
locale: config.appSetting.locale, final locale =
themeMode: config.themeProps.themeMode, ref.watch(appSettingProvider.select((state) => state.locale));
primaryColor: config.themeProps.primaryColor, final themeProps = ref.watch(themeSettingProvider);
prueBlack: config.themeProps.prueBlack,
fontFamily: config.themeProps.fontFamily,
),
builder: (_, state, child) {
return DynamicColorBuilder( return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) { builder: (lightDynamic, darkDynamic) {
_updateSystemColorSchemes(lightDynamic, darkDynamic); _updateSystemColorSchemes(lightDynamic, darkDynamic);
@@ -201,11 +179,9 @@ class ApplicationState extends State<Application> {
return MessageManager( return MessageManager(
child: LayoutBuilder( child: LayoutBuilder(
builder: (_, container) { builder: (_, container) {
final appController = globalState.appController; globalState.appController.updateViewWidth(
final maxWidth = container.maxWidth; container.maxWidth,
if (appController.appState.viewWidth != maxWidth) { );
globalState.appController.updateViewWidth(maxWidth);
}
return _buildPage(child!); return _buildPage(child!);
}, },
), ),
@@ -213,28 +189,28 @@ class ApplicationState extends State<Application> {
}, },
scrollBehavior: BaseScrollBehavior(), scrollBehavior: BaseScrollBehavior(),
title: appName, title: appName,
locale: other.getLocaleForString(state.locale), locale: other.getLocaleForString(locale),
supportedLocales: AppLocalizations.delegate.supportedLocales, supportedLocales: AppLocalizations.delegate.supportedLocales,
themeMode: state.themeMode, themeMode: themeProps.themeMode,
theme: ThemeData( theme: ThemeData(
useMaterial3: true, useMaterial3: true,
fontFamily: state.fontFamily.value, fontFamily: themeProps.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme, pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme( colorScheme: _getAppColorScheme(
brightness: Brightness.light, brightness: Brightness.light,
systemColorSchemes: systemColorSchemes, systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor, primaryColor: themeProps.primaryColor,
), ),
), ),
darkTheme: ThemeData( darkTheme: ThemeData(
useMaterial3: true, useMaterial3: true,
fontFamily: state.fontFamily.value, fontFamily: themeProps.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme, pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme( colorScheme: _getAppColorScheme(
brightness: Brightness.dark, brightness: Brightness.dark,
systemColorSchemes: systemColorSchemes, systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor, primaryColor: themeProps.primaryColor,
).toPrueBlack(state.prueBlack), ).toPrueBlack(themeProps.prueBlack),
), ),
home: child, home: child,
); );
@@ -252,7 +228,7 @@ class ApplicationState extends State<Application> {
linkManager.destroy(); linkManager.destroy();
_autoUpdateGroupTaskTimer?.cancel(); _autoUpdateGroupTaskTimer?.cancel();
_autoUpdateProfilesTaskTimer?.cancel(); _autoUpdateProfilesTaskTimer?.cancel();
await clashService?.destroy(); await clashCore.destroy();
await globalState.appController.savePreferences(); await globalState.appController.savePreferences();
await globalState.appController.handleExit(); await globalState.appController.handleExit();
super.dispose(); super.dispose();

View File

@@ -13,7 +13,7 @@ import 'package:path/path.dart';
class ClashCore { class ClashCore {
static ClashCore? _instance; static ClashCore? _instance;
late ClashInterface clashInterface; late ClashHandlerInterface clashInterface;
ClashCore._internal() { ClashCore._internal() {
if (Platform.isAndroid) { if (Platform.isAndroid) {
@@ -28,8 +28,12 @@ class ClashCore {
return _instance!; return _instance!;
} }
Future<void> _initGeo() async { Future<bool> preload() {
final homePath = await appPath.getHomeDirPath(); return clashInterface.preload();
}
static Future<void> initGeo() async {
final homePath = await appPath.homeDirPath;
final homeDir = Directory(homePath); final homeDir = Directory(homePath);
final isExists = await homeDir.exists(); final isExists = await homeDir.exists();
if (!isExists) { if (!isExists) {
@@ -59,15 +63,16 @@ class ClashCore {
} }
} }
Future<bool> init({ Future<bool> init() async {
required ClashConfig clashConfig, await initGeo();
required Config config, final homeDirPath = await appPath.homeDirPath;
}) async {
await _initGeo();
final homeDirPath = await appPath.getHomeDirPath();
return await clashInterface.init(homeDirPath); return await clashInterface.init(homeDirPath);
} }
Future<bool> setState(CoreState state) async {
return await clashInterface.setState(state);
}
shutdown() async { shutdown() async {
await clashInterface.shutdown(); await clashInterface.shutdown();
} }
@@ -135,6 +140,9 @@ class ClashCore {
Future<List<ExternalProvider>> getExternalProviders() async { Future<List<ExternalProvider>> getExternalProviders() async {
final externalProvidersRawString = final externalProvidersRawString =
await clashInterface.getExternalProviders(); await clashInterface.getExternalProviders();
if (externalProvidersRawString.isEmpty) {
return [];
}
return Isolate.run<List<ExternalProvider>>( return Isolate.run<List<ExternalProvider>>(
() { () {
final externalProviders = final externalProviders =
@@ -152,7 +160,7 @@ class ClashCore {
String externalProviderName) async { String externalProviderName) async {
final externalProvidersRawString = final externalProvidersRawString =
await clashInterface.getExternalProvider(externalProviderName); await clashInterface.getExternalProvider(externalProviderName);
if (externalProvidersRawString == null) { if (externalProvidersRawString.isEmpty) {
return null; return null;
} }
if (externalProvidersRawString.isEmpty) { if (externalProvidersRawString.isEmpty) {
@@ -161,11 +169,8 @@ class ClashCore {
return ExternalProvider.fromJson(json.decode(externalProvidersRawString)); return ExternalProvider.fromJson(json.decode(externalProvidersRawString));
} }
Future<String> updateGeoData({ Future<String> updateGeoData(UpdateGeoDataParams params) {
required String geoType, return clashInterface.updateGeoData(params);
required String geoName,
}) {
return clashInterface.updateGeoData(geoType: geoType, geoName: geoName);
} }
Future<String> sideLoadExternalProvider({ Future<String> sideLoadExternalProvider({
@@ -190,13 +195,16 @@ class ClashCore {
await clashInterface.stopListener(); await clashInterface.stopListener();
} }
Future<Delay> getDelay(String proxyName) async { Future<Delay> getDelay(String url, String proxyName) async {
final data = await clashInterface.asyncTestDelay(proxyName); final data = await clashInterface.asyncTestDelay(url, proxyName);
return Delay.fromJson(json.decode(data)); return Delay.fromJson(json.decode(data));
} }
Future<Traffic> getTraffic(bool value) async { Future<Traffic> getTraffic() async {
final trafficString = await clashInterface.getTraffic(value); final trafficString = await clashInterface.getTraffic();
if (trafficString.isEmpty) {
return Traffic();
}
return Traffic.fromMap(json.decode(trafficString)); return Traffic.fromMap(json.decode(trafficString));
} }
@@ -211,16 +219,30 @@ class ClashCore {
); );
} }
Future<Traffic> getTotalTraffic(bool value) async { Future<Traffic> getTotalTraffic() async {
final totalTrafficString = await clashInterface.getTotalTraffic(value); final totalTrafficString = await clashInterface.getTotalTraffic();
if (totalTrafficString.isEmpty) {
return Traffic();
}
return Traffic.fromMap(json.decode(totalTrafficString)); return Traffic.fromMap(json.decode(totalTrafficString));
} }
Future<int> getMemory() async { Future<int> getMemory() async {
final value = await clashInterface.getMemory(); final value = await clashInterface.getMemory();
if (value.isEmpty) {
return 0;
}
return int.parse(value); return int.parse(value);
} }
Future<ClashConfig?> getProfile(String id) async {
final res = await clashInterface.getProfile(id);
if (res.isEmpty) {
return null;
}
return ClashConfig.fromJson(json.decode(res));
}
resetTraffic() { resetTraffic() {
clashInterface.resetTraffic(); clashInterface.resetTraffic();
} }
@@ -236,6 +258,10 @@ class ClashCore {
requestGc() { requestGc() {
clashInterface.forceGc(); clashInterface.forceGc();
} }
destroy() async {
await clashInterface.destroy();
}
} }
final clashCore = ClashCore(); final clashCore = ClashCore();

View File

@@ -2362,18 +2362,39 @@ class ClashFFI {
late final _initNativeApiBridge = _initNativeApiBridgePtr late final _initNativeApiBridge = _initNativeApiBridgePtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>(); .asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void initMessage( void attachMessagePort(
int port, int mPort,
) { ) {
return _initMessage( return _attachMessagePort(
port, mPort,
); );
} }
late final _initMessagePtr = late final _attachMessagePortPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>( _lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
'initMessage'); 'attachMessagePort');
late final _initMessage = _initMessagePtr.asFunction<void Function(int)>(); 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( void freeCString(
ffi.Pointer<ffi.Char> s, ffi.Pointer<ffi.Char> s,
@@ -2389,19 +2410,22 @@ class ClashFFI {
late final _freeCString = late final _freeCString =
_freeCStringPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>(); _freeCStringPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
int initClash( void invokeAction(
ffi.Pointer<ffi.Char> homeDirStr, ffi.Pointer<ffi.Char> paramsChar,
int port,
) { ) {
return _initClash( return _invokeAction(
homeDirStr, paramsChar,
port,
); );
} }
late final _initClashPtr = late final _invokeActionPtr = _lookup<
_lookup<ffi.NativeFunction<GoUint8 Function(ffi.Pointer<ffi.Char>)>>( ffi.NativeFunction<
'initClash'); ffi.Void Function(
late final _initClash = ffi.Pointer<ffi.Char>, ffi.LongLong)>>('invokeAction');
_initClashPtr.asFunction<int Function(ffi.Pointer<ffi.Char>)>(); late final _invokeAction =
_invokeActionPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
void startListener() { void startListener() {
return _startListener(); return _startListener();
@@ -2419,317 +2443,55 @@ class ClashFFI {
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopListener'); _lookup<ffi.NativeFunction<ffi.Void Function()>>('stopListener');
late final _stopListener = _stopListenerPtr.asFunction<void Function()>(); late final _stopListener = _stopListenerPtr.asFunction<void Function()>();
int getIsInit() { void attachInvokePort(
return _getIsInit(); int mPort,
) {
return _attachInvokePort(
mPort,
);
} }
late final _getIsInitPtr = late final _attachInvokePortPtr =
_lookup<ffi.NativeFunction<GoUint8 Function()>>('getIsInit'); _lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
late final _getIsInit = _getIsInitPtr.asFunction<int Function()>(); 'attachInvokePort');
late final _attachInvokePort =
_attachInvokePortPtr.asFunction<void Function(int)>();
int shutdownClash() { void quickStart(
return _shutdownClash(); ffi.Pointer<ffi.Char> dirChar,
} ffi.Pointer<ffi.Char> paramsChar,
ffi.Pointer<ffi.Char> stateParamsChar,
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,
int port, int port,
) { ) {
return _validateConfig( return _quickStart(
s, dirChar,
paramsChar,
stateParamsChar,
port, port,
); );
} }
late final _validateConfigPtr = _lookup< late final _quickStartPtr = _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 getMemory(
int port,
) {
return _getMemory(
port,
);
}
late final _getMemoryPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>('getMemory');
late final _getMemory = _getMemoryPtr.asFunction<void Function(int)>();
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<
ffi.NativeFunction< ffi.NativeFunction<
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.LongLong)>>('updateGeoData'); ffi.Pointer<ffi.Char>, ffi.LongLong)>>('quickStart');
late final _updateGeoData = _updateGeoDataPtr.asFunction< late final _quickStart = _quickStartPtr.asFunction<
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>(); void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>, int)>();
void updateExternalProvider( ffi.Pointer<ffi.Char> startTUN(
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 getCountryCode(
ffi.Pointer<ffi.Char> ipChar,
int port,
) {
return _getCountryCode(
ipChar,
port,
);
}
late final _getCountryCodePtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('getCountryCode');
late final _getCountryCode = _getCountryCodePtr
.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(
int fd, int fd,
int port,
) { ) {
return _startTUN( return _startTUN(
fd, fd,
port,
); );
} }
late final _startTUNPtr = late final _startTUNPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int, ffi.LongLong)>>( _lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
'startTUN'); '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() { ffi.Pointer<ffi.Char> getRunTime() {
return _getRunTime(); return _getRunTime();
@@ -2750,30 +2512,18 @@ class ClashFFI {
late final _stopTun = _stopTunPtr.asFunction<void Function()>(); late final _stopTun = _stopTunPtr.asFunction<void Function()>();
void setFdMap( void setFdMap(
int fd, ffi.Pointer<ffi.Char> fdIdChar,
) { ) {
return _setFdMap( return _setFdMap(
fd, fdIdChar,
); );
} }
late final _setFdMapPtr = 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>)>>( _lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'setProcessMap'); 'setFdMap');
late final _setProcessMap = late final _setFdMap =
_setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>(); _setFdMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> getCurrentProfileName() { ffi.Pointer<ffi.Char> getCurrentProfileName() {
return _getCurrentProfileName(); return _getCurrentProfileName();
@@ -2822,6 +2572,20 @@ class ClashFFI {
'updateDns'); 'updateDns');
late final _updateDns = late final _updateDns =
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>(); _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 { final class __mbstate_t extends ffi.Union {
@@ -3994,8 +3758,6 @@ final class GoSlice extends ffi.Struct {
typedef GoInt = GoInt64; typedef GoInt = GoInt64;
typedef GoInt64 = ffi.LongLong; typedef GoInt64 = ffi.LongLong;
typedef DartGoInt64 = int; typedef DartGoInt64 = int;
typedef GoUint8 = ffi.UnsignedChar;
typedef DartGoUint8 = int;
const int __has_safe_buffers = 1; const int __has_safe_buffers = 1;

View File

@@ -1,19 +1,25 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:fl_clash/clash/message.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
mixin ClashInterface { 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); FutureOr<String> validateConfig(String data);
Future<String> asyncTestDelay(String proxyName); Future<String> asyncTestDelay(String url, String proxyName);
FutureOr<String> updateConfig(UpdateConfigParams updateConfigParams); FutureOr<String> updateConfig(UpdateConfigParams updateConfigParams);
@@ -29,10 +35,7 @@ mixin ClashInterface {
FutureOr<String>? getExternalProvider(String externalProviderName); FutureOr<String>? getExternalProvider(String externalProviderName);
Future<String> updateGeoData({ Future<String> updateGeoData(UpdateGeoDataParams params);
required String geoType,
required String geoName,
});
Future<String> sideLoadExternalProvider({ Future<String> sideLoadExternalProvider({
required String providerName, required String providerName,
@@ -41,9 +44,9 @@ mixin ClashInterface {
Future<String> updateExternalProvider(String providerName); 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> getCountryCode(String ip);
@@ -60,4 +63,361 @@ mixin ClashInterface {
FutureOr<bool> closeConnection(String id); FutureOr<bool> closeConnection(String id);
FutureOr<bool> closeConnections(); FutureOr<bool> closeConnections();
FutureOr<String> getProfile(String id);
Future<bool> setState(CoreState state);
}
mixin AndroidClashInterface {
Future<bool> setFdMap(int fd);
Future<bool> setProcessMap(ProcessMapItem item);
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:
case ActionMethod.setState:
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 (_) {
commonPrint.log(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
Future<bool> setState(CoreState state) {
return invoke<bool>(
method: ActionMethod.setState,
data: json.encode(state),
);
}
@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),
timeout: Duration(minutes: 2),
);
}
@override
Future<String> getProxies() {
return invoke<String>(
method: ActionMethod.getProxies,
timeout: Duration(seconds: 5),
);
}
@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
Future<String> getProfile(String id) {
return invoke<String>(
method: ActionMethod.getProfile,
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,
);
}
} }

View File

@@ -3,28 +3,58 @@ import 'dart:convert';
import 'dart:ffi'; import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'dart:ui';
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import 'package:fl_clash/common/constant.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/models/models.dart';
import 'package:fl_clash/plugins/service.dart';
import 'package:fl_clash/state.dart';
import 'generated/clash_ffi.dart'; import 'generated/clash_ffi.dart';
import 'interface.dart'; import 'interface.dart';
class ClashLib with ClashInterface { class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
static ClashLib? _instance; static ClashLib? _instance;
final receiver = ReceivePort(); Completer<bool> _canSendCompleter = Completer();
SendPort? sendPort;
late final ClashFFI clashFFI; final receiverPort = ReceivePort();
late final DynamicLibrary lib;
ClashLib._internal() { ClashLib._internal() {
lib = DynamicLibrary.open("libclash.so"); _initService();
clashFFI = ClashFFI(lib); }
clashFFI.initNativeApiBridge(
NativeApi.initializeApiDLData, @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() { factory ClashLib() {
@@ -32,227 +62,145 @@ class ClashLib with ClashInterface {
return _instance!; return _instance!;
} }
initMessage() { @override
clashFFI.initMessage( Future<bool> nextHandleResult(result, completer) async {
receiver.sendPort.nativePort, switch (result.method) {
); case ActionMethod.setFdMap:
case ActionMethod.setProcessMap:
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 @override
bool init(String homeDir) { destroy() async {
final homeDirChar = homeDir.toNativeUtf8().cast<Char>(); await service?.destroy();
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);
return true; return true;
} }
@override @override
closeConnections() { reStart() {
clashFFI.closeConnections(); _initService();
}
@override
Future<bool> shutdown() async {
await super.shutdown();
destroy();
return true; return true;
} }
@override @override
startListener() async { sendMessage(String message) async {
clashFFI.startListener(); await _canSendCompleter.future;
return true; sendPort?.send(message);
} }
@override @override
stopListener() async { Future<bool> setFdMap(int fd) {
clashFFI.stopListener(); return invoke<bool>(
return true; method: ActionMethod.setFdMap,
data: json.encode(fd),
);
} }
@override @override
Future<String> asyncTestDelay(String proxyName) { Future<bool> setProcessMap(item) {
final delayParams = { return invoke<bool>(
"proxy-name": proxyName, method: ActionMethod.setProcessMap,
"timeout": httpTimeoutDuration.inMilliseconds, data: item,
}; );
}
@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 completer = Completer<String>();
final receiver = ReceivePort(); final receiver = ReceivePort();
receiver.listen((message) { receiver.listen((message) {
@@ -261,89 +209,33 @@ class ClashLib with ClashInterface {
receiver.close(); receiver.close();
} }
}); });
final delayParamsChar = final actionParamsChar = actionParams.toNativeUtf8().cast<Char>();
json.encode(delayParams).toNativeUtf8().cast<Char>(); clashFFI.invokeAction(
clashFFI.asyncTestDelay( actionParamsChar,
delayParamsChar,
receiver.sendPort.nativePort, receiver.sendPort.nativePort,
); );
malloc.free(delayParamsChar); malloc.free(actionParamsChar);
return completer.future; return completer.future;
} }
@override attachMessagePort(int messagePort) {
String getTraffic(bool value) { clashFFI.attachMessagePort(
final trafficRaw = clashFFI.getTraffic(value ? 1 : 0); messagePort,
final trafficString = trafficRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(trafficRaw);
return trafficString;
}
@override
String getTotalTraffic(bool value) {
final trafficRaw = clashFFI.getTotalTraffic(value ? 1 : 0);
clashFFI.freeCString(trafficRaw);
return trafficRaw.cast<Utf8>().toDartString();
}
@override
void resetTraffic() {
clashFFI.resetTraffic();
}
@override
void startLog() {
clashFFI.startLog();
}
@override
stopLog() {
clashFFI.stopLog();
}
@override
forceGc() {
clashFFI.forceGc();
}
@override
FutureOr<String> getCountryCode(String ip) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final ipChar = ip.toNativeUtf8().cast<Char>();
clashFFI.getCountryCode(
ipChar,
receiver.sendPort.nativePort,
); );
malloc.free(ipChar);
return completer.future;
} }
@override attachInvokePort(int invokePort) {
FutureOr<String> getMemory() { clashFFI.attachInvokePort(
final completer = Completer<String>(); invokePort,
final receiver = ReceivePort(); );
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
clashFFI.getMemory(receiver.sendPort.nativePort);
return completer.future;
} }
/// Android DateTime? startTun(int fd) {
final runTimeRaw = clashFFI.startTUN(fd);
startTun(int fd, int port) { final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
if (!Platform.isAndroid) return; clashFFI.freeCString(runTimeRaw);
clashFFI.startTUN(fd, port); if (runTimeString.isEmpty) return null;
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
} }
stopTun() { stopTun() {
@@ -351,7 +243,6 @@ class ClashLib with ClashInterface {
} }
updateDns(String dns) { updateDns(String dns) {
if (!Platform.isAndroid) return;
final dnsChar = dns.toNativeUtf8().cast<Char>(); final dnsChar = dns.toNativeUtf8().cast<Char>();
clashFFI.updateDns(dnsChar); clashFFI.updateDns(dnsChar);
malloc.free(dnsChar); malloc.free(dnsChar);
@@ -359,7 +250,7 @@ class ClashLib with ClashInterface {
setProcessMap(ProcessMapItem processMapItem) { setProcessMap(ProcessMapItem processMapItem) {
final processMapItemChar = final processMapItemChar =
json.encode(processMapItem).toNativeUtf8().cast<Char>(); json.encode(processMapItem).toNativeUtf8().cast<Char>();
clashFFI.setProcessMap(processMapItemChar); clashFFI.setProcessMap(processMapItemChar);
malloc.free(processMapItemChar); malloc.free(processMapItemChar);
} }
@@ -384,8 +275,70 @@ class ClashLib with ClashInterface {
return AndroidVpnOptions.fromJson(vpnOptions); return AndroidVpnOptions.fromJson(vpnOptions);
} }
setFdMap(int fd) { Traffic getTraffic() {
clashFFI.setFdMap(fd); 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() { DateTime? getRunTime() {
@@ -397,4 +350,5 @@ class ClashLib with ClashInterface {
} }
} }
final clashLib = Platform.isAndroid ? ClashLib() : null; ClashLib? get clashLib =>
Platform.isAndroid && !globalState.isService ? ClashLib() : null;

View File

@@ -1,18 +1,19 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
class ClashMessage { class ClashMessage {
final controller = StreamController(); final controller = StreamController<String>();
ClashMessage._() { ClashMessage._() {
clashLib?.receiver.listen(controller.add);
controller.stream.listen( controller.stream.listen(
(message) { (message) {
if(message.isEmpty){
return;
}
final m = AppMessage.fromJson(json.decode(message)); final m = AppMessage.fromJson(json.decode(message));
for (final AppMessageListener listener in _listeners) { for (final AppMessageListener listener in _listeners) {
switch (m.type) { switch (m.type) {
@@ -25,9 +26,6 @@ class ClashMessage {
case AppMessageType.request: case AppMessageType.request:
listener.onRequest(Connection.fromJson(m.data)); listener.onRequest(Connection.fromJson(m.data));
break; break;
case AppMessageType.started:
listener.onStarted(m.data);
break;
case AppMessageType.loaded: case AppMessageType.loaded:
listener.onLoaded(m.data); listener.onLoaded(m.data);
break; break;

View File

@@ -3,20 +3,19 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/clash/interface.dart'; import 'package:fl_clash/clash/interface.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/core.dart'; import 'package:fl_clash/models/core.dart';
import 'package:fl_clash/state.dart';
class ClashService with ClashInterface { class ClashService extends ClashHandlerInterface {
static ClashService? _instance; static ClashService? _instance;
Completer<ServerSocket> serverCompleter = Completer(); Completer<ServerSocket> serverCompleter = Completer();
Completer<Socket> socketCompleter = Completer(); Completer<Socket> socketCompleter = Completer();
Map<String, Completer> callbackCompleterMap = {}; bool isStarting = false;
Process? process; Process? process;
@@ -26,52 +25,66 @@ class ClashService with ClashInterface {
} }
ClashService._internal() { ClashService._internal() {
_createServer(); _initServer();
startCore(); reStart();
} }
_createServer() async { _initServer() async {
final address = !Platform.isWindows runZonedGuarded(() async {
? InternetAddress( final address = !Platform.isWindows
unixSocketPath, ? InternetAddress(
type: InternetAddressType.unix, unixSocketPath,
) type: InternetAddressType.unix,
: InternetAddress( )
localhost, : InternetAddress(
type: InternetAddressType.IPv4, localhost,
); type: InternetAddressType.IPv4,
await _deleteSocketFile(); );
final server = await ServerSocket.bind( await _deleteSocketFile();
address, final server = await ServerSocket.bind(
0, address,
shared: true, 0,
); shared: true,
serverCompleter.complete(server); );
await for (final socket in server) { serverCompleter.complete(server);
await _destroySocket(); await for (final socket in server) {
socketCompleter.complete(socket); await _destroySocket();
socket socketCompleter.complete(socket);
.transform( socket
StreamTransformer<Uint8List, String>.fromHandlers( .transform(
handleData: (Uint8List data, EventSink<String> sink) { StreamTransformer<Uint8List, String>.fromHandlers(
sink.add(utf8.decode(data, allowMalformed: true)); handleData: (Uint8List data, EventSink<String> sink) {
sink.add(utf8.decode(data, allowMalformed: true));
},
),
)
.transform(LineSplitter())
.listen(
(data) {
handleResult(
ActionResult.fromJson(
json.decode(data.trim()),
),
);
}, },
), );
) }
.transform(LineSplitter()) }, (error, stack) {
.listen( commonPrint.log(error.toString());
(data) { if(error is SocketException){
_handleAction( globalState.showNotifier(error.toString());
Action.fromJson( globalState.appController.restartCore();
json.decode(data.trim()), }
), });
);
},
);
}
} }
startCore() async { @override
reStart() async {
if (isStarting == true) {
return;
}
isStarting = true;
socketCompleter = Completer();
if (process != null) { if (process != null) {
await shutdown(); await shutdown();
} }
@@ -93,6 +106,21 @@ class ClashService with ClashInterface {
], ],
); );
process!.stdout.listen((_) {}); process!.stdout.listen((_) {});
isStarting = false;
}
@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 { _deleteSocketFile() async {
@@ -112,327 +140,21 @@ 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:
case ActionMethod.getCountryCode:
case ActionMethod.getMemory:
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:
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 ??
() {
if (T is String) {
return "" as T;
}
if (T is bool) {
return false as T;
}
return null as T;
},
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 @override
shutdown() async { shutdown() async {
await _invoke<bool>(
method: ActionMethod.shutdown,
);
if (Platform.isWindows) { if (Platform.isWindows) {
await request.stopCoreByHelper(); await request.stopCoreByHelper();
} }
await _destroySocket(); await _destroySocket();
process?.kill(); process?.kill();
process = null; process = null;
return true;
} }
@override @override
Future<bool> get isInit { Future<bool> preload() async {
return _invoke<bool>( await serverCompleter.future;
method: ActionMethod.getIsInit, return true;
);
}
@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();
}
@override
FutureOr<String> getCountryCode(String ip) {
return _invoke<String>(
method: ActionMethod.getCountryCode,
data: ip,
);
}
@override
FutureOr<String> getMemory() {
return _invoke<String>(
method: ActionMethod.getMemory,
);
} }
} }

View File

@@ -11,7 +11,7 @@ extension ColorExtension on Color {
} }
Color get toSoft { Color get toSoft {
return withOpacity(0.12); return withOpacity(0.15);
} }
Color get toLittle { Color get toLittle {

View File

@@ -34,3 +34,6 @@ export 'text.dart';
export 'tray.dart'; export 'tray.dart';
export 'window.dart'; export 'window.dart';
export 'windows.dart'; export 'windows.dart';
export 'render.dart';
export 'mixin.dart';
export 'print.dart';

View File

@@ -11,6 +11,8 @@ import 'package:flutter/material.dart';
const appName = "FlClash"; const appName = "FlClash";
const appHelperService = "FlClashHelperService"; const appHelperService = "FlClashHelperService";
const coreName = "clash.meta"; const coreName = "clash.meta";
const browserUa =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
const packageName = "com.follow.clash"; const packageName = "com.follow.clash";
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock"; final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
const helperPort = 47890; const helperPort = 47890;
@@ -33,16 +35,6 @@ final double kHeaderHeight = system.isDesktop
? 40 ? 40
: 28 : 28
: 0; : 0;
const GeoXMap defaultGeoXMap = {
"mmdb":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
"asn":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb",
"geoip":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat",
"geosite":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
};
const profilesDirectoryName = "profiles"; const profilesDirectoryName = "profiles";
const localhost = "127.0.0.1"; const localhost = "127.0.0.1";
const clashConfigKey = "clash_config"; const clashConfigKey = "clash_config";
@@ -53,8 +45,6 @@ const repository = "chen08209/FlClash";
const defaultExternalController = "127.0.0.1:9090"; const defaultExternalController = "127.0.0.1:9090";
const maxMobileWidth = 600; const maxMobileWidth = 600;
const maxLaptopWidth = 840; const maxLaptopWidth = 840;
const geodataLoaderMemconservative = "memconservative";
const geodataLoaderStandard = "standard";
const defaultTestUrl = "https://www.gstatic.com/generate_204"; const defaultTestUrl = "https://www.gstatic.com/generate_204";
final filter = ImageFilter.blur( final filter = ImageFilter.blur(
sigmaX: 5, sigmaX: 5,
@@ -73,7 +63,7 @@ const hotKeyActionListEquality = ListEquality<HotKeyAction>();
const stringAndStringMapEquality = MapEquality<String, String>(); const stringAndStringMapEquality = MapEquality<String, String>();
const stringAndStringMapEntryIterableEquality = const stringAndStringMapEntryIterableEquality =
IterableEquality<MapEntry<String, String>>(); IterableEquality<MapEntry<String, String>>();
const stringAndIntQMapEquality = MapEquality<String, int?>(); const delayMapEquality = MapEquality<String, Map<String, int?>>();
const stringSetEquality = SetEquality<String>(); const stringSetEquality = SetEquality<String>();
const keyboardModifierListEquality = SetEquality<KeyboardModifier>(); const keyboardModifierListEquality = SetEquality<KeyboardModifier>();
@@ -88,3 +78,7 @@ const defaultPrimaryColor = Colors.brown;
double getWidgetHeight(num lines) { double getWidgetHeight(num lines) {
return max(lines * 84 + (lines - 1) * 16, 0); return max(lines * 84 + (lines - 1) * 16, 0);
} }
final mainIsolate = "FlClashMainIsolate";
final serviceIsolate = "FlClashServiceIsolate";

View File

@@ -22,4 +22,23 @@ extension BuildContextExtension on BuildContext {
ColorScheme get colorScheme => Theme.of(this).colorScheme; ColorScheme get colorScheme => Theme.of(this).colorScheme;
TextTheme get textTheme => Theme.of(this).textTheme; 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;
}
} }

View File

@@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
class Debouncer { class Debouncer {
Map<dynamic, Timer> operators = {}; final Map<dynamic, Timer> _operations = {};
call( call(
dynamic tag, dynamic tag,
@@ -9,14 +9,15 @@ class Debouncer {
List<dynamic>? args, List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600), Duration duration = const Duration(milliseconds: 600),
}) { }) {
final timer = operators[tag]; final timer = _operations[tag];
if (timer != null) { if (timer != null) {
timer.cancel(); timer.cancel();
} }
operators[tag] = Timer( _operations[tag] = Timer(
duration, duration,
() { () {
operators.remove(tag); _operations[tag]?.cancel();
_operations.remove(tag);
Function.apply( Function.apply(
func, func,
args, args,
@@ -26,8 +27,59 @@ class Debouncer {
} }
cancel(dynamic tag) { cancel(dynamic tag) {
operators[tag]?.cancel(); _operations[tag]?.cancel();
} }
} }
class Throttler {
final Map<dynamic, Timer> _operations = {};
call(
String tag,
Function func, {
List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600),
}) {
final timer = _operations[tag];
if (timer != null) {
return true;
}
_operations[tag] = Timer(
duration,
() {
_operations[tag]?.cancel();
_operations.remove(tag);
Function.apply(
func,
args,
);
},
);
return false;
}
cancel(dynamic tag) {
_operations[tag]?.cancel();
}
}
Future<T> retry<T>({
required Future<T> Function() task,
int maxAttempts = 3,
required bool Function(T res) retryIf,
Duration delay = Duration.zero,
}) async {
int attempts = 0;
while (attempts < maxAttempts) {
final res = await task();
if (!retryIf(res) || attempts >= maxAttempts) {
return res;
}
attempts++;
}
throw "unknown error";
}
final debouncer = Debouncer(); final debouncer = Debouncer();
final throttler = Throttler();

View File

@@ -10,8 +10,8 @@ extension CompleterExt<T> on Completer<T> {
FutureOr<T> Function()? onTimeout, FutureOr<T> Function()? onTimeout,
required String functionName, required String functionName,
}) { }) {
final realTimeout = timeout ?? const Duration(minutes: 1); final realTimeout = timeout ?? const Duration(seconds: 30);
Timer(realTimeout + moreDuration, () { Timer(realTimeout + commonDuration, () {
if (onLast != null) { if (onLast != null) {
onLast(); onLast();
} }

View File

@@ -1,9 +1,8 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:fl_clash/common/common.dart';
import '../state.dart'; import '../state.dart';
import 'constant.dart';
class FlClashHttpOverrides extends HttpOverrides { class FlClashHttpOverrides extends HttpOverrides {
@override @override
@@ -14,10 +13,9 @@ class FlClashHttpOverrides extends HttpOverrides {
if ([localhost].contains(url.host)) { if ([localhost].contains(url.host)) {
return "DIRECT"; return "DIRECT";
} }
final appController = globalState.appController; final port = globalState.config.patchClashConfig.mixedPort;
final port = appController.clashConfig.mixedPort; final isStart = globalState.appState.runTime != null;
final isStart = appController.appFlowingState.isStart; commonPrint.log("find $url proxy:$isStart");
debugPrint("find $url proxy:$isStart");
if (!isStart) return "DIRECT"; if (!isStart) return "DIRECT";
return "PROXY localhost:$port"; return "PROXY localhost:$port";
}; };

View File

@@ -1,7 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:app_links/app_links.dart'; import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart';
import 'print.dart';
typedef InstallConfigCallBack = void Function(String url); typedef InstallConfigCallBack = void Function(String url);
@@ -15,11 +16,11 @@ class LinkManager {
} }
initAppLinksListen(installConfigCallBack) async { initAppLinksListen(installConfigCallBack) async {
debugPrint("initAppLinksListen"); commonPrint.log("initAppLinksListen");
destroy(); destroy();
subscription = _appLinks.uriLinkStream.listen( subscription = _appLinks.uriLinkStream.listen(
(uri) { (uri) {
debugPrint('onAppLink: $uri'); commonPrint.log('onAppLink: $uri');
if (uri.host == 'install-config') { if (uri.host == 'install-config') {
final parameters = uri.queryParameters; final parameters = uri.queryParameters;
final url = parameters['url']; final url = parameters['url'];

View File

@@ -1,3 +1,66 @@
import 'dart:collection';
class FixedList<T> {
final int maxLength;
final List<T> _list;
FixedList(this.maxLength, {List<T>? list}) : _list = list ?? [];
add(T item) {
if (_list.length == maxLength) {
_list.removeAt(0);
}
_list.add(item);
}
clear() {
_list.clear();
}
List<T> get list => List.unmodifiable(_list);
int get length => _list.length;
T operator [](int index) => _list[index];
FixedList<T> copyWith() {
return FixedList(
maxLength,
list: _list,
);
}
}
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> { extension ListExtension<T> on List<T> {
List<T> intersection(List<T> list) { List<T> intersection(List<T> list) {
return where((item) => list.contains(item)).toList(); return where((item) => list.contains(item)).toList();
@@ -17,8 +80,8 @@ extension ListExtension<T> on List<T> {
} }
List<T> safeSublist(int start) { List<T> safeSublist(int start) {
if(start <= 0) return this; if (start <= 0) return this;
if(start > length) return []; if (start > length) return [];
return sublist(start); return sublist(start);
} }
} }

View File

@@ -15,7 +15,7 @@ class SingleInstanceLock {
Future<bool> acquire() async { Future<bool> acquire() async {
try { try {
final lockFilePath = await appPath.getLockFilePath(); final lockFilePath = await appPath.lockFilePath;
final lockFile = File(lockFilePath); final lockFile = File(lockFilePath);
await lockFile.create(); await lockFile.create();
_accessFile = await lockFile.open(mode: FileMode.write); _accessFile = await lockFile.open(mode: FileMode.write);

View File

@@ -4,20 +4,32 @@ import 'package:flutter/material.dart';
class Measure { class Measure {
final TextScaler _textScale; final TextScaler _textScale;
late BuildContext context; final BuildContext context;
final String? _fontFamily;
Measure.of(this.context) Measure.of(this.context, {String? fontFamily})
: _textScale = TextScaler.linear( : _textScale = TextScaler.linear(
WidgetsBinding.instance.platformDispatcher.textScaleFactor, WidgetsBinding.instance.platformDispatcher.textScaleFactor,
); ),
_fontFamily = fontFamily ?? "";
Size computeTextSize(Text text) { Size computeTextSize(
Text text, {
double maxWidth = double.infinity,
}) {
final textPainter = TextPainter( final textPainter = TextPainter(
text: TextSpan(text: text.data, style: text.style), text: TextSpan(
text: text.data,
style: text.style?.copyWith(
fontFamily: _fontFamily,
),
),
maxLines: text.maxLines, maxLines: text.maxLines,
textScaler: _textScale, textScaler: _textScale,
textDirection: text.textDirection ?? TextDirection.ltr, textDirection: text.textDirection ?? TextDirection.ltr,
)..layout(); )..layout(
maxWidth: maxWidth,
);
return textPainter.size; return textPainter.size;
} }

46
lib/common/mixin.dart Normal file
View File

@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:riverpod/riverpod.dart';
import 'context.dart';
mixin AutoDisposeNotifierMixin<T> on AutoDisposeNotifier<T> {
set value(T value) {
state = value;
}
@override
bool updateShouldNotify(previous, next) {
final res = super.updateShouldNotify(previous, next);
if (res) {
onUpdate(next);
}
return res;
}
onUpdate(T value) {}
}
mixin PageMixin<T extends StatefulWidget> on State<T> {
void onPageShow() {
initPageState();
}
initPageState() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState = context.commonScaffoldState;
commonScaffoldState?.actions = actions;
commonScaffoldState?.floatingActionButton = floatingActionButton;
commonScaffoldState?.onSearch = onSearch;
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
});
}
void onPageHidden() {}
List<Widget> get actions => [];
Widget? get floatingActionButton => null;
Function(String)? get onSearch => null;
Function(List<String>)? get onKeywordsUpdate => null;
}

View File

@@ -6,55 +6,81 @@ import 'package:flutter/material.dart';
class Navigation { class Navigation {
static Navigation? _instance; static Navigation? _instance;
getItems({ List<NavigationItem> getItems({
bool openLogs = false, bool openLogs = false,
bool hasProxies = false, bool hasProxies = false,
}) { }) {
return [ return [
const NavigationItem( const NavigationItem(
icon: Icon(Icons.space_dashboard), icon: Icon(Icons.space_dashboard),
label: "dashboard", label: PageLabel.dashboard,
fragment: DashboardFragment(), fragment: DashboardFragment(
key: GlobalObjectKey(PageLabel.dashboard),
),
), ),
NavigationItem( NavigationItem(
icon: const Icon(Icons.rocket), icon: const Icon(Icons.article),
label: "proxies", label: PageLabel.proxies,
fragment: const ProxiesFragment(), fragment: const ProxiesFragment(
key: GlobalObjectKey(
PageLabel.proxies,
),
),
modes: hasProxies modes: hasProxies
? [NavigationItemMode.mobile, NavigationItemMode.desktop] ? [NavigationItemMode.mobile, NavigationItemMode.desktop]
: [], : [],
), ),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.folder), icon: Icon(Icons.folder),
label: "profiles", label: PageLabel.profiles,
fragment: ProfilesFragment(), fragment: ProfilesFragment(
key: GlobalObjectKey(
PageLabel.profiles,
),
),
), ),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.view_timeline), icon: Icon(Icons.view_timeline),
label: "requests", label: PageLabel.requests,
fragment: RequestsFragment(), fragment: RequestsFragment(
key: GlobalObjectKey(
PageLabel.requests,
),
),
description: "requestsDesc", description: "requestsDesc",
modes: [NavigationItemMode.desktop, NavigationItemMode.more], modes: [NavigationItemMode.desktop, NavigationItemMode.more],
), ),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.ballot), icon: Icon(Icons.ballot),
label: "connections", label: PageLabel.connections,
fragment: ConnectionsFragment(), fragment: ConnectionsFragment(
key: GlobalObjectKey(
PageLabel.connections,
),
),
description: "connectionsDesc", description: "connectionsDesc",
modes: [NavigationItemMode.desktop, NavigationItemMode.more], modes: [NavigationItemMode.desktop, NavigationItemMode.more],
), ),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.storage), icon: Icon(Icons.storage),
label: "resources", label: PageLabel.resources,
description: "resourcesDesc", description: "resourcesDesc",
keep: false, keep: false,
fragment: Resources(), fragment: Resources(
modes: [NavigationItemMode.desktop, NavigationItemMode.more], key: GlobalObjectKey(
PageLabel.resources,
),
),
modes: [NavigationItemMode.more],
), ),
NavigationItem( NavigationItem(
icon: const Icon(Icons.adb), icon: const Icon(Icons.adb),
label: "logs", label: PageLabel.logs,
fragment: const LogsFragment(), fragment: const LogsFragment(
key: GlobalObjectKey(
PageLabel.logs,
),
),
description: "logsDesc", description: "logsDesc",
modes: openLogs modes: openLogs
? [NavigationItemMode.desktop, NavigationItemMode.more] ? [NavigationItemMode.desktop, NavigationItemMode.more]
@@ -62,8 +88,12 @@ class Navigation {
), ),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.construction), icon: Icon(Icons.construction),
label: "tools", label: PageLabel.tools,
fragment: ToolsFragment(), fragment: ToolsFragment(
key: GlobalObjectKey(
PageLabel.tools,
),
),
modes: [NavigationItemMode.desktop, NavigationItemMode.mobile], modes: [NavigationItemMode.desktop, NavigationItemMode.mobile],
), ),
]; ];

View File

@@ -1,8 +1,18 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class BaseNavigator { class BaseNavigator {
static Future<T?> push<T>(BuildContext context, Widget child) async { static Future<T?> push<T>(BuildContext context, Widget child) async {
if (globalState.appState.viewMode != ViewMode.mobile) {
return await Navigator.of(context).push<T>(
CommonDesktopRoute(
builder: (context) => child,
),
);
}
return await Navigator.of(context).push<T>( return await Navigator.of(context).push<T>(
CommonRoute( CommonRoute(
builder: (context) => child, builder: (context) => child,
@@ -11,6 +21,46 @@ class BaseNavigator {
} }
} }
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> { class CommonRoute<T> extends MaterialPageRoute<T> {
CommonRoute({ CommonRoute({
required super.builder, required super.builder,
@@ -20,7 +70,7 @@ class CommonRoute<T> extends MaterialPageRoute<T> {
Duration get transitionDuration => const Duration(milliseconds: 500); Duration get transitionDuration => const Duration(milliseconds: 500);
@override @override
Duration get reverseTransitionDuration => const Duration(milliseconds: 300); Duration get reverseTransitionDuration => const Duration(milliseconds: 250);
} }
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>( final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
@@ -224,7 +274,7 @@ class _CommonEdgeShadowPainter extends BoxPainter {
return; return;
} }
final double shadowWidth = 0.05 * configuration.size!.width; final double shadowWidth = 0.03 * configuration.size!.width;
final double shadowHeight = configuration.size!.height; final double shadowHeight = configuration.size!.height;
final double bandWidth = shadowWidth / (colors.length - 1); final double bandWidth = shadowWidth / (colors.length - 1);

View File

@@ -2,8 +2,15 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
extension NumExt on num { extension NumExt on num {
String fixed({digit = 2}) { String fixed({decimals = 2}) {
return toStringAsFixed(truncateToDouble() == this ? 0 : digit); String formatted = toStringAsFixed(decimals);
if (formatted.contains('.')) {
formatted = formatted.replaceAll(RegExp(r'0*$'), '');
if (formatted.endsWith('.')) {
formatted = formatted.substring(0, formatted.length - 1);
}
}
return formatted;
} }
} }

View File

@@ -1,15 +1,11 @@
import 'dart:io'; import 'dart:io';
import 'dart:isolate';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data';
import 'dart:ui'; import 'dart:ui';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;
import 'package:lpinyin/lpinyin.dart'; import 'package:lpinyin/lpinyin.dart';
import 'package:zxing2/qrcode.dart';
class Other { class Other {
Color? getDelayColor(int? delay) { Color? getDelayColor(int? delay) {
@@ -34,6 +30,26 @@ 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 { String get uuidV4 {
final Random random = Random(); final Random random = Random();
final bytes = List.generate(16, (_) => random.nextInt(256)); final bytes = List.generate(16, (_) => random.nextInt(256));
@@ -165,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) { String? getFileNameForDisposition(String? disposition) {
if (disposition == null) return null; if (disposition == null) return null;
final parseValue = HeaderValue.parse(disposition); final parseValue = HeaderValue.parse(disposition);

View File

@@ -48,35 +48,40 @@ class AppPath {
return join(executableDirPath, "$appHelperService$executableExtension"); return join(executableDirPath, "$appHelperService$executableExtension");
} }
Future<String> getDownloadDirPath() async { Future<String> get downloadDirPath async {
final directory = await downloadDir.future; final directory = await downloadDir.future;
return directory.path; return directory.path;
} }
Future<String> getHomeDirPath() async { Future<String> get homeDirPath async {
final directory = await dataDir.future; final directory = await dataDir.future;
return directory.path; return directory.path;
} }
Future<String> getLockFilePath() async { Future<String> get lockFilePath async {
final directory = await dataDir.future; final directory = await dataDir.future;
return join(directory.path, "FlClash.lock"); 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; final directory = await dataDir.future;
return join(directory.path, profilesDirectoryName); return join(directory.path, profilesDirectoryName);
} }
Future<String?> getProfilePath(String? id) async { Future<String?> getProfilePath(String? id) async {
if (id == null) return null; if (id == null) return null;
final directory = await getProfilesPath(); final directory = await profilesPath;
return join(directory, "$id.yaml"); return join(directory, "$id.yaml");
} }
Future<String?> getProvidersPath(String? id) async { Future<String?> getProvidersPath(String? id) async {
if (id == null) return null; if (id == null) return null;
final directory = await getProfilesPath(); final directory = await profilesPath;
return join(directory, "providers", id); return join(directory, "providers", id);
} }

View File

@@ -4,13 +4,14 @@ import 'dart:typed_data';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class Picker { class Picker {
Future<PlatformFile?> pickerFile() async { Future<PlatformFile?> pickerFile() async {
final filePickerResult = await FilePicker.platform.pickFiles( final filePickerResult = await FilePicker.platform.pickFiles(
withData: true, withData: true,
allowMultiple: false, allowMultiple: false,
initialDirectory: await appPath.getDownloadDirPath(), initialDirectory: await appPath.downloadDirPath,
); );
return filePickerResult?.files.first; return filePickerResult?.files.first;
} }
@@ -18,7 +19,7 @@ class Picker {
Future<String?> saveFile(String fileName, Uint8List bytes) async { Future<String?> saveFile(String fileName, Uint8List bytes) async {
final path = await FilePicker.platform.saveFile( final path = await FilePicker.platform.saveFile(
fileName: fileName, fileName: fileName,
initialDirectory: await appPath.getDownloadDirPath(), initialDirectory: await appPath.downloadDirPath,
bytes: Platform.isAndroid ? bytes : null, bytes: Platform.isAndroid ? bytes : null,
); );
if (!Platform.isAndroid && path != null) { if (!Platform.isAndroid && path != null) {
@@ -30,9 +31,14 @@ class Picker {
Future<String?> pickerConfigQRCode() async { Future<String?> pickerConfigQRCode() async {
final xFile = await ImagePicker().pickImage(source: ImageSource.gallery); final xFile = await ImagePicker().pickImage(source: ImageSource.gallery);
final bytes = await xFile?.readAsBytes(); if (xFile == null) {
if (bytes == null) return null; return null;
final result = await other.parseQRCode(bytes); }
final controller = MobileScannerController();
final capture = await controller.analyzeImage(xFile.path, formats: [
BarcodeFormat.qrCode,
]);
final result = capture?.barcodes.first.rawValue;
if (result == null || !result.isUrl) { if (result == null || !result.isUrl) {
throw appLocalizations.pleaseUploadValidQrcode; throw appLocalizations.pleaseUploadValidQrcode;
} }

View File

@@ -1,19 +1,22 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/cupertino.dart'; import 'package:fl_clash/models/models.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../models/models.dart';
import 'constant.dart'; import 'constant.dart';
class Preferences { class Preferences {
static Preferences? _instance; static Preferences? _instance;
Completer<SharedPreferences> sharedPreferencesCompleter = Completer(); Completer<SharedPreferences?> sharedPreferencesCompleter = Completer();
Future<bool> get isInit async =>
await sharedPreferencesCompleter.future != null;
Preferences._internal() { Preferences._internal() {
SharedPreferences.getInstance() SharedPreferences.getInstance()
.then((value) => sharedPreferencesCompleter.complete(value)); .then((value) => sharedPreferencesCompleter.complete(value))
.onError((_, __) => sharedPreferencesCompleter.complete(null));
} }
factory Preferences() { factory Preferences() {
@@ -23,50 +26,38 @@ class Preferences {
Future<ClashConfig?> getClashConfig() async { Future<ClashConfig?> getClashConfig() async {
final preferences = await sharedPreferencesCompleter.future; final preferences = await sharedPreferencesCompleter.future;
final clashConfigString = preferences.getString(clashConfigKey); final clashConfigString = preferences?.getString(clashConfigKey);
if (clashConfigString == null) return null; if (clashConfigString == null) return null;
final clashConfigMap = json.decode(clashConfigString); final clashConfigMap = json.decode(clashConfigString);
try { return ClashConfig.fromJson(clashConfigMap);
return ClashConfig.fromJson(clashConfigMap);
} catch (e) {
debugPrint(e.toString());
return null;
}
}
Future<bool> saveClashConfig(ClashConfig clashConfig) async {
final preferences = await sharedPreferencesCompleter.future;
return preferences.setString(
clashConfigKey,
json.encode(clashConfig),
);
} }
Future<Config?> getConfig() async { Future<Config?> getConfig() async {
final preferences = await sharedPreferencesCompleter.future; final preferences = await sharedPreferencesCompleter.future;
final configString = preferences.getString(configKey); final configString = preferences?.getString(configKey);
if (configString == null) return null; if (configString == null) return null;
final configMap = json.decode(configString); final configMap = json.decode(configString);
try { return Config.compatibleFromJson(configMap);
return Config.fromJson(configMap);
} catch (e) {
debugPrint(e.toString());
return null;
}
} }
Future<bool> saveConfig(Config config) async { Future<bool> saveConfig(Config config) async {
final preferences = await sharedPreferencesCompleter.future; final preferences = await sharedPreferencesCompleter.future;
return preferences.setString( return await preferences?.setString(
configKey, configKey,
json.encode(config), json.encode(config),
); ) ??
false;
}
clearClashConfig() async {
final preferences = await sharedPreferencesCompleter.future;
preferences?.remove(clashConfigKey);
} }
clearPreferences() async { clearPreferences() async {
final sharedPreferencesIns = await sharedPreferencesCompleter.future; final sharedPreferencesIns = await sharedPreferencesCompleter.future;
sharedPreferencesIns.clear(); sharedPreferencesIns?.clear();
} }
} }
final preferences = Preferences(); final preferences = Preferences();

31
lib/common/print.dart Normal file
View File

@@ -0,0 +1,31 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/cupertino.dart';
class CommonPrint {
static CommonPrint? _instance;
CommonPrint._internal();
factory CommonPrint() {
_instance ??= CommonPrint._internal();
return _instance!;
}
log(String? text) {
final payload = "[FlClash] $text";
debugPrint(payload);
if (globalState.isService) {
return;
}
globalState.appController.addLog(
Log(
logLevel: LogLevel.info,
payload: payload,
),
);
}
}
final commonPrint = CommonPrint();

57
lib/common/render.dart Normal file
View File

@@ -0,0 +1,57 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.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(
DebounceTag.renderPause,
_pause,
duration: Duration(seconds: 15),
);
}
resume() {
debouncer.cancel(DebounceTag.renderPause);
_resume();
}
void _pause() {
if (_isPaused) return;
_isPaused = true;
_beginFrame = _dispatcher.onBeginFrame;
_drawFrame = _dispatcher.onDrawFrame;
_dispatcher.onBeginFrame = null;
_dispatcher.onDrawFrame = null;
commonPrint.log("pause");
}
void _resume() {
if (!_isPaused) return;
_isPaused = false;
_dispatcher.onBeginFrame = _beginFrame;
_dispatcher.onDrawFrame = _drawFrame;
_dispatcher.scheduleFrame();
commonPrint.log("resume");
}
}
final render = system.isDesktop ? Render() : null;

View File

@@ -3,7 +3,6 @@ import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
@@ -14,11 +13,10 @@ class Request {
String? userAgent; String? userAgent;
Request() { Request() {
_dio = Dio(); _dio = Dio(
_dio.interceptors.add( BaseOptions(
InterceptorsWrapper( headers: {
onRequest: (options, handler) { "User-Agent": browserUa,
return handler.next(options); // 继续请求
}, },
), ),
); );
@@ -30,7 +28,7 @@ class Request {
url, url,
options: Options( options: Options(
headers: { headers: {
"User-Agent": globalState.appController.clashConfig.globalUa "User-Agent": globalState.ua,
}, },
responseType: ResponseType.bytes, responseType: ResponseType.bytes,
), ),
@@ -71,31 +69,32 @@ class Request {
return data; return data;
} }
final List<String> _ipInfoSources = [ final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
"https://ipwho.is/?fields=ip&output=csv", "https://ipwho.is/": IpInfo.fromIpwhoIsJson,
"https://ipinfo.io/ip", "https://api.ip.sb/geoip/": IpInfo.fromIpSbJson,
"https://ifconfig.me/ip/", "https://ipapi.co/json/": IpInfo.fromIpApiCoJson,
]; "https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
};
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async { Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
for (final source in _ipInfoSources) { for (final source in _ipInfoSources.entries) {
try { try {
final response = await _dio final response = await _dio.get<Map<String, dynamic>>(
.get<String>( source.key,
source, cancelToken: cancelToken,
cancelToken: cancelToken, options: Options(
) responseType: ResponseType.json,
.timeout(httpTimeoutDuration); ),
);
if (response.statusCode != 200 || response.data == null) { if (response.statusCode != 200 || response.data == null) {
continue; continue;
} }
final ipInfo = await clashCore.getCountryCode(response.data!); if (response.data == null) {
if (ipInfo == null && source != _ipInfoSources.last) {
continue; continue;
} }
return ipInfo; return source.value(response.data!);
} catch (e) { } catch (e) {
debugPrint("checkIp error ===> $e"); commonPrint.log("checkIp error ===> $e");
if (e is DioException && e.type == DioExceptionType.cancel) { if (e is DioException && e.type == DioExceptionType.cancel) {
throw "cancelled"; throw "cancelled";
} }
@@ -110,6 +109,9 @@ class Request {
.get( .get(
"http://$localhost:$helperPort/ping", "http://$localhost:$helperPort/ping",
options: Options( options: Options(
headers: {
"User-Agent": browserUa,
},
responseType: ResponseType.plain, responseType: ResponseType.plain,
), ),
) )

View File

@@ -1,3 +1,4 @@
import 'dart:math';
import 'dart:ui'; import 'dart:ui';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
@@ -15,6 +16,8 @@ class BaseScrollBehavior extends MaterialScrollBehavior {
}; };
} }
class BaseScrollBehavior2 extends ScrollBehavior {}
class HiddenBarScrollBehavior extends BaseScrollBehavior { class HiddenBarScrollBehavior extends BaseScrollBehavior {
@override @override
Widget buildScrollbar( Widget buildScrollbar(
@@ -40,3 +43,95 @@ class ShowBarScrollBehavior extends BaseScrollBehavior {
); );
} }
} }
class NextClampingScrollPhysics extends ClampingScrollPhysics {
const NextClampingScrollPhysics({super.parent});
@override
NextClampingScrollPhysics applyTo(ScrollPhysics? ancestor) {
return NextClampingScrollPhysics(parent: buildParent(ancestor));
}
@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,
});
bool _isInit = false;
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
if (!_isInit) {
correctPixels(maxScrollExtent);
_isInit = true;
}
return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
}
}

0
lib/common/state.dart Normal file
View File

View File

@@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'print.dart';
extension StringExtension on String { extension StringExtension on String {
bool get isUrl { bool get isUrl {
@@ -43,8 +43,17 @@ extension StringExtension on String {
RegExp(this); RegExp(this);
return true; return true;
} catch (e) { } catch (e) {
debugPrint(e.toString()); commonPrint.log(e.toString());
return false; return false;
} }
} }
} }
extension StringExtensionSafe on String? {
String getSafeValue(String defaultValue) {
if (this == null || this!.isEmpty) {
return defaultValue;
}
return this!;
}
}

View File

@@ -46,7 +46,7 @@ class System {
} else if (Platform.isLinux) { } else if (Platform.isLinux) {
final result = await Process.run('stat', ['-c', '%U:%G %A', corePath]); final result = await Process.run('stat', ['-c', '%U:%G %A', corePath]);
final output = result.stdout.trim(); final output = result.stdout.trim();
if (output.startsWith('root:') && output.contains('rwx')) { if (output.startsWith('root:') && output.contains('rws')) {
return true; return true;
} }
return false; return false;

View File

@@ -39,10 +39,7 @@ class Tray {
} }
update({ update({
required AppState appState, required TrayState trayState,
required AppFlowingState appFlowingState,
required Config config,
required ClashConfig clashConfig,
bool focus = false, bool focus = false,
}) async { }) async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
@@ -50,7 +47,7 @@ class Tray {
} }
if (!Platform.isLinux) { if (!Platform.isLinux) {
await _updateSystemTray( await _updateSystemTray(
brightness: appState.brightness, brightness: trayState.brightness,
force: focus, force: focus,
); );
} }
@@ -63,9 +60,7 @@ class Tray {
); );
menuItems.add(showMenuItem); menuItems.add(showMenuItem);
final startMenuItem = MenuItem.checkbox( final startMenuItem = MenuItem.checkbox(
label: appFlowingState.isStart label: trayState.isStart ? appLocalizations.stop : appLocalizations.start,
? appLocalizations.stop
: appLocalizations.start,
onClick: (_) async { onClick: (_) async {
globalState.appController.updateStart(); globalState.appController.updateStart();
}, },
@@ -80,23 +75,22 @@ class Tray {
onClick: (_) { onClick: (_) {
globalState.appController.changeMode(mode); globalState.appController.changeMode(mode);
}, },
checked: mode == clashConfig.mode, checked: mode == trayState.mode,
), ),
); );
} }
menuItems.add(MenuItem.separator()); menuItems.add(MenuItem.separator());
if (!Platform.isWindows) { if (!Platform.isWindows) {
final groups = appState.currentGroups; for (final group in trayState.groups) {
for (final group in groups) {
List<MenuItem> subMenuItems = []; List<MenuItem> subMenuItems = [];
for (final proxy in group.all) { for (final proxy in group.all) {
subMenuItems.add( subMenuItems.add(
MenuItem.checkbox( MenuItem.checkbox(
label: proxy.name, label: proxy.name,
checked: appState.selectedMap[group.name] == proxy.name, checked: trayState.selectedMap[group.name] == proxy.name,
onClick: (_) { onClick: (_) {
final appController = globalState.appController; final appController = globalState.appController;
appController.config.updateCurrentSelectedMap( appController.updateCurrentSelectedMap(
group.name, group.name,
proxy.name, proxy.name,
); );
@@ -117,18 +111,18 @@ class Tray {
), ),
); );
} }
if (groups.isNotEmpty) { if (trayState.groups.isNotEmpty) {
menuItems.add(MenuItem.separator()); menuItems.add(MenuItem.separator());
} }
} }
if (appFlowingState.isStart) { if (trayState.isStart) {
menuItems.add( menuItems.add(
MenuItem.checkbox( MenuItem.checkbox(
label: appLocalizations.tun, label: appLocalizations.tun,
onClick: (_) { onClick: (_) {
globalState.appController.updateTun(); globalState.appController.updateTun();
}, },
checked: clashConfig.tun.enable, checked: trayState.tunEnable,
), ),
); );
menuItems.add( menuItems.add(
@@ -137,7 +131,7 @@ class Tray {
onClick: (_) { onClick: (_) {
globalState.appController.updateSystemProxy(); globalState.appController.updateSystemProxy();
}, },
checked: config.networkProps.systemProxy, checked: trayState.systemProxy,
), ),
); );
menuItems.add(MenuItem.separator()); menuItems.add(MenuItem.separator());
@@ -147,12 +141,12 @@ class Tray {
onClick: (_) async { onClick: (_) async {
globalState.appController.updateAutoLaunch(); globalState.appController.updateAutoLaunch();
}, },
checked: config.appSetting.autoLaunch, checked: trayState.autoLaunch,
); );
final copyEnvVarMenuItem = MenuItem( final copyEnvVarMenuItem = MenuItem(
label: appLocalizations.copyEnvVar, label: appLocalizations.copyEnvVar,
onClick: (_) async { onClick: (_) async {
await _copyEnv(clashConfig.mixedPort); await _copyEnv(trayState.port);
}, },
); );
menuItems.add(autoStartMenuItem); menuItems.add(autoStartMenuItem);
@@ -169,12 +163,25 @@ class Tray {
await trayManager.setContextMenu(menu); await trayManager.setContextMenu(menu);
if (Platform.isLinux) { if (Platform.isLinux) {
await _updateSystemTray( await _updateSystemTray(
brightness: appState.brightness, brightness: trayState.brightness,
force: focus, force: focus,
); );
} }
} }
updateTrayTitle([Traffic? traffic]) async {
// if (!Platform.isMacOS) {
// return;
// }
// if (traffic == null) {
// await trayManager.setTitle("");
// } else {
// await trayManager.setTitle(
// "${traffic.up.shortShow} ↑ \n${traffic.down.shortShow} ↓",
// );
// }
}
Future<void> _copyEnv(int port) async { Future<void> _copyEnv(int port) async {
final url = "http://127.0.0.1:$port"; final url = "http://127.0.0.1:$port";

View File

@@ -1,13 +1,14 @@
import 'dart:io'; import 'dart:io';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/config.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:screen_retriever/screen_retriever.dart'; import 'package:screen_retriever/screen_retriever.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
class Window { class Window {
init(WindowProps props, int version) async { init(int version) async {
final props = globalState.config.windowProps;
final acquire = await singleInstanceLock.acquire(); final acquire = await singleInstanceLock.acquire();
if (!acquire) { if (!acquire) {
exit(0); exit(0);
@@ -24,6 +25,8 @@ class Window {
); );
if (!Platform.isMacOS || version > 10) { if (!Platform.isMacOS || version > 10) {
await windowManager.setTitleBarStyle(TitleBarStyle.hidden); await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
}
if(!Platform.isMacOS){
final left = props.left ?? 0; final left = props.left ?? 0;
final top = props.top ?? 0; final top = props.top ?? 0;
final right = left + props.width; final right = left + props.width;
@@ -33,7 +36,7 @@ class Window {
} else { } else {
final displays = await screenRetriever.getAllDisplays(); final displays = await screenRetriever.getAllDisplays();
final isPositionValid = displays.any( final isPositionValid = displays.any(
(display) { (display) {
final displayBounds = Rect.fromLTWH( final displayBounds = Rect.fromLTWH(
display.visiblePosition!.dx, display.visiblePosition!.dx,
display.visiblePosition!.dy, display.visiblePosition!.dy,
@@ -60,6 +63,7 @@ class Window {
} }
show() async { show() async {
render?.resume();
await windowManager.show(); await windowManager.show();
await windowManager.focus(); await windowManager.focus();
await windowManager.setSkipTaskbar(false); await windowManager.setSkipTaskbar(false);
@@ -74,6 +78,7 @@ class Window {
} }
hide() async { hide() async {
render?.pause();
await windowManager.hide(); await windowManager.hide();
await windowManager.setSkipTaskbar(true); await windowManager.setSkipTaskbar(true);
} }

View File

@@ -4,7 +4,6 @@ import 'dart:io';
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/cupertino.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
class Windows { class Windows {
@@ -54,7 +53,7 @@ class Windows {
calloc.free(argumentsPtr); calloc.free(argumentsPtr);
calloc.free(operationPtr); calloc.free(operationPtr);
debugPrint("[Windows] runas: $command $arguments resultCode:$result"); commonPrint.log("windows runas: $command $arguments resultCode:$result");
if (result < 42) { if (result < 42) {
return false; return false;

View File

@@ -8,28 +8,24 @@ import 'package:archive/archive.dart';
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/archive.dart'; import 'package:fl_clash/common/archive.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'common/common.dart'; import 'common/common.dart';
import 'models/models.dart'; import 'models/models.dart';
class AppController { class AppController {
final BuildContext context; bool lastTunEnable = false;
late AppState appState; int? lastProfileModified;
late AppFlowingState appFlowingState;
late Config config;
late ClashConfig clashConfig;
AppController(this.context) { final BuildContext context;
appState = context.read<AppState>(); final WidgetRef _ref;
config = context.read<Config>();
clashConfig = context.read<ClashConfig>(); AppController(this.context, WidgetRef ref) : _ref = ref;
appFlowingState = context.read<AppFlowingState>();
}
updateClashConfigDebounce() { updateClashConfigDebounce() {
debouncer.call(DebounceTag.updateClashConfig, updateClashConfig); debouncer.call(DebounceTag.updateClashConfig, updateClashConfig);
@@ -41,13 +37,13 @@ class AppController {
addCheckIpNumDebounce() { addCheckIpNumDebounce() {
debouncer.call(DebounceTag.addCheckIpNum, () { debouncer.call(DebounceTag.addCheckIpNum, () {
appState.checkIpNum++; _ref.read(checkIpNumProvider.notifier).add();
}); });
} }
applyProfileDebounce() { applyProfileDebounce() {
debouncer.call(DebounceTag.addCheckIpNum, () { debouncer.call(DebounceTag.addCheckIpNum, () {
applyProfile(isPrue: true); applyProfile();
}); });
} }
@@ -67,30 +63,27 @@ class AppController {
} }
restartCore() async { restartCore() async {
await globalState.restartCore( await clashService?.reStart();
appState: appState, await initCore();
clashConfig: clashConfig,
config: config, if (_ref.read(runTimeProvider.notifier).isStart) {
); await globalState.handleStart();
}
} }
updateStatus(bool isStart) async { updateStatus(bool isStart) async {
if (isStart) { if (isStart) {
await globalState.handleStart(); await globalState.handleStart([
updateRunTime();
updateTraffic();
globalState.updateFunctionLists = [
updateRunTime, updateRunTime,
updateTraffic, updateTraffic,
]; ]);
final currentLastModified = final currentLastModified =
await config.getCurrentProfile()?.profileLastModified; await _ref.read(currentProfileProvider)?.profileLastModified;
if (currentLastModified == null || if (currentLastModified == null || lastProfileModified == null) {
globalState.lastProfileModified == null) {
addCheckIpNumDebounce(); addCheckIpNumDebounce();
return; return;
} }
if (currentLastModified <= (globalState.lastProfileModified ?? 0)) { if (currentLastModified <= (lastProfileModified ?? 0)) {
addCheckIpNumDebounce(); addCheckIpNumDebounce();
return; return;
} }
@@ -98,9 +91,10 @@ class AppController {
} else { } else {
await globalState.handleStop(); await globalState.handleStop();
await clashCore.resetTraffic(); await clashCore.resetTraffic();
appFlowingState.traffics = []; _ref.read(trafficsProvider.notifier).clear();
appFlowingState.totalTraffic = Traffic(); _ref.read(totalTrafficProvider.notifier).value = Traffic();
appFlowingState.runTime = null; _ref.read(runTimeProvider.notifier).value = null;
// tray.updateTrayTitle(null);
addCheckIpNumDebounce(); addCheckIpNumDebounce();
} }
} }
@@ -110,100 +104,214 @@ class AppController {
if (startTime != null) { if (startTime != null) {
final startTimeStamp = startTime.millisecondsSinceEpoch; final startTimeStamp = startTime.millisecondsSinceEpoch;
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch; final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
appFlowingState.runTime = nowTimeStamp - startTimeStamp; _ref.read(runTimeProvider.notifier).value = nowTimeStamp - startTimeStamp;
} else { } else {
appFlowingState.runTime = null; _ref.read(runTimeProvider.notifier).value = null;
} }
} }
updateTraffic() { updateTraffic() async {
globalState.updateTraffic( final traffic = await clashCore.getTraffic();
config: config, _ref.read(trafficsProvider.notifier).addTraffic(traffic);
appFlowingState: appFlowingState, _ref.read(totalTrafficProvider.notifier).value =
); await clashCore.getTotalTraffic();
} }
addProfile(Profile profile) async { addProfile(Profile profile) async {
config.setProfile(profile); _ref.read(profilesProvider.notifier).setProfile(profile);
if (config.currentProfileId != null) return; if (_ref.read(currentProfileIdProvider) != null) return;
await changeProfile(profile.id); _ref.read(currentProfileIdProvider.notifier).value = profile.id;
} }
deleteProfile(String id) async { deleteProfile(String id) async {
config.deleteProfileById(id); _ref.read(profilesProvider.notifier).deleteProfileById(id);
clearEffect(id); clearEffect(id);
if (config.currentProfileId == id) { if (globalState.config.currentProfileId == id) {
if (config.profiles.isNotEmpty) { final profiles = globalState.config.profiles;
final updateId = config.profiles.first.id; final currentProfileId = _ref.read(currentProfileIdProvider.notifier);
changeProfile(updateId); if (profiles.isNotEmpty) {
final updateId = profiles.first.id;
currentProfileId.value = updateId;
} else { } else {
changeProfile(null); currentProfileId.value = null;
updateStatus(false); updateStatus(false);
} }
} }
} }
updateProviders() async { updateProviders() async {
await globalState.updateProviders(appState); _ref.read(providersProvider.notifier).value =
await clashCore.getExternalProviders();
} }
updateLocalIp() async { updateLocalIp() async {
appFlowingState.localIp = null; _ref.read(localIpProvider.notifier).value = null;
await Future.delayed(commonDuration); await Future.delayed(commonDuration);
appFlowingState.localIp = await other.getLocalIpAddress(); _ref.read(localIpProvider.notifier).value = await other.getLocalIpAddress();
} }
Future<void> updateProfile(Profile profile) async { Future<void> updateProfile(Profile profile) async {
final newProfile = await profile.update(); final newProfile = await profile.update();
config.setProfile( _ref
newProfile.copyWith(isUpdating: false), .read(profilesProvider.notifier)
); .setProfile(newProfile.copyWith(isUpdating: false));
if (profile.id == config.currentProfile?.id) { if (profile.id == _ref.read(currentProfileIdProvider)) {
applyProfileDebounce(); applyProfileDebounce();
} }
} }
Future<void> updateClashConfig({bool isPatch = true}) async { _setProfile(Profile profile) {
_ref.read(profilesProvider.notifier).setProfile(profile);
}
setProfile(Profile profile) {
_setProfile(profile);
if (profile.id == _ref.read(currentProfileIdProvider)) {
applyProfileDebounce();
}
}
setProfiles(List<Profile> profiles) {
_ref.read(profilesProvider.notifier).value = profiles;
}
addLog(Log log) {
_ref.read(logsProvider).add(log);
}
updateOrAddHotKeyAction(HotKeyAction hotKeyAction) {
final hotKeyActions = _ref.read(hotKeyActionsProvider);
final index =
hotKeyActions.indexWhere((item) => item.action == hotKeyAction.action);
if (index == -1) {
_ref.read(hotKeyActionsProvider.notifier).value = List.from(hotKeyActions)
..add(hotKeyAction);
} else {
_ref.read(hotKeyActionsProvider.notifier).value = List.from(hotKeyActions)
..[index] = hotKeyAction;
}
_ref.read(hotKeyActionsProvider.notifier).value = index == -1
? (List.from(hotKeyActions)..add(hotKeyAction))
: (List.from(hotKeyActions)..[index] = hotKeyAction);
}
List<Group> getCurrentGroups() {
return _ref.read(currentGroupsStateProvider.select((state) => state.value));
}
String getRealTestUrl(String? url) {
return _ref.read(getRealTestUrlProvider(url));
}
int getProxiesColumns() {
return _ref.read(getProxiesColumnsProvider);
}
addSortNum() {
return _ref.read(sortNumProvider.notifier).add();
}
getCurrentGroupName() {
final currentGroupName = _ref.read(currentProfileProvider.select(
(state) => state?.currentGroupName,
));
return currentGroupName;
}
getRealProxyName(proxyName) {
return _ref.read(getRealTestUrlProvider(proxyName));
}
getSelectedProxyName(groupName) {
return _ref.read(getSelectedProxyNameProvider(groupName));
}
updateCurrentGroupName(String groupName) {
final profile = _ref.read(currentProfileProvider);
if (profile == null || profile.currentGroupName == groupName) {
return;
}
_setProfile(
profile.copyWith(currentGroupName: groupName),
);
}
Future<void> updateClashConfig([bool? isPatch]) async {
final commonScaffoldState = globalState.homeScaffoldKey.currentState; final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return; if (commonScaffoldState?.mounted != true) return;
await commonScaffoldState?.loadingRun(() async { await commonScaffoldState?.loadingRun(() async {
await globalState.updateClashConfig( await _updateClashConfig(
appState: appState, isPatch,
clashConfig: clashConfig,
config: config,
isPatch: isPatch,
); );
}); });
} }
Future applyProfile({bool isPrue = false}) async { Future<void> _updateClashConfig([bool? isPatch]) async {
if (isPrue) { final profile = _ref.watch(currentProfileProvider);
await globalState.applyProfile( await _ref.read(currentProfileProvider)?.checkAndUpdate();
appState: appState, final patchConfig = _ref.read(patchClashConfigProvider);
config: config, final appSetting = _ref.read(appSettingProvider);
clashConfig: clashConfig, bool enableTun = patchConfig.tun.enable;
); if (enableTun != lastTunEnable &&
lastTunEnable == false &&
!Platform.isAndroid) {
final code = await system.authorizeCore();
switch (code) {
case AuthorizeCode.none:
break;
case AuthorizeCode.success:
lastTunEnable = enableTun;
await restartCore();
return;
case AuthorizeCode.error:
enableTun = false;
}
}
if (appSetting.openLogs) {
clashCore.startLog();
} else {
clashCore.stopLog();
}
final res = await clashCore.updateConfig(
globalState.getUpdateConfigParams(isPatch),
);
if (res.isNotEmpty) throw res;
lastTunEnable = enableTun;
lastProfileModified = await profile?.profileLastModified;
}
Future _applyProfile() async {
await clashCore.requestGc();
await updateClashConfig();
await updateGroups();
await updateProviders();
}
Future applyProfile({bool silence = false}) async {
if (silence) {
await _applyProfile();
} else { } else {
final commonScaffoldState = globalState.homeScaffoldKey.currentState; final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return; if (commonScaffoldState?.mounted != true) return;
await commonScaffoldState?.loadingRun(() async { await commonScaffoldState?.loadingRun(() async {
await globalState.applyProfile( await _applyProfile();
appState: appState,
config: config,
clashConfig: clashConfig,
);
}); });
} }
addCheckIpNumDebounce(); addCheckIpNumDebounce();
} }
changeProfile(String? value) async { handleChangeProfile() {
if (value == config.currentProfileId) return; _ref.read(delayDataSourceProvider.notifier).value = {};
config.currentProfileId = value; applyProfile();
}
updateBrightness(Brightness brightness) {
_ref.read(appBrightnessProvider.notifier).value = brightness;
} }
autoUpdateProfiles() async { autoUpdateProfiles() async {
for (final profile in config.profiles) { for (final profile in _ref.read(profilesProvider)) {
if (!profile.autoUpdate) continue; if (!profile.autoUpdate) continue;
final isNotNeedUpdate = profile.lastUpdateDate final isNotNeedUpdate = profile.lastUpdateDate
?.add( ?.add(
@@ -214,20 +322,29 @@ class AppController {
continue; continue;
} }
try { try {
updateProfile(profile); await updateProfile(profile);
} catch (e) { } catch (e) {
appFlowingState.addLog( _ref.read(logsProvider.notifier).addLog(
Log( Log(
logLevel: LogLevel.info, logLevel: LogLevel.info,
payload: e.toString(), payload: e.toString(),
), ),
); );
} }
} }
} }
Future<void> updateGroups() async {
_ref.read(groupsProvider.notifier).value = await retry(
task: () async {
return await clashCore.getProxiesGroups();
},
retryIf: (res) => res.isEmpty,
);
}
updateProfiles() async { updateProfiles() async {
for (final profile in config.profiles) { for (final profile in _ref.read(profilesProvider)) {
if (profile.type == ProfileType.file) { if (profile.type == ProfileType.file) {
continue; continue;
} }
@@ -235,34 +352,33 @@ class AppController {
} }
} }
Future<void> updateGroups() async { updateSystemColorSchemes(ColorSchemes colorSchemes) {
await globalState.updateGroups(appState); _ref.read(appSchemesProvider.notifier).value = colorSchemes;
}
updateSystemColorSchemes(SystemColorSchemes systemColorSchemes) {
appState.systemColorSchemes = systemColorSchemes;
} }
savePreferences() async { savePreferences() async {
debugPrint("[APP] savePreferences"); commonPrint.log("save preferences");
await preferences.saveConfig(config); await preferences.saveConfig(globalState.config);
await preferences.saveClashConfig(clashConfig);
} }
changeProxy({ changeProxy({
required String groupName, required String groupName,
required String proxyName, required String proxyName,
}) async { }) async {
await globalState.changeProxy( await clashCore.changeProxy(
config: config, ChangeProxyParams(
groupName: groupName, groupName: groupName,
proxyName: proxyName, proxyName: proxyName,
),
); );
if (_ref.read(appSettingProvider).closeConnections) {
clashCore.closeConnections();
}
addCheckIpNumDebounce(); addCheckIpNumDebounce();
} }
handleBackOrExit() async { handleBackOrExit() async {
if (config.appSetting.minimizeOnExit) { if (_ref.read(appSettingProvider).minimizeOnExit) {
if (system.isDesktop) { if (system.isDesktop) {
await savePreferencesDebounce(); await savePreferencesDebounce();
} }
@@ -279,12 +395,13 @@ class AppController {
await clashService?.destroy(); await clashService?.destroy();
await proxy?.stopProxy(); await proxy?.stopProxy();
await savePreferences(); await savePreferences();
} catch (_) {} } finally {
system.exit(); system.exit();
}
} }
autoCheckUpdate() async { autoCheckUpdate() async {
if (!config.appSetting.autoCheckUpdate) return; if (!_ref.read(appSettingProvider).autoCheckUpdate) return;
final res = await request.checkForUpdate(); final res = await request.checkForUpdate();
checkUpdateResultHandle(data: res); checkUpdateResultHandle(data: res);
} }
@@ -298,7 +415,7 @@ class AppController {
final body = data['body']; final body = data['body'];
final submits = other.parseReleaseBody(body); final submits = other.parseReleaseBody(body);
final textTheme = context.textTheme; final textTheme = context.textTheme;
globalState.showMessage( final res = await globalState.showMessage(
title: appLocalizations.discoverNewVersion, title: appLocalizations.discoverNewVersion,
message: TextSpan( message: TextSpan(
text: "$tagName \n", text: "$tagName \n",
@@ -315,13 +432,14 @@ class AppController {
), ),
], ],
), ),
onTab: () {
launchUrl(
Uri.parse("https://github.com/$repository/releases/latest"),
);
},
confirmText: appLocalizations.goDownload, confirmText: appLocalizations.goDownload,
); );
if (res != true) {
return;
}
launchUrl(
Uri.parse("https://github.com/$repository/releases/latest"),
);
} else if (handleError) { } else if (handleError) {
globalState.showMessage( globalState.showMessage(
title: appLocalizations.checkUpdate, title: appLocalizations.checkUpdate,
@@ -332,33 +450,61 @@ class AppController {
} }
} }
init() async { _handlePreference() async {
final isDisclaimerAccepted = await handlerDisclaimer(); if (await preferences.isInit) {
if (!isDisclaimerAccepted) { return;
handleExit();
} }
if (!config.appSetting.silentLaunch) { final res = await globalState.showMessage(
window?.show(); title: appLocalizations.tip,
} message: TextSpan(text: appLocalizations.cacheCorrupt),
await globalState.initCore(
appState: appState,
clashConfig: clashConfig,
config: config,
); );
if (res == true) {
final file = File(await appPath.sharedPreferencesPath);
final isExists = await file.exists();
if (isExists) {
await file.delete();
}
}
await handleExit();
}
Future<void> initCore() async {
final isInit = await clashCore.isInit;
if (!isInit) {
await clashCore.setState(
globalState.getCoreState(),
);
await clashCore.init();
}
await applyProfile();
}
init() async {
await _handlePreference();
await _handlerDisclaimer();
await initCore();
await _initStatus(); await _initStatus();
updateTray(true);
autoLaunch?.updateStatus( autoLaunch?.updateStatus(
config.appSetting.autoLaunch, _ref.read(appSettingProvider).autoLaunch,
); );
autoUpdateProfiles(); autoUpdateProfiles();
autoCheckUpdate(); autoCheckUpdate();
if (!_ref.read(appSettingProvider).silentLaunch) {
window?.show();
} else {
window?.hide();
}
_ref.read(initProvider.notifier).value = true;
} }
_initStatus() async { _initStatus() async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
globalState.updateStartTime(); await globalState.updateStartTime();
} }
final status = final status = globalState.isStart == true
globalState.isStart == true ? true : config.appSetting.autoRun; ? true
: _ref.read(appSettingProvider).autoRun;
await updateStatus(status); await updateStatus(status);
if (!status) { if (!status) {
@@ -367,15 +513,23 @@ class AppController {
} }
setDelay(Delay delay) { setDelay(Delay delay) {
appState.setDelay(delay); _ref.read(delayDataSourceProvider.notifier).setDelay(delay);
} }
toPage(int index, {bool hasAnimate = false}) { toPage(
if (index > appState.currentNavigationItems.length - 1) { int index, {
bool hasAnimate = false,
}) {
final navigations = _ref.read(currentNavigationsStateProvider).value;
if (index > navigations.length - 1) {
return; return;
} }
appState.currentLabel = appState.currentNavigationItems[index].label; _ref.read(currentPageLabelProvider.notifier).value =
if ((config.appSetting.isAnimateToPage || hasAnimate)) { navigations[index].label;
final isAnimateToPage = _ref.read(appSettingProvider).isAnimateToPage;
final isMobile =
_ref.read(viewWidthProvider.notifier).viewMode == ViewMode.mobile;
if (isAnimateToPage && isMobile || hasAnimate) {
globalState.pageController?.animateToPage( globalState.pageController?.animateToPage(
index, index,
duration: kTabScrollDuration, duration: kTabScrollDuration,
@@ -387,9 +541,9 @@ class AppController {
} }
toProfiles() { toProfiles() {
final index = appState.currentNavigationItems.indexWhere( final index = _ref.read(currentNavigationsStateProvider).value.indexWhere(
(element) => element.label == "profiles", (element) => element.label == PageLabel.profiles,
); );
if (index != -1) { if (index != -1) {
toPage(index); toPage(index);
} }
@@ -397,8 +551,8 @@ class AppController {
initLink() { initLink() {
linkManager.initAppLinksListen( linkManager.initAppLinksListen(
(url) { (url) async {
globalState.showMessage( final res = await globalState.showMessage(
title: "${appLocalizations.add}${appLocalizations.profile}", title: "${appLocalizations.add}${appLocalizations.profile}",
message: TextSpan( message: TextSpan(
children: [ children: [
@@ -416,10 +570,12 @@ class AppController {
"${appLocalizations.create}${appLocalizations.profile}"), "${appLocalizations.create}${appLocalizations.profile}"),
], ],
), ),
onTab: () {
addProfileFormURL(url);
},
); );
if (res != true) {
return;
}
addProfileFormURL(url);
}, },
); );
} }
@@ -447,9 +603,9 @@ class AppController {
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
config.appSetting = config.appSetting.copyWith( _ref.read(appSettingProvider.notifier).updateState(
disclaimerAccepted: true, (state) => state.copyWith(disclaimerAccepted: true),
); );
Navigator.of(context).pop<bool>(true); Navigator.of(context).pop<bool>(true);
}, },
child: Text(appLocalizations.agree), child: Text(appLocalizations.agree),
@@ -460,11 +616,15 @@ class AppController {
false; false;
} }
Future<bool> handlerDisclaimer() async { _handlerDisclaimer() async {
if (config.appSetting.disclaimerAccepted) { if (_ref.read(appSettingProvider).disclaimerAccepted) {
return true; return;
} }
return showDisclaimer(); final isDisclaimerAccepted = await showDisclaimer();
if (!isDisclaimerAccepted) {
await handleExit();
}
return;
} }
addProfileFormURL(String url) async { addProfileFormURL(String url) async {
@@ -518,10 +678,14 @@ class AppController {
updateViewWidth(double width) { updateViewWidth(double width) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
appState.viewWidth = width; _ref.read(viewWidthProvider.notifier).value = width;
}); });
} }
setProvider(ExternalProvider? provider) {
_ref.read(providersProvider.notifier).setProvider(provider);
}
List<Proxy> _sortOfName(List<Proxy> proxies) { List<Proxy> _sortOfName(List<Proxy> proxies) {
return List.of(proxies) return List.of(proxies)
..sort( ..sort(
@@ -532,12 +696,17 @@ class AppController {
); );
} }
List<Proxy> _sortOfDelay(List<Proxy> proxies) { List<Proxy> _sortOfDelay({
return proxies = List.of(proxies) required List<Proxy> proxies,
String? testUrl,
}) {
return List.of(proxies)
..sort( ..sort(
(a, b) { (a, b) {
final aDelay = appState.getDelay(a.name); final aDelay =
final bDelay = appState.getDelay(b.name); _ref.read(getDelayProvider(proxyName: a.name, testUrl: testUrl));
final bDelay =
_ref.read(getDelayProvider(proxyName: b.name, testUrl: testUrl));
if (aDelay == null && bDelay == null) { if (aDelay == null && bDelay == null) {
return 0; return 0;
} }
@@ -552,21 +721,17 @@ class AppController {
); );
} }
List<Proxy> getSortProxies(List<Proxy> proxies) { List<Proxy> getSortProxies(List<Proxy> proxies, [String? url]) {
return switch (config.proxiesStyle.sortType) { return switch (_ref.read(proxiesStyleSettingProvider).sortType) {
ProxiesSortType.none => proxies, ProxiesSortType.none => proxies,
ProxiesSortType.delay => _sortOfDelay(proxies), ProxiesSortType.delay => _sortOfDelay(
proxies: proxies,
testUrl: url,
),
ProxiesSortType.name => _sortOfName(proxies), ProxiesSortType.name => _sortOfName(proxies),
}; };
} }
String getCurrentSelectedName(String groupName) {
final group = appState.getGroupWithName(groupName);
return group?.getCurrentSelectedName(
config.currentSelectedMap[groupName] ?? '') ??
'';
}
clearEffect(String profileId) async { clearEffect(String profileId) async {
final profilePath = await appPath.getProfilePath(profileId); final profilePath = await appPath.getProfilePath(profileId);
final providersPath = await appPath.getProvidersPath(profileId); final providersPath = await appPath.getProvidersPath(profileId);
@@ -581,33 +746,66 @@ class AppController {
} }
updateTun() { updateTun() {
clashConfig.tun = clashConfig.tun.copyWith( _ref.read(patchClashConfigProvider.notifier).updateState(
enable: !clashConfig.tun.enable, (state) => state.copyWith.tun(enable: !state.tun.enable),
); );
} }
updateSystemProxy() { updateSystemProxy() {
config.networkProps = config.networkProps.copyWith( _ref.read(networkSettingProvider.notifier).updateState(
systemProxy: !config.networkProps.systemProxy, (state) => state.copyWith(
); systemProxy: state.systemProxy,
),
);
} }
updateStart() { updateStart() {
updateStatus(!appFlowingState.isStart); updateStatus(_ref.read(runTimeProvider.notifier).isStart);
}
updateCurrentSelectedMap(String groupName, String proxyName) {
final currentProfile = _ref.read(currentProfileProvider);
if (currentProfile != null &&
currentProfile.selectedMap[groupName] != proxyName) {
final SelectedMap selectedMap = Map.from(
currentProfile.selectedMap,
)..[groupName] = proxyName;
_ref.read(profilesProvider.notifier).setProfile(
currentProfile.copyWith(
selectedMap: selectedMap,
),
);
}
}
updateCurrentUnfoldSet(Set<String> value) {
final currentProfile = _ref.read(currentProfileProvider);
if (currentProfile == null) {
return;
}
_ref.read(profilesProvider.notifier).setProfile(
currentProfile.copyWith(
unfoldSet: value,
),
);
} }
changeMode(Mode mode) { changeMode(Mode mode) {
clashConfig.mode = mode; _ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(mode: mode),
);
if (mode == Mode.global) { if (mode == Mode.global) {
config.updateCurrentGroupName(GroupName.GLOBAL.name); updateCurrentGroupName(GroupName.GLOBAL.name);
} }
addCheckIpNumDebounce(); addCheckIpNumDebounce();
} }
updateAutoLaunch() { updateAutoLaunch() {
config.appSetting = config.appSetting.copyWith( _ref.read(appSettingProvider.notifier).updateState(
autoLaunch: !config.appSetting.autoLaunch, (state) => state.copyWith(
); autoLaunch: !state.autoLaunch,
),
);
} }
updateVisible() async { updateVisible() async {
@@ -620,18 +818,24 @@ class AppController {
} }
updateMode() { updateMode() {
final index = Mode.values.indexWhere((item) => item == clashConfig.mode); _ref.read(patchClashConfigProvider.notifier).updateState(
if (index == -1) { (state) {
return; final index = Mode.values.indexWhere((item) => item == state.mode);
} if (index == -1) {
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1; return null;
clashConfig.mode = Mode.values[nextIndex]; }
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1;
return state.copyWith(
mode: Mode.values[nextIndex],
);
},
);
} }
Future<bool> exportLogs() async { Future<bool> exportLogs() async {
final logsRaw = appFlowingState.logs.map( final logsRaw = _ref.read(logsProvider).list.map(
(item) => item.toString(), (item) => item.toString(),
); );
final data = await Isolate.run<List<int>>(() async { final data = await Isolate.run<List<int>>(() async {
final logsRawString = logsRaw.join("\n"); final logsRawString = logsRaw.join("\n");
return utf8.encode(logsRawString); return utf8.encode(logsRawString);
@@ -644,14 +848,12 @@ class AppController {
} }
Future<List<int>> backupData() async { Future<List<int>> backupData() async {
final homeDirPath = await appPath.getHomeDirPath(); final homeDirPath = await appPath.homeDirPath;
final profilesPath = await appPath.getProfilesPath(); final profilesPath = await appPath.profilesPath;
final configJson = config.toJson(); final configJson = globalState.config.toJson();
final clashConfigJson = clashConfig.toJson();
return Isolate.run<List<int>>(() async { return Isolate.run<List<int>>(() async {
final archive = Archive(); final archive = Archive();
archive.add("config.json", configJson); archive.add("config.json", configJson);
archive.add("clashConfig.json", clashConfigJson);
await archive.addDirectoryToArchive(profilesPath, homeDirPath); await archive.addDirectoryToArchive(profilesPath, homeDirPath);
final zipEncoder = ZipEncoder(); final zipEncoder = ZipEncoder();
return zipEncoder.encode(archive) ?? []; return zipEncoder.encode(archive) ?? [];
@@ -660,11 +862,7 @@ class AppController {
updateTray([bool focus = false]) async { updateTray([bool focus = false]) async {
tray.update( tray.update(
appState: appState, trayState: _ref.read(trayStateProvider),
appFlowingState: appFlowingState,
config: config,
clashConfig: clashConfig,
focus: focus,
); );
} }
@@ -676,39 +874,71 @@ class AppController {
final zipDecoder = ZipDecoder(); final zipDecoder = ZipDecoder();
return zipDecoder.decodeBytes(data); return zipDecoder.decodeBytes(data);
}); });
final homeDirPath = await appPath.getHomeDirPath(); final homeDirPath = await appPath.homeDirPath;
final configs = final configs =
archive.files.where((item) => item.name.endsWith(".json")).toList(); archive.files.where((item) => item.name.endsWith(".json")).toList();
final profiles = final profiles =
archive.files.where((item) => !item.name.endsWith(".json")); archive.files.where((item) => !item.name.endsWith(".json"));
final configIndex = final configIndex =
configs.indexWhere((config) => config.name == "config.json"); configs.indexWhere((config) => config.name == "config.json");
final clashConfigIndex = if (configIndex == -1) throw "invalid backup file";
configs.indexWhere((config) => config.name == "clashConfig.json");
if (configIndex == -1 || clashConfigIndex == -1) throw "invalid backup.zip";
final configFile = configs[configIndex]; final configFile = configs[configIndex];
final clashConfigFile = configs[clashConfigIndex]; var tempConfig = Config.compatibleFromJson(
final tempConfig = Config.fromJson(
json.decode( json.decode(
utf8.decode(configFile.content), utf8.decode(configFile.content),
), ),
); );
final tempClashConfig = ClashConfig.fromJson(
json.decode(
utf8.decode(clashConfigFile.content),
),
);
for (final profile in profiles) { for (final profile in profiles) {
final filePath = join(homeDirPath, profile.name); final filePath = join(homeDirPath, profile.name);
final file = File(filePath); final file = File(filePath);
await file.create(recursive: true); await file.create(recursive: true);
await file.writeAsBytes(profile.content); await file.writeAsBytes(profile.content);
} }
if (recoveryOption == RecoveryOption.onlyProfiles) { final clashConfigIndex =
config.update(tempConfig, RecoveryOption.onlyProfiles); configs.indexWhere((config) => config.name == "clashConfig.json");
} else { if (clashConfigIndex != -1) {
config.update(tempConfig, RecoveryOption.all); final clashConfigFile = configs[clashConfigIndex];
clashConfig.update(tempClashConfig); tempConfig = tempConfig.copyWith(
patchClashConfig: ClashConfig.fromJson(
json.decode(
utf8.decode(
clashConfigFile.content,
),
),
),
);
} }
_recovery(
tempConfig,
recoveryOption,
);
}
_recovery(Config config, RecoveryOption recoveryOption) {
final profiles = config.profiles;
for (final profile in profiles) {
_ref.read(profilesProvider.notifier).setProfile(profile);
}
final onlyProfiles = recoveryOption == RecoveryOption.onlyProfiles;
if (onlyProfiles) {
final currentProfile = _ref.read(currentProfileProvider);
if (currentProfile != null) {
_ref.read(currentProfileIdProvider.notifier).value = profiles.first.id;
}
return;
}
_ref.read(patchClashConfigProvider.notifier).value =
config.patchClashConfig;
_ref.read(appSettingProvider.notifier).value = config.appSetting;
_ref.read(currentProfileIdProvider.notifier).value =
config.currentProfileId;
_ref.read(appDAVSettingProvider.notifier).value = config.dav;
_ref.read(themeSettingProvider.notifier).value = config.themeProps;
_ref.read(windowSettingProvider.notifier).value = config.windowProps;
_ref.read(vpnSettingProvider.notifier).value = config.vpnProps;
_ref.read(proxiesStyleSettingProvider.notifier).value = config.proxiesStyle;
_ref.read(overrideDnsProvider.notifier).value = config.overrideDns;
_ref.read(networkSettingProvider.notifier).value = config.networkProps;
_ref.read(hotKeyActionsProvider.notifier).value = config.hotKeyActions;
} }
} }

View File

@@ -34,7 +34,24 @@ const desktopPlatforms = [
SupportPlatform.Windows, SupportPlatform.Windows,
]; ];
enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay } enum GroupType {
Selector,
URLTest,
Fallback,
LoadBalance,
Relay;
static GroupType parseProfileType(String type) {
return switch (type) {
"url-test" => URLTest,
"select" => Selector,
"fallback" => Fallback,
"load-balance" => LoadBalance,
"relay" => Relay,
String() => throw UnimplementedError(),
};
}
}
enum GroupName { GLOBAL, Proxy, Auto, Fallback } enum GroupName { GLOBAL, Proxy, Auto, Fallback }
@@ -45,7 +62,7 @@ extension GroupTypeExtension on GroupType {
) )
.toList(); .toList();
bool get isURLTestOrFallback { bool get isComputedSelected {
return [GroupType.URLTest, GroupType.Fallback].contains(this); return [GroupType.URLTest, GroupType.Fallback].contains(this);
} }
@@ -100,15 +117,12 @@ enum AppMessageType {
log, log,
delay, delay,
request, request,
started,
loaded, loaded,
} }
enum ServiceMessageType { enum InvokeMessageType {
protect, protect,
process, process,
started,
loaded,
} }
enum FindProcessMode { always, off } enum FindProcessMode { always, off }
@@ -141,6 +155,13 @@ enum DnsMode {
hosts hosts
} }
enum ExternalControllerStatus {
@JsonValue("")
close,
@JsonValue("127.0.0.1:9090")
open
}
enum KeyboardModifier { enum KeyboardModifier {
alt([ alt([
PhysicalKeyboardKey.altLeft, PhysicalKeyboardKey.altLeft,
@@ -241,6 +262,18 @@ enum ActionMethod {
stopListener, stopListener,
getCountryCode, getCountryCode,
getMemory, getMemory,
getProfile,
///Android,
setFdMap,
setProcessMap,
setState,
startTun,
stopTun,
getRunTime,
updateDns,
getAndroidVpnOptions,
getCurrentProfileName,
} }
enum AuthorizeCode { none, success, error } enum AuthorizeCode { none, success, error }
@@ -262,7 +295,11 @@ enum DebounceTag {
handleWill, handleWill,
updateDelay, updateDelay,
vpnTip, vpnTip,
autoLaunch autoLaunch,
renderPause,
updatePageIndex,
pageChange,
proxiesTabChange,
} }
enum DashboardWidget { enum DashboardWidget {
@@ -333,3 +370,19 @@ enum DashboardWidget {
return dashboardWidgets[index]; return dashboardWidgets[index];
} }
} }
enum GeodataLoader {
standard,
memconservative,
}
enum PageLabel {
dashboard,
proxies,
profiles,
tools,
logs,
requests,
resources,
connections,
}

View File

@@ -4,41 +4,50 @@ import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class AccessFragment extends StatefulWidget { class AccessFragment extends ConsumerStatefulWidget {
const AccessFragment({super.key}); const AccessFragment({super.key});
@override @override
State<AccessFragment> createState() => _AccessFragmentState(); ConsumerState<AccessFragment> createState() => _AccessFragmentState();
} }
class _AccessFragmentState extends State<AccessFragment> { class _AccessFragmentState extends ConsumerState<AccessFragment> {
List<String> acceptList = []; List<String> acceptList = [];
List<String> rejectList = []; List<String> rejectList = [];
late ScrollController _controller;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_updateInitList(); _updateInitList();
_controller = ScrollController();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appController.appState; final appState = globalState.appState;
if (appState.packages.isEmpty) { if (appState.packages.isEmpty) {
Future.delayed(const Duration(milliseconds: 300), () async { Future.delayed(const Duration(milliseconds: 300), () async {
appState.packages = await app?.getPackages() ?? []; ref.read(packagesProvider.notifier).value =
await app?.getPackages() ?? [];
}); });
} }
}); });
} }
@override
void dispose() {
_controller.dispose();
super.dispose();
}
_updateInitList() { _updateInitList() {
final accessControl = globalState.appController.config.accessControl; acceptList = globalState.config.vpnProps.accessControl.acceptList;
acceptList = accessControl.acceptList; rejectList = globalState.config.vpnProps.accessControl.rejectList;
rejectList = accessControl.rejectList;
} }
Widget _buildSearchButton() { Widget _buildSearchButton() {
@@ -51,9 +60,13 @@ class _AccessFragmentState extends State<AccessFragment> {
acceptList: acceptList, acceptList: acceptList,
rejectList: rejectList, rejectList: rejectList,
), ),
).then((_) => setState(() { ).then(
_updateInitList(); (_) => setState(
})); () {
_updateInitList();
},
),
);
}, },
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
); );
@@ -69,28 +82,29 @@ class _AccessFragmentState extends State<AccessFragment> {
return IconButton( return IconButton(
tooltip: tooltip, tooltip: tooltip,
onPressed: () { onPressed: () {
final config = globalState.appController.config; ref.read(vpnSettingProvider.notifier).updateState((state) {
final isAccept = final isAccept =
config.accessControl.mode == AccessControlMode.acceptSelected; state.accessControl.mode == AccessControlMode.acceptSelected;
if (isSelectedAll) { if (isSelectedAll) {
config.accessControl = switch (isAccept) { return switch (isAccept) {
true => config.accessControl.copyWith( true => state.copyWith.accessControl(
acceptList: [], acceptList: [],
), ),
false => config.accessControl.copyWith( false => state.copyWith.accessControl(
rejectList: [], rejectList: [],
), ),
}; };
} else { } else {
config.accessControl = switch (isAccept) { return switch (isAccept) {
true => config.accessControl.copyWith( true => state.copyWith.accessControl(
acceptList: allValueList, acceptList: allValueList,
), ),
false => config.accessControl.copyWith( false => state.copyWith.accessControl(
rejectList: allValueList, rejectList: allValueList,
), ),
}; };
} }
});
}, },
icon: isSelectedAll icon: isSelectedAll
? const Icon(Icons.deselect) ? const Icon(Icons.deselect)
@@ -98,218 +112,239 @@ class _AccessFragmentState extends State<AccessFragment> {
); );
} }
Widget _buildSettingButton() { _intelligentSelected() async {
return IconButton( final appState = globalState.appState;
onPressed: () { final config = globalState.config;
showSheet( final accessControl = config.vpnProps.accessControl;
title: appLocalizations.proxiesSetting, final packageNames = appState.packages
context: context, .where(
body: AccessControlWidget( (item) =>
context: context, accessControl.isFilterSystemApp ? item.isSystem == false : true,
)
.map((item) => item.packageName);
final commonScaffoldState = context.commonScaffoldState;
if (commonScaffoldState?.mounted != true) return;
final selectedPackageNames =
(await commonScaffoldState?.loadingRun<List<String>>(
() async {
return await app?.getChinaPackageNames() ?? [];
},
))
?.toSet() ??
{};
final acceptList = packageNames
.where((item) => !selectedPackageNames.contains(item))
.toList();
final rejectList = packageNames
.where((item) => selectedPackageNames.contains(item))
.toList();
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith.accessControl(
acceptList: acceptList,
rejectList: rejectList,
), ),
); );
}
Widget _buildSettingButton() {
return IconButton(
onPressed: () async {
final res = await showSheet<int>(
title: appLocalizations.proxiesSetting,
context: context,
body: AccessControlPanel(),
);
if (res == 1) {
_intelligentSelected();
}
}, },
icon: const Icon(Icons.tune), icon: const Icon(Icons.tune),
); );
} }
_handleSelected(List<String> valueList, Package package, bool? value) {
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
ref.read(vpnSettingProvider.notifier).updateState((state) {
return switch (
state.accessControl.mode == AccessControlMode.acceptSelected) {
true => state.copyWith.accessControl(
acceptList: valueList,
),
false => state.copyWith.accessControl(
rejectList: valueList,
),
};
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<Config, bool>( final state = ref.watch(packageListSelectorStateProvider);
selector: (_, config) => config.isAccessControl, final accessControl = state.accessControl;
builder: (_, isAccessControl, child) { final accessControlMode = accessControl.mode;
return Column( final packages = state.getList(
mainAxisSize: MainAxisSize.max, accessControlMode == AccessControlMode.acceptSelected
children: [ ? acceptList
Flexible( : rejectList,
flex: 0, );
child: ListItem.switchItem( final currentList = accessControl.currentList;
title: Text(appLocalizations.appAccessControl), final packageNameList = packages.map((e) => e.packageName).toList();
delegate: SwitchDelegate( final valueList = currentList.intersection(packageNameList);
value: isAccessControl, final describe = accessControlMode == AccessControlMode.acceptSelected
onChanged: (isAccessControl) { ? appLocalizations.accessControlAllowDesc
final config = context.read<Config>(); : appLocalizations.accessControlNotAllowDesc;
config.isAccessControl = isAccessControl; return Column(
}, mainAxisSize: MainAxisSize.max,
), children: [
), Flexible(
flex: 0,
child: ListItem.switchItem(
title: Text(appLocalizations.appAccessControl),
delegate: SwitchDelegate(
value: accessControl.enable,
onChanged: (enable) {
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith.accessControl(
enable: enable,
),
);
},
), ),
const Padding( ),
padding: EdgeInsets.symmetric(horizontal: 16), ),
child: Divider( const Padding(
height: 12, padding: EdgeInsets.symmetric(horizontal: 16),
), child: Divider(
), height: 12,
Flexible( ),
child: child!, ),
), Flexible(
], child: DisabledMask(
); status: !accessControl.enable,
}, child: Column(
child: Selector<AppState, List<Package>>( children: [
selector: (_, appState) => appState.packages, ActivateBox(
builder: (_, packages, ___) { active: accessControl.enable,
return Selector2<AppState, Config, PackageListSelectorState>( child: Padding(
selector: (_, appState, config) => PackageListSelectorState( padding: const EdgeInsets.only(
accessControl: config.accessControl, top: 4,
isAccessControl: config.isAccessControl, bottom: 4,
packages: appState.packages, left: 16,
), right: 8,
builder: (context, state, __) { ),
final accessControl = state.accessControl; child: Row(
final isAccessControl = state.isAccessControl; mainAxisAlignment: MainAxisAlignment.spaceBetween,
final accessControlMode = accessControl.mode; mainAxisSize: MainAxisSize.max,
final packages = state.getList( children: [
accessControlMode == AccessControlMode.acceptSelected Expanded(
? acceptList child: IntrinsicHeight(
: rejectList, child: Column(
); mainAxisSize: MainAxisSize.max,
final currentList = accessControl.currentList; crossAxisAlignment: CrossAxisAlignment.start,
final packageNameList =
packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList);
final describe =
accessControlMode == AccessControlMode.acceptSelected
? appLocalizations.accessControlAllowDesc
: appLocalizations.accessControlNotAllowDesc;
return DisabledMask(
status: !isAccessControl,
child: Column(
children: [
ActivateBox(
active: isAccessControl,
child: Padding(
padding: const EdgeInsets.only(
top: 4,
bottom: 4,
left: 16,
right: 8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: IntrinsicHeight(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Row(
children: [
Flexible(
child: Text(
appLocalizations.selected,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
const Flexible(
child: SizedBox(
width: 8,
),
),
Flexible(
child: Text(
"${valueList.length}",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
],
),
),
Flexible(
child: Text(describe),
)
],
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
Flexible( Expanded(
child: _buildSearchButton(), child: Row(
), children: [
Flexible( Flexible(
child: _buildSelectedAllButton( child: Text(
isSelectedAll: valueList.length == appLocalizations.selected,
packageNameList.length, style: Theme.of(context)
allValueList: packageNameList, .textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
const Flexible(
child: SizedBox(
width: 8,
),
),
Flexible(
child: Text(
"${valueList.length}",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
],
), ),
), ),
Flexible( Flexible(
child: _buildSettingButton(), child: Text(describe),
), )
], ],
), ),
),
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Flexible(
child: _buildSearchButton(),
),
Flexible(
child: _buildSelectedAllButton(
isSelectedAll:
valueList.length == packageNameList.length,
allValueList: packageNameList,
),
),
Flexible(
child: _buildSettingButton(),
),
], ],
), ),
), ],
), ),
Expanded( ),
flex: 1,
child: packages.isEmpty
? const Center(
child: CircularProgressIndicator(),
)
: ListView.builder(
itemCount: packages.length,
itemBuilder: (_, index) {
final package = packages[index];
return PackageListItem(
key: Key(package.packageName),
package: package,
value:
valueList.contains(package.packageName),
isActive: isAccessControl,
onChanged: (value) {
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
final config =
globalState.appController.config;
if (accessControlMode ==
AccessControlMode.acceptSelected) {
config.accessControl =
config.accessControl.copyWith(
acceptList: valueList,
);
} else {
config.accessControl =
config.accessControl.copyWith(
rejectList: valueList,
);
}
},
);
},
),
),
],
), ),
); Expanded(
}, flex: 1,
); child: packages.isEmpty
}, ? const Center(
), child: CircularProgressIndicator(),
)
: 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: accessControl.enable,
onChanged: (value) {
_handleSelected(valueList, package, value);
},
);
},
),
),
),
],
),
),
),
],
); );
} }
} }
@@ -330,45 +365,47 @@ class PackageListItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ActivateBox( return FadeScaleEnterBox(
active: isActive, child: ActivateBox(
child: ListItem.checkbox( active: isActive,
leading: SizedBox( child: ListItem.checkbox(
width: 48, leading: SizedBox(
height: 48, width: 48,
child: FutureBuilder<ImageProvider?>( height: 48,
future: app?.getPackageIcon(package.packageName), child: FutureBuilder<ImageProvider?>(
builder: (_, snapshot) { future: app?.getPackageIcon(package.packageName),
if (!snapshot.hasData && snapshot.data == null) { builder: (_, snapshot) {
return Container(); if (!snapshot.hasData && snapshot.data == null) {
} else { return Container();
return Image( } else {
image: snapshot.data!, return Image(
gaplessPlayback: true, image: snapshot.data!,
width: 48, gaplessPlayback: true,
height: 48, width: 48,
); height: 48,
} );
}, }
},
),
), ),
), title: Text(
title: Text( package.label,
package.label, style: const TextStyle(
style: const TextStyle( overflow: TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis, ),
maxLines: 1,
), ),
maxLines: 1, subtitle: Text(
), package.packageName,
subtitle: Text( style: const TextStyle(
package.packageName, overflow: TextOverflow.ellipsis,
style: const TextStyle( ),
overflow: TextOverflow.ellipsis, maxLines: 1,
),
delegate: CheckboxDelegate(
value: value,
onChanged: onChanged,
), ),
maxLines: 1,
),
delegate: CheckboxDelegate(
value: value,
onChanged: onChanged,
), ),
), ),
); );
@@ -413,15 +450,31 @@ class AccessControlSearchDelegate extends SearchDelegate {
); );
} }
_handleSelected(
WidgetRef ref, List<String> valueList, Package package, bool? value) {
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
ref.read(vpnSettingProvider.notifier).updateState((state) {
return switch (
state.accessControl.mode == AccessControlMode.acceptSelected) {
true => state.copyWith.accessControl(
acceptList: valueList,
),
false => state.copyWith.accessControl(
rejectList: valueList,
),
};
});
}
Widget _packageList() { Widget _packageList() {
final lowQuery = query.toLowerCase(); final lowQuery = query.toLowerCase();
return Selector2<AppState, Config, PackageListSelectorState>( return Consumer(
selector: (_, appState, config) => PackageListSelectorState( builder: (context, ref, __) {
packages: appState.packages, final state = ref.watch(packageListSelectorStateProvider);
accessControl: config.accessControl,
isAccessControl: config.isAccessControl,
),
builder: (context, state, __) {
final accessControl = state.accessControl; final accessControl = state.accessControl;
final accessControlMode = accessControl.mode; final accessControlMode = accessControl.mode;
final packages = state.getList( final packages = state.getList(
@@ -436,7 +489,7 @@ class AccessControlSearchDelegate extends SearchDelegate {
package.packageName.contains(lowQuery), package.packageName.contains(lowQuery),
) )
.toList(); .toList();
final isAccessControl = state.isAccessControl; final isAccessControl = state.accessControl.enable;
final currentList = accessControl.currentList; final currentList = accessControl.currentList;
final packageNameList = packages.map((e) => e.packageName).toList(); final packageNameList = packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList); final valueList = currentList.intersection(packageNameList);
@@ -452,21 +505,12 @@ class AccessControlSearchDelegate extends SearchDelegate {
value: valueList.contains(package.packageName), value: valueList.contains(package.packageName),
isActive: isAccessControl, isActive: isAccessControl,
onChanged: (value) { onChanged: (value) {
if (value == true) { _handleSelected(
valueList.add(package.packageName); ref,
} else { valueList,
valueList.remove(package.packageName); package,
} value,
final config = globalState.appController.config; );
if (accessControlMode == AccessControlMode.acceptSelected) {
config.accessControl = config.accessControl.copyWith(
acceptList: valueList,
);
} else {
config.accessControl = config.accessControl.copyWith(
rejectList: valueList,
);
}
}, },
); );
}, },
@@ -487,14 +531,16 @@ class AccessControlSearchDelegate extends SearchDelegate {
} }
} }
class AccessControlWidget extends StatelessWidget { class AccessControlPanel extends ConsumerStatefulWidget {
final BuildContext context; const AccessControlPanel({
const AccessControlWidget({
super.key, super.key,
required this.context,
}); });
@override
ConsumerState createState() => _AccessControlPanelState();
}
class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
IconData _getIconWithAccessControlMode(AccessControlMode mode) { IconData _getIconWithAccessControlMode(AccessControlMode mode) {
return switch (mode) { return switch (mode) {
AccessControlMode.acceptSelected => Icons.adjust_outlined, AccessControlMode.acceptSelected => Icons.adjust_outlined,
@@ -539,9 +585,11 @@ class AccessControlWidget extends StatelessWidget {
SingleChildScrollView( SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Selector<Config, AccessControlMode>( child: Consumer(
selector: (_, config) => config.accessControl.mode, builder: (_, ref, __) {
builder: (_, accessControlMode, __) { final accessControlMode = ref.watch(
vpnSettingProvider.select((state) => state.accessControl.mode),
);
return Wrap( return Wrap(
spacing: 16, spacing: 16,
children: [ children: [
@@ -553,10 +601,11 @@ class AccessControlWidget extends StatelessWidget {
), ),
isSelected: accessControlMode == item, isSelected: accessControlMode == item,
onPressed: () { onPressed: () {
final config = globalState.appController.config; ref.read(vpnSettingProvider.notifier).updateState(
config.accessControl = config.accessControl.copyWith( (state) => state.copyWith.accessControl(
mode: item, mode: item,
); ),
);
}, },
) )
], ],
@@ -575,9 +624,11 @@ class AccessControlWidget extends StatelessWidget {
SingleChildScrollView( SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Selector<Config, AccessSortType>( child: Consumer(
selector: (_, config) => config.accessControl.sort, builder: (_, ref, __) {
builder: (_, accessSortType, __) { final accessSortType = ref.watch(
vpnSettingProvider.select((state) => state.accessControl.sort),
);
return Wrap( return Wrap(
spacing: 16, spacing: 16,
children: [ children: [
@@ -589,10 +640,11 @@ class AccessControlWidget extends StatelessWidget {
), ),
isSelected: accessSortType == item, isSelected: accessSortType == item,
onPressed: () { onPressed: () {
final config = globalState.appController.config; ref.read(vpnSettingProvider.notifier).updateState(
config.accessControl = config.accessControl.copyWith( (state) => state.copyWith.accessControl(
sort: item, sort: item,
); ),
);
}, },
), ),
], ],
@@ -611,9 +663,12 @@ class AccessControlWidget extends StatelessWidget {
SingleChildScrollView( SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Selector<Config, bool>( child: Consumer(
selector: (_, config) => config.accessControl.isFilterSystemApp, builder: (_, ref, __) {
builder: (_, isFilterSystemApp, __) { final isFilterSystemApp = ref.watch(
vpnSettingProvider
.select((state) => state.accessControl.isFilterSystemApp),
);
return Wrap( return Wrap(
spacing: 16, spacing: 16,
children: [ children: [
@@ -622,10 +677,11 @@ class AccessControlWidget extends StatelessWidget {
_getTextWithIsFilterSystemApp(item), _getTextWithIsFilterSystemApp(item),
isSelected: isFilterSystemApp == item, isSelected: isFilterSystemApp == item,
onPressed: () { onPressed: () {
final config = globalState.appController.config; ref.read(vpnSettingProvider.notifier).updateState(
config.accessControl = config.accessControl.copyWith( (state) => state.copyWith.accessControl(
isFilterSystemApp: item, isFilterSystemApp: item,
); ),
);
}, },
) )
], ],
@@ -637,63 +693,35 @@ class AccessControlWidget extends StatelessWidget {
); );
} }
_intelligentSelected() async {
final appState = globalState.appController.appState;
final config = globalState.appController.config;
final accessControl = config.accessControl;
final packageNames = appState.packages
.where(
(item) =>
accessControl.isFilterSystemApp ? item.isSystem == false : true,
)
.map((item) => item.packageName);
Navigator.of(context).pop();
final commonScaffoldState = context.commonScaffoldState;
if (commonScaffoldState?.mounted != true) return;
final selectedPackageNames =
(await commonScaffoldState?.loadingRun<List<String>>(
() async {
return await app?.getChinaPackageNames() ?? [];
},
))
?.toSet() ??
{};
final acceptList = packageNames
.where((item) => !selectedPackageNames.contains(item))
.toList();
final rejectList = packageNames
.where((item) => selectedPackageNames.contains(item))
.toList();
config.accessControl = accessControl.copyWith(
acceptList: acceptList,
rejectList: rejectList,
);
}
_copyToClipboard() async { _copyToClipboard() async {
await globalState.safeRun(() { await globalState.safeRun(() {
final data = globalState.appController.config.accessControl.toJson(); final data = globalState.config.vpnProps.accessControl.toJson();
Clipboard.setData( Clipboard.setData(
ClipboardData( ClipboardData(
text: json.encode(data), text: json.encode(data),
), ),
); );
}); });
if (!context.mounted) return; if (!mounted) return;
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
_pasteToClipboard() async { _pasteToClipboard() async {
await globalState.safeRun(() async { await globalState.safeRun(
final config = globalState.appController.config; () async {
final data = await Clipboard.getData('text/plain'); final data = await Clipboard.getData('text/plain');
final text = data?.text; final text = data?.text;
if (text == null) return; if (text == null) return;
config.accessControl = AccessControl.fromJson( ref.read(vpnSettingProvider.notifier).updateState(
json.decode(text), (state) => state.copyWith(
); accessControl: AccessControl.fromJson(
}); json.decode(text),
if (!context.mounted) return; ),
),
);
},
);
if (!mounted) return;
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
@@ -712,7 +740,9 @@ class AccessControlWidget extends StatelessWidget {
CommonChip( CommonChip(
avatar: const Icon(Icons.auto_awesome), avatar: const Icon(Icons.auto_awesome),
label: appLocalizations.intelligentSelected, label: appLocalizations.intelligentSelected,
onPressed: _intelligentSelected, onPressed: () {
Navigator.of(context).pop(1);
},
), ),
CommonChip( CommonChip(
avatar: const Icon(Icons.paste), avatar: const Icon(Icons.paste),

View File

@@ -1,61 +1,258 @@
import 'dart:io'; import 'dart:io';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class CloseConnectionsSwitch extends StatelessWidget { class CloseConnectionsItem extends ConsumerWidget {
const CloseConnectionsSwitch({super.key}); const CloseConnectionsItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<Config, bool>( final closeConnections = ref.watch(
selector: (_, config) => config.appSetting.closeConnections, appSettingProvider.select((state) => state.closeConnections),
builder: (_, closeConnections, __) { );
return ListItem.switchItem( return ListItem.switchItem(
title: Text(appLocalizations.autoCloseConnections), title: Text(appLocalizations.autoCloseConnections),
subtitle: Text(appLocalizations.autoCloseConnectionsDesc), subtitle: Text(appLocalizations.autoCloseConnectionsDesc),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: closeConnections, value: closeConnections,
onChanged: (value) async { onChanged: (value) async {
final config = globalState.appController.config; ref.read(appSettingProvider.notifier).updateState(
config.appSetting = config.appSetting.copyWith( (state) => state.copyWith(
closeConnections: value, closeConnections: value,
),
); );
}, },
), ),
);
},
); );
} }
} }
class UsageSwitch extends StatelessWidget { class UsageItem extends ConsumerWidget {
const UsageSwitch({super.key}); const UsageItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<Config, bool>( final onlyStatisticsProxy = ref.watch(
selector: (_, config) => config.appSetting.onlyProxy, appSettingProvider.select((state) => state.onlyStatisticsProxy),
builder: (_, onlyProxy, __) { );
return ListItem.switchItem( return ListItem.switchItem(
title: Text(appLocalizations.onlyStatisticsProxy), title: Text(appLocalizations.onlyStatisticsProxy),
subtitle: Text(appLocalizations.onlyStatisticsProxyDesc), subtitle: Text(appLocalizations.onlyStatisticsProxyDesc),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: onlyProxy, value: onlyStatisticsProxy,
onChanged: (bool value) async { onChanged: (bool value) async {
final config = globalState.appController.config; ref.read(appSettingProvider.notifier).updateState(
config.appSetting = config.appSetting.copyWith( (state) => state.copyWith(
onlyProxy: value, onlyStatisticsProxy: value,
),
); );
}, },
), ),
); );
}, }
}
class MinimizeItem extends ConsumerWidget {
const MinimizeItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final minimizeOnExit = ref.watch(
appSettingProvider.select((state) => state.minimizeOnExit),
);
return ListItem.switchItem(
title: Text(appLocalizations.minimizeOnExit),
subtitle: Text(appLocalizations.minimizeOnExitDesc),
delegate: SwitchDelegate(
value: minimizeOnExit,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
minimizeOnExit: value,
),
);
},
),
);
}
}
class AutoLaunchItem extends ConsumerWidget {
const AutoLaunchItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final autoLaunch = ref.watch(
appSettingProvider.select((state) => state.autoLaunch),
);
return ListItem.switchItem(
title: Text(appLocalizations.autoLaunch),
subtitle: Text(appLocalizations.autoLaunchDesc),
delegate: SwitchDelegate(
value: autoLaunch,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
autoLaunch: value,
),
);
},
),
);
}
}
class SilentLaunchItem extends ConsumerWidget {
const SilentLaunchItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final silentLaunch = ref.watch(
appSettingProvider.select((state) => state.silentLaunch),
);
return ListItem.switchItem(
title: Text(appLocalizations.silentLaunch),
subtitle: Text(appLocalizations.silentLaunchDesc),
delegate: SwitchDelegate(
value: silentLaunch,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
silentLaunch: value,
),
);
},
),
);
}
}
class AutoRunItem extends ConsumerWidget {
const AutoRunItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final autoRun = ref.watch(
appSettingProvider.select((state) => state.autoRun),
);
return ListItem.switchItem(
title: Text(appLocalizations.autoRun),
subtitle: Text(appLocalizations.autoRunDesc),
delegate: SwitchDelegate(
value: autoRun,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
autoRun: value,
),
);
},
),
);
}
}
class HiddenItem extends ConsumerWidget {
const HiddenItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final hidden = ref.watch(
appSettingProvider.select((state) => state.hidden),
);
return ListItem.switchItem(
title: Text(appLocalizations.exclude),
subtitle: Text(appLocalizations.excludeDesc),
delegate: SwitchDelegate(
value: hidden,
onChanged: (value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
hidden: value,
),
);
},
),
);
}
}
class AnimateTabItem extends ConsumerWidget {
const AnimateTabItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isAnimateToPage = ref.watch(
appSettingProvider.select((state) => state.isAnimateToPage),
);
return ListItem.switchItem(
title: Text(appLocalizations.tabAnimation),
subtitle: Text(appLocalizations.tabAnimationDesc),
delegate: SwitchDelegate(
value: isAnimateToPage,
onChanged: (value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
isAnimateToPage: value,
),
);
},
),
);
}
}
class OpenLogsItem extends ConsumerWidget {
const OpenLogsItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final openLogs = ref.watch(
appSettingProvider.select((state) => state.openLogs),
);
return ListItem.switchItem(
title: Text(appLocalizations.logcat),
subtitle: Text(appLocalizations.logcatDesc),
delegate: SwitchDelegate(
value: openLogs,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
openLogs: value,
),
);
},
),
);
}
}
class AutoCheckUpdateItem extends ConsumerWidget {
const AutoCheckUpdateItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final autoCheckUpdate = ref.watch(
appSettingProvider.select((state) => state.autoCheckUpdate),
);
return ListItem.switchItem(
title: Text(appLocalizations.autoCheckUpdate),
subtitle: Text(appLocalizations.autoCheckUpdateDesc),
delegate: SwitchDelegate(
value: autoCheckUpdate,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
autoCheckUpdate: value,
),
);
},
),
); );
} }
} }
@@ -71,156 +268,20 @@ class ApplicationSettingFragment extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<Widget> items = [ List<Widget> items = [
Selector<Config, bool>( MinimizeItem(),
selector: (_, config) => config.appSetting.minimizeOnExit, if (system.isDesktop) ...[
builder: (_, isMinimizeOnExit, child) { AutoLaunchItem(),
return ListItem.switchItem( SilentLaunchItem(),
title: Text(appLocalizations.minimizeOnExit), ],
subtitle: Text(appLocalizations.minimizeOnExitDesc), AutoRunItem(),
delegate: SwitchDelegate( if (Platform.isAndroid) ...[
value: isMinimizeOnExit, HiddenItem(),
onChanged: (bool value) { AnimateTabItem(),
final config = context.read<Config>(); ],
config.appSetting = config.appSetting.copyWith( OpenLogsItem(),
minimizeOnExit: value, CloseConnectionsItem(),
); UsageItem(),
}, AutoCheckUpdateItem(),
),
);
},
),
if (system.isDesktop)
Selector<Config, bool>(
selector: (_, config) => config.appSetting.autoLaunch,
builder: (_, autoLaunch, child) {
return ListItem.switchItem(
title: Text(appLocalizations.autoLaunch),
subtitle: Text(appLocalizations.autoLaunchDesc),
delegate: SwitchDelegate(
value: autoLaunch,
onChanged: (bool value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
autoLaunch: value,
);
},
),
);
},
),
if (system.isDesktop)
Selector<Config, bool>(
selector: (_, config) => config.appSetting.silentLaunch,
builder: (_, silentLaunch, child) {
return ListItem.switchItem(
title: Text(appLocalizations.silentLaunch),
subtitle: Text(appLocalizations.silentLaunchDesc),
delegate: SwitchDelegate(
value: silentLaunch,
onChanged: (bool value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
silentLaunch: value,
);
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.appSetting.autoRun,
builder: (_, autoRun, child) {
return ListItem.switchItem(
title: Text(appLocalizations.autoRun),
subtitle: Text(appLocalizations.autoRunDesc),
delegate: SwitchDelegate(
value: autoRun,
onChanged: (bool value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
autoRun: value,
);
},
),
);
},
),
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.appSetting.hidden,
builder: (_, isExclude, child) {
return ListItem.switchItem(
title: Text(appLocalizations.exclude),
subtitle: Text(appLocalizations.excludeDesc),
delegate: SwitchDelegate(
value: isExclude,
onChanged: (value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
hidden: value,
);
},
),
);
},
),
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.appSetting.isAnimateToPage,
builder: (_, isAnimateToPage, child) {
return ListItem.switchItem(
title: Text(appLocalizations.tabAnimation),
subtitle: Text(appLocalizations.tabAnimationDesc),
delegate: SwitchDelegate(
value: isAnimateToPage,
onChanged: (value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
isAnimateToPage: value,
);
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.appSetting.openLogs,
builder: (_, openLogs, child) {
return ListItem.switchItem(
title: Text(appLocalizations.logcat),
subtitle: Text(appLocalizations.logcatDesc),
delegate: SwitchDelegate(
value: openLogs,
onChanged: (bool value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
openLogs: value,
);
},
),
);
},
),
const CloseConnectionsSwitch(),
const UsageSwitch(),
Selector<Config, bool>(
selector: (_, config) => config.appSetting.autoCheckUpdate,
builder: (_, autoCheckUpdate, child) {
return ListItem.switchItem(
title: Text(appLocalizations.autoCheckUpdate),
subtitle: Text(appLocalizations.autoCheckUpdateDesc),
delegate: SwitchDelegate(
value: autoCheckUpdate,
onChanged: (bool value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
autoCheckUpdate: value,
);
},
),
);
},
),
]; ];
return ListView.separated( return ListView.separated(
itemBuilder: (_, index) { itemBuilder: (_, index) {

View File

@@ -4,14 +4,15 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/dav_client.dart'; import 'package:fl_clash/common/dav_client.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/fade_box.dart'; import 'package:fl_clash/widgets/fade_box.dart';
import 'package:fl_clash/widgets/list.dart'; import 'package:fl_clash/widgets/list.dart';
import 'package:fl_clash/widgets/text.dart'; import 'package:fl_clash/widgets/text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class BackupAndRecovery extends StatelessWidget { class BackupAndRecovery extends ConsumerWidget {
const BackupAndRecovery({super.key}); const BackupAndRecovery({super.key});
_showAddWebDAV(DAV? dav) async { _showAddWebDAV(DAV? dav) async {
@@ -121,139 +122,140 @@ class BackupAndRecovery extends StatelessWidget {
_recoveryOnLocal(context, recoveryOption); _recoveryOnLocal(context, recoveryOption);
} }
@override _handleChange(String? value, WidgetRef ref) {
Widget build(BuildContext context) { if (value == null) {
return Selector<Config, DAV?>( return;
selector: (_, config) => config.dav, }
builder: (_, dav, __) { ref.read(appDAVSettingProvider.notifier).updateState(
final client = dav != null ? DAVClient(dav) : null; (state) => state?.copyWith(
return ListView( fileName: value,
children: [ ),
ListHeader(title: appLocalizations.remote),
if (dav == null)
ListItem(
leading: const Icon(Icons.account_box),
title: Text(appLocalizations.noInfo),
subtitle: Text(appLocalizations.pleaseBindWebDAV),
trailing: FilledButton.tonal(
onPressed: () {
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.bind,
),
),
)
else ...[
ListItem(
leading: const Icon(Icons.account_box),
title: TooltipText(
text: Text(
dav.user,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(appLocalizations.connectivity),
FutureBuilder<bool>(
future: client!.pingCompleter.future,
builder: (_, snapshot) {
return Center(
child: FadeBox(
child: snapshot.connectionState ==
ConnectionState.waiting
? const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 1,
),
)
: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: snapshot.data == true
? Colors.green
: Colors.red,
),
width: 12,
height: 12,
),
),
);
},
),
],
),
),
trailing: FilledButton.tonal(
onPressed: () {
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.edit,
),
),
),
const SizedBox(
height: 4,
),
ListItem.input(
title: Text(appLocalizations.file),
subtitle: Text(dav.fileName),
delegate: InputDelegate(
title: appLocalizations.file,
value: dav.fileName,
resetValue: defaultDavFileName,
onChanged: (String? value) {
if (value == null) {
return;
}
globalState.appController.config.dav =
globalState.appController.config.dav?.copyWith(
fileName: value,
);
},
),
),
ListItem(
onTap: () {
_backupOnWebDAV(context, client);
},
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.remoteBackupDesc),
),
ListItem(
onTap: () {
_handleRecoveryOnWebDAV(context, client);
},
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.remoteRecoveryDesc),
),
],
ListHeader(title: appLocalizations.local),
ListItem(
onTap: () {
_backupOnLocal(context);
},
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.localBackupDesc),
),
ListItem(
onTap: () {
_handleRecoveryOnLocal(context);
},
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.localRecoveryDesc),
),
],
); );
}, }
@override
Widget build(BuildContext context, ref) {
final dav = ref.watch(appDAVSettingProvider);
final client = dav != null ? DAVClient(dav) : null;
return ListView(
children: [
ListHeader(title: appLocalizations.remote),
if (dav == null)
ListItem(
leading: const Icon(Icons.account_box),
title: Text(appLocalizations.noInfo),
subtitle: Text(appLocalizations.pleaseBindWebDAV),
trailing: FilledButton.tonal(
onPressed: () {
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.bind,
),
),
)
else ...[
ListItem(
leading: const Icon(Icons.account_box),
title: TooltipText(
text: Text(
dav.user,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(appLocalizations.connectivity),
FutureBuilder<bool>(
future: client!.pingCompleter.future,
builder: (_, snapshot) {
return Center(
child: FadeBox(
child: snapshot.connectionState ==
ConnectionState.waiting
? const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 1,
),
)
: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: snapshot.data == true
? Colors.green
: Colors.red,
),
width: 12,
height: 12,
),
),
);
},
),
],
),
),
trailing: FilledButton.tonal(
onPressed: () {
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.edit,
),
),
),
const SizedBox(
height: 4,
),
ListItem.input(
title: Text(appLocalizations.file),
subtitle: Text(dav.fileName),
delegate: InputDelegate(
title: appLocalizations.file,
value: dav.fileName,
resetValue: defaultDavFileName,
onChanged: (value) {
_handleChange(value, ref);
},
),
),
ListItem(
onTap: () {
_backupOnWebDAV(context, client);
},
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.remoteBackupDesc),
),
ListItem(
onTap: () {
_handleRecoveryOnWebDAV(context, client);
},
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.remoteRecoveryDesc),
),
],
ListHeader(title: appLocalizations.local),
ListItem(
onTap: () {
_backupOnLocal(context);
},
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.localBackupDesc),
),
ListItem(
onTap: () {
_handleRecoveryOnLocal(context);
},
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.localRecoveryDesc),
),
],
); );
} }
} }
@@ -302,16 +304,16 @@ class _RecoveryOptionsDialogState extends State<RecoveryOptionsDialog> {
} }
} }
class WebDAVFormDialog extends StatefulWidget { class WebDAVFormDialog extends ConsumerStatefulWidget {
final DAV? dav; final DAV? dav;
const WebDAVFormDialog({super.key, this.dav}); const WebDAVFormDialog({super.key, this.dav});
@override @override
State<WebDAVFormDialog> createState() => _WebDAVFormDialogState(); ConsumerState<WebDAVFormDialog> createState() => _WebDAVFormDialogState();
} }
class _WebDAVFormDialogState extends State<WebDAVFormDialog> { class _WebDAVFormDialogState extends ConsumerState<WebDAVFormDialog> {
late TextEditingController uriController; late TextEditingController uriController;
late TextEditingController userController; late TextEditingController userController;
late TextEditingController passwordController; late TextEditingController passwordController;
@@ -328,7 +330,7 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
_submit() { _submit() {
if (!_formKey.currentState!.validate()) return; if (!_formKey.currentState!.validate()) return;
globalState.appController.config.dav = DAV( ref.read(appDAVSettingProvider.notifier).value = DAV(
uri: uriController.text, uri: uriController.text,
user: userController.text, user: userController.text,
password: passwordController.text, password: passwordController.text,
@@ -337,7 +339,7 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
} }
_delete() { _delete() {
globalState.appController.config.dav = null; ref.read(appDAVSettingProvider.notifier).value = null;
Navigator.pop(context); Navigator.pop(context);
} }

View File

@@ -1,208 +1,172 @@
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class OverrideItem extends StatelessWidget { class OverrideItem extends ConsumerWidget {
const OverrideItem({super.key}); const OverrideItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<Config, bool>( final override = ref.watch(overrideDnsProvider);
selector: (_, config) => config.overrideDns, return ListItem.switchItem(
builder: (_, override, __) { title: Text(appLocalizations.overrideDns),
return ListItem.switchItem( subtitle: Text(appLocalizations.overrideDnsDesc),
title: Text(appLocalizations.overrideDns), delegate: SwitchDelegate(
subtitle: Text(appLocalizations.overrideDnsDesc), value: override,
delegate: SwitchDelegate( onChanged: (bool value) async {
value: override, ref.read(overrideDnsProvider.notifier).value = value;
onChanged: (bool value) async { },
final config = globalState.appController.config; ),
config.overrideDns = value;
},
),
);
},
); );
} }
} }
class StatusItem extends StatelessWidget { class StatusItem extends ConsumerWidget {
const StatusItem({super.key}); const StatusItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final enable =
selector: (_, clashConfig) => clashConfig.dns.enable, ref.watch(patchClashConfigProvider.select((state) => state.dns.enable));
builder: (_, enable, __) { return ListItem.switchItem(
return ListItem.switchItem( title: Text(appLocalizations.status),
title: Text(appLocalizations.status), subtitle: Text(appLocalizations.statusDesc),
subtitle: Text(appLocalizations.statusDesc), delegate: SwitchDelegate(
delegate: SwitchDelegate( value: enable,
value: enable, onChanged: (bool value) async {
onChanged: (bool value) async { ref
final clashConfig = globalState.appController.clashConfig; .read(patchClashConfigProvider.notifier)
final dns = clashConfig.dns; .updateState((state) => state.copyWith.dns(enable: value));
clashConfig.dns = dns.copyWith( },
enable: value, ),
);
},
),
);
},
); );
} }
} }
class PreferH3Item extends StatelessWidget { class PreferH3Item extends ConsumerWidget {
const PreferH3Item({super.key}); const PreferH3Item({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final preferH3 = ref
selector: (_, clashConfig) => clashConfig.dns.preferH3, .watch(patchClashConfigProvider.select((state) => state.dns.preferH3));
builder: (_, preferH3, __) { return ListItem.switchItem(
return ListItem.switchItem( title: const Text("PreferH3"),
title: const Text("PreferH3"), subtitle: Text(appLocalizations.preferH3Desc),
subtitle: Text(appLocalizations.preferH3Desc), delegate: SwitchDelegate(
delegate: SwitchDelegate( value: preferH3,
value: preferH3, onChanged: (bool value) async {
onChanged: (bool value) async { ref
final clashConfig = globalState.appController.clashConfig; .read(patchClashConfigProvider.notifier)
final dns = clashConfig.dns; .updateState((state) => state.copyWith.dns(preferH3: value));
clashConfig.dns = dns.copyWith( },
preferH3: value, ),
);
},
),
);
},
); );
} }
} }
class IPv6Item extends StatelessWidget { class IPv6Item extends ConsumerWidget {
const IPv6Item({super.key}); const IPv6Item({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final ipv6 = ref.watch(
selector: (_, clashConfig) => clashConfig.dns.ipv6, patchClashConfigProvider.select((state) => state.dns.ipv6),
builder: (_, ipv6, __) { );
return ListItem.switchItem( return ListItem.switchItem(
title: const Text("IPv6"), title: const Text("IPv6"),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: ipv6, value: ipv6,
onChanged: (bool value) async { onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig; ref
final dns = clashConfig.dns; .read(patchClashConfigProvider.notifier)
clashConfig.dns = dns.copyWith( .updateState((state) => state.copyWith.dns(ipv6: value));
ipv6: value, },
); ),
},
),
);
},
); );
} }
} }
class RespectRulesItem extends StatelessWidget { class RespectRulesItem extends ConsumerWidget {
const RespectRulesItem({super.key}); const RespectRulesItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final respectRules = ref.watch(
selector: (_, clashConfig) => clashConfig.dns.respectRules, patchClashConfigProvider.select((state) => state.dns.respectRules),
builder: (_, respectRules, __) { );
return ListItem.switchItem( return ListItem.switchItem(
title: Text(appLocalizations.respectRules), title: Text(appLocalizations.respectRules),
subtitle: Text(appLocalizations.respectRulesDesc), subtitle: Text(appLocalizations.respectRulesDesc),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: respectRules, value: respectRules,
onChanged: (bool value) async { onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig; ref
final dns = clashConfig.dns; .read(patchClashConfigProvider.notifier)
clashConfig.dns = dns.copyWith( .updateState((state) => state.copyWith.dns(respectRules: value));
respectRules: value, },
); ),
},
),
);
},
); );
} }
} }
class DnsModeItem extends StatelessWidget { class DnsModeItem extends ConsumerWidget {
const DnsModeItem({super.key}); const DnsModeItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, DnsMode>( final enhancedMode = ref.watch(
selector: (_, clashConfig) => clashConfig.dns.enhancedMode, patchClashConfigProvider.select((state) => state.dns.enhancedMode),
builder: (_, enhancedMode, __) { );
return ListItem<DnsMode>.options( return ListItem<DnsMode>.options(
title: Text(appLocalizations.dnsMode), title: Text(appLocalizations.dnsMode),
subtitle: Text(enhancedMode.name), subtitle: Text(enhancedMode.name),
delegate: OptionsDelegate( delegate: OptionsDelegate(
title: appLocalizations.dnsMode, title: appLocalizations.dnsMode,
options: DnsMode.values, options: DnsMode.values,
onChanged: (value) { onChanged: (value) {
if (value == null) { if (value == null) {
return; return;
} }
final clashConfig = globalState.appController.clashConfig; ref
final dns = clashConfig.dns; .read(patchClashConfigProvider.notifier)
clashConfig.dns = dns.copyWith(enhancedMode: value); .updateState((state) => state.copyWith.dns(enhancedMode: value));
}, },
textBuilder: (dnsMode) => dnsMode.name, textBuilder: (dnsMode) => dnsMode.name,
value: enhancedMode, value: enhancedMode,
), ),
);
},
); );
} }
} }
class FakeIpRangeItem extends StatelessWidget { class FakeIpRangeItem extends ConsumerWidget {
const FakeIpRangeItem({super.key}); const FakeIpRangeItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, String>( final fakeIpRange = ref.watch(
selector: (_, clashConfig) => clashConfig.dns.fakeIpRange, patchClashConfigProvider.select((state) => state.dns.fakeIpRange),
builder: (_, fakeIpRange, __) { );
return ListItem.input( return ListItem.input(
title: Text(appLocalizations.fakeipRange), title: Text(appLocalizations.fakeipRange),
subtitle: Text(fakeIpRange), subtitle: Text(fakeIpRange),
delegate: InputDelegate( delegate: InputDelegate(
title: appLocalizations.fakeipRange, title: appLocalizations.fakeipRange,
value: fakeIpRange, value: fakeIpRange,
onChanged: (String? value) { onChanged: (String? value) {
if (value != null) { if (value == null) {
try { return;
final clashConfig = globalState.appController.clashConfig; }
clashConfig.dns = clashConfig.dns.copyWith( ref
fakeIpRange: value, .read(patchClashConfigProvider.notifier)
); .updateState((state) => state.copyWith.dns(fakeIpRange: value));
} catch (e) { },
globalState.showMessage( ),
title: appLocalizations.fakeipRange,
message: TextSpan(
text: e.toString(),
),
);
}
}
},
),
);
},
); );
} }
} }
@@ -217,20 +181,22 @@ class FakeIpFilterItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.fakeipFilter, title: appLocalizations.fakeipFilter,
widget: Selector<ClashConfig, List<String>>( widget: Consumer(
selector: (_, clashConfig) => clashConfig.dns.fakeIpFilter, builder: (_, ref, __) {
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next), final fakeIpFilter = ref.watch(
builder: (_, fakeIpFilter, __) { patchClashConfigProvider
.select((state) => state.dns.fakeIpFilter),
);
return ListPage( return ListPage(
title: appLocalizations.fakeipFilter, title: appLocalizations.fakeipFilter,
items: fakeIpFilter, items: fakeIpFilter,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
onChange: (items){ onChange: (items) {
final clashConfig = globalState.appController.clashConfig; ref
final dns = clashConfig.dns; .read(patchClashConfigProvider.notifier)
clashConfig.dns = dns.copyWith( .updateState((state) => state.copyWith.dns(
fakeIpFilter: List.from(items), fakeIpFilter: List.from(items),
); ));
}, },
); );
}, },
@@ -252,24 +218,24 @@ class DefaultNameserverItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.defaultNameserver, title: appLocalizations.defaultNameserver,
widget: Selector<ClashConfig, List<String>>( widget: Consumer(builder: (_, ref, __) {
selector: (_, clashConfig) => clashConfig.dns.defaultNameserver, final defaultNameserver = ref.watch(
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next), patchClashConfigProvider
builder: (_, defaultNameserver, __) { .select((state) => state.dns.defaultNameserver),
return ListPage( );
title: appLocalizations.defaultNameserver, return ListPage(
items: defaultNameserver, title: appLocalizations.defaultNameserver,
titleBuilder: (item) => Text(item), items: defaultNameserver,
onChange: (items){ titleBuilder: (item) => Text(item),
final clashConfig = globalState.appController.clashConfig; onChange: (items) {
final dns = clashConfig.dns; ref.read(patchClashConfigProvider.notifier).updateState(
clashConfig.dns = dns.copyWith( (state) => state.copyWith.dns(
defaultNameserver: List.from(items), defaultNameserver: List.from(items),
); ),
}, );
); },
}, );
), }),
extendPageWidth: 360, extendPageWidth: 360,
), ),
); );
@@ -287,78 +253,71 @@ class NameserverItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
title: appLocalizations.nameserver, title: appLocalizations.nameserver,
isBlur: false, isBlur: false,
widget: Selector<ClashConfig, List<String>>( widget: Consumer(builder: (_, ref, __) {
selector: (_, clashConfig) => clashConfig.dns.nameserver, final nameserver = ref.watch(
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next), patchClashConfigProvider.select((state) => state.dns.nameserver),
builder: (_, nameserver, __) { );
return ListPage( return ListPage(
title: "域名服务器", title: "域名服务器",
items: nameserver, items: nameserver,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
onChange: (items){ onChange: (items) {
final clashConfig = globalState.appController.clashConfig; ref.read(patchClashConfigProvider.notifier).updateState(
final dns = clashConfig.dns; (state) => state.copyWith.dns(
clashConfig.dns = dns.copyWith( nameserver: List.from(items),
nameserver: List.from(items), ),
); );
}, },
); );
}, }),
),
extendPageWidth: 360, extendPageWidth: 360,
), ),
); );
} }
} }
class UseHostsItem extends StatelessWidget { class UseHostsItem extends ConsumerWidget {
const UseHostsItem({super.key}); const UseHostsItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final useHosts = ref.watch(
selector: (_, clashConfig) => clashConfig.dns.useHosts, patchClashConfigProvider.select((state) => state.dns.useHosts),
builder: (_, useHosts, __) { );
return ListItem.switchItem( return ListItem.switchItem(
title: Text(appLocalizations.useHosts), title: Text(appLocalizations.useHosts),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: useHosts, value: useHosts,
onChanged: (bool value) async { onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig; ref
final dns = clashConfig.dns; .read(patchClashConfigProvider.notifier)
clashConfig.dns = dns.copyWith( .updateState((state) => state.copyWith.dns(useHosts: value));
useHosts: value, },
); ),
},
),
);
},
); );
} }
} }
class UseSystemHostsItem extends StatelessWidget { class UseSystemHostsItem extends ConsumerWidget {
const UseSystemHostsItem({super.key}); const UseSystemHostsItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final useSystemHosts = ref.watch(
selector: (_, clashConfig) => clashConfig.dns.useSystemHosts, patchClashConfigProvider.select((state) => state.dns.useSystemHosts),
builder: (_, useSystemHosts, __) { );
return ListItem.switchItem( return ListItem.switchItem(
title: Text(appLocalizations.useSystemHosts), title: Text(appLocalizations.useSystemHosts),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: useSystemHosts, value: useSystemHosts,
onChanged: (bool value) async { onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig; ref
final dns = clashConfig.dns; .read(patchClashConfigProvider.notifier)
clashConfig.dns = dns.copyWith( .updateState((state) => state.copyWith.dns(
useSystemHosts: value, useSystemHosts: value,
); ));
}, },
), ),
);
},
); );
} }
} }
@@ -374,26 +333,25 @@ class NameserverPolicyItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.nameserverPolicy, title: appLocalizations.nameserverPolicy,
widget: Selector<ClashConfig, Map<String, String>>( widget: Consumer(builder: (_, ref, __) {
selector: (_, clashConfig) => clashConfig.dns.nameserverPolicy, final nameserverPolicy = ref.watch(
shouldRebuild: (prev, next) => patchClashConfigProvider
!const MapEquality<String, String>().equals(prev, next), .select((state) => state.dns.nameserverPolicy),
builder: (_, nameserverPolicy, __) { );
return ListPage( return ListPage(
title: appLocalizations.nameserverPolicy, title: appLocalizations.nameserverPolicy,
items: nameserverPolicy.entries, items: nameserverPolicy.entries,
titleBuilder: (item) => Text(item.key), titleBuilder: (item) => Text(item.key),
subtitleBuilder: (item) => Text(item.value), subtitleBuilder: (item) => Text(item.value),
onChange: (items){ onChange: (items) {
final clashConfig = globalState.appController.clashConfig; ref.read(patchClashConfigProvider.notifier).updateState(
final dns = clashConfig.dns; (state) => state.copyWith.dns(
clashConfig.dns = dns.copyWith( nameserverPolicy: Map.fromEntries(items),
nameserverPolicy: Map.fromEntries(items), ),
); );
}, },
); );
}, }),
),
extendPageWidth: 360, extendPageWidth: 360,
), ),
); );
@@ -411,20 +369,22 @@ class ProxyServerNameserverItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.proxyNameserver, title: appLocalizations.proxyNameserver,
widget: Selector<ClashConfig, List<String>>( widget: Consumer(
selector: (_, clashConfig) => clashConfig.dns.proxyServerNameserver, builder: (_, ref, __) {
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next), final proxyServerNameserver = ref.watch(
builder: (_, proxyServerNameserver, __) { patchClashConfigProvider
.select((state) => state.dns.proxyServerNameserver),
);
return ListPage( return ListPage(
title: appLocalizations.proxyNameserver, title: appLocalizations.proxyNameserver,
items: proxyServerNameserver, items: proxyServerNameserver,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
onChange: (items){ onChange: (items) {
final clashConfig = globalState.appController.clashConfig; ref.read(patchClashConfigProvider.notifier).updateState(
final dns = clashConfig.dns; (state) => state.copyWith.dns(
clashConfig.dns = dns.copyWith( proxyServerNameserver: List.from(items),
proxyServerNameserver: List.from(items), ),
); );
}, },
); );
}, },
@@ -446,93 +406,80 @@ class FallbackItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.fallback, title: appLocalizations.fallback,
widget: Selector<ClashConfig, List<String>>( widget: Consumer(builder: (_, ref, __) {
selector: (_, clashConfig) => clashConfig.dns.fallback, final fallback = ref.watch(
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next), patchClashConfigProvider.select((state) => state.dns.fallback),
builder: (_, fallback, __) { );
return ListPage( return ListPage(
title: appLocalizations.fallback, title: appLocalizations.fallback,
items: fallback, items: fallback,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
onChange: (items){ onChange: (items) {
final clashConfig = globalState.appController.clashConfig; ref.read(patchClashConfigProvider.notifier).updateState(
final dns = clashConfig.dns; (state) => state.copyWith.dns(
clashConfig.dns = dns.copyWith( fallback: List.from(items),
fallback: List.from(items), ),
); );
}, },
); );
}, }),
),
extendPageWidth: 360, extendPageWidth: 360,
), ),
); );
} }
} }
class GeoipItem extends StatelessWidget { class GeoipItem extends ConsumerWidget {
const GeoipItem({super.key}); const GeoipItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final geoip = ref.watch(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoip, patchClashConfigProvider
builder: (_, geoip, __) { .select((state) => state.dns.fallbackFilter.geoip),
return ListItem.switchItem( );
title: const Text("Geoip"), return ListItem.switchItem(
delegate: SwitchDelegate( title: const Text("Geoip"),
value: geoip, delegate: SwitchDelegate(
onChanged: (bool value) async { value: geoip,
final clashConfig = globalState.appController.clashConfig; onChanged: (bool value) async {
final dns = clashConfig.dns; ref
clashConfig.dns = dns.copyWith( .read(patchClashConfigProvider.notifier)
fallbackFilter: dns.fallbackFilter.copyWith(geoip: value), .updateState((state) => state.copyWith.dns.fallbackFilter(
); geoip: value,
}, ));
), },
); ),
},
); );
} }
} }
class GeoipCodeItem extends StatelessWidget { class GeoipCodeItem extends ConsumerWidget {
const GeoipCodeItem({super.key}); const GeoipCodeItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, String>( final geoipCode = ref.watch(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoipCode, patchClashConfigProvider
builder: (_, geoipCode, __) { .select((state) => state.dns.fallbackFilter.geoipCode),
return ListItem.input( );
title: Text(appLocalizations.geoipCode), return ListItem.input(
subtitle: Text(geoipCode), title: Text(appLocalizations.geoipCode),
delegate: InputDelegate( subtitle: Text(geoipCode),
title: appLocalizations.geoipCode, delegate: InputDelegate(
value: geoipCode, title: appLocalizations.geoipCode,
onChanged: (String? value) { value: geoipCode,
if (value != null) { onChanged: (String? value) {
try { if (value == null) {
final clashConfig = globalState.appController.clashConfig; return;
final dns = clashConfig.dns; }
clashConfig.dns = dns.copyWith( ref.read(patchClashConfigProvider.notifier).updateState(
fallbackFilter: dns.fallbackFilter.copyWith( (state) => state.copyWith.dns.fallbackFilter(
geoipCode: value, geoipCode: value,
), ),
); );
} catch (e) { },
globalState.showMessage( ),
title: appLocalizations.geoipCode,
message: TextSpan(
text: e.toString(),
),
);
}
}
},
),
);
},
); );
} }
} }
@@ -547,26 +494,24 @@ class GeositeItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: "Geosite", title: "Geosite",
widget: Selector<ClashConfig, List<String>>( widget: Consumer(builder: (_, ref, __) {
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geosite, final geosite = ref.watch(
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next), patchClashConfigProvider
builder: (_, geosite, __) { .select((state) => state.dns.fallbackFilter.geosite),
return ListPage( );
title: "Geosite", return ListPage(
items: geosite, title: "Geosite",
titleBuilder: (item) => Text(item), items: geosite,
onChange: (items){ titleBuilder: (item) => Text(item),
final clashConfig = globalState.appController.clashConfig; onChange: (items) {
final dns = clashConfig.dns; ref.read(patchClashConfigProvider.notifier).updateState(
clashConfig.dns = dns.copyWith( (state) => state.copyWith.dns.fallbackFilter(
fallbackFilter: dns.fallbackFilter.copyWith( geosite: List.from(items),
geosite: List.from(items), ),
), );
); },
}, );
); }),
},
),
extendPageWidth: 360, extendPageWidth: 360,
), ),
); );
@@ -583,26 +528,24 @@ class IpcidrItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.ipcidr, title: appLocalizations.ipcidr,
widget: Selector<ClashConfig, List<String>>( widget: Consumer(builder: (_, ref, ___) {
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.ipcidr, final ipcidr = ref.watch(
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next), patchClashConfigProvider
builder: (_, ipcidr, __) { .select((state) => state.dns.fallbackFilter.ipcidr),
return ListPage( );
title: appLocalizations.ipcidr, return ListPage(
items: ipcidr, title: appLocalizations.ipcidr,
titleBuilder: (item) => Text(item), items: ipcidr,
onChange: (items){ titleBuilder: (item) => Text(item),
final clashConfig = globalState.appController.clashConfig; onChange: (items) {
final dns = clashConfig.dns; ref
clashConfig.dns = dns.copyWith( .read(patchClashConfigProvider.notifier)
fallbackFilter: dns.fallbackFilter.copyWith( .updateState((state) => state.copyWith.dns.fallbackFilter(
ipcidr: List.from(items), ipcidr: List.from(items),
), ));
); },
}, );
); }),
},
),
extendPageWidth: 360, extendPageWidth: 360,
), ),
); );
@@ -619,26 +562,24 @@ class DomainItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.domain, title: appLocalizations.domain,
widget: Selector<ClashConfig, List<String>>( widget: Consumer(builder: (_, ref, __) {
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.domain, final domain = ref.watch(
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next), patchClashConfigProvider
builder: (_, domain, __) { .select((state) => state.dns.fallbackFilter.domain),
return ListPage( );
title: appLocalizations.domain, return ListPage(
items: domain, title: appLocalizations.domain,
titleBuilder: (item) => Text(item), items: domain,
onChange: (items){ titleBuilder: (item) => Text(item),
final clashConfig = globalState.appController.clashConfig; onChange: (items) {
final dns = clashConfig.dns; ref.read(patchClashConfigProvider.notifier).updateState(
clashConfig.dns = dns.copyWith( (state) => state.copyWith.dns.fallbackFilter(
fallbackFilter: dns.fallbackFilter.copyWith( domain: List.from(items),
domain: List.from(items), ),
), );
); },
}, );
); }),
},
),
extendPageWidth: 360, extendPageWidth: 360,
), ),
); );
@@ -700,25 +641,29 @@ const dnsItems = <Widget>[
FallbackFilterOptions(), FallbackFilterOptions(),
]; ];
class DnsListView extends StatelessWidget { class DnsListView extends ConsumerWidget {
const DnsListView({super.key}); const DnsListView({super.key});
_initActions(BuildContext context) { _initActions(BuildContext context, WidgetRef ref) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState = context.commonScaffoldState?.actions = [
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton( IconButton(
onPressed: () { onPressed: () async {
globalState.showMessage( final res = await globalState.showMessage(
title: appLocalizations.reset, title: appLocalizations.reset,
message: TextSpan( message: TextSpan(
text: appLocalizations.resetTip, text: appLocalizations.resetTip,
), ),
onTab: () { );
globalState.appController.clashConfig.dns = defaultDns; if (res != true) {
Navigator.of(context).pop(); return;
}); }
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
dns: defaultDns,
),
);
}, },
tooltip: appLocalizations.reset, tooltip: appLocalizations.reset,
icon: const Icon( icon: const Icon(
@@ -730,8 +675,8 @@ class DnsListView extends StatelessWidget {
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
_initActions(context); _initActions(context, ref);
return generateListView( return generateListView(
dnsItems, dnsItems,
); );

View File

@@ -1,198 +1,190 @@
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class LogLevelItem extends StatelessWidget { class LogLevelItem extends ConsumerWidget {
const LogLevelItem({super.key}); const LogLevelItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, LogLevel>( final logLevel =
selector: (_, clashConfig) => clashConfig.logLevel, ref.watch(patchClashConfigProvider.select((state) => state.logLevel));
builder: (_, value, __) { return ListItem<LogLevel>.options(
return ListItem<LogLevel>.options( leading: const Icon(Icons.info_outline),
leading: const Icon(Icons.info_outline), title: Text(appLocalizations.logLevel),
title: Text(appLocalizations.logLevel), subtitle: Text(logLevel.name),
subtitle: Text(value.name), delegate: OptionsDelegate<LogLevel>(
delegate: OptionsDelegate<LogLevel>( title: appLocalizations.logLevel,
title: appLocalizations.logLevel, options: LogLevel.values,
options: LogLevel.values, onChanged: (LogLevel? value) {
onChanged: (LogLevel? value) { if (value == null) {
if (value == null) { return;
return; }
} ref.read(patchClashConfigProvider.notifier).updateState(
final appController = globalState.appController; (state) => state.copyWith(
appController.clashConfig.logLevel = value; logLevel: value,
}, ),
textBuilder: (logLevel) => logLevel.name, );
value: value, },
), textBuilder: (logLevel) => logLevel.name,
); value: logLevel,
}, ),
); );
} }
} }
class UaItem extends StatelessWidget { class UaItem extends ConsumerWidget {
const UaItem({super.key}); const UaItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, String?>( final globalUa =
selector: (_, clashConfig) => clashConfig.globalRealUa, ref.watch(patchClashConfigProvider.select((state) => state.globalUa));
builder: (_, value, __) { return ListItem<String?>.options(
return ListItem<String?>.options( leading: const Icon(Icons.computer_outlined),
leading: const Icon(Icons.computer_outlined), title: const Text("UA"),
title: const Text("UA"), subtitle: Text(globalUa ?? appLocalizations.defaultText),
subtitle: Text(value ?? appLocalizations.defaultText), delegate: OptionsDelegate<String?>(
delegate: OptionsDelegate<String?>( title: "UA",
title: "UA", options: [
options: [ null,
null, "clash-verge/v1.6.6",
"clash-verge/v1.6.6", "ClashforWindows/0.19.23",
"ClashforWindows/0.19.23", ],
], value: globalUa,
value: value, onChanged: (value) {
onChanged: (ua) { ref.read(patchClashConfigProvider.notifier).updateState(
final appController = globalState.appController; (state) => state.copyWith(
appController.clashConfig.globalRealUa = ua; globalUa: value,
}, ),
textBuilder: (ua) => ua ?? appLocalizations.defaultText, );
), },
); textBuilder: (ua) => ua ?? appLocalizations.defaultText,
}, ),
); );
} }
} }
class KeepAliveIntervalItem extends StatelessWidget { class KeepAliveIntervalItem extends ConsumerWidget {
const KeepAliveIntervalItem({super.key}); const KeepAliveIntervalItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, int>( final keepAliveInterval = ref.watch(
selector: (_, config) => config.keepAliveInterval, patchClashConfigProvider.select((state) => state.keepAliveInterval));
builder: (_, value, __) { return ListItem.input(
return ListItem.input( leading: const Icon(Icons.timer_outlined),
leading: const Icon(Icons.timer_outlined), title: Text(appLocalizations.keepAliveIntervalDesc),
title: Text(appLocalizations.keepAliveIntervalDesc), subtitle: Text("$keepAliveInterval ${appLocalizations.seconds}"),
subtitle: Text("$value ${appLocalizations.seconds}"), delegate: InputDelegate(
delegate: InputDelegate( title: appLocalizations.keepAliveIntervalDesc,
title: appLocalizations.keepAliveIntervalDesc, suffixText: appLocalizations.seconds,
suffixText: appLocalizations.seconds, resetValue: "$defaultKeepAliveInterval",
resetValue: "$defaultKeepAliveInterval", value: "$keepAliveInterval",
value: "$value", onChanged: (String? value) {
onChanged: (String? value) { if (value == null) {
if (value != null) { return;
try { }
final intValue = int.parse(value); globalState.safeRun(
if (intValue <= 0) { () {
throw "Invalid keepAliveInterval"; final intValue = int.parse(value);
} if (intValue <= 0) {
globalState.appController.clashConfig.keepAliveInterval = throw "Invalid keepAliveInterval";
intValue; }
} catch (e) { ref.read(patchClashConfigProvider.notifier).updateState(
globalState.showMessage( (state) => state.copyWith(
title: appLocalizations.keepAliveIntervalDesc, keepAliveInterval: intValue,
message: TextSpan(
text: e.toString(),
), ),
); );
}
}
}, },
), silence: false,
); title: appLocalizations.keepAliveIntervalDesc,
}, );
},
),
); );
} }
} }
class TestUrlItem extends StatelessWidget { class TestUrlItem extends ConsumerWidget {
const TestUrlItem({super.key}); const TestUrlItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<Config, String>( final testUrl =
selector: (_, config) => config.appSetting.testUrl, ref.watch(appSettingProvider.select((state) => state.testUrl));
builder: (_, value, __) { return ListItem.input(
return ListItem.input( leading: const Icon(Icons.timeline),
leading: const Icon(Icons.timeline), title: Text(appLocalizations.testUrl),
title: Text(appLocalizations.testUrl), subtitle: Text(testUrl),
subtitle: Text(value), delegate: InputDelegate(
delegate: InputDelegate( resetValue: defaultTestUrl,
resetValue: defaultTestUrl, title: appLocalizations.testUrl,
title: appLocalizations.testUrl, value: testUrl,
value: value, onChanged: (String? value) {
onChanged: (String? value) { if (value == null) {
if (value != null) { return;
try { }
if (!value.isUrl) { globalState.safeRun(
throw "Invalid url"; () {
} if (!value.isUrl) {
final config = globalState.appController.config; throw "Invalid url";
config.appSetting = config.appSetting.copyWith(
testUrl: value,
);
} catch (e) {
globalState.showMessage(
title: appLocalizations.testUrl,
message: TextSpan(
text: e.toString(),
),
);
} }
} ref.read(appSettingProvider.notifier).updateState(
}, (state) => state.copyWith(
), testUrl: value,
); ),
}, );
},
silence: false,
title: appLocalizations.testUrl,
);
}),
); );
} }
} }
class MixedPortItem extends StatelessWidget { class MixedPortItem extends ConsumerWidget {
const MixedPortItem({super.key}); const MixedPortItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, int>( final mixedPort =
selector: (_, clashConfig) => clashConfig.mixedPort, ref.watch(patchClashConfigProvider.select((state) => state.mixedPort));
builder: (_, value, __) { return ListItem.input(
return ListItem.input( leading: const Icon(Icons.adjust_outlined),
leading: const Icon(Icons.adjust_outlined), title: Text(appLocalizations.proxyPort),
title: Text(appLocalizations.proxyPort), subtitle: Text("$mixedPort"),
subtitle: Text("$value"), delegate: InputDelegate(
delegate: InputDelegate( title: appLocalizations.proxyPort,
title: appLocalizations.proxyPort, value: "$mixedPort",
value: "$value", onChanged: (String? value) {
onChanged: (String? value) { if (value == null) {
if (value != null) { return;
try { }
final mixedPort = int.parse(value); globalState.safeRun(
if (mixedPort < 1024 || mixedPort > 49151) { () {
throw "Invalid port"; final mixedPort = int.parse(value);
} if (mixedPort < 1024 || mixedPort > 49151) {
globalState.appController.clashConfig.mixedPort = mixedPort; throw "Invalid port";
} catch (e) { }
globalState.showMessage( ref.read(patchClashConfigProvider.notifier).updateState(
title: appLocalizations.proxyPort, (state) => state.copyWith(
message: TextSpan( mixedPort: mixedPort,
text: e.toString(),
), ),
); );
}
}
}, },
resetValue: "$defaultMixedPort", silence: false,
), title: appLocalizations.proxyPort,
); );
}, },
resetValue: "$defaultMixedPort",
),
); );
} }
} }
@@ -209,20 +201,21 @@ class HostsItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: "Hosts", title: "Hosts",
widget: Selector<ClashConfig, HostsMap>( widget: Consumer(
selector: (_, clashConfig) => clashConfig.hosts, builder: (_, ref, __) {
shouldRebuild: (prev, next) => final hosts = ref
!const MapEquality<String, String>().equals(prev, next), .watch(patchClashConfigProvider.select((state) => state.hosts));
builder: (_, hosts, ___) {
final entries = hosts.entries;
return ListPage( return ListPage(
title: "Hosts", title: "Hosts",
items: entries, items: hosts.entries,
titleBuilder: (item) => Text(item.key), titleBuilder: (item) => Text(item.key),
subtitleBuilder: (item) => Text(item.value), subtitleBuilder: (item) => Text(item.value),
onChange: (items){ onChange: (items) {
final clashConfig = globalState.appController.clashConfig; ref.read(patchClashConfigProvider.notifier).updateState(
clashConfig.hosts = Map.fromEntries(items); (state) => state.copyWith(
hosts: Map.fromEntries(items),
),
);
}, },
); );
}, },
@@ -233,190 +226,192 @@ class HostsItem extends StatelessWidget {
} }
} }
class Ipv6Item extends StatelessWidget { class Ipv6Item extends ConsumerWidget {
const Ipv6Item({super.key}); const Ipv6Item({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final ipv6 =
selector: (_, clashConfig) => clashConfig.ipv6, ref.watch(patchClashConfigProvider.select((state) => state.ipv6));
builder: (_, ipv6, __) { return ListItem.switchItem(
return ListItem.switchItem( leading: const Icon(Icons.water_outlined),
leading: const Icon(Icons.water_outlined), title: const Text("IPv6"),
title: const Text("IPv6"), subtitle: Text(appLocalizations.ipv6Desc),
subtitle: Text(appLocalizations.ipv6Desc), delegate: SwitchDelegate(
delegate: SwitchDelegate( value: ipv6,
value: ipv6, onChanged: (bool value) async {
onChanged: (bool value) async { ref.read(patchClashConfigProvider.notifier).updateState(
final appController = globalState.appController; (state) => state.copyWith(
appController.clashConfig.ipv6 = value; ipv6: value,
}, ),
), );
); },
}, ),
); );
} }
} }
class AllowLanItem extends StatelessWidget { class AllowLanItem extends ConsumerWidget {
const AllowLanItem({super.key}); const AllowLanItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final allowLan =
selector: (_, clashConfig) => clashConfig.allowLan, ref.watch(patchClashConfigProvider.select((state) => state.allowLan));
builder: (_, allowLan, __) { return ListItem.switchItem(
return ListItem.switchItem( leading: const Icon(Icons.device_hub),
leading: const Icon(Icons.device_hub), title: Text(appLocalizations.allowLan),
title: Text(appLocalizations.allowLan), subtitle: Text(appLocalizations.allowLanDesc),
subtitle: Text(appLocalizations.allowLanDesc), delegate: SwitchDelegate(
delegate: SwitchDelegate( value: allowLan,
value: allowLan, onChanged: (bool value) async {
onChanged: (bool value) async { ref.read(patchClashConfigProvider.notifier).updateState(
final clashConfig = context.read<ClashConfig>(); (state) => state.copyWith(
clashConfig.allowLan = value; allowLan: value,
}, ),
), );
); },
}, ),
); );
} }
} }
class UnifiedDelayItem extends StatelessWidget { class UnifiedDelayItem extends ConsumerWidget {
const UnifiedDelayItem({super.key}); const UnifiedDelayItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final unifiedDelay = ref
selector: (_, clashConfig) => clashConfig.unifiedDelay, .watch(patchClashConfigProvider.select((state) => state.unifiedDelay));
builder: (_, unifiedDelay, __) {
return ListItem.switchItem( return ListItem.switchItem(
leading: const Icon(Icons.compress_outlined), leading: const Icon(Icons.compress_outlined),
title: Text(appLocalizations.unifiedDelay), title: Text(appLocalizations.unifiedDelay),
subtitle: Text(appLocalizations.unifiedDelayDesc), subtitle: Text(appLocalizations.unifiedDelayDesc),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: unifiedDelay, value: unifiedDelay,
onChanged: (bool value) async { onChanged: (bool value) async {
final appController = globalState.appController; ref.read(patchClashConfigProvider.notifier).updateState(
appController.clashConfig.unifiedDelay = value; (state) => state.copyWith(
}, unifiedDelay: value,
), ),
); );
}, },
),
); );
} }
} }
class FindProcessItem extends StatelessWidget { class FindProcessItem extends ConsumerWidget {
const FindProcessItem({super.key}); const FindProcessItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final findProcess = ref.watch(patchClashConfigProvider
selector: (_, clashConfig) => .select((state) => state.findProcessMode == FindProcessMode.always));
clashConfig.findProcessMode == FindProcessMode.always,
builder: (_, findProcess, __) { return ListItem.switchItem(
return ListItem.switchItem( leading: const Icon(Icons.polymer_outlined),
leading: const Icon(Icons.polymer_outlined), title: Text(appLocalizations.findProcessMode),
title: Text(appLocalizations.findProcessMode), subtitle: Text(appLocalizations.findProcessModeDesc),
subtitle: Text(appLocalizations.findProcessModeDesc), delegate: SwitchDelegate(
delegate: SwitchDelegate( value: findProcess,
value: findProcess, onChanged: (bool value) async {
onChanged: (bool value) async { ref.read(patchClashConfigProvider.notifier).updateState(
final appController = globalState.appController; (state) => state.copyWith(
appController.clashConfig.findProcessMode = findProcessMode:
value ? FindProcessMode.always : FindProcessMode.off; value ? FindProcessMode.always : FindProcessMode.off,
}, ),
), );
); },
}, ),
); );
} }
} }
class TcpConcurrentItem extends StatelessWidget { class TcpConcurrentItem extends ConsumerWidget {
const TcpConcurrentItem({super.key}); const TcpConcurrentItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final tcpConcurrent = ref
selector: (_, clashConfig) => clashConfig.tcpConcurrent, .watch(patchClashConfigProvider.select((state) => state.tcpConcurrent));
builder: (_, tcpConcurrent, __) { return ListItem.switchItem(
return ListItem.switchItem( leading: const Icon(Icons.double_arrow_outlined),
leading: const Icon(Icons.double_arrow_outlined), title: Text(appLocalizations.tcpConcurrent),
title: Text(appLocalizations.tcpConcurrent), subtitle: Text(appLocalizations.tcpConcurrentDesc),
subtitle: Text(appLocalizations.tcpConcurrentDesc), delegate: SwitchDelegate(
delegate: SwitchDelegate( value: tcpConcurrent,
value: tcpConcurrent, onChanged: (value) async {
onChanged: (bool value) async { ref.read(patchClashConfigProvider.notifier).updateState(
final appController = globalState.appController; (state) => state.copyWith(
appController.clashConfig.tcpConcurrent = value; tcpConcurrent: value,
}, ),
), );
); },
}, ),
); );
} }
} }
class GeodataLoaderItem extends StatelessWidget { class GeodataLoaderItem extends ConsumerWidget {
const GeodataLoaderItem({super.key}); const GeodataLoaderItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final isMemconservative = ref.watch(patchClashConfigProvider.select(
selector: (_, clashConfig) => (state) => state.geodataLoader == GeodataLoader.memconservative));
clashConfig.geodataLoader == geodataLoaderMemconservative, return ListItem.switchItem(
builder: (_, memconservative, __) { leading: const Icon(Icons.memory),
return ListItem.switchItem( title: Text(appLocalizations.geodataLoader),
leading: const Icon(Icons.memory), subtitle: Text(appLocalizations.geodataLoaderDesc),
title: Text(appLocalizations.geodataLoader), delegate: SwitchDelegate(
subtitle: Text(appLocalizations.geodataLoaderDesc), value: isMemconservative,
delegate: SwitchDelegate( onChanged: (bool value) async {
value: memconservative, ref.read(patchClashConfigProvider.notifier).updateState(
onChanged: (bool value) async { (state) => state.copyWith(
final appController = globalState.appController; geodataLoader: value
appController.clashConfig.geodataLoader = ? GeodataLoader.memconservative
value ? geodataLoaderMemconservative : geodataLoaderStandard; : GeodataLoader.standard,
}, ),
), );
); },
}, ),
); );
} }
} }
class ExternalControllerItem extends StatelessWidget { class ExternalControllerItem extends ConsumerWidget {
const ExternalControllerItem({super.key}); const ExternalControllerItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final hasExternalController = ref.watch(patchClashConfigProvider.select(
selector: (_, clashConfig) => clashConfig.externalController.isNotEmpty, (state) => state.externalController == ExternalControllerStatus.open));
builder: (_, hasExternalController, __) { return ListItem.switchItem(
return ListItem.switchItem( leading: const Icon(Icons.api_outlined),
leading: const Icon(Icons.api_outlined), title: Text(appLocalizations.externalController),
title: Text(appLocalizations.externalController), subtitle: Text(appLocalizations.externalControllerDesc),
subtitle: Text(appLocalizations.externalControllerDesc), delegate: SwitchDelegate(
delegate: SwitchDelegate( value: hasExternalController,
value: hasExternalController, onChanged: (bool value) async {
onChanged: (bool value) async { ref.read(patchClashConfigProvider.notifier).updateState(
final appController = globalState.appController; (state) => state.copyWith(
appController.clashConfig.externalController = externalController: value
value ? defaultExternalController : ''; ? ExternalControllerStatus.open
}, : ExternalControllerStatus.close,
), ),
); );
}, },
),
); );
} }
} }
final generalItems = const [ final generalItems = <Widget>[
LogLevelItem(), LogLevelItem(),
UaItem(), UaItem(),
KeepAliveIntervalItem(), if (system.isDesktop) KeepAliveIntervalItem(),
TestUrlItem(), TestUrlItem(),
MixedPortItem(), MixedPortItem(),
HostsItem(), HostsItem(),

View File

@@ -3,200 +3,185 @@ import 'dart:io';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class VPNItem extends StatelessWidget { class VPNItem extends ConsumerWidget {
const VPNItem({super.key}); const VPNItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<Config, bool>( final enable =
selector: (_, config) => config.vpnProps.enable, ref.watch(vpnSettingProvider.select((state) => state.enable));
builder: (_, enable, __) { return ListItem.switchItem(
return ListItem.switchItem( title: const Text("VPN"),
title: const Text("VPN"), subtitle: Text(appLocalizations.vpnEnableDesc),
subtitle: Text(appLocalizations.vpnEnableDesc), delegate: SwitchDelegate(
delegate: SwitchDelegate( value: enable,
value: enable, onChanged: (value) async {
onChanged: (value) async { ref.read(vpnSettingProvider.notifier).updateState(
final config = globalState.appController.config; (state) => state.copyWith(
config.vpnProps = config.vpnProps.copyWith( enable: value,
enable: value, ),
); );
}, },
), ),
);
},
); );
} }
} }
class TUNItem extends StatelessWidget { class TUNItem extends ConsumerWidget {
const TUNItem({super.key}); const TUNItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final enable =
selector: (_, clashConfig) => clashConfig.tun.enable, ref.watch(patchClashConfigProvider.select((state) => state.tun.enable));
builder: (_, enable, __) {
return ListItem.switchItem( return ListItem.switchItem(
title: Text(appLocalizations.tun), title: Text(appLocalizations.tun),
subtitle: Text(appLocalizations.tunDesc), subtitle: Text(appLocalizations.tunDesc),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: enable, value: enable,
onChanged: (value) async { onChanged: (value) async {
final clashConfig = globalState.appController.clashConfig; ref.read(patchClashConfigProvider.notifier).updateState(
clashConfig.tun = clashConfig.tun.copyWith( (state) => state.copyWith.tun(
enable: value, enable: value,
),
); );
}, },
), ),
);
},
); );
} }
} }
class AllowBypassItem extends StatelessWidget { class AllowBypassItem extends ConsumerWidget {
const AllowBypassItem({super.key}); const AllowBypassItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<Config, bool>( final allowBypass =
selector: (_, config) => config.vpnProps.allowBypass, ref.watch(vpnSettingProvider.select((state) => state.allowBypass));
builder: (_, allowBypass, __) { return ListItem.switchItem(
return ListItem.switchItem( title: Text(appLocalizations.allowBypass),
title: Text(appLocalizations.allowBypass), subtitle: Text(appLocalizations.allowBypassDesc),
subtitle: Text(appLocalizations.allowBypassDesc), delegate: SwitchDelegate(
delegate: SwitchDelegate( value: allowBypass,
value: allowBypass, onChanged: (bool value) async {
onChanged: (bool value) async { ref.read(vpnSettingProvider.notifier).updateState(
final config = globalState.appController.config; (state) => state.copyWith(
final vpnProps = config.vpnProps; allowBypass: value,
config.vpnProps = vpnProps.copyWith( ),
allowBypass: value,
); );
}, },
), ),
);
},
); );
} }
} }
class VpnSystemProxyItem extends StatelessWidget { class VpnSystemProxyItem extends ConsumerWidget {
const VpnSystemProxyItem({super.key}); const VpnSystemProxyItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<Config, bool>( final systemProxy =
selector: (_, config) => config.vpnProps.systemProxy, ref.watch(vpnSettingProvider.select((state) => state.systemProxy));
builder: (_, systemProxy, __) { return ListItem.switchItem(
return ListItem.switchItem( title: Text(appLocalizations.systemProxy),
title: Text(appLocalizations.systemProxy), subtitle: Text(appLocalizations.systemProxyDesc),
subtitle: Text(appLocalizations.systemProxyDesc), delegate: SwitchDelegate(
delegate: SwitchDelegate( value: systemProxy,
value: systemProxy, onChanged: (bool value) async {
onChanged: (bool value) async { ref.read(vpnSettingProvider.notifier).updateState(
final config = globalState.appController.config; (state) => state.copyWith(
final vpnProps = config.vpnProps; systemProxy: value,
config.vpnProps = vpnProps.copyWith( ),
systemProxy: value,
); );
}, },
), ),
);
},
); );
} }
} }
class SystemProxyItem extends StatelessWidget { class SystemProxyItem extends ConsumerWidget {
const SystemProxyItem({super.key}); const SystemProxyItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<Config, bool>( final systemProxy =
selector: (_, config) => config.networkProps.systemProxy, ref.watch(networkSettingProvider.select((state) => state.systemProxy));
builder: (_, systemProxy, __) {
return ListItem.switchItem( return ListItem.switchItem(
title: Text(appLocalizations.systemProxy), title: Text(appLocalizations.systemProxy),
subtitle: Text(appLocalizations.systemProxyDesc), subtitle: Text(appLocalizations.systemProxyDesc),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: systemProxy, value: systemProxy,
onChanged: (bool value) async { onChanged: (bool value) async {
final config = globalState.appController.config; ref.read(networkSettingProvider.notifier).updateState(
final networkProps = config.networkProps; (state) => state.copyWith(
config.networkProps = networkProps.copyWith( systemProxy: value,
systemProxy: value, ),
); );
}, },
), ),
);
},
); );
} }
} }
class Ipv6Item extends StatelessWidget { class Ipv6Item extends ConsumerWidget {
const Ipv6Item({super.key}); const Ipv6Item({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<Config, bool>( final ipv6 = ref.watch(vpnSettingProvider.select((state) => state.ipv6));
selector: (_, config) => config.vpnProps.ipv6, return ListItem.switchItem(
builder: (_, ipv6, __) { title: const Text("IPv6"),
return ListItem.switchItem( subtitle: Text(appLocalizations.ipv6InboundDesc),
title: const Text("IPv6"), delegate: SwitchDelegate(
subtitle: Text(appLocalizations.ipv6InboundDesc), value: ipv6,
delegate: SwitchDelegate( onChanged: (bool value) async {
value: ipv6, ref.read(vpnSettingProvider.notifier).updateState(
onChanged: (bool value) async { (state) => state.copyWith(
final config = globalState.appController.config; ipv6: value,
final vpnProps = config.vpnProps; ),
config.vpnProps = vpnProps.copyWith(
ipv6: value,
); );
}, },
), ),
);
},
); );
} }
} }
class TunStackItem extends StatelessWidget { class TunStackItem extends ConsumerWidget {
const TunStackItem({super.key}); const TunStackItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, TunStack>( final stack =
selector: (_, clashConfig) => clashConfig.tun.stack, ref.watch(patchClashConfigProvider.select((state) => state.tun.stack));
builder: (_, stack, __) {
return ListItem.options( return ListItem.options(
title: Text(appLocalizations.stackMode), title: Text(appLocalizations.stackMode),
subtitle: Text(stack.name), subtitle: Text(stack.name),
delegate: OptionsDelegate<TunStack>( delegate: OptionsDelegate<TunStack>(
value: stack, value: stack,
options: TunStack.values, options: TunStack.values,
textBuilder: (value) => value.name, textBuilder: (value) => value.name,
onChanged: (value) { onChanged: (value) {
if (value == null) { if (value == null) {
return; return;
} }
final clashConfig = globalState.appController.clashConfig; ref.read(patchClashConfigProvider.notifier).updateState(
clashConfig.tun = clashConfig.tun.copyWith( (state) => state.copyWith.tun(
stack: value, stack: value,
),
); );
}, },
title: appLocalizations.stackMode, title: appLocalizations.stackMode,
), ),
);
},
); );
} }
} }
@@ -204,26 +189,25 @@ class TunStackItem extends StatelessWidget {
class BypassDomainItem extends StatelessWidget { class BypassDomainItem extends StatelessWidget {
const BypassDomainItem({super.key}); const BypassDomainItem({super.key});
_initActions(BuildContext context) { _initActions(BuildContext context, WidgetRef ref) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState = context.commonScaffoldState?.actions = [
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton( IconButton(
onPressed: () { onPressed: () async {
globalState.showMessage( final res = await globalState.showMessage(
title: appLocalizations.reset, title: appLocalizations.reset,
message: TextSpan( message: TextSpan(
text: appLocalizations.resetTip, text: appLocalizations.resetTip,
), ),
onTab: () {
final config = globalState.appController.config;
config.networkProps = config.networkProps.copyWith(
bypassDomain: defaultBypassDomain,
);
Navigator.of(context).pop();
},
); );
if (res != true) {
return;
}
ref.read(networkSettingProvider.notifier).updateState(
(state) => state.copyWith(
bypassDomain: defaultBypassDomain,
),
);
}, },
tooltip: appLocalizations.reset, tooltip: appLocalizations.reset,
icon: const Icon( icon: const Icon(
@@ -243,20 +227,21 @@ class BypassDomainItem extends StatelessWidget {
isBlur: false, isBlur: false,
isScaffold: true, isScaffold: true,
title: appLocalizations.bypassDomain, title: appLocalizations.bypassDomain,
widget: Selector<Config, List<String>>( widget: Consumer(
selector: (_, config) => config.networkProps.bypassDomain, builder: (_, ref, __) {
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next), _initActions(context, ref);
builder: (context, bypassDomain, __) { final bypassDomain = ref.watch(
_initActions(context); networkSettingProvider.select((state) => state.bypassDomain));
return ListPage( return ListPage(
title: appLocalizations.bypassDomain, title: appLocalizations.bypassDomain,
items: bypassDomain, items: bypassDomain,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
onChange: (items) { onChange: (items) {
final config = globalState.appController.config; ref.read(networkSettingProvider.notifier).updateState(
config.networkProps = config.networkProps.copyWith( (state) => state.copyWith(
bypassDomain: List.from(items), bypassDomain: List.from(items),
); ),
);
}, },
); );
}, },
@@ -267,77 +252,74 @@ class BypassDomainItem extends StatelessWidget {
} }
} }
class RouteModeItem extends StatelessWidget { class RouteModeItem extends ConsumerWidget {
const RouteModeItem({super.key}); const RouteModeItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, RouteMode>( final routeMode =
selector: (_, clashConfig) => clashConfig.routeMode, ref.watch(networkSettingProvider.select((state) => state.routeMode));
builder: (_, value, __) { return ListItem<RouteMode>.options(
return ListItem<RouteMode>.options( title: Text(appLocalizations.routeMode),
title: Text(appLocalizations.routeMode), subtitle: Text(Intl.message("routeMode_${routeMode.name}")),
subtitle: Text(Intl.message("routeMode_${value.name}")), delegate: OptionsDelegate<RouteMode>(
delegate: OptionsDelegate<RouteMode>( title: appLocalizations.routeMode,
title: appLocalizations.routeMode, options: RouteMode.values,
options: RouteMode.values, onChanged: (RouteMode? value) {
onChanged: (RouteMode? value) { if (value == null) {
if (value == null) { return;
return; }
} ref.read(networkSettingProvider.notifier).updateState(
final appController = globalState.appController; (state) => state.copyWith(
appController.clashConfig.routeMode = value; routeMode: value,
}, ),
textBuilder: (routeMode) => Intl.message( );
"routeMode_${routeMode.name}", },
), textBuilder: (routeMode) => Intl.message(
value: value, "routeMode_${routeMode.name}",
), ),
); value: routeMode,
}, ),
); );
} }
} }
class RouteAddressItem extends StatelessWidget { class RouteAddressItem extends ConsumerWidget {
const RouteAddressItem({super.key}); const RouteAddressItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final bypassPrivate = ref.watch(networkSettingProvider
selector: (_, clashConfig) => clashConfig.routeMode == RouteMode.config, .select((state) => state.routeMode == RouteMode.bypassPrivate));
builder: (_, value, child) { if (bypassPrivate) {
if (value) { return Container();
return child!; }
} return ListItem.open(
return Container(); title: Text(appLocalizations.routeAddress),
}, subtitle: Text(appLocalizations.routeAddressDesc),
child: ListItem.open( delegate: OpenDelegate(
title: Text(appLocalizations.routeAddress), isBlur: false,
subtitle: Text(appLocalizations.routeAddressDesc), isScaffold: true,
delegate: OpenDelegate( title: appLocalizations.routeAddress,
isBlur: false, widget: Consumer(
isScaffold: true, builder: (_, ref, __) {
title: appLocalizations.routeAddress, final routeAddress = ref.watch(patchClashConfigProvider
widget: Selector<ClashConfig, List<String>>( .select((state) => state.tun.routeAddress));
selector: (_, clashConfig) => clashConfig.includeRouteAddress, return ListPage(
shouldRebuild: (prev, next) => title: appLocalizations.routeAddress,
!stringListEquality.equals(prev, next), items: routeAddress,
builder: (context, routeAddress, __) { titleBuilder: (item) => Text(item),
return ListPage( onChange: (items) {
title: appLocalizations.routeAddress, ref.read(patchClashConfigProvider.notifier).updateState(
items: routeAddress, (state) => state.copyWith.tun(
titleBuilder: (item) => Text(item), routeAddress: List.from(items),
onChange: (items) { ),
final clashConfig = globalState.appController.clashConfig; );
clashConfig.includeRouteAddress = },
Set<String>.from(items).toList(); );
}, },
);
},
),
extendPageWidth: 360,
), ),
extendPageWidth: 360,
), ),
); );
} }
@@ -349,7 +331,8 @@ final networkItems = [
...generateSection( ...generateSection(
title: "VPN", title: "VPN",
items: [ items: [
const SystemProxyItem(), const VpnSystemProxyItem(),
const BypassDomainItem(),
const AllowBypassItem(), const AllowBypassItem(),
const Ipv6Item(), const Ipv6Item(),
], ],
@@ -373,28 +356,31 @@ final networkItems = [
), ),
]; ];
class NetworkListView extends StatelessWidget { class NetworkListView extends ConsumerWidget {
const NetworkListView({super.key}); const NetworkListView({super.key});
_initActions(BuildContext context) { _initActions(BuildContext context, WidgetRef ref) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState = context.commonScaffoldState?.actions = [
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton( IconButton(
onPressed: () { onPressed: () async {
globalState.showMessage( final res = await globalState.showMessage(
title: appLocalizations.reset, title: appLocalizations.reset,
message: TextSpan( message: TextSpan(
text: appLocalizations.resetTip, text: appLocalizations.resetTip,
), ),
onTab: () {
final appController = globalState.appController;
appController.config.vpnProps = defaultVpnProps;
appController.clashConfig.tun = defaultTun;
Navigator.of(context).pop();
},
); );
if (res != true) {
return;
}
ref.read(vpnSettingProvider.notifier).updateState(
(state) => defaultVpnProps,
);
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
tun: defaultTun,
),
);
}, },
tooltip: appLocalizations.reset, tooltip: appLocalizations.reset,
icon: const Icon( icon: const Icon(
@@ -406,8 +392,8 @@ class NetworkListView extends StatelessWidget {
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
_initActions(context); _initActions(context, ref);
return generateListView( return generateListView(
networkItems, networkItems,
); );

View File

@@ -0,0 +1,150 @@
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/providers/providers.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'item.dart';
class ConnectionsFragment extends ConsumerStatefulWidget {
const ConnectionsFragment({super.key});
@override
ConsumerState<ConnectionsFragment> createState() =>
_ConnectionsFragmentState();
}
class _ConnectionsFragmentState extends ConsumerState<ConnectionsFragment>
with PageMixin {
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();
ref.listenManual(
isCurrentPageProvider(
PageLabel.connections,
handler: (pageLabel, viewMode) =>
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
),
(prev, next) {
if (prev != next && next == true) {
initPageState();
}
},
fireImmediately: true,
);
_updateConnections();
}
_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 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,
),
);
},
);
}
}

View File

@@ -0,0 +1,155 @@
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/providers/config.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class FindProcessBuilder extends StatelessWidget {
final Widget Function(bool value) builder;
const FindProcessBuilder({
super.key,
required this.builder,
});
@override
Widget build(BuildContext context) {
return Consumer(
builder: (_, ref, __) {
final value = ref.watch(
patchClashConfigProvider.select(
(state) =>
state.findProcessMode == FindProcessMode.always &&
Platform.isAndroid,
),
);
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,
);
},
);
}
}

View File

@@ -0,0 +1,212 @@
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/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'item.dart';
double _preOffset = 0;
class RequestsFragment extends ConsumerStatefulWidget {
const RequestsFragment({super.key});
@override
ConsumerState<RequestsFragment> createState() => _RequestsFragmentState();
}
class _RequestsFragmentState extends ConsumerState<RequestsFragment>
with PageMixin {
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();
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
connections: globalState.appState.requests.list,
);
ref.listenManual(
isCurrentPageProvider(
PageLabel.requests,
handler: (pageLabel, viewMode) =>
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
),
(prev, next) {
if (prev != next && next == true) {
initPageState();
}
},
fireImmediately: true,
);
ref.listenManual(
requestsProvider.select((state) => state.list),
(prev, next) {
if (!connectionListEquality.equals(prev, next)) {
_requests = next;
updateRequestsThrottler();
}
},
fireImmediately: true,
);
}
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();
}
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);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (_, constraints) {
return FindProcessBuilder(builder: (value) {
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0));
return 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,
),
),
),
);
},
);
});
},
);
}
}

View File

@@ -1,349 +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),
),
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() {
timer?.cancel();
connectionsNotifier.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<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,
),
)
],
);
},
);
}
}

View File

@@ -1,32 +1,43 @@
import 'dart:math'; import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'widgets/start_button.dart'; import 'widgets/start_button.dart';
class DashboardFragment extends StatefulWidget { class DashboardFragment extends ConsumerStatefulWidget {
const DashboardFragment({super.key}); const DashboardFragment({super.key});
@override @override
State<DashboardFragment> createState() => _DashboardFragmentState(); ConsumerState<DashboardFragment> createState() => _DashboardFragmentState();
} }
class _DashboardFragmentState extends State<DashboardFragment> { class _DashboardFragmentState extends ConsumerState<DashboardFragment>
with PageMixin {
final key = GlobalKey<SuperGridState>(); final key = GlobalKey<SuperGridState>();
_initScaffold(bool isCurrent) { @override
if (!isCurrent) { initState() {
return; ref.listenManual(
} isCurrentPageProvider(PageLabel.dashboard),
WidgetsBinding.instance.addPostFrameCallback((_) { (prev, next) {
final commonScaffoldState = if (prev != next && next == true) {
context.findAncestorStateOfType<CommonScaffoldState>(); initPageState();
commonScaffoldState?.floatingActionButton = const StartButton(); }
commonScaffoldState?.actions = [ },
fireImmediately: true,
);
return super.initState();
}
@override
Widget? get floatingActionButton => const StartButton();
@override
List<Widget> get actions => [
ValueListenableBuilder( ValueListenableBuilder(
valueListenable: key.currentState!.addedChildrenNotifier, valueListenable: key.currentState!.addedChildrenNotifier,
builder: (_, addedChildren, child) { builder: (_, addedChildren, child) {
@@ -67,72 +78,59 @@ class _DashboardFragmentState extends State<DashboardFragment> {
}, },
), ),
]; ];
});
_handleSave(List<GridItem> girdItems, WidgetRef ref) {
final dashboardWidgets = girdItems
.map(
(item) => DashboardWidget.getDashboardWidget(item),
)
.toList();
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(dashboardWidgets: dashboardWidgets),
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ActiveBuilder( final dashboardState = ref.watch(dashboardStateProvider);
label: "dashboard", final columns = max(4 * ((dashboardState.viewWidth / 350).ceil()), 8);
builder: (isCurrent, child) { return Align(
_initScaffold(isCurrent); alignment: Alignment.topCenter,
return child!; child: SingleChildScrollView(
}, padding: const EdgeInsets.all(16).copyWith(
child: Align( bottom: 88,
alignment: Alignment.topCenter, ),
child: SingleChildScrollView( child: SuperGrid(
padding: const EdgeInsets.all(16).copyWith( key: key,
bottom: 88, crossAxisCount: columns,
), crossAxisSpacing: 16,
child: Selector2<AppState, Config, DashboardState>( mainAxisSpacing: 16,
selector: (_, appState, config) => DashboardState( children: [
dashboardWidgets: config.appSetting.dashboardWidgets, ...dashboardState.dashboardWidgets
viewWidth: appState.viewWidth, .where(
), (item) => item.platforms.contains(
builder: (_, state, ___) { SupportPlatform.currentPlatform,
final columns = max(4 * ((state.viewWidth / 350).ceil()), 8); ),
return SuperGrid( )
key: key, .map(
crossAxisCount: columns, (item) => item.widget,
crossAxisSpacing: 16, ),
mainAxisSpacing: 16, ],
children: [ onSave: (girdItems) {
...state.dashboardWidgets _handleSave(girdItems, ref);
.where( },
(item) => item.platforms.contains( addedItemsBuilder: (girdItems) {
SupportPlatform.currentPlatform, return DashboardWidget.values
), .where(
) (item) =>
.map( !girdItems.contains(item.widget) &&
(item) => item.widget, item.platforms.contains(
SupportPlatform.currentPlatform,
), ),
], )
onSave: (girdItems) { .map((item) => item.widget)
final dashboardWidgets = girdItems .toList();
.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();
},
);
},
),
), ),
), ),
); );

View File

@@ -1,58 +0,0 @@
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:fl_clash/common/common.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CoreInfo extends StatelessWidget {
const CoreInfo({super.key});
@override
Widget build(BuildContext context) {
return Selector<AppState, VersionInfo?>(
selector: (_, appState) => appState.versionInfo,
builder: (_, versionInfo, __) {
return CommonCard(
onPressed: () {},
info: Info(
label: appLocalizations.coreInfo,
iconData: Icons.memory,
),
child: Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.all(16).copyWith(top: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
flex: 1,
child: Text(
versionInfo?.clashName ?? '',
style: context
.textTheme
.titleMedium
?.toSoftBold,
),
),
const SizedBox(
height: 8,
),
Flexible(
flex: 1,
child: Text(
versionInfo?.version ?? '',
style: context
.textTheme
.titleLarge
?.toSoftBold,
),
),
],
),
),
);
},
);
}
}

View File

@@ -1,9 +1,9 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/app.dart'; import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class IntranetIP extends StatelessWidget { class IntranetIP extends StatelessWidget {
const IntranetIP({super.key}); const IntranetIP({super.key});
@@ -28,15 +28,15 @@ class IntranetIP extends StatelessWidget {
children: [ children: [
SizedBox( SizedBox(
height: globalState.measure.bodyMediumHeight + 2, height: globalState.measure.bodyMediumHeight + 2,
child: Selector<AppFlowingState, String?>( child: Consumer(
selector: (_, appFlowingState) => appFlowingState.localIp, builder: (_, ref, __) {
builder: (_, value, __) { final localIp = ref.watch(localIpProvider);
return FadeBox( return FadeBox(
child: value != null child: localIp != null
? TooltipText( ? TooltipText(
text: Text( text: Text(
value.isNotEmpty localIp.isNotEmpty
? value ? localIp
: appLocalizations.noNetwork, : appLocalizations.noNetwork,
style: context.textTheme.bodyMedium?.toLight style: context.textTheme.bodyMedium?.toLight
.adjustSize(1), .adjustSize(1),
@@ -48,7 +48,9 @@ class IntranetIP extends StatelessWidget {
padding: EdgeInsets.all(2), padding: EdgeInsets.all(2),
child: AspectRatio( child: AspectRatio(
aspectRatio: 1, aspectRatio: 1,
child: CircularProgressIndicator(), child: CircularProgressIndicator(
strokeWidth: 2,
),
), ),
), ),
); );

View File

@@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
@@ -6,8 +7,9 @@ import 'package:fl_clash/models/common.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
final _memoryInfoStateNotifier = final _memoryInfoStateNotifier = ValueNotifier<TrafficValue>(
ValueNotifier<TrafficValue>(TrafficValue(value: 0)); TrafficValue(value: 0),
);
class MemoryInfo extends StatefulWidget { class MemoryInfo extends StatefulWidget {
const MemoryInfo({super.key}); const MemoryInfo({super.key});
@@ -22,10 +24,7 @@ class _MemoryInfoState extends State<MemoryInfo> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
clashCore.getMemory().then((memory) { _updateMemory();
_memoryInfoStateNotifier.value = TrafficValue(value: memory);
});
_updateMemoryData();
} }
@override @override
@@ -34,16 +33,25 @@ class _MemoryInfoState extends State<MemoryInfo> {
super.dispose(); super.dispose();
} }
_updateMemoryData() { _updateMemory() async {
timer = Timer(Duration(seconds: 2), () async { WidgetsBinding.instance.addPostFrameCallback((_) async {
final memory = await clashCore.getMemory(); final rss = ProcessInfo.currentRss;
_memoryInfoStateNotifier.value = TrafficValue(value: memory); _memoryInfoStateNotifier.value = TrafficValue(
_updateMemoryData(); value: clashLib != null ? rss : await clashCore.getMemory() + rss,
);
timer = Timer(Duration(seconds: 5), () async {
_updateMemory();
});
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final darkenLighter = context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.1)
.toLighter;
final darken = context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.1);
return SizedBox( return SizedBox(
height: getWidgetHeight(2), height: getWidgetHeight(2),
child: CommonCard( child: CommonCard(
@@ -87,17 +95,14 @@ class _MemoryInfoState extends State<MemoryInfo> {
child: WaveView( child: WaveView(
waveAmplitude: 12.0, waveAmplitude: 12.0,
waveFrequency: 0.35, waveFrequency: 0.35,
waveColor: context.colorScheme.secondaryContainer waveColor: darkenLighter,
.blendDarken(context, factor: 0.1)
.toLighter,
), ),
), ),
Positioned.fill( Positioned.fill(
child: WaveView( child: WaveView(
waveAmplitude: 12.0, waveAmplitude: 12.0,
waveFrequency: 0.9, waveFrequency: 0.9,
waveColor: context.colorScheme.secondaryContainer waveColor: darken,
.blendDarken(context, factor: 0.1),
), ),
), ),
], ],

View File

@@ -4,10 +4,11 @@ import 'package:dio/dio.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
final _networkDetectionState = ValueNotifier<NetworkDetectionState>( final _networkDetectionState = ValueNotifier<NetworkDetectionState>(
const NetworkDetectionState( const NetworkDetectionState(
@@ -16,14 +17,14 @@ final _networkDetectionState = ValueNotifier<NetworkDetectionState>(
), ),
); );
class NetworkDetection extends StatefulWidget { class NetworkDetection extends ConsumerStatefulWidget {
const NetworkDetection({super.key}); const NetworkDetection({super.key});
@override @override
State<NetworkDetection> createState() => _NetworkDetectionState(); ConsumerState<NetworkDetection> createState() => _NetworkDetectionState();
} }
class _NetworkDetectionState extends State<NetworkDetection> { class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
bool? _preIsStart; bool? _preIsStart;
Timer? _setTimeoutTimer; Timer? _setTimeoutTimer;
CancelToken? cancelToken; CancelToken? cancelToken;
@@ -31,6 +32,11 @@ class _NetworkDetectionState extends State<NetworkDetection> {
@override @override
void initState() { void initState() {
ref.listenManual(checkIpNumProvider, (prev, next) {
if (prev != next) {
_startCheck();
}
});
super.initState(); super.initState();
} }
@@ -47,12 +53,13 @@ class _NetworkDetectionState extends State<NetworkDetection> {
} }
_checkIp() async { _checkIp() async {
final appState = globalState.appController.appState; final appState = globalState.appState;
final appFlowingState = globalState.appController.appFlowingState;
final isInit = appState.isInit; final isInit = appState.isInit;
if (!isInit) return; if (!isInit) return;
final isStart = appFlowingState.isStart; final isStart = appState.runTime != null;
if (_preIsStart == false && _preIsStart == isStart) return; if (_preIsStart == false &&
_preIsStart == isStart &&
_networkDetectionState.value.ipInfo != null) return;
_clearSetTimeoutTimer(); _clearSetTimeoutTimer();
_networkDetectionState.value = _networkDetectionState.value.copyWith( _networkDetectionState.value = _networkDetectionState.value.copyWith(
isTesting: true, isTesting: true,
@@ -109,24 +116,6 @@ class _NetworkDetectionState extends State<NetworkDetection> {
} }
} }
_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) { _countryCodeToEmoji(String countryCode) {
final String code = countryCode.toUpperCase(); final String code = countryCode.toUpperCase();
if (code.length != 2) { if (code.length != 2) {
@@ -141,109 +130,130 @@ class _NetworkDetectionState extends State<NetworkDetection> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
height: getWidgetHeight(1), height: getWidgetHeight(1),
child: _checkIpContainer( child: ValueListenableBuilder<NetworkDetectionState>(
ValueListenableBuilder<NetworkDetectionState>( valueListenable: _networkDetectionState,
valueListenable: _networkDetectionState, builder: (_, state, __) {
builder: (_, state, __) { final ipInfo = state.ipInfo;
final ipInfo = state.ipInfo; final isTesting = state.isTesting;
final isTesting = state.isTesting; return CommonCard(
return CommonCard( onPressed: () {},
onPressed: () {}, child: Column(
child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
children: [ Container(
Container( height: globalState.measure.titleMediumHeight + 16,
height: globalState.measure.titleMediumHeight + 16, padding: baseInfoEdgeInsets.copyWith(
padding: baseInfoEdgeInsets.copyWith( bottom: 0,
bottom: 0, ),
), child: Row(
child: Row( mainAxisSize: MainAxisSize.max,
mainAxisSize: MainAxisSize.max, children: [
children: [ ipInfo != null
ipInfo != null ? Text(
? Text( _countryCodeToEmoji(
_countryCodeToEmoji( ipInfo.countryCode,
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) style: Theme.of(context)
.textTheme .textTheme
.titleSmall .titleMedium
?.copyWith( ?.toLight
color: context.colorScheme.onSurfaceVariant, .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,
),
), ),
), ),
], ),
), SizedBox(width: 2),
AspectRatio(
aspectRatio: 1,
child: IconButton(
padding: EdgeInsets.zero,
onPressed: () {
globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(
text: appLocalizations.detectionTip,
),
cancelable: false,
);
},
icon: Icon(
size: 16,
Icons.info_outline,
color: context.colorScheme.onSurfaceVariant,
),
),
)
],
), ),
Container( ),
padding: baseInfoEdgeInsets.copyWith( Container(
top: 0, padding: baseInfoEdgeInsets.copyWith(
), top: 0,
child: SizedBox( ),
height: globalState.measure.bodyMediumHeight + 2, child: SizedBox(
child: FadeBox( height: globalState.measure.bodyMediumHeight + 2,
child: ipInfo != null child: FadeBox(
? TooltipText( child: ipInfo != null
text: Text( ? TooltipText(
ipInfo.ip, text: Text(
style: context.textTheme.bodyMedium?.toLight ipInfo.ip,
.adjustSize(1), style: context.textTheme.bodyMedium?.toLight
maxLines: 1, .adjustSize(1),
overflow: TextOverflow.ellipsis, maxLines: 1,
), overflow: TextOverflow.ellipsis,
) ),
: FadeBox( )
child: isTesting == false && ipInfo == null : FadeBox(
? Text( child: isTesting == false && ipInfo == null
"timeout", ? Text(
style: context.textTheme.bodyMedium "timeout",
?.copyWith(color: Colors.red) style: context.textTheme.bodyMedium
.adjustSize(1), ?.copyWith(color: Colors.red)
maxLines: 1, .adjustSize(1),
overflow: TextOverflow.ellipsis, maxLines: 1,
) overflow: TextOverflow.ellipsis,
: Container( )
padding: const EdgeInsets.all(2), : Container(
child: const AspectRatio( padding: const EdgeInsets.all(2),
aspectRatio: 1, child: const AspectRatio(
child: CircularProgressIndicator(), aspectRatio: 1,
child: CircularProgressIndicator(
strokeWidth: 2,
), ),
), ),
), ),
), ),
), ),
) ),
], )
), ],
); ),
}, );
), },
), ),
); );
} }

View File

@@ -1,8 +1,9 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class NetworkSpeed extends StatefulWidget { class NetworkSpeed extends StatefulWidget {
const NetworkSpeed({super.key}); const NetworkSpeed({super.key});
@@ -49,9 +50,9 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
label: appLocalizations.networkSpeed, label: appLocalizations.networkSpeed,
iconData: Icons.speed_sharp, iconData: Icons.speed_sharp,
), ),
child: Selector<AppFlowingState, List<Traffic>>( child: Consumer(
selector: (_, appFlowingState) => appFlowingState.traffics, builder: (_, ref, __) {
builder: (_, traffics, __) { final traffics = ref.watch(trafficsProvider).list;
return Stack( return Stack(
children: [ children: [
Positioned.fill( Positioned.fill(
@@ -76,41 +77,11 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
-16, -16,
-20, -20,
), ),
child: Row( child: Text(
mainAxisAlignment: MainAxisAlignment.start, "${_getLastTraffic(traffics).up}${_getLastTraffic(traffics).down}",
children: [ style: context.textTheme.bodySmall?.copyWith(
Icon( color: color,
Icons.arrow_upward, ),
color: color,
size: 16,
),
SizedBox(
width: 2,
),
Text(
"${_getLastTraffic(traffics).up}/s",
style: context.textTheme.bodySmall?.copyWith(
color: color,
),
),
SizedBox(
width: 16,
),
Icon(
Icons.arrow_downward,
color: color,
size: 16,
),
SizedBox(
width: 2,
),
Text(
"${_getLastTraffic(traffics).down}/s",
style: context.textTheme.bodySmall?.copyWith(
color: color,
),
),
],
), ),
), ),
), ),

View File

@@ -1,11 +1,11 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class OutboundMode extends StatelessWidget { class OutboundMode extends StatelessWidget {
const OutboundMode({super.key}); const OutboundMode({super.key});
@@ -15,9 +15,10 @@ class OutboundMode extends StatelessWidget {
final height = getWidgetHeight(2); final height = getWidgetHeight(2);
return SizedBox( return SizedBox(
height: height, height: height,
child: Selector<ClashConfig, Mode>( child: Consumer(
selector: (_, clashConfig) => clashConfig.mode, builder: (_, ref, __) {
builder: (_, mode, __) { final mode =
ref.watch(patchClashConfigProvider.select((state) => state.mode));
return CommonCard( return CommonCard(
onPressed: () {}, onPressed: () {},
info: Info( info: Info(

View File

@@ -1,78 +1,76 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/fragments/config/network.dart'; import 'package:fl_clash/fragments/config/network.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class TUNButton extends StatelessWidget { class TUNButton extends StatelessWidget {
const TUNButton({super.key}); const TUNButton({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LocaleBuilder( return SizedBox(
builder: (_) => SizedBox( height: getWidgetHeight(1),
height: getWidgetHeight(1), child: CommonCard(
child: CommonCard( onPressed: () {
onPressed: () { showSheet(
showSheet( context: context,
context: context, body: generateListView(generateSection(
body: generateListView(generateSection( items: [
items: [ if (system.isDesktop) const TUNItem(),
if (system.isDesktop) const TUNItem(), const TunStackItem(),
const TunStackItem(), ],
], )),
)), title: appLocalizations.tun,
title: appLocalizations.tun, );
); },
}, info: Info(
info: Info( label: appLocalizations.tun,
label: appLocalizations.tun, iconData: Icons.stacked_line_chart,
iconData: Icons.stacked_line_chart, ),
child: Container(
padding: baseInfoEdgeInsets.copyWith(
top: 4,
bottom: 8,
right: 8,
), ),
child: Container( child: Row(
padding: baseInfoEdgeInsets.copyWith( mainAxisSize: MainAxisSize.max,
top: 4, mainAxisAlignment: MainAxisAlignment.spaceBetween,
bottom: 8, children: [
right: 8, Flexible(
), flex: 1,
child: Row( child: TooltipText(
mainAxisSize: MainAxisSize.max, text: Text(
mainAxisAlignment: MainAxisAlignment.spaceBetween, appLocalizations.options,
children: [ maxLines: 1,
Flexible( overflow: TextOverflow.ellipsis,
flex: 1, style: Theme.of(context)
child: TooltipText( .textTheme
text: Text( .titleSmall
appLocalizations.options, ?.adjustSize(-2)
maxLines: 1, .toLight,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleSmall
?.adjustSize(-2)
.toLight,
),
), ),
), ),
Selector<ClashConfig, bool>( ),
selector: (_, clashConfig) => clashConfig.tun.enable, Consumer(
builder: (_, enable, __) { builder: (_, ref, __) {
return Switch( final enable = ref.watch(patchClashConfigProvider
value: enable, .select((state) => state.tun.enable));
onChanged: (value) { return Switch(
final clashConfig = value: enable,
globalState.appController.clashConfig; onChanged: (value) {
clashConfig.tun = clashConfig.tun.copyWith( ref.read(patchClashConfigProvider.notifier).updateState(
enable: value, (state) => state.copyWith.tun(
); enable: value,
}, ),
); );
}, },
) );
], },
), )
],
), ),
), ),
), ),
@@ -87,67 +85,68 @@ class SystemProxyButton extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
height: getWidgetHeight(1), height: getWidgetHeight(1),
child: LocaleBuilder( child: CommonCard(
builder: (_) => CommonCard( onPressed: () {
onPressed: () { showSheet(
showSheet( context: context,
context: context, body: generateListView(
body: generateListView( generateSection(
generateSection( items: [
items: [ SystemProxyItem(),
SystemProxyItem(), BypassDomainItem(),
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( title: appLocalizations.systemProxy,
mainAxisSize: MainAxisSize.max, );
mainAxisAlignment: MainAxisAlignment.spaceBetween, },
children: [ info: Info(
Flexible( label: appLocalizations.systemProxy,
flex: 1, iconData: Icons.shuffle,
child: TooltipText( ),
text: Text( child: Container(
appLocalizations.options, padding: baseInfoEdgeInsets.copyWith(
maxLines: 1, top: 4,
overflow: TextOverflow.ellipsis, bottom: 8,
style: Theme.of(context) right: 8,
.textTheme ),
.titleSmall child: Row(
?.adjustSize(-2) mainAxisSize: MainAxisSize.max,
.toLight, 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, Consumer(
builder: (_, systemProxy, __) { builder: (_, ref, __) {
return Switch( final systemProxy = ref.watch(networkSettingProvider
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, .select((state) => state.systemProxy));
value: systemProxy, return Switch(
onChanged: (value) { materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
final config = globalState.appController.config; value: systemProxy,
config.networkProps = onChanged: (value) {
config.networkProps.copyWith(systemProxy: value); ref.read(networkSettingProvider.notifier).updateState(
}, (state) => state.copyWith(
); systemProxy: value,
}, ),
) );
], },
), );
},
)
],
), ),
), ),
), ),

View File

@@ -1,8 +1,9 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class StartButton extends StatefulWidget { class StartButton extends StatefulWidget {
const StartButton({super.key}); const StartButton({super.key});
@@ -19,7 +20,7 @@ class _StartButtonState extends State<StartButton>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
isStart = globalState.appController.appFlowingState.isStart; isStart = globalState.appState.runTime != null;
_controller = AnimationController( _controller = AnimationController(
vsync: this, vsync: this,
value: isStart ? 1 : 0, value: isStart ? 1 : 0,
@@ -34,11 +35,10 @@ class _StartButtonState extends State<StartButton>
} }
handleSwitchStart() { handleSwitchStart() {
final appController = globalState.appController; if (isStart == globalState.appState.isStart) {
if (isStart == appController.appFlowingState.isStart) {
isStart = !isStart; isStart = !isStart;
updateController(); updateController();
appController.updateStatus(isStart); globalState.appController.updateStatus(isStart);
} }
} }
@@ -50,31 +50,24 @@ class _StartButtonState extends State<StartButton>
} }
} }
Widget _updateControllerContainer(Widget child) {
return Selector<AppFlowingState, bool>(
selector: (_, appFlowingState) => appFlowingState.isStart,
builder: (_, isStart, child) {
if (isStart != this.isStart) {
this.isStart = isStart;
updateController();
}
return child!;
},
child: child,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector2<AppState, Config, StartButtonSelectorState>( return Consumer(
selector: (_, appState, config) => StartButtonSelectorState( builder: (_, ref, child) {
isInit: appState.isInit, final state = ref.watch(startButtonSelectorStateProvider);
hasProfile: config.profiles.isNotEmpty,
),
builder: (_, state, child) {
if (!state.isInit || !state.hasProfile) { if (!state.isInit || !state.hasProfile) {
return Container(); return Container();
} }
ref.listenManual(
runTimeProvider.select((state) => state != null),
(prev, next) {
if (next != isStart) {
isStart = next;
updateController();
}
},
fireImmediately: true,
);
final textWidth = globalState.measure final textWidth = globalState.measure
.computeTextSize( .computeTextSize(
Text( Text(
@@ -86,53 +79,51 @@ class _StartButtonState extends State<StartButton>
) )
.width + .width +
16; 16;
return _updateControllerContainer( return AnimatedBuilder(
AnimatedBuilder( animation: _controller.view,
animation: _controller.view, builder: (_, child) {
builder: (_, child) { return SizedBox(
return SizedBox( width: 56 + textWidth * _controller.value,
width: 56 + textWidth * _controller.value, height: 56,
height: 56, child: FloatingActionButton(
child: FloatingActionButton( heroTag: null,
heroTag: null, onPressed: () {
onPressed: () { handleSwitchStart();
handleSwitchStart(); },
}, child: Row(
child: Row( children: [
children: [ Container(
Container( width: 56,
width: 56, height: 56,
height: 56, alignment: Alignment.center,
alignment: Alignment.center, child: AnimatedIcon(
child: AnimatedIcon( icon: AnimatedIcons.play_pause,
icon: AnimatedIcons.play_pause, progress: _controller,
progress: _controller,
),
), ),
Expanded( ),
child: ClipRect( Expanded(
child: OverflowBox( child: ClipRect(
maxWidth: textWidth, child: OverflowBox(
child: Container( maxWidth: textWidth,
alignment: Alignment.centerLeft, child: Container(
child: child!, alignment: Alignment.centerLeft,
), child: child!,
), ),
), ),
), ),
], ),
), ],
), ),
); ),
}, );
child: child, },
), child: child,
); );
}, },
child: Selector<AppFlowingState, int?>( child: Consumer(
selector: (_, appFlowingState) => appFlowingState.runTime, builder: (_, ref, __) {
builder: (_, int? value, __) { final runTime = ref.watch(runTimeProvider);
final text = other.getTimeText(value); final text = other.getTimeText(runTime);
return Text( return Text(
text, text,
style: Theme.of(context).textTheme.titleMedium?.toSoftBold, style: Theme.of(context).textTheme.titleMedium?.toSoftBold,

View File

@@ -2,10 +2,11 @@ import 'dart:math';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class TrafficUsage extends StatelessWidget { class TrafficUsage extends StatelessWidget {
const TrafficUsage({super.key}); const TrafficUsage({super.key});
@@ -62,9 +63,9 @@ class TrafficUsage extends StatelessWidget {
iconData: Icons.data_saver_off, iconData: Icons.data_saver_off,
), ),
onPressed: () {}, onPressed: () {},
child: Selector<AppFlowingState, Traffic>( child: Consumer(
selector: (_, appFlowingState) => appFlowingState.totalTraffic, builder: (_, ref, __) {
builder: (_, totalTraffic, __) { final totalTraffic = ref.watch(totalTrafficProvider);
final upTotalTrafficValue = totalTraffic.up; final upTotalTrafficValue = totalTraffic.up;
final downTotalTrafficValue = totalTraffic.down; final downTotalTrafficValue = totalTraffic.down;
return Padding( return Padding(

View File

@@ -3,11 +3,11 @@ export 'dashboard/dashboard.dart';
export 'tools.dart'; export 'tools.dart';
export 'profiles/profiles.dart'; export 'profiles/profiles.dart';
export 'logs.dart'; export 'logs.dart';
export 'connections.dart';
export 'access.dart'; export 'access.dart';
export 'config/config.dart'; export 'config/config.dart';
export 'application_setting.dart'; export 'application_setting.dart';
export 'about.dart'; export 'about.dart';
export 'backup_and_recovery.dart'; export 'backup_and_recovery.dart';
export 'resources.dart'; export 'resources.dart';
export 'requests.dart'; export 'connection/requests.dart';
export 'connection/connections.dart';

View File

@@ -1,13 +1,14 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/card.dart'; import 'package:fl_clash/widgets/card.dart';
import 'package:fl_clash/widgets/list.dart'; import 'package:fl_clash/widgets/list.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
extension IntlExt on Intl { extension IntlExt on Intl {
static actionMessage(String messageText) => static actionMessage(String messageText) =>
@@ -38,29 +39,20 @@ class HotKeyFragment extends StatelessWidget {
itemCount: HotAction.values.length, itemCount: HotAction.values.length,
itemBuilder: (_, index) { itemBuilder: (_, index) {
final hotAction = HotAction.values[index]; final hotAction = HotAction.values[index];
return Selector<Config, HotKeyAction>( return Consumer(
selector: (_, config) { builder: (_, ref, __) {
final index = config.hotKeyActions.indexWhere( final hotKeyAction = ref.watch(getHotKeyActionProvider(hotAction));
(item) => item.action == hotAction,
);
return index != -1
? config.hotKeyActions[index]
: HotKeyAction(
action: hotAction,
);
},
builder: (_, value, __) {
return ListItem( return ListItem(
title: Text(IntlExt.actionMessage(hotAction.name)), title: Text(IntlExt.actionMessage(hotAction.name)),
subtitle: Text( subtitle: Text(
getSubtitle(value), getSubtitle(hotKeyAction),
style: context.textTheme.bodyMedium style: context.textTheme.bodyMedium
?.copyWith(color: context.colorScheme.primary), ?.copyWith(color: context.colorScheme.primary),
), ),
onTap: () { onTap: () {
globalState.showCommonDialog( globalState.showCommonDialog(
child: HotKeyRecorder( child: HotKeyRecorder(
hotKeyAction: value, hotKeyAction: hotKeyAction,
), ),
); );
}, },
@@ -121,8 +113,7 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
_handleRemove() { _handleRemove() {
Navigator.of(context).pop(); Navigator.of(context).pop();
final config = globalState.appController.config; globalState.appController.updateOrAddHotKeyAction(
config.updateOrAddHotKeyAction(
hotKeyActionNotifier.value.copyWith( hotKeyActionNotifier.value.copyWith(
modifiers: {}, modifiers: {},
key: null, key: null,
@@ -132,7 +123,7 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
_handleConfirm() { _handleConfirm() {
Navigator.of(context).pop(); Navigator.of(context).pop();
final config = globalState.appController.config; final config = globalState.config;
final currentHotkeyAction = hotKeyActionNotifier.value; final currentHotkeyAction = hotKeyActionNotifier.value;
if (currentHotkeyAction.key == null || if (currentHotkeyAction.key == null ||
currentHotkeyAction.modifiers.isEmpty) { currentHotkeyAction.modifiers.isEmpty) {
@@ -158,7 +149,7 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
); );
return; return;
} }
config.updateOrAddHotKeyAction( globalState.appController.updateOrAddHotKeyAction(
currentHotkeyAction, currentHotkeyAction,
); );
} }

View File

@@ -1,61 +1,106 @@
import 'dart:async';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/models.dart'; import '../models/models.dart';
import '../widgets/widgets.dart'; import '../widgets/widgets.dart';
class LogsFragment extends StatefulWidget { double _preOffset = 0;
class LogsFragment extends ConsumerStatefulWidget {
const LogsFragment({super.key}); const LogsFragment({super.key});
@override @override
State<LogsFragment> createState() => _LogsFragmentState(); ConsumerState<LogsFragment> createState() => _LogsFragmentState();
} }
class _LogsFragmentState extends State<LogsFragment> { class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
final logsNotifier = ValueNotifier<LogsAndKeywords>(const LogsAndKeywords()); final _logsStateNotifier = ValueNotifier<LogsState>(LogsState());
final scrollController = ScrollController( final _scrollController = ScrollController(
keepScrollOffset: false, initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
); );
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
double _currentMaxWidth = 0;
Timer? timer; List<Log> _logs = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { _logsStateNotifier.value = _logsStateNotifier.value.copyWith(
final appFlowingState = globalState.appController.appFlowingState; logs: globalState.appState.logs.list,
logsNotifier.value = );
logsNotifier.value.copyWith(logs: appFlowingState.logs); ref.listenManual(
if (timer != null) { logsProvider.select((state) => state.list),
timer?.cancel(); (prev, next) {
timer = null; if (prev != next) {
} final isEquality = logListEquality.equals(prev, next);
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) { if (!isEquality) {
final logs = appFlowingState.logs; _logs = next;
if (!logListEquality.equals( updateLogsThrottler();
logsNotifier.value.logs, }
logs,
)) {
logsNotifier.value = logsNotifier.value.copyWith(
logs: logs,
);
} }
}); },
}); fireImmediately: true,
);
ref.listenManual(
isCurrentPageProvider(
PageLabel.logs,
handler: (pageLabel, viewMode) =>
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
),
(prev, next) {
if (prev != next && next == true) {
initPageState();
}
},
fireImmediately: true,
);
} }
@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 @override
void dispose() { void dispose() {
timer?.cancel(); _logsStateNotifier.dispose();
logsNotifier.dispose(); _scrollController.dispose();
scrollController.dispose(); _cacheDynamicHeightMap.clear();
timer = null;
super.dispose(); super.dispose();
} }
_handleTryClearCache(double maxWidth) {
if (_currentMaxWidth != maxWidth) {
_currentMaxWidth = maxWidth;
_cacheDynamicHeightMap.clear();
}
}
_handleExport() async { _handleExport() async {
final commonScaffoldState = context.commonScaffoldState; final commonScaffoldState = context.commonScaffoldState;
final res = await commonScaffoldState?.loadingRun<bool>( final res = await commonScaffoldState?.loadingRun<bool>(
@@ -71,295 +116,115 @@ class _LogsFragmentState extends State<LogsFragment> {
); );
} }
_initActions() { double _calcCacheHeight(String text) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { final cacheHeight = _cacheDynamicHeightMap.get(text);
final commonScaffoldState = if (cacheHeight != null) {
context.findAncestorStateOfType<CommonScaffoldState>(); return cacheHeight;
commonScaffoldState?.actions = [ }
IconButton( final size = globalState.measure.computeTextSize(
onPressed: () { Text(
showSearch( text,
context: context, style: globalState.appController.context.textTheme.bodyLarge,
delegate: LogsSearchDelegate( ),
logs: logsNotifier.value, maxWidth: _currentMaxWidth,
), );
); _cacheDynamicHeightMap.put(text, size.height);
}, return size.height;
icon: const Icon(Icons.search),
),
IconButton(
onPressed: () {
_handleExport();
},
icon: const Icon(
Icons.file_download_outlined,
),
),
];
});
} }
_addKeyword(String keyword) { double _getItemHeight(Log log) {
final isContains = logsNotifier.value.keywords.contains(keyword); final measure = globalState.measure;
if (isContains) return; final bodySmallHeight = measure.bodySmallHeight;
final keywords = List<String>.from(logsNotifier.value.keywords) final bodyMediumHeight = measure.bodyMediumHeight;
..add(keyword); final height = _calcCacheHeight(log.payload ?? "");
logsNotifier.value = logsNotifier.value.copyWith( return height + bodySmallHeight + 8 + bodyMediumHeight + 40;
keywords: keywords,
);
} }
_deleteKeyword(String keyword) { updateLogsThrottler() {
final isContains = logsNotifier.value.keywords.contains(keyword); throttler.call("logs", () {
if (!isContains) return; final isEquality = logListEquality.equals(
final keywords = List<String>.from(logsNotifier.value.keywords) _logs,
..remove(keyword); _logsStateNotifier.value.logs,
logsNotifier.value = logsNotifier.value.copyWith( );
keywords: keywords, if (isEquality) {
); return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
logs: _logs,
);
});
}, duration: commonDuration);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<AppState, bool?>( return LayoutBuilder(
selector: (_, appState) => builder: (_, constraints) {
appState.currentLabel == 'logs' || _handleTryClearCache(constraints.maxWidth - 40);
appState.viewMode == ViewMode.mobile && return Align(
appState.currentLabel == "tools", alignment: Alignment.topCenter,
builder: (_, isCurrent, child) { child: ValueListenableBuilder<LogsState>(
if (isCurrent == null || isCurrent) { valueListenable: _logsStateNotifier,
_initActions(); builder: (_, state, __) {
} final logs = state.list;
return child!; if (logs.isEmpty) {
}, return NullStatus(
child: ValueListenableBuilder<LogsAndKeywords>( label: appLocalizations.nullLogsDesc,
valueListenable: logsNotifier, );
builder: (_, state, __) { }
final logs = state.filteredLogs; final items = logs
if (logs.isEmpty) { .map<Widget>(
return NullStatus( (log) => LogItem(
label: appLocalizations.nullLogsDesc, key: Key(log.dateTime.toString()),
); log: log,
} onClick: (value) {
final reversedLogs = logs.reversed.toList(); context.commonScaffoldState?.addKeyword(value);
final logWidgets = reversedLogs },
.map<Widget>( ),
(log) => LogItem( )
key: Key(log.dateTime.toString()), .separated(
log: log, const Divider(
onClick: _addKeyword, height: 0,
), ),
) )
.separated( .toList();
const Divider( return NotificationListener<ScrollEndNotification>(
height: 0, onNotification: (details) {
), _preOffset = details.metrics.pixels;
) return false;
.toList(); },
return Column( child: CommonScrollBar(
crossAxisAlignment: CrossAxisAlignment.start, controller: _scrollController,
children: [ child: ListView.builder(
if (state.keywords.isNotEmpty) reverse: true,
Padding( shrinkWrap: true,
padding: const EdgeInsets.symmetric( physics: NextClampingScrollPhysics(),
horizontal: 16, controller: _scrollController,
vertical: 16, itemBuilder: (_, index) {
), return items[index];
child: Wrap(
runSpacing: 8,
spacing: 8,
children: [
for (final keyword in state.keywords)
CommonChip(
label: keyword,
type: ChipType.delete,
onPressed: () {
_deleteKeyword(keyword);
},
),
],
),
),
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() {
logsNotifier.dispose();
super.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);
}, },
); itemExtentBuilder: (index, __) {
}, final item = items[index];
separatorBuilder: (BuildContext context, int index) { if (item.runtimeType == Divider) {
return const Divider( return 0;
height: 0, }
); final log = logs[(index / 2).floor()];
}, return _getItemHeight(log);
itemCount: _results.length, },
), itemCount: items.length,
) ),
], ),
);
},
),
); );
}, },
); );
} }
} }
class LogItem extends StatefulWidget { class LogItem extends StatelessWidget {
final Log log; final Log log;
final Function(String)? onClick; final Function(String)? onClick;
@@ -369,14 +234,8 @@ class LogItem extends StatefulWidget {
this.onClick, this.onClick,
}); });
@override
State<LogItem> createState() => _LogItemState();
}
class _LogItemState extends State<LogItem> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final log = widget.log;
return ListItem( return ListItem(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
@@ -384,14 +243,16 @@ class _LogItemState extends State<LogItem> {
), ),
title: SelectableText( title: SelectableText(
log.payload ?? '', log.payload ?? '',
style: context.textTheme.bodyLarge,
), ),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SelectableText( SelectableText(
"${log.dateTime}", "${log.dateTime}",
style: context.textTheme.bodySmall style: context.textTheme.bodySmall?.copyWith(
?.copyWith(color: context.colorScheme.primary), color: context.colorScheme.primary,
),
), ),
const SizedBox( const SizedBox(
height: 8, height: 8,
@@ -400,8 +261,8 @@ class _LogItemState extends State<LogItem> {
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: CommonChip( child: CommonChip(
onPressed: () { onPressed: () {
if (widget.onClick == null) return; if (onClick == null) return;
widget.onClick!(log.logLevel.name); onClick!(log.logLevel.name);
}, },
label: log.logLevel.name, label: log.logLevel.name,
), ),
@@ -411,3 +272,11 @@ class _LogItemState extends State<LogItem> {
); );
} }
} }
class NoGlowScrollBehavior extends ScrollBehavior {
@override
Widget buildOverscrollIndicator(
BuildContext context, Widget child, ScrollableDetails details) {
return child; // 禁用过度滚动效果
}
}

View File

@@ -0,0 +1,54 @@
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart';
class CustomProfile extends StatefulWidget {
final String profileId;
const CustomProfile({
super.key,
required this.profileId,
});
@override
State<CustomProfile> createState() => _CustomProfileState();
}
class _CustomProfileState extends State<CustomProfile> {
final _currentClashConfigNotifier = ValueNotifier<ClashConfig?>(null);
@override
void initState() {
super.initState();
_initCurrentClashConfig();
}
_initCurrentClashConfig() async {
// final currentProfileId = globalState.config.currentProfileId;
// if (currentProfileId == null) {
// return;
// }
// _currentClashConfigNotifier.value =
// await clashCore.getProfile(currentProfileId);
}
@override
Widget build(BuildContext context) {
return CommonScaffold(
body: ValueListenableBuilder(
valueListenable: _currentClashConfigNotifier,
builder: (_, clashConfig, ___) {
if (clashConfig == null) {
return Center(
child: CircularProgressIndicator(),
);
}
return Column(
children: [],
);
},
),
title: "自定义",
);
}
}

View File

@@ -1,15 +1,15 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.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/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
class EditProfile extends StatefulWidget { class EditProfile extends StatefulWidget {
final Profile profile; final Profile profile;
@@ -30,10 +30,13 @@ class _EditProfileState extends State<EditProfile> {
late TextEditingController urlController; late TextEditingController urlController;
late TextEditingController autoUpdateDurationController; late TextEditingController autoUpdateDurationController;
late bool autoUpdate; late bool autoUpdate;
String? rawText;
final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final fileInfoNotifier = ValueNotifier<FileInfo?>(null); final fileInfoNotifier = ValueNotifier<FileInfo?>(null);
Uint8List? fileData; Uint8List? fileData;
Profile get profile => widget.profile;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -51,28 +54,43 @@ class _EditProfileState extends State<EditProfile> {
_handleConfirm() async { _handleConfirm() async {
if (!_formKey.currentState!.validate()) return; if (!_formKey.currentState!.validate()) return;
final config = widget.context.read<Config>(); final appController = globalState.appController;
final profile = widget.profile.copyWith( Profile profile = this.profile.copyWith(
url: urlController.text, url: urlController.text,
label: labelController.text, label: labelController.text,
autoUpdate: autoUpdate, autoUpdate: autoUpdate,
autoUpdateDuration: Duration( autoUpdateDuration: Duration(
minutes: int.parse( minutes: int.parse(
autoUpdateDurationController.text, autoUpdateDurationController.text,
), ),
), ),
); );
final hasUpdate = widget.profile.url != profile.url; final hasUpdate = widget.profile.url != profile.url;
if (fileData != null) { 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 { } else {
config.setProfile(profile);
}
if (hasUpdate) {
globalState.homeScaffoldKey.currentState?.loadingRun( globalState.homeScaffoldKey.currentState?.loadingRun(
() async { () async {
await Future.delayed(
commonDuration,
);
if (hasUpdate) { if (hasUpdate) {
await globalState.appController.updateProfile(profile); await appController.updateProfile(profile);
} }
}, },
); );
@@ -102,22 +120,69 @@ class _EditProfileState extends State<EditProfile> {
); );
} }
_editProfileFile() async { _handleSaveEdit(BuildContext context, String data) async {
final profilePath = await appPath.getProfilePath(widget.profile.id); final message = await globalState.safeRun<String>(
if (profilePath == null) return; () async {
globalState.safeRun(() async { final message = await clashCore.validateConfig(data);
if (Platform.isAndroid) { return message;
await app?.openFile( },
profilePath, silence: false,
); );
return; if (message?.isNotEmpty == true) {
} globalState.showMessage(
await launchUrl( title: appLocalizations.tip,
Uri.file( message: TextSpan(text: message),
profilePath,
),
); );
}); 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 { _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final items = [ final items = [
@@ -245,34 +324,45 @@ class _EditProfileState extends State<EditProfile> {
}, },
), ),
]; ];
return FloatLayout( return PopScope(
floatingWidget: FloatWrapper( canPop: false,
child: FloatingActionButton.extended( onPopInvokedWithResult: (didPop, __) {
heroTag: null, if (didPop) return;
onPressed: _handleConfirm, if (fileData == null) {
label: Text(appLocalizations.save), Navigator.of(context).pop();
icon: const Icon(Icons.save), return;
), }
), _handleBack();
child: Form( },
key: _formKey, child: FloatLayout(
child: Padding( floatingWidget: FloatWrapper(
padding: const EdgeInsets.symmetric( child: FloatingActionButton.extended(
vertical: 16, heroTag: null,
onPressed: _handleConfirm,
label: Text(appLocalizations.save),
icon: const Icon(Icons.save),
), ),
child: ListView.separated( ),
padding: kMaterialListPadding.copyWith( child: Form(
bottom: 72, 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,
), ),
), ),
), ),

View File

@@ -4,21 +4,14 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/profiles/edit_profile.dart'; import 'package:fl_clash/fragments/profiles/edit_profile.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'add_profile.dart'; import 'add_profile.dart';
enum PopupMenuItemEnum { delete, edit }
enum ProfileActions {
edit,
update,
delete,
}
class ProfilesFragment extends StatefulWidget { class ProfilesFragment extends StatefulWidget {
const ProfilesFragment({super.key}); const ProfilesFragment({super.key});
@@ -26,7 +19,7 @@ class ProfilesFragment extends StatefulWidget {
State<ProfilesFragment> createState() => _ProfilesFragmentState(); State<ProfilesFragment> createState() => _ProfilesFragmentState();
} }
class _ProfilesFragmentState extends State<ProfilesFragment> { class _ProfilesFragmentState extends State<ProfilesFragment> with PageMixin {
Function? applyConfigDebounce; Function? applyConfigDebounce;
_handleShowAddExtendPage() { _handleShowAddExtendPage() {
@@ -40,21 +33,19 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
} }
_updateProfiles() async { _updateProfiles() async {
final appController = globalState.appController; final profiles = globalState.config.profiles;
final config = appController.config;
final profiles = appController.config.profiles;
final messages = []; final messages = [];
final updateProfiles = profiles.map<Future>( final updateProfiles = profiles.map<Future>(
(profile) async { (profile) async {
if (profile.type == ProfileType.file) return; if (profile.type == ProfileType.file) return;
config.setProfile( globalState.appController.setProfile(
profile.copyWith(isUpdating: true), profile.copyWith(isUpdating: true),
); );
try { try {
await appController.updateProfile(profile); await globalState.appController.updateProfile(profile);
} catch (e) { } catch (e) {
messages.add("${profile.label ?? profile.id}: $e \n"); messages.add("${profile.label ?? profile.id}: $e \n");
config.setProfile( globalState.appController.setProfile(
profile.copyWith( profile.copyWith(
isUpdating: false, isUpdating: false,
), ),
@@ -77,97 +68,90 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
} }
} }
_initScaffold() { @override
WidgetsBinding.instance.addPostFrameCallback( List<Widget> get actions => [
(_) { IconButton(
if (!mounted) return; onPressed: () {
final commonScaffoldState = _updateProfiles();
context.findAncestorStateOfType<CommonScaffoldState>(); },
commonScaffoldState?.actions = [ icon: const Icon(Icons.sync),
IconButton( ),
onPressed: () { IconButton(
_updateProfiles(); onPressed: () {
}, final profiles = globalState.config.profiles;
icon: const Icon(Icons.sync), showSheet(
), title: appLocalizations.profilesSort,
IconButton( context: context,
onPressed: () { body: SizedBox(
final profiles = globalState.appController.config.profiles; height: 400,
showSheet( child: ReorderableProfiles(profiles: profiles),
title: appLocalizations.profilesSort, ),
context: context, );
body: SizedBox( },
height: 400, icon: const Icon(Icons.sort),
child: ReorderableProfiles(profiles: profiles), iconSize: 26,
), ),
); ];
},
icon: const Icon(Icons.sort), @override
iconSize: 26, Widget? get floatingActionButton => FloatingActionButton(
), heroTag: null,
]; onPressed: _handleShowAddExtendPage,
commonScaffoldState?.floatingActionButton = FloatingActionButton( child: const Icon(
heroTag: null, Icons.add,
onPressed: _handleShowAddExtendPage, ),
child: const Icon( );
Icons.add,
),
);
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ActiveBuilder( return Consumer(
label: "profiles", builder: (_, ref, __) {
builder: (isCurrent, child) { ref.listenManual(
if (isCurrent) { isCurrentPageProvider(PageLabel.profiles),
_initScaffold(); (prev, next) {
} if (prev != next && next == true) {
return child!; initPageState();
}, }
child: Selector2<AppState, Config, ProfilesSelectorState>( },
selector: (_, appState, config) => ProfilesSelectorState( fireImmediately: true,
profiles: config.profiles, );
currentProfileId: config.currentProfileId, final profilesSelectorState = ref.watch(profilesSelectorStateProvider);
columns: other.getProfilesColumns(appState.viewWidth), if (profilesSelectorState.profiles.isEmpty) {
), return NullStatus(
builder: (context, state, child) { label: appLocalizations.nullProfileDesc,
if (state.profiles.isEmpty) {
return NullStatus(
label: appLocalizations.nullProfileDesc,
);
}
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 88,
),
child: Grid(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: state.columns,
children: [
for (int i = 0; i < state.profiles.length; i++)
GridItem(
child: ProfileItem(
key: Key(state.profiles[i].id),
profile: state.profiles[i],
groupValue: state.currentProfileId,
onChanged: globalState.appController.changeProfile,
),
),
],
),
),
); );
}, }
), return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 88,
),
child: Grid(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: profilesSelectorState.columns,
children: [
for (int i = 0; i < profilesSelectorState.profiles.length; i++)
GridItem(
child: ProfileItem(
key: Key(profilesSelectorState.profiles[i].id),
profile: profilesSelectorState.profiles[i],
groupValue: profilesSelectorState.currentProfileId,
onChanged: (profileId) {
ref.read(currentProfileIdProvider.notifier).value =
profileId;
},
),
),
],
),
),
);
},
); );
} }
} }
@@ -185,18 +169,16 @@ class ProfileItem extends StatelessWidget {
}); });
_handleDeleteProfile(BuildContext context) async { _handleDeleteProfile(BuildContext context) async {
globalState.showMessage( final res = await globalState.showMessage(
title: appLocalizations.tip, title: appLocalizations.tip,
message: TextSpan( message: TextSpan(
text: appLocalizations.deleteProfileTip, 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 { _handleUpdateProfile() async {
@@ -205,18 +187,17 @@ class ProfileItem extends StatelessWidget {
Future updateProfile() async { Future updateProfile() async {
final appController = globalState.appController; final appController = globalState.appController;
final config = appController.config;
if (profile.type == ProfileType.file) return; if (profile.type == ProfileType.file) return;
await globalState.safeRun(silence: false, () async { await globalState.safeRun(silence: false, () async {
try { try {
config.setProfile( appController.setProfile(
profile.copyWith( profile.copyWith(
isUpdating: true, isUpdating: true,
), ),
); );
await appController.updateProfile(profile); await appController.updateProfile(profile);
} catch (e) { } catch (e) {
config.setProfile( appController.setProfile(
profile.copyWith( profile.copyWith(
isUpdating: false, isUpdating: false,
), ),
@@ -266,8 +247,48 @@ 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);
}
}
// _handlePushCustomPage(BuildContext context, String id) {
// BaseNavigator.push(
// context,
// CustomProfile(
// profileId: id,
// ),
// );
// }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final key = GlobalKey<CommonPopupBoxState>();
return CommonCard( return CommonCard(
isSelected: profile.id == groupValue, isSelected: profile.id == groupValue,
onPressed: () { onPressed: () {
@@ -286,46 +307,69 @@ class ProfileItem extends StatelessWidget {
padding: EdgeInsets.all(8), padding: EdgeInsets.all(8),
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
) )
: CommonPopupMenu<ProfileActions>( : CommonPopupBox(
icon: Icon(Icons.more_vert), key: key,
items: [ popup: CommonPopupMenu(
CommonPopupMenuItem( items: [
action: ProfileActions.edit, ActionItemData(
label: appLocalizations.edit, icon: Icons.edit_outlined,
iconData: Icons.edit, label: appLocalizations.edit,
), onPressed: () {
if (profile.type == ProfileType.url) _handleShowEditExtendPage(context);
CommonPopupMenuItem( },
action: ProfileActions.update,
label: appLocalizations.update,
iconData: Icons.sync,
), ),
CommonPopupMenuItem( if (profile.type == ProfileType.url) ...[
action: ProfileActions.delete, ActionItemData(
label: appLocalizations.delete, icon: Icons.sync_alt_sharp,
iconData: Icons.delete, label: appLocalizations.sync,
), onPressed: () {
], _handleUpdateProfile();
onSelected: (ProfileActions? action) async { },
switch (action) { ),
case ProfileActions.edit: // ActionItemData(
_handleShowEditExtendPage(context); // icon: Icons.copy,
break; // label: appLocalizations.copyLink,
case ProfileActions.delete: // onPressed: () {
_handleDeleteProfile(context); // _handleCopyLink(context);
break; // },
case ProfileActions.update: // ),
_handleUpdateProfile(); ],
break; // ActionItemData(
case null: // icon: Icons.extension_outlined,
break; // label: "自定义",
} // onPressed: () {
}, // _handlePushCustomPage(context, profile.id);
// },
// ),
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( title: Container(
padding: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(vertical: 8),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -462,7 +506,7 @@ class _ReorderableProfilesState extends State<ReorderableProfiles> {
child: FilledButton.tonal( child: FilledButton.tonal(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
globalState.appController.config.profiles = profiles; globalState.appController.setProfiles(profiles);
}, },
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStateProperty.all( padding: WidgetStateProperty.all(

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