Compare commits

...

6 Commits

Author SHA1 Message Date
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
127 changed files with 7044 additions and 6594 deletions

View File

@@ -1,3 +1,35 @@
## 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

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

@@ -25,4 +25,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

@@ -11,13 +11,16 @@ import android.net.NetworkRequest
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
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 +29,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 +56,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 +66,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,16 +82,11 @@ 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)
} }
@@ -102,13 +102,6 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
} }
} }
"startForeground" -> {
val title = call.argument<String>("title") as String
val content = call.argument<String>("content") as String
startForeground(title, content)
result.success(true)
}
"resolverProcess" -> { "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 +137,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 +150,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 +181,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 +212,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 +258,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 {
@@ -105,12 +105,6 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
} }
} }
fun updateUnderlyingNetworks(networks: Array<Network>) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
this.setUnderlyingNetworks(networks)
}
}
override fun stop() { 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 +116,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

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

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

View File

@@ -1,18 +1,19 @@
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"
) )
type ConfigExtendedParams struct { type ConfigExtendedParams struct {
IsPatch bool `json:"is-patch"` IsPatch bool `json:"is-patch"`
IsCompatible bool `json:"is-compatible"` IsCompatible bool `json:"is-compatible"`
SelectedMap map[string]string `json:"selected-map"` SelectedMap map[string]string `json:"selected-map"`
TestURL *string `json:"test-url"` TestURL *string `json:"test-url"`
OverrideDns bool `json:"override-dns"` OverrideDns bool `json:"override-dns"`
OnlyStatisticsProxy bool `json:"only-statistics-proxy"`
} }
type GenerateConfigParams struct { type GenerateConfigParams struct {
@@ -28,14 +29,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 +71,23 @@ 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"
) )
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

@@ -26,8 +26,10 @@ import (
) )
var ( var (
isInit = false isInit = false
configParams = ConfigExtendedParams{} configParams = ConfigExtendedParams{
OnlyStatisticsProxy: false,
}
externalProviders = map[string]cp.Provider{} externalProviders = map[string]cp.Provider{}
logSubscriber observable.Subscription[log.Event] logSubscriber observable.Subscription[log.Event]
currentConfig *config.Config currentConfig *config.Config
@@ -149,8 +151,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(configParams.OnlyStatisticsProxy)
traffic := map[string]int64{ traffic := map[string]int64{
"up": up, "up": up,
"down": down, "down": down,
@@ -163,8 +165,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(configParams.OnlyStatisticsProxy)
traffic := map[string]int64{ traffic := map[string]int64{
"up": up, "up": up,
"down": down, "down": down,
@@ -213,7 +215,13 @@ 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
}
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 +248,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 +392,7 @@ func handleStartLog() {
Type: LogMessage, Type: LogMessage,
Data: logData, Data: logData,
} }
SendMessage(*message) sendMessage(*message)
} }
}() }()
} }
@@ -427,8 +424,9 @@ func handleGetMemory(fn func(value string)) {
} }
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 +434,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(bytes []byte) {
bridge.SendToPort(i, string(bytes))
})
}
func sendMessage(message Message) {
if messagePort == -1 {
return
}
res, err := message.Json()
if err != nil {
return
}
bridge.SendToPort(messagePort, string(Action{
Method: messageMethod,
}.wrapMessage(res)))
} }
//export startListener //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,165 @@ 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) load(id string) string {
res, ok := m.invokeMap.Load(id)
if ok {
return res.(string)
}
return ""
}
func (m *InvokeManager) delete(id string) {
m.invokeMap.Delete(id)
}
func (m *InvokeManager) completer(id string, value string) {
m.invokeMap.Store(id, value)
m.chanLock.Lock()
if ch, ok := m.chanMap[id]; ok {
close(ch)
delete(m.chanMap, id)
}
m.chanLock.Unlock()
}
func (m *InvokeManager) await(id string) {
m.chanLock.Lock()
if _, ok := m.chanMap[id]; !ok {
m.chanMap[id] = make(chan struct{})
}
ch := m.chanMap[id]
m.chanLock.Unlock()
timeout := time.After(500 * time.Millisecond)
select {
case <-ch:
return
case <-timeout:
m.completer(id, "")
return
}
} }
var ( 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 +187,15 @@ 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 { fdInvokeMap.delete(id)
case <-timeout:
return
default:
exists := fdMap.Load(id)
if exists {
return
}
time.Sleep(20 * time.Millisecond)
}
}
}) })
} }
} }
@@ -176,58 +209,19 @@ 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,
},
}) })
processInvokeMap.await(id)
for { res := processInvokeMap.load(id)
select { processInvokeMap.delete(id)
case <-timeout: return res, nil
return "", errors.New("package resolver timeout")
default:
value, exists := processMap.Load(id)
if exists {
return value, nil
}
time.Sleep(20 * time.Millisecond)
}
}
} }
} }
//export setProcessMap func handleGetAndroidVpnOptions() string {
func setProcessMap(s *C.char) {
if s == nil {
return
}
paramsString := C.GoString(s)
go func() {
var processMapItem = &ProcessMapItem{}
err := json.Unmarshal([]byte(paramsString), processMapItem)
if err == nil {
processMap.Store(processMapItem.Id, processMapItem.Value)
}
}()
}
//export getCurrentProfileName
func getCurrentProfileName() *C.char {
if state.CurrentState == nil {
return C.CString("")
}
return C.CString(state.CurrentState.CurrentProfileName)
}
//export getAndroidVpnOptions
func getAndroidVpnOptions() *C.char {
tunLock.Lock() tunLock.Lock()
defer tunLock.Unlock() defer tunLock.Unlock()
options := state.AndroidVpnOptions{ options := state.AndroidVpnOptions{
@@ -245,26 +239,140 @@ func getAndroidVpnOptions() *C.char {
data, err := json.Marshal(options) data, err := json.Marshal(options)
if err != nil { if err != nil {
fmt.Println("Error:", err) fmt.Println("Error:", err)
return C.CString("") return ""
} }
return C.CString(string(data)) return string(data)
}
func handleSetState(params string) {
_ = json.Unmarshal([]byte(params), state.CurrentState)
}
func handleUpdateDns(value string) {
go func() {
log.Infoln("[DNS] updateDns %s", value)
dns.UpdateSystemDNS(strings.Split(value, ","))
dns.FlushCacheWithDefaultResolver()
}()
}
func handleGetCurrentProfileName() string {
if state.CurrentState == nil {
return ""
}
return state.CurrentState.CurrentProfileName
}
func nextHandle(action *Action, send func([]byte)) bool {
switch action.Method {
case startTunMethod:
data := action.Data.(string)
var fd int
_ = json.Unmarshal([]byte(data), &fd)
send(action.wrapMessage(handleStartTun(fd)))
return true
case stopTunMethod:
handleStopTun()
send(action.wrapMessage(true))
return true
case setStateMethod:
data := action.Data.(string)
handleSetState(data)
send(action.wrapMessage(true))
return true
case getAndroidVpnOptionsMethod:
send(action.wrapMessage(handleGetAndroidVpnOptions()))
return true
case updateDnsMethod:
data := action.Data.(string)
handleUpdateDns(data)
send(action.wrapMessage(true))
return true
case setFdMapMethod:
fdId := action.Data.(string)
handleSetFdMap(fdId)
send(action.wrapMessage(true))
return true
case setProcessMapMethod:
data := action.Data.(string)
handleSetProcessMap(data)
send(action.wrapMessage(true))
return true
case getRunTimeMethod:
send(action.wrapMessage(handleGetRunTime()))
return true
case getCurrentProfileNameMethod:
send(action.wrapMessage(handleGetCurrentProfileName()))
return true
}
return false
}
//export quickStart
func quickStart(dirChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) {
i := int64(port)
dir := C.GoString(dirChar)
bytes := []byte(C.GoString(paramsChar))
stateParams := C.GoString(stateParamsChar)
go func() {
res := handleInitClash(dir)
if res == false {
bridge.SendToPort(i, "init error")
}
handleSetState(stateParams)
bridge.SendToPort(i, handleUpdateConfig(bytes))
}()
}
//export startTUN
func startTUN(fd C.int) *C.char {
f := int(fd)
return C.CString(handleStartTun(f))
}
//export getRunTime
func getRunTime() *C.char {
return C.CString(handleGetRunTime())
}
//export stopTun
func stopTun() {
handleStopTun()
}
//export setFdMap
func setFdMap(fdIdChar *C.char) {
fdId := C.GoString(fdIdChar)
handleSetFdMap(fdId)
}
//export getCurrentProfileName
func getCurrentProfileName() *C.char {
return C.CString(handleGetCurrentProfileName())
}
//export getAndroidVpnOptions
func getAndroidVpnOptions() *C.char {
return C.CString(handleGetAndroidVpnOptions())
} }
//export setState //export setState
func setState(s *C.char) { func setState(s *C.char) {
paramsString := C.GoString(s) paramsString := C.GoString(s)
err := json.Unmarshal([]byte(paramsString), state.CurrentState) handleSetState(paramsString)
if err != nil {
return
}
} }
//export updateDns //export updateDns
func updateDns(s *C.char) { func updateDns(s *C.char) {
dnsList := C.GoString(s) dnsList := C.GoString(s)
go func() { handleUpdateDns(dnsList)
log.Infoln("[DNS] updateDns %s", dnsList) }
dns.UpdateSystemDNS(strings.Split(dnsList, ","))
dns.FlushCacheWithDefaultResolver() //export setProcessMap
}() func setProcessMap(s *C.char) {
if s == nil {
return
}
paramsString := C.GoString(s)
handleSetProcessMap(paramsString)
} }

11
core/lib_no_android.go Normal file
View File

@@ -0,0 +1,11 @@
//go:build !android && cgo
package main
func nextHandle(action *Action) {
return action
}
func nextHandle(action *Action, send func([]byte)) bool {
return false
}

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,
}.wrapMessage(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(bytes []byte) {
send(bytes)
})
} }
} }
func handleAction(action *Action) { func nextHandle(action *Action, send func([]byte)) 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

@@ -153,7 +153,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,
), ),
), ),
@@ -175,8 +178,8 @@ class ApplicationState extends State<Application> {
@override @override
Widget build(context) { Widget build(context) {
return _buildWrap( return _buildPlatformWrap(
_buildPlatformWrap( _buildWrap(
Selector2<AppState, Config, ApplicationSelectorState>( Selector2<AppState, Config, ApplicationSelectorState>(
selector: (_, appState, config) => ApplicationSelectorState( selector: (_, appState, config) => ApplicationSelectorState(
locale: config.appSetting.locale, locale: config.appSetting.locale,
@@ -252,7 +255,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) {
@@ -63,8 +67,8 @@ class ClashCore {
required ClashConfig clashConfig, required ClashConfig clashConfig,
required Config config, required Config config,
}) async { }) async {
await _initGeo(); await initGeo();
final homeDirPath = await appPath.getHomeDirPath(); final homeDirPath = await appPath.homeDirPath;
return await clashInterface.init(homeDirPath); return await clashInterface.init(homeDirPath);
} }
@@ -135,6 +139,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 +159,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 +168,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 +194,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,13 +218,19 @@ 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);
} }
@@ -236,6 +249,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,28 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:fl_clash/clash/message.dart';
import 'package:fl_clash/common/constant.dart';
import 'package:fl_clash/common/future.dart';
import 'package:fl_clash/common/other.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart' hide Action;
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 +38,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 +47,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);
@@ -61,3 +67,339 @@ mixin ClashInterface {
FutureOr<bool> closeConnections(); FutureOr<bool> closeConnections();
} }
mixin AndroidClashInterface {
Future<bool> setFdMap(int fd);
Future<bool> setProcessMap(ProcessMapItem item);
Future<bool> setState(CoreState state);
Future<bool> stopTun();
Future<bool> updateDns(String value);
Future<DateTime?> startTun(int fd);
Future<AndroidVpnOptions?> getAndroidVpnOptions();
Future<String> getCurrentProfileName();
Future<DateTime?> getRunTime();
}
abstract class ClashHandlerInterface with ClashInterface {
Map<String, Completer> callbackCompleterMap = {};
Future<bool> nextHandleResult(ActionResult result, Completer? completer) =>
Future.value(false);
handleResult(ActionResult result) async {
final completer = callbackCompleterMap[result.id];
try {
switch (result.method) {
case ActionMethod.initClash:
case ActionMethod.shutdown:
case ActionMethod.getIsInit:
case ActionMethod.startListener:
case ActionMethod.resetTraffic:
case ActionMethod.closeConnections:
case ActionMethod.closeConnection:
case ActionMethod.stopListener:
completer?.complete(result.data as bool);
return;
case ActionMethod.changeProxy:
case ActionMethod.getProxies:
case ActionMethod.getTraffic:
case ActionMethod.getTotalTraffic:
case ActionMethod.asyncTestDelay:
case ActionMethod.getConnections:
case ActionMethod.getExternalProviders:
case ActionMethod.getExternalProvider:
case ActionMethod.validateConfig:
case ActionMethod.updateConfig:
case ActionMethod.updateGeoData:
case ActionMethod.updateExternalProvider:
case ActionMethod.sideLoadExternalProvider:
case ActionMethod.getCountryCode:
case ActionMethod.getMemory:
completer?.complete(result.data as String);
return;
case ActionMethod.message:
clashMessage.controller.add(result.data as String);
completer?.complete(true);
return;
default:
final isHandled = await nextHandleResult(result, completer);
if (isHandled) {
return;
}
completer?.complete(result.data);
}
} catch (_) {
debugPrint(result.id);
}
}
sendMessage(String message);
reStart();
FutureOr<bool> destroy();
Future<T> invoke<T>({
required ActionMethod method,
dynamic data,
Duration? timeout,
FutureOr<T> Function()? onTimeout,
}) async {
final id = "${method.name}#${other.id}";
callbackCompleterMap[id] = Completer<T>();
dynamic defaultValue;
if (T == String) {
defaultValue = "";
}
if (T == bool) {
defaultValue = false;
}
sendMessage(
json.encode(
Action(
id: id,
method: method,
data: data,
defaultValue: defaultValue,
),
),
);
return (callbackCompleterMap[id] as Completer<T>).safeFuture(
timeout: timeout,
onLast: () {
callbackCompleterMap.remove(id);
},
onTimeout: onTimeout ??
() {
return defaultValue;
},
functionName: id,
);
}
@override
Future<bool> init(String homeDir) {
return invoke<bool>(
method: ActionMethod.initClash,
data: homeDir,
);
}
@override
shutdown() async {
return await invoke<bool>(
method: ActionMethod.shutdown,
);
}
@override
Future<bool> get isInit {
return invoke<bool>(
method: ActionMethod.getIsInit,
);
}
@override
Future<bool> forceGc() {
return invoke<bool>(
method: ActionMethod.forceGc,
);
}
@override
FutureOr<String> validateConfig(String data) {
return invoke<String>(
method: ActionMethod.validateConfig,
data: data,
);
}
@override
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
return await invoke<String>(
method: ActionMethod.updateConfig,
data: json.encode(updateConfigParams),
);
}
@override
Future<String> getProxies() {
return invoke<String>(
method: ActionMethod.getProxies,
);
}
@override
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) {
return invoke<String>(
method: ActionMethod.changeProxy,
data: json.encode(changeProxyParams),
);
}
@override
FutureOr<String> getExternalProviders() {
return invoke<String>(
method: ActionMethod.getExternalProviders,
);
}
@override
FutureOr<String> getExternalProvider(String externalProviderName) {
return invoke<String>(
method: ActionMethod.getExternalProvider,
data: externalProviderName,
);
}
@override
Future<String> updateGeoData(UpdateGeoDataParams params) {
return invoke<String>(
method: ActionMethod.updateGeoData,
data: json.encode(params),
timeout: Duration(minutes: 1));
}
@override
Future<String> sideLoadExternalProvider({
required String providerName,
required String data,
}) {
return invoke<String>(
method: ActionMethod.sideLoadExternalProvider,
data: json.encode({
"providerName": providerName,
"data": data,
}),
);
}
@override
Future<String> updateExternalProvider(String providerName) {
return invoke<String>(
method: ActionMethod.updateExternalProvider,
data: providerName,
timeout: Duration(minutes: 1),
);
}
@override
FutureOr<String> getConnections() {
return invoke<String>(
method: ActionMethod.getConnections,
);
}
@override
Future<bool> closeConnections() {
return invoke<bool>(
method: ActionMethod.closeConnections,
);
}
@override
Future<bool> closeConnection(String id) {
return invoke<bool>(
method: ActionMethod.closeConnection,
data: id,
);
}
@override
FutureOr<String> getTotalTraffic() {
return invoke<String>(
method: ActionMethod.getTotalTraffic,
);
}
@override
FutureOr<String> getTraffic() {
return invoke<String>(
method: ActionMethod.getTraffic,
);
}
@override
resetTraffic() {
invoke(method: ActionMethod.resetTraffic);
}
@override
startLog() {
invoke(method: ActionMethod.startLog);
}
@override
stopLog() {
invoke<bool>(
method: ActionMethod.stopLog,
);
}
@override
Future<bool> startListener() {
return invoke<bool>(
method: ActionMethod.startListener,
);
}
@override
stopListener() {
return invoke<bool>(
method: ActionMethod.stopListener,
);
}
@override
Future<String> asyncTestDelay(String url, String proxyName) {
final delayParams = {
"proxy-name": proxyName,
"timeout": httpTimeoutDuration.inMilliseconds,
"test-url": url,
};
return invoke<String>(
method: ActionMethod.asyncTestDelay,
data: json.encode(delayParams),
timeout: Duration(
milliseconds: 6000,
),
onTimeout: () {
return json.encode(
Delay(
name: proxyName,
value: -1,
url: url,
),
);
},
);
}
@override
FutureOr<String> getCountryCode(String ip) {
return invoke<String>(
method: ActionMethod.getCountryCode,
data: ip,
);
}
@override
FutureOr<String> getMemory() {
return invoke<String>(
method: ActionMethod.getMemory,
);
}
}

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,154 @@ 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.setState:
case ActionMethod.stopTun:
case ActionMethod.updateDns:
completer?.complete(result.data as bool);
return true;
case ActionMethod.getRunTime:
case ActionMethod.startTun:
case ActionMethod.getAndroidVpnOptions:
case ActionMethod.getCurrentProfileName:
completer?.complete(result.data as String);
return true;
default:
return false;
}
} }
@override @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<bool> setState(CoreState state) {
return invoke<bool>(
method: ActionMethod.setState,
data: json.encode(state),
);
}
@override
Future<DateTime?> startTun(int fd) async {
final res = await invoke<String>(
method: ActionMethod.startTun,
data: json.encode(fd),
);
if (res.isEmpty) {
return null;
}
return DateTime.fromMillisecondsSinceEpoch(int.parse(res));
}
@override
Future<bool> stopTun() {
return invoke<bool>(
method: ActionMethod.stopTun,
);
}
@override
Future<AndroidVpnOptions?> getAndroidVpnOptions() async {
final res = await invoke<String>(
method: ActionMethod.getAndroidVpnOptions,
);
if (res.isEmpty) {
return null;
}
return AndroidVpnOptions.fromJson(json.decode(res));
}
@override
Future<bool> updateDns(String value) {
return invoke<bool>(
method: ActionMethod.updateDns,
data: value,
);
}
@override
Future<DateTime?> getRunTime() async {
final runTimeString = await invoke<String>(
method: ActionMethod.getRunTime,
);
if (runTimeString.isEmpty) {
return null;
}
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
}
@override
Future<String> getCurrentProfileName() {
return invoke<String>(
method: ActionMethod.getCurrentProfileName,
);
}
}
class ClashLibHandler {
static ClashLibHandler? _instance;
late final ClashFFI clashFFI;
late final DynamicLibrary lib;
ClashLibHandler._internal() {
lib = DynamicLibrary.open("libclash.so");
clashFFI = ClashFFI(lib);
clashFFI.initNativeApiBridge(
NativeApi.initializeApiDLData,
);
}
factory ClashLibHandler() {
_instance ??= ClashLibHandler._internal();
return _instance!;
}
Future<String> invokeAction(String actionParams) {
final completer = Completer<String>(); final completer = Completer<String>();
final receiver = ReceivePort(); final receiver = ReceivePort();
receiver.listen((message) { receiver.listen((message) {
@@ -261,89 +218,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 +252,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);
@@ -384,8 +284,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 +359,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,21 +3,17 @@ 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';
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 = {};
Process? process; Process? process;
factory ClashService() { factory ClashService() {
@@ -26,11 +22,11 @@ class ClashService with ClashInterface {
} }
ClashService._internal() { ClashService._internal() {
_createServer(); _initServer();
startCore(); reStart();
} }
_createServer() async { _initServer() async {
final address = !Platform.isWindows final address = !Platform.isWindows
? InternetAddress( ? InternetAddress(
unixSocketPath, unixSocketPath,
@@ -61,8 +57,8 @@ class ClashService with ClashInterface {
.transform(LineSplitter()) .transform(LineSplitter())
.listen( .listen(
(data) { (data) {
_handleAction( handleResult(
Action.fromJson( ActionResult.fromJson(
json.decode(data.trim()), json.decode(data.trim()),
), ),
); );
@@ -71,7 +67,8 @@ class ClashService with ClashInterface {
} }
} }
startCore() async { @override
reStart() async {
if (process != null) { if (process != null) {
await shutdown(); await shutdown();
} }
@@ -95,6 +92,20 @@ class ClashService with ClashInterface {
process!.stdout.listen((_) {}); process!.stdout.listen((_) {});
} }
@override
destroy() async {
final server = await serverCompleter.future;
await server.close();
await _deleteSocketFile();
return true;
}
@override
sendMessage(String message) async {
final socket = await socketCompleter.future;
socket.writeln(message);
}
_deleteSocketFile() async { _deleteSocketFile() async {
if (!Platform.isWindows) { if (!Platform.isWindows) {
final file = File(unixSocketPath); final file = File(unixSocketPath);
@@ -112,327 +123,22 @@ class ClashService with ClashInterface {
} }
} }
_handleAction(Action action) {
final completer = callbackCompleterMap[action.id];
switch (action.method) {
case ActionMethod.initClash:
case ActionMethod.shutdown:
case ActionMethod.getIsInit:
case ActionMethod.startListener:
case ActionMethod.resetTraffic:
case ActionMethod.closeConnections:
case ActionMethod.closeConnection:
case ActionMethod.stopListener:
completer?.complete(action.data as bool);
return;
case ActionMethod.changeProxy:
case ActionMethod.getProxies:
case ActionMethod.getTraffic:
case ActionMethod.getTotalTraffic:
case ActionMethod.asyncTestDelay:
case ActionMethod.getConnections:
case ActionMethod.getExternalProviders:
case ActionMethod.getExternalProvider:
case ActionMethod.validateConfig:
case ActionMethod.updateConfig:
case ActionMethod.updateGeoData:
case ActionMethod.updateExternalProvider:
case ActionMethod.sideLoadExternalProvider:
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>( await super.shutdown();
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

@@ -34,3 +34,5 @@ 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 'view.dart';

View File

@@ -73,7 +73,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 +88,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,43 @@ 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();
}
}
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: 1);
Timer(realTimeout + moreDuration, () { Timer(realTimeout + commonDuration, () {
if (onLast != null) { if (onLast != null) {
onLast(); onLast();
} }

View File

@@ -1,3 +1,55 @@
import 'dart:collection';
class FixedList<T> {
final int maxLength;
final List<T> _list = [];
FixedList(this.maxLength);
add(T item) {
if (_list.length == maxLength) {
_list.removeAt(0);
}
_list.add(item);
}
List<T> get list => List.unmodifiable(_list);
int get length => _list.length;
T operator [](int index) => _list[index];
}
class FixedMap<K, V> {
final int maxSize;
final Map<K, V> _map = {};
final Queue<K> _queue = Queue<K>();
FixedMap(this.maxSize);
put(K key, V value) {
if (_map.length == maxSize) {
final oldestKey = _queue.removeFirst();
_map.remove(oldestKey);
}
_map[key] = value;
_queue.add(key);
}
clear(){
_map.clear();
_queue.clear();
}
V? get(K key) => _map[key];
bool containsKey(K key) => _map.containsKey(key);
int get length => _map.length;
Map<K, V> get map => Map.unmodifiable(_map);
}
extension ListExtension<T> on List<T> { 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 +69,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

@@ -11,13 +11,18 @@ class Measure {
WidgetsBinding.instance.platformDispatcher.textScaleFactor, WidgetsBinding.instance.platformDispatcher.textScaleFactor,
); );
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),
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;
} }

View File

@@ -30,16 +30,16 @@ class Navigation {
fragment: ProfilesFragment(), fragment: ProfilesFragment(),
), ),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.view_timeline), icon: Icon(Icons.view_timeline),
label: "requests", label: "requests",
fragment: RequestsFragment(), fragment: RequestsFragment(),
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: "connections",
fragment: ConnectionsFragment(), fragment: ConnectionsFragment(),
description: "connectionsDesc", description: "connectionsDesc",
modes: [NavigationItemMode.desktop, NavigationItemMode.more], modes: [NavigationItemMode.desktop, NavigationItemMode.more],
), ),
@@ -49,7 +49,7 @@ class Navigation {
description: "resourcesDesc", description: "resourcesDesc",
keep: false, keep: false,
fragment: Resources(), fragment: Resources(),
modes: [NavigationItemMode.desktop, NavigationItemMode.more], modes: [NavigationItemMode.more],
), ),
NavigationItem( NavigationItem(
icon: const Icon(Icons.adb), icon: const Icon(Icons.adb),

View File

@@ -1,8 +1,16 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.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.appController.isMobileView) {
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 +19,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,

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,21 @@
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() {
@@ -21,52 +23,44 @@ class Preferences {
return _instance!; return _instance!;
} }
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 { Future<bool> saveClashConfig(ClashConfig clashConfig) async {
final preferences = await sharedPreferencesCompleter.future; final preferences = await sharedPreferencesCompleter.future;
return preferences.setString( preferences?.setString(
clashConfigKey, clashConfigKey,
json.encode(clashConfig), json.encode(clashConfig),
); );
return true;
} }
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.fromJson(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;
} }
clearPreferences() async { clearPreferences() async {
final sharedPreferencesIns = await sharedPreferencesCompleter.future; final sharedPreferencesIns = await sharedPreferencesCompleter.future;
sharedPreferencesIns.clear(); sharedPreferencesIns?.clear();
} }
} }
final preferences = Preferences(); final preferences = Preferences();

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

@@ -0,0 +1,57 @@
import 'package:fl_clash/common/common.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/scheduler.dart';
class Render {
static Render? _instance;
bool _isPaused = false;
final _dispatcher = SchedulerBinding.instance.platformDispatcher;
FrameCallback? _beginFrame;
VoidCallback? _drawFrame;
Render._internal();
factory Render() {
_instance ??= Render._internal();
return _instance!;
}
active() {
resume();
pause();
}
pause() {
debouncer.call(
"render_pause",
_pause,
duration: Duration(seconds: 5),
);
}
resume() {
debouncer.cancel("render_pause");
_resume();
}
void _pause() {
if (_isPaused) return;
_isPaused = true;
_beginFrame = _dispatcher.onBeginFrame;
_drawFrame = _dispatcher.onDrawFrame;
_dispatcher.onBeginFrame = null;
_dispatcher.onDrawFrame = null;
debugPrint("[App] pause");
}
void _resume() {
if (!_isPaused) return;
_isPaused = false;
_dispatcher.onBeginFrame = _beginFrame;
_dispatcher.onDrawFrame = _drawFrame;
_dispatcher.scheduleFrame();
debugPrint("[App] resume");
}
}
final render = system.isDesktop ? Render() : null;

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,90 @@ class ShowBarScrollBehavior extends BaseScrollBehavior {
); );
} }
} }
class NextClampingScrollPhysics extends ClampingScrollPhysics {
@override
Simulation? createBallisticSimulation(
ScrollMetrics position, double velocity) {
final Tolerance tolerance = toleranceFor(position);
if (position.outOfRange) {
double? end;
if (position.pixels > position.maxScrollExtent) {
end = position.maxScrollExtent;
}
if (position.pixels < position.minScrollExtent) {
end = position.minScrollExtent;
}
assert(end != null);
return ScrollSpringSimulation(
spring,
end!,
end,
min(0.0, velocity),
tolerance: tolerance,
);
}
if (velocity.abs() < tolerance.velocity) {
return null;
}
if (velocity > 0.0 && position.pixels >= position.maxScrollExtent) {
return null;
}
if (velocity < 0.0 && position.pixels <= position.minScrollExtent) {
return null;
}
return ClampingScrollSimulation(
position: position.pixels,
velocity: velocity,
tolerance: tolerance,
);
}
}
class ReverseScrollController extends ScrollController {
ReverseScrollController({
super.initialScrollOffset,
super.keepScrollOffset,
super.debugLabel,
});
@override
ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition? oldPosition,
) {
return ReverseScrollPosition(
physics: physics,
context: context,
initialPixels: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
}
}
class ReverseScrollPosition extends ScrollPositionWithSingleContext {
ReverseScrollPosition({
required super.physics,
required super.context,
super.initialPixels = 0.0,
super.keepScrollOffset,
super.oldPosition,
super.debugLabel,
}) : _initialPixels = initialPixels ?? 0;
final double _initialPixels;
bool _isInit = false;
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
if (!_isInit) {
correctPixels(maxScrollExtent);
_isInit = true;
}
return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
}
}

20
lib/common/view.dart Normal file
View File

@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'context.dart';
mixin ViewMixin<T extends StatefulWidget> on State<T> {
List<Widget> get actions => [];
Widget? get floatingActionButton => null;
initViewState() {
final commonScaffoldState = context.commonScaffoldState;
commonScaffoldState?.actions = actions;
commonScaffoldState?.floatingActionButton = floatingActionButton;
commonScaffoldState?.onSearch = onSearch;
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
}
Function(String)? get onSearch => null;
Function(List<String>)? get onKeywordsUpdate => null;
}

View File

@@ -63,6 +63,7 @@ class Window {
await windowManager.show(); await windowManager.show();
await windowManager.focus(); await windowManager.focus();
await windowManager.setSkipTaskbar(false); await windowManager.setSkipTaskbar(false);
render?.resume();
} }
Future<bool> isVisible() async { Future<bool> isVisible() async {
@@ -76,6 +77,7 @@ class Window {
hide() async { hide() async {
await windowManager.hide(); await windowManager.hide();
await windowManager.setSkipTaskbar(true); await windowManager.setSkipTaskbar(true);
render?.pause();
} }
} }

View File

@@ -76,13 +76,10 @@ class AppController {
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 config.getCurrentProfile()?.profileLastModified;
if (currentLastModified == null || if (currentLastModified == null ||
@@ -163,6 +160,13 @@ class AppController {
} }
} }
setProfile(Profile profile) {
config.setProfile(profile);
if (profile.id == config.currentProfile?.id) {
applyProfileDebounce();
}
}
Future<void> updateClashConfig({bool isPatch = true}) async { Future<void> updateClashConfig({bool isPatch = true}) async {
final commonScaffoldState = globalState.homeScaffoldKey.currentState; final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return; if (commonScaffoldState?.mounted != true) return;
@@ -279,8 +283,9 @@ 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 {
@@ -298,7 +303,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 +320,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,14 +338,27 @@ class AppController {
} }
} }
_handlePreference() async {
if (await preferences.isInit) {
return;
}
final res = await globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(text: appLocalizations.cacheCorrupt),
);
if (res) {
final file = File(await appPath.sharedPreferencesPath);
final isExists = await file.exists();
if (isExists) {
await file.delete();
}
}
await handleExit();
}
init() async { init() async {
final isDisclaimerAccepted = await handlerDisclaimer(); await _handlePreference();
if (!isDisclaimerAccepted) { await _handlerDisclaimer();
handleExit();
}
if (!config.appSetting.silentLaunch) {
window?.show();
}
await globalState.initCore( await globalState.initCore(
appState: appState, appState: appState,
clashConfig: clashConfig, clashConfig: clashConfig,
@@ -351,11 +370,16 @@ class AppController {
); );
autoUpdateProfiles(); autoUpdateProfiles();
autoCheckUpdate(); autoCheckUpdate();
if (!config.appSetting.silentLaunch) {
window?.show();
} else {
window?.hide();
}
} }
_initStatus() async { _initStatus() async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
globalState.updateStartTime(); await globalState.updateStartTime();
} }
final status = final status =
globalState.isStart == true ? true : config.appSetting.autoRun; globalState.isStart == true ? true : config.appSetting.autoRun;
@@ -370,7 +394,10 @@ class AppController {
appState.setDelay(delay); appState.setDelay(delay);
} }
toPage(int index, {bool hasAnimate = false}) { toPage(
int index, {
bool hasAnimate = false,
}) {
if (index > appState.currentNavigationItems.length - 1) { if (index > appState.currentNavigationItems.length - 1) {
return; return;
} }
@@ -397,8 +424,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 +443,12 @@ class AppController {
"${appLocalizations.create}${appLocalizations.profile}"), "${appLocalizations.create}${appLocalizations.profile}"),
], ],
), ),
onTab: () {
addProfileFormURL(url);
},
); );
if (res != true) {
return;
}
addProfileFormURL(url);
}, },
); );
} }
@@ -460,11 +489,15 @@ class AppController {
false; false;
} }
Future<bool> handlerDisclaimer() async { _handlerDisclaimer() async {
if (config.appSetting.disclaimerAccepted) { if (config.appSetting.disclaimerAccepted) {
return true; return;
} }
return showDisclaimer(); final isDisclaimerAccepted = await showDisclaimer();
if (!isDisclaimerAccepted) {
await handleExit();
}
return;
} }
addProfileFormURL(String url) async { addProfileFormURL(String url) async {
@@ -522,6 +555,18 @@ class AppController {
}); });
} }
int? getDelay(String proxyName, [String? url]) {
final currentDelayMap = appState.delayMap[getRealTestUrl(url)];
return currentDelayMap?[appState.getRealProxyName(proxyName)];
}
String getRealTestUrl(String? url) {
if (url == null || url.isEmpty) {
return config.appSetting.testUrl;
}
return url;
}
List<Proxy> _sortOfName(List<Proxy> proxies) { List<Proxy> _sortOfName(List<Proxy> proxies) {
return List.of(proxies) return List.of(proxies)
..sort( ..sort(
@@ -532,12 +577,12 @@ class AppController {
); );
} }
List<Proxy> _sortOfDelay(List<Proxy> proxies) { List<Proxy> _sortOfDelay(String url, List<Proxy> proxies) {
return proxies = List.of(proxies) return List.of(proxies)
..sort( ..sort(
(a, b) { (a, b) {
final aDelay = appState.getDelay(a.name); final aDelay = getDelay(a.name, url);
final bDelay = appState.getDelay(b.name); final bDelay = getDelay(b.name, url);
if (aDelay == null && bDelay == null) { if (aDelay == null && bDelay == null) {
return 0; return 0;
} }
@@ -552,10 +597,10 @@ class AppController {
); );
} }
List<Proxy> getSortProxies(List<Proxy> proxies) { List<Proxy> getSortProxies(List<Proxy> proxies, [String? url]) {
return switch (config.proxiesStyle.sortType) { return switch (config.proxiesStyle.sortType) {
ProxiesSortType.none => proxies, ProxiesSortType.none => proxies,
ProxiesSortType.delay => _sortOfDelay(proxies), ProxiesSortType.delay => _sortOfDelay(getRealTestUrl(url), proxies),
ProxiesSortType.name => _sortOfName(proxies), ProxiesSortType.name => _sortOfName(proxies),
}; };
} }
@@ -580,6 +625,10 @@ class AppController {
}); });
} }
bool get isMobileView {
return appState.viewMode == ViewMode.mobile;
}
updateTun() { updateTun() {
clashConfig.tun = clashConfig.tun.copyWith( clashConfig.tun = clashConfig.tun.copyWith(
enable: !clashConfig.tun.enable, enable: !clashConfig.tun.enable,
@@ -644,8 +693,8 @@ 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 = config.toJson();
final clashConfigJson = clashConfig.toJson(); final clashConfigJson = clashConfig.toJson();
return Isolate.run<List<int>>(() async { return Isolate.run<List<int>>(() async {
@@ -676,7 +725,7 @@ 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 =

View File

@@ -100,15 +100,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 }
@@ -241,6 +238,17 @@ enum ActionMethod {
stopListener, stopListener,
getCountryCode, getCountryCode,
getMemory, getMemory,
///Android,
setFdMap,
setProcessMap,
setState,
startTun,
stopTun,
getRunTime,
updateDns,
getAndroidVpnOptions,
getCurrentProfileName,
} }
enum AuthorizeCode { none, success, error } enum AuthorizeCode { none, success, error }

View File

@@ -20,11 +20,13 @@ class AccessFragment extends StatefulWidget {
class _AccessFragmentState extends State<AccessFragment> { class _AccessFragmentState extends State<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.appController.appState;
if (appState.packages.isEmpty) { if (appState.packages.isEmpty) {
@@ -35,6 +37,12 @@ class _AccessFragmentState extends State<AccessFragment> {
}); });
} }
@override
void dispose() {
_controller.dispose();
super.dispose();
}
_updateInitList() { _updateInitList() {
final accessControl = globalState.appController.config.accessControl; final accessControl = globalState.appController.config.accessControl;
acceptList = accessControl.acceptList; acceptList = accessControl.acceptList;
@@ -52,8 +60,8 @@ class _AccessFragmentState extends State<AccessFragment> {
rejectList: rejectList, rejectList: rejectList,
), ),
).then((_) => setState(() { ).then((_) => setState(() {
_updateInitList(); _updateInitList();
})); }));
}, },
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
); );
@@ -268,39 +276,44 @@ class _AccessFragmentState extends State<AccessFragment> {
? const Center( ? const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
) )
: ListView.builder( : CommonScrollBar(
itemCount: packages.length, controller: _controller,
itemBuilder: (_, index) { child: ListView.builder(
final package = packages[index]; controller: _controller,
return PackageListItem( itemCount: packages.length,
key: Key(package.packageName), itemExtent: 72,
package: package, itemBuilder: (_, index) {
value: final package = packages[index];
valueList.contains(package.packageName), return PackageListItem(
isActive: isAccessControl, key: Key(package.packageName),
onChanged: (value) { package: package,
if (value == true) { value:
valueList.add(package.packageName); valueList.contains(package.packageName),
} else { isActive: isAccessControl,
valueList.remove(package.packageName); onChanged: (value) {
} if (value == true) {
final config = valueList.add(package.packageName);
globalState.appController.config; } else {
if (accessControlMode == valueList.remove(package.packageName);
AccessControlMode.acceptSelected) { }
config.accessControl = final config =
config.accessControl.copyWith( globalState.appController.config;
acceptList: valueList, if (accessControlMode ==
); AccessControlMode.acceptSelected) {
} else { config.accessControl =
config.accessControl = config.accessControl.copyWith(
config.accessControl.copyWith( acceptList: valueList,
rejectList: valueList, );
); } else {
} config.accessControl =
}, config.accessControl.copyWith(
); rejectList: valueList,
}, );
}
},
);
},
),
), ),
), ),
], ],

View File

@@ -40,7 +40,7 @@ class UsageSwitch extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<Config, bool>( return Selector<Config, bool>(
selector: (_, config) => config.appSetting.onlyProxy, selector: (_, config) => config.appSetting.onlyStatisticsProxy,
builder: (_, onlyProxy, __) { builder: (_, onlyProxy, __) {
return ListItem.switchItem( return ListItem.switchItem(
title: Text(appLocalizations.onlyStatisticsProxy), title: Text(appLocalizations.onlyStatisticsProxy),
@@ -50,7 +50,7 @@ class UsageSwitch extends StatelessWidget {
onChanged: (bool value) async { onChanged: (bool value) async {
final config = globalState.appController.config; final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith( config.appSetting = config.appSetting.copyWith(
onlyProxy: value, onlyStatisticsProxy: value,
); );
}, },
), ),

View File

@@ -225,7 +225,7 @@ class FakeIpFilterItem extends StatelessWidget {
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; final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns; final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith( clashConfig.dns = dns.copyWith(
@@ -260,7 +260,7 @@ class DefaultNameserverItem extends StatelessWidget {
title: appLocalizations.defaultNameserver, title: appLocalizations.defaultNameserver,
items: defaultNameserver, items: defaultNameserver,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
onChange: (items){ onChange: (items) {
final clashConfig = globalState.appController.clashConfig; final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns; final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith( clashConfig.dns = dns.copyWith(
@@ -295,7 +295,7 @@ class NameserverItem extends StatelessWidget {
title: "域名服务器", title: "域名服务器",
items: nameserver, items: nameserver,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
onChange: (items){ onChange: (items) {
final clashConfig = globalState.appController.clashConfig; final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns; final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith( clashConfig.dns = dns.copyWith(
@@ -384,7 +384,7 @@ class NameserverPolicyItem extends StatelessWidget {
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; final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns; final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith( clashConfig.dns = dns.copyWith(
@@ -419,7 +419,7 @@ class ProxyServerNameserverItem extends StatelessWidget {
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; final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns; final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith( clashConfig.dns = dns.copyWith(
@@ -454,7 +454,7 @@ class FallbackItem extends StatelessWidget {
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; final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns; final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith( clashConfig.dns = dns.copyWith(
@@ -555,7 +555,7 @@ class GeositeItem extends StatelessWidget {
title: "Geosite", title: "Geosite",
items: geosite, items: geosite,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
onChange: (items){ onChange: (items) {
final clashConfig = globalState.appController.clashConfig; final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns; final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith( clashConfig.dns = dns.copyWith(
@@ -591,7 +591,7 @@ class IpcidrItem extends StatelessWidget {
title: appLocalizations.ipcidr, title: appLocalizations.ipcidr,
items: ipcidr, items: ipcidr,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
onChange: (items){ onChange: (items) {
final clashConfig = globalState.appController.clashConfig; final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns; final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith( clashConfig.dns = dns.copyWith(
@@ -627,7 +627,7 @@ class DomainItem extends StatelessWidget {
title: appLocalizations.domain, title: appLocalizations.domain,
items: domain, items: domain,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
onChange: (items){ onChange: (items) {
final clashConfig = globalState.appController.clashConfig; final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns; final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith( clashConfig.dns = dns.copyWith(
@@ -705,20 +705,19 @@ class DnsListView extends StatelessWidget {
_initActions(BuildContext context) { _initActions(BuildContext context) {
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;
}); }
globalState.appController.clashConfig.dns = defaultDns;
}, },
tooltip: appLocalizations.reset, tooltip: appLocalizations.reset,
icon: const Icon( icon: const Icon(

View File

@@ -206,23 +206,21 @@ class BypassDomainItem extends StatelessWidget {
_initActions(BuildContext context) { _initActions(BuildContext context) {
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; if (res != true) {
config.networkProps = config.networkProps.copyWith( return;
bypassDomain: defaultBypassDomain, }
); final config = globalState.appController.config;
Navigator.of(context).pop(); config.networkProps = config.networkProps.copyWith(
}, bypassDomain: defaultBypassDomain,
); );
}, },
tooltip: appLocalizations.reset, tooltip: appLocalizations.reset,
@@ -378,23 +376,21 @@ class NetworkListView extends StatelessWidget {
_initActions(BuildContext context) { _initActions(BuildContext context) {
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;
}
final appController = globalState.appController;
appController.config.vpnProps = defaultVpnProps;
appController.clashConfig.tun = defaultTun;
}, },
tooltip: appLocalizations.reset, tooltip: appLocalizations.reset,
icon: const Icon( icon: const Icon(

View File

@@ -0,0 +1,155 @@
import 'dart:async';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'item.dart';
class ConnectionsFragment extends StatefulWidget {
const ConnectionsFragment({super.key});
@override
State<ConnectionsFragment> createState() => _ConnectionsFragmentState();
}
class _ConnectionsFragmentState extends State<ConnectionsFragment>
with ViewMixin {
final _connectionsStateNotifier = ValueNotifier<ConnectionsState>(
const ConnectionsState(),
);
final ScrollController _scrollController = ScrollController(
keepScrollOffset: false,
);
Timer? timer;
@override
List<Widget> get actions => [
IconButton(
onPressed: () async {
clashCore.closeConnections();
_connectionsStateNotifier.value =
_connectionsStateNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
},
icon: const Icon(Icons.delete_sweep_outlined),
),
];
@override
get onSearch => (value) {
_connectionsStateNotifier.value =
_connectionsStateNotifier.value.copyWith(
query: value,
);
};
@override
get onKeywordsUpdate => (keywords) {
_connectionsStateNotifier.value =
_connectionsStateNotifier.value.copyWith(keywords: keywords);
};
_updateConnections() async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
_connectionsStateNotifier.value =
_connectionsStateNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
timer = Timer(Duration(seconds: 1), () async {
_updateConnections();
});
});
}
@override
void initState() {
super.initState();
_updateConnections();
}
_initActions() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
initViewState();
},
);
}
_handleBlockConnection(String id) async {
clashCore.closeConnection(id);
_connectionsStateNotifier.value = _connectionsStateNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
}
@override
void dispose() {
timer?.cancel();
_connectionsStateNotifier.dispose();
_scrollController.dispose();
timer = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return Selector<AppState, bool?>(
selector: (_, appState) =>
appState.currentLabel == 'connections' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions();
}
return child!;
},
child: ValueListenableBuilder<ConnectionsState>(
valueListenable: _connectionsStateNotifier,
builder: (_, state, __) {
final connections = state.list;
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullConnectionsDesc,
);
}
return CommonScrollBar(
controller: _scrollController,
child: ListView.separated(
controller: _scrollController,
itemBuilder: (_, index) {
final connection = connections[index];
return ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
trailing: IconButton(
icon: const Icon(Icons.block),
onPressed: () {
_handleBlockConnection(connection.id);
},
),
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: connections.length,
),
);
},
),
);
}
}

View File

@@ -0,0 +1,150 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class FindProcessBuilder extends StatelessWidget {
final Widget Function(bool value) builder;
const FindProcessBuilder({
super.key,
required this.builder,
});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.findProcessMode == FindProcessMode.always &&
Platform.isAndroid,
builder: (_, value, __) {
return builder(value);
},
);
}
}
class ConnectionItem extends StatelessWidget {
final Connection connection;
final Function(String)? onClick;
final Widget? trailing;
const ConnectionItem({
super.key,
required this.connection,
this.onClick,
this.trailing,
});
Future<ImageProvider?> _getPackageIcon(Connection connection) async {
return await app?.getPackageIcon(connection.metadata.process);
}
String _getSourceText(Connection connection) {
final metadata = connection.metadata;
if (metadata.process.isEmpty) {
return connection.start.lastUpdateTimeDesc;
}
return "${metadata.process} · ${connection.start.lastUpdateTimeDesc}";
}
@override
Widget build(BuildContext context) {
final title = Text(
connection.desc,
style: context.textTheme.bodyLarge,
);
final subTitle = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8,
),
Text(
_getSourceText(connection),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(
height: 8,
),
Wrap(
runSpacing: 6,
spacing: 6,
children: [
for (final chain in connection.chains)
CommonChip(
label: chain,
onPressed: () {
if (onClick == null) return;
onClick!(chain);
},
),
],
),
],
);
if (!Platform.isAndroid) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
title: title,
subtitle: subTitle,
trailing: trailing,
);
}
return FindProcessBuilder(
builder: (bool value) {
final leading = value
? GestureDetector(
onTap: () {
if (onClick == null) return;
final process = connection.metadata.process;
if (process.isEmpty) return;
onClick!(process);
},
child: Container(
margin: const EdgeInsets.only(top: 4),
width: 48,
height: 48,
child: FutureBuilder<ImageProvider?>(
future: _getPackageIcon(connection),
builder: (_, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Container();
} else {
return Image(
image: snapshot.data!,
gaplessPlayback: true,
width: 48,
height: 48,
);
}
},
),
),
)
: null;
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
leading: leading,
title: title,
subtitle: subTitle,
trailing: trailing,
);
},
);
}
}

View File

@@ -0,0 +1,239 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'item.dart';
double _preOffset = 0;
class RequestsFragment extends StatefulWidget {
const RequestsFragment({super.key});
@override
State<RequestsFragment> createState() => _RequestsFragmentState();
}
class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
final _requestsStateNotifier =
ValueNotifier<ConnectionsState>(const ConnectionsState());
List<Connection> _requests = [];
final ScrollController _scrollController = ScrollController(
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
);
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
double _currentMaxWidth = 0;
@override
get onSearch => (value) {
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
query: value,
);
};
@override
get onKeywordsUpdate => (keywords) {
_requestsStateNotifier.value =
_requestsStateNotifier.value.copyWith(keywords: keywords);
};
@override
void initState() {
super.initState();
final appController = globalState.appController;
final appState = appController.appState;
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
connections: appState.requests,
);
}
_initActions() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
initViewState();
},
);
}
double _calcCacheHeight(Connection item) {
final cacheHeight = _cacheDynamicHeightMap.get(item.id);
if (cacheHeight != null) {
return cacheHeight;
}
final size = globalState.measure.computeTextSize(
Text(
item.desc,
style: context.textTheme.bodyLarge,
),
maxWidth: _currentMaxWidth,
);
final chainsText = item.chains.join("");
final length = item.chains.length;
final chainSize = globalState.measure.computeTextSize(
Text(
chainsText,
style: context.textTheme.bodyMedium,
),
maxWidth: (_currentMaxWidth - (length - 1) * 6 - length * 24),
);
final baseHeight = globalState.measure.bodyMediumHeight;
final lines = (chainSize.height / baseHeight).round();
final computerHeight =
size.height + chainSize.height + 24 + 24 * (lines - 1);
_cacheDynamicHeightMap.put(item.id, computerHeight);
return computerHeight;
}
_handleTryClearCache(double maxWidth) {
if (_currentMaxWidth != maxWidth) {
_currentMaxWidth = maxWidth;
_cacheDynamicHeightMap.clear();
}
}
@override
void dispose() {
_requestsStateNotifier.dispose();
_scrollController.dispose();
_currentMaxWidth = 0;
_cacheDynamicHeightMap.clear();
super.dispose();
}
Widget _wrapPage(Widget child) {
return Selector<AppState, bool?>(
selector: (_, appState) =>
appState.currentLabel == 'requests' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions();
}
return child!;
},
child: child,
);
}
updateRequestsThrottler() {
throttler.call("request", () {
final isEquality = connectionListEquality.equals(
_requests,
_requestsStateNotifier.value.connections,
);
if (isEquality) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
connections: _requests,
);
});
}, duration: commonDuration);
}
Widget _wrapRequestsUpdate(Widget child) {
return Selector<AppState, List<Connection>>(
selector: (_, appState) => appState.requests,
shouldRebuild: (prev, next) {
final isEquality = connectionListEquality.equals(prev, next);
if (!isEquality) {
_requests = next;
updateRequestsThrottler();
}
return !isEquality;
},
builder: (_, next, child) {
return child!;
},
child: child,
);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (_, constraints) {
return FindProcessBuilder(builder: (value) {
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0));
return _wrapPage(
_wrapRequestsUpdate(
ValueListenableBuilder<ConnectionsState>(
valueListenable: _requestsStateNotifier,
builder: (_, state, __) {
final connections = state.list;
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullRequestsDesc,
);
}
final items = connections
.map<Widget>(
(connection) => ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
),
)
.separated(
const Divider(
height: 0,
),
)
.toList();
return Align(
alignment: Alignment.topCenter,
child: NotificationListener<ScrollEndNotification>(
onNotification: (details) {
_preOffset = details.metrics.pixels;
return false;
},
child: CommonScrollBar(
controller: _scrollController,
child: ListView.builder(
reverse: true,
shrinkWrap: true,
physics: NextClampingScrollPhysics(),
controller: _scrollController,
itemExtentBuilder: (index, __) {
final widget = items[index];
if (widget.runtimeType == Divider) {
return 0;
}
final measure = globalState.measure;
final bodyMediumHeight = measure.bodyMediumHeight;
final connection = connections[(index / 2).floor()];
final height = _calcCacheHeight(connection);
return height + bodyMediumHeight + 32;
},
itemBuilder: (_, index) {
return items[index];
},
itemCount: items.length,
),
),
),
);
},
),
),
);
});
},
);
}
}

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,5 +1,6 @@
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/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
@@ -23,8 +24,7 @@ class _DashboardFragmentState extends State<DashboardFragment> {
return; return;
} }
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState = final commonScaffoldState = context.commonScaffoldState;
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.floatingActionButton = const StartButton(); commonScaffoldState?.floatingActionButton = const StartButton();
commonScaffoldState?.actions = [ commonScaffoldState?.actions = [
ValueListenableBuilder( ValueListenableBuilder(

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,11 +33,15 @@ 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();
});
}); });
} }

View File

@@ -76,41 +76,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

@@ -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,12 +1,16 @@
import 'dart:async'; 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/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../models/models.dart'; import '../models/models.dart';
import '../widgets/widgets.dart'; import '../widgets/widgets.dart';
double _preOffset = 0;
class LogsFragment extends StatefulWidget { class LogsFragment extends StatefulWidget {
const LogsFragment({super.key}); const LogsFragment({super.key});
@@ -14,48 +18,66 @@ class LogsFragment extends StatefulWidget {
State<LogsFragment> createState() => _LogsFragmentState(); State<LogsFragment> createState() => _LogsFragmentState();
} }
class _LogsFragmentState extends State<LogsFragment> { class _LogsFragmentState extends State<LogsFragment> with ViewMixin {
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((_) { final appController = globalState.appController;
final appFlowingState = globalState.appController.appFlowingState; final appFlowingState = appController.appFlowingState;
logsNotifier.value = _logsStateNotifier.value = _logsStateNotifier.value.copyWith(
logsNotifier.value.copyWith(logs: appFlowingState.logs); logs: appFlowingState.logs,
if (timer != null) { );
timer?.cancel();
timer = null;
}
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
final logs = appFlowingState.logs;
if (!logListEquality.equals(
logsNotifier.value.logs,
logs,
)) {
logsNotifier.value = logsNotifier.value.copyWith(
logs: logs,
);
}
});
});
} }
@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>(
@@ -73,293 +95,151 @@ class _LogsFragmentState extends State<LogsFragment> {
_initActions() { _initActions() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState = super.initViewState();
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
showSearch(
context: context,
delegate: LogsSearchDelegate(
logs: logsNotifier.value,
),
);
},
icon: const Icon(Icons.search),
),
IconButton(
onPressed: () {
_handleExport();
},
icon: const Icon(
Icons.file_download_outlined,
),
),
];
}); });
} }
_addKeyword(String keyword) { double _calcCacheHeight(String text) {
final isContains = logsNotifier.value.keywords.contains(keyword); final cacheHeight = _cacheDynamicHeightMap.get(text);
if (isContains) return; if (cacheHeight != null) {
final keywords = List<String>.from(logsNotifier.value.keywords) return cacheHeight;
..add(keyword); }
logsNotifier.value = logsNotifier.value.copyWith( final size = globalState.measure.computeTextSize(
keywords: keywords, Text(
text,
style: globalState.appController.context.textTheme.bodyLarge,
),
maxWidth: _currentMaxWidth,
); );
_cacheDynamicHeightMap.put(text, size.height);
return size.height;
} }
_deleteKeyword(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;
..remove(keyword); final height = _calcCacheHeight(log.payload ?? "");
logsNotifier.value = logsNotifier.value.copyWith( return height + bodySmallHeight + 8 + bodyMediumHeight + 40;
keywords: keywords, }
updateLogsThrottler() {
throttler.call("logs", () {
final isEquality = logListEquality.equals(
_logs,
_logsStateNotifier.value.logs,
);
if (isEquality) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
logs: _logs,
);
});
}, duration: commonDuration);
}
Widget _wrapLogsUpdate(Widget child) {
return Selector<AppFlowingState, List<Log>>(
selector: (_, appFlowingState) => appFlowingState.logs,
shouldRebuild: (prev, next) {
final isEquality = logListEquality.equals(prev, next);
if (!isEquality) {
_logs = next;
updateLogsThrottler();
}
return !isEquality;
},
builder: (_, next, child) {
return child!;
},
child: child,
); );
} }
@override @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 Selector<AppState, bool?>(
appState.currentLabel == "tools", selector: (_, appState) =>
builder: (_, isCurrent, child) { appState.currentLabel == 'logs' ||
if (isCurrent == null || isCurrent) { appState.viewMode == ViewMode.mobile &&
_initActions(); appState.currentLabel == "tools",
} builder: (_, isCurrent, child) {
return child!; if (isCurrent == null || isCurrent) {
}, _initActions();
child: ValueListenableBuilder<LogsAndKeywords>( }
valueListenable: logsNotifier, return child!;
builder: (_, state, __) { },
final logs = state.filteredLogs; child: _wrapLogsUpdate(
if (logs.isEmpty) { Align(
return NullStatus( alignment: Alignment.topCenter,
label: appLocalizations.nullLogsDesc, child: ValueListenableBuilder<LogsState>(
); valueListenable: _logsStateNotifier,
} builder: (_, state, __) {
final reversedLogs = logs.reversed.toList(); final logs = state.list;
final logWidgets = reversedLogs if (logs.isEmpty) {
.map<Widget>( return NullStatus(
(log) => LogItem( label: appLocalizations.nullLogsDesc,
key: Key(log.dateTime.toString()), );
log: log, }
onClick: _addKeyword, final items = logs
), .map<Widget>(
) (log) => LogItem(
.separated( key: Key(log.dateTime.toString()),
const Divider( log: log,
height: 0, onClick: (value) {
), context.commonScaffoldState?.addKeyword(value);
)
.toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state.keywords.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: Wrap(
runSpacing: 8,
spacing: 8,
children: [
for (final keyword in state.keywords)
CommonChip(
label: keyword,
type: ChipType.delete,
onPressed: () {
_deleteKeyword(keyword);
}, },
), ),
], )
), .separated(
), const Divider(
Expanded( height: 0,
child: LayoutBuilder( ),
builder: (_, constraints) { )
return ScrollConfiguration( .toList();
behavior: ShowBarScrollBehavior(), return NotificationListener<ScrollEndNotification>(
child: ListView.builder( onNotification: (details) {
controller: scrollController, _preOffset = details.metrics.pixels;
itemExtentBuilder: (index, __) { return false;
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);
}, },
child: CommonScrollBar(
controller: _scrollController,
child: ListView.builder(
reverse: true,
shrinkWrap: true,
physics: NextClampingScrollPhysics(),
controller: _scrollController,
itemBuilder: (_, index) {
return items[index];
},
itemExtentBuilder: (index, __) {
final item = items[index];
if (item.runtimeType == Divider) {
return 0;
}
final log = logs[(index / 2).floor()];
return _getItemHeight(log);
},
itemCount: items.length,
),
),
); );
}, },
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: _results.length,
), ),
) ),
], ),
); );
}, },
); );
} }
} }
class LogItem extends StatefulWidget { class LogItem extends StatelessWidget {
final Log log; final Log log;
final Function(String)? onClick; final Function(String)? onClick;
@@ -369,14 +249,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 +258,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 +276,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 +287,11 @@ class _LogItemState extends State<LogItem> {
); );
} }
} }
class NoGlowScrollBehavior extends ScrollBehavior {
@override
Widget buildOverscrollIndicator(
BuildContext context, Widget child, ScrollableDetails details) {
return child; // 禁用过度滚动效果
}
}

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

@@ -7,18 +7,11 @@ import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
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});
@@ -81,8 +74,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
WidgetsBinding.instance.addPostFrameCallback( WidgetsBinding.instance.addPostFrameCallback(
(_) { (_) {
if (!mounted) return; if (!mounted) return;
final commonScaffoldState = final commonScaffoldState = context.commonScaffoldState;
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [ commonScaffoldState?.actions = [
IconButton( IconButton(
onPressed: () { onPressed: () {
@@ -185,18 +177,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 {
@@ -266,8 +256,39 @@ class ProfileItem extends StatelessWidget {
]; ];
} }
_handleCopyLink(BuildContext context) async {
await Clipboard.setData(
ClipboardData(
text: profile.url,
),
);
if (context.mounted) {
context.showNotifier(appLocalizations.copySuccess);
}
}
_handleExportFile(BuildContext context) async {
final commonScaffoldState = context.commonScaffoldState;
final res = await commonScaffoldState?.loadingRun<bool>(
() async {
final file = await profile.getFile();
final value = await picker.saveFile(
profile.label ?? profile.id,
file.readAsBytesSync(),
);
if (value == null) return false;
return true;
},
title: appLocalizations.tip,
);
if (res == true && context.mounted) {
context.showNotifier(appLocalizations.exportSuccess);
}
}
@override @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,62 @@ 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.file_copy_outlined,
break; 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,

View File

@@ -1,232 +0,0 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart';
import 'package:re_editor/re_editor.dart';
import 'package:re_highlight/languages/yaml.dart';
import 'package:re_highlight/styles/atom-one-light.dart';
class ViewProfile extends StatefulWidget {
final Profile profile;
const ViewProfile({
super.key,
required this.profile,
});
@override
State<ViewProfile> createState() => _ViewProfileState();
}
class _ViewProfileState extends State<ViewProfile> {
bool readOnly = true;
final CodeLineEditingController _controller = CodeLineEditingController();
final key = GlobalKey<CommonScaffoldState>();
final _focusNode = FocusNode();
String? rawText;
@override
void initState() {
super.initState();
appPath.getProfilePath(widget.profile.id).then((path) async {
if (path == null) return;
final file = File(path);
rawText = await file.readAsString();
_controller.text = rawText ?? "";
});
}
@override
void dispose() {
_controller.dispose();
_focusNode.dispose();
super.dispose();
}
Profile get profile => widget.profile;
_handleChangeReadOnly() async {
if (readOnly == true) {
setState(() {
readOnly = false;
});
} else {
if (_controller.text == rawText) return;
final newProfile = await key.currentState?.loadingRun<Profile>(() async {
return await profile.saveFileWithString(_controller.text);
});
if (newProfile == null) return;
globalState.appController.config.setProfile(newProfile);
setState(() {
readOnly = true;
});
}
}
@override
Widget build(BuildContext context) {
return CommonScaffold(
key: key,
actions: [
IconButton(
onPressed: _controller.undo,
icon: const Icon(Icons.undo),
),
IconButton(
onPressed: _controller.redo,
icon: const Icon(Icons.redo),
),
IconButton(
onPressed: _handleChangeReadOnly,
icon: readOnly ? const Icon(Icons.edit) : const Icon(Icons.save),
),
],
body: CodeEditor(
readOnly: readOnly,
focusNode: _focusNode,
scrollbarBuilder: (context, child, details) {
return Scrollbar(
controller: details.controller,
thickness: 8,
radius: const Radius.circular(2),
interactive: true,
child: child,
);
},
showCursorWhenReadOnly: false,
controller: _controller,
shortcutsActivatorsBuilder:
const DefaultCodeShortcutsActivatorsBuilder(),
indicatorBuilder: (
context,
editingController,
chunkController,
notifier,
) {
return Row(
children: [
DefaultCodeLineNumber(
controller: editingController,
notifier: notifier,
),
DefaultCodeChunkIndicator(
width: 20,
controller: chunkController,
notifier: notifier,
)
],
);
},
toolbarController:
!readOnly ? ContextMenuControllerImpl(_focusNode) : null,
style: CodeEditorStyle(
fontSize: 14,
codeTheme: CodeHighlightTheme(
languages: {
'yaml': CodeHighlightThemeMode(
mode: langYaml,
)
},
theme: atomOneLightTheme,
),
),
),
title: widget.profile.label ?? widget.profile.id,
);
}
}
class ContextMenuItemWidget extends PopupMenuItem<void> {
ContextMenuItemWidget({
super.key,
required String text,
required VoidCallback super.onTap,
}) : super(child: Text(text));
}
class ContextMenuControllerImpl implements SelectionToolbarController {
OverlayEntry? _overlayEntry;
final FocusNode focusNode;
ContextMenuControllerImpl(
this.focusNode,
);
_removeOverLayEntry() {
_overlayEntry?.remove();
_overlayEntry = null;
}
@override
void hide(BuildContext context) {
// _removeOverLayEntry();
}
// _handleCut(CodeLineEditingController controller) {
// controller.cut();
// _removeOverLayEntry();
// }
//
// _handleCopy(CodeLineEditingController controller) async {
// await controller.copy();
// _removeOverLayEntry();
// }
//
// _handlePaste(CodeLineEditingController controller) {
// controller.paste();
// _removeOverLayEntry();
// }
@override
void show({
required BuildContext context,
required CodeLineEditingController controller,
required TextSelectionToolbarAnchors anchors,
Rect? renderRect,
required LayerLink layerLink,
required ValueNotifier<bool> visibility,
}) {
if (controller.selectedText.isEmpty) {
return;
}
_removeOverLayEntry();
final relativeRect = RelativeRect.fromSize(
(anchors.primaryAnchor) &
const Size(150, double.infinity),
MediaQuery.of(context).size,
);
_overlayEntry ??= OverlayEntry(
builder: (context) => ValueListenableBuilder<CodeLineEditingValue>(
valueListenable: controller,
builder: (_, __, child) {
if (controller.selectedText.isEmpty) {
_removeOverLayEntry();
}
return child!;
},
child: Positioned(
left: relativeRect.left,
top: relativeRect.top,
child: Material(
color: Colors.transparent,
child: GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(focusNode);
},
child: Container(
width: 200,
height: 200,
color: Colors.green,
),
),
),
),
),
);
Overlay.of(context).insert(_overlayEntry!);
}
}

View File

@@ -12,10 +12,12 @@ class ProxyCard extends StatelessWidget {
final Proxy proxy; final Proxy proxy;
final GroupType groupType; final GroupType groupType;
final ProxyCardType type; final ProxyCardType type;
final String? testUrl;
const ProxyCard({ const ProxyCard({
super.key, super.key,
required this.groupName, required this.groupName,
required this.testUrl,
required this.proxy, required this.proxy,
required this.groupType, required this.groupType,
required this.type, required this.type,
@@ -24,16 +26,18 @@ class ProxyCard extends StatelessWidget {
Measure get measure => globalState.measure; Measure get measure => globalState.measure;
_handleTestCurrentDelay() { _handleTestCurrentDelay() {
proxyDelayTest(proxy); proxyDelayTest(
proxy,
testUrl,
);
} }
Widget _buildDelayText() { Widget _buildDelayText() {
return SizedBox( return SizedBox(
height: measure.labelSmallHeight, height: measure.labelSmallHeight,
child: Selector<AppState, int?>( child: Selector<AppState, int?>(
selector: (context, appState) => appState.getDelay( selector: (context, appState) =>
proxy.name, globalState.appController.getDelay(proxy.name,testUrl),
),
builder: (context, delay, __) { builder: (context, delay, __) {
return FadeBox( return FadeBox(
child: Builder( child: Builder(

View File

@@ -30,7 +30,7 @@ double get listHeaderHeight {
double getItemHeight(ProxyCardType proxyCardType) { double getItemHeight(ProxyCardType proxyCardType) {
final measure = globalState.measure; final measure = globalState.measure;
final baseHeight = final baseHeight =
12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8; 12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8 + 4;
return switch (proxyCardType) { return switch (proxyCardType) {
ProxyCardType.expand => baseHeight + measure.labelSmallHeight + 8, ProxyCardType.expand => baseHeight + measure.labelSmallHeight + 8,
ProxyCardType.shrink => baseHeight, ProxyCardType.shrink => baseHeight,
@@ -38,33 +38,48 @@ double getItemHeight(ProxyCardType proxyCardType) {
}; };
} }
proxyDelayTest(Proxy proxy) async { proxyDelayTest(Proxy proxy, [String? testUrl]) async {
final appController = globalState.appController; final appController = globalState.appController;
final proxyName = appController.appState.getRealProxyName(proxy.name); final proxyName = appController.appState.getRealProxyName(proxy.name);
final url = appController.getRealTestUrl(testUrl);
globalState.appController.setDelay( globalState.appController.setDelay(
Delay( Delay(
url: url,
name: proxyName, name: proxyName,
value: 0, value: 0,
), ),
); );
globalState.appController.setDelay(await clashCore.getDelay(proxyName)); globalState.appController.setDelay(
await clashCore.getDelay(
url,
proxyName,
),
);
} }
delayTest(List<Proxy> proxies) async { delayTest(List<Proxy> proxies, [String? testUrl]) async {
final appController = globalState.appController; final appController = globalState.appController;
final proxyNames = proxies final proxyNames = proxies
.map((proxy) => appController.appState.getRealProxyName(proxy.name)) .map((proxy) => appController.appState.getRealProxyName(proxy.name))
.toSet() .toSet()
.toList(); .toList();
final url = appController.getRealTestUrl(testUrl);
final delayProxies = proxyNames.map<Future>((proxyName) async { final delayProxies = proxyNames.map<Future>((proxyName) async {
globalState.appController.setDelay( globalState.appController.setDelay(
Delay( Delay(
url: url,
name: proxyName, name: proxyName,
value: 0, value: 0,
), ),
); );
globalState.appController.setDelay(await clashCore.getDelay(proxyName)); globalState.appController.setDelay(
await clashCore.getDelay(
url,
proxyName,
),
);
}).toList(); }).toList();
final batchesDelayProxies = delayProxies.batch(100); final batchesDelayProxies = delayProxies.batch(100);
@@ -86,7 +101,7 @@ double getScrollToSelectedOffset({
final proxyCardType = appController.config.proxiesStyle.cardType; final proxyCardType = appController.config.proxiesStyle.cardType;
final selectedName = appController.getCurrentSelectedName(groupName); final selectedName = appController.getCurrentSelectedName(groupName);
final findSelectedIndex = proxies.indexWhere( final findSelectedIndex = proxies.indexWhere(
(proxy) => proxy.name == selectedName, (proxy) => proxy.name == selectedName,
); );
final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0; final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0;
final rows = (selectedIndex / columns).floor(); final rows = (selectedIndex / columns).floor();

View File

@@ -134,6 +134,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
if (isExpand) { if (isExpand) {
final sortedProxies = globalState.appController.getSortProxies( final sortedProxies = globalState.appController.getSortProxies(
group.all, group.all,
group.testUrl,
); );
groupNameProxiesMap[groupName] = sortedProxies; groupNameProxiesMap[groupName] = sortedProxies;
final chunks = sortedProxies.chunks(columns); final chunks = sortedProxies.chunks(columns);
@@ -142,6 +143,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
.map<Widget>( .map<Widget>(
(proxy) => Flexible( (proxy) => Flexible(
child: ProxyCard( child: ProxyCard(
testUrl: group.testUrl,
type: type, type: type,
groupType: group.type, groupType: group.type,
key: ValueKey('$groupName.${proxy.name}'), key: ValueKey('$groupName.${proxy.name}'),
@@ -259,6 +261,11 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
return prev != next; return prev != next;
}, },
builder: (_, state, __) { builder: (_, state, __) {
if (state.groupNames.isEmpty) {
return NullStatus(
label: appLocalizations.nullProxies,
);
}
final items = _buildItems( final items = _buildItems(
groupNames: state.groupNames, groupNames: state.groupNames,
currentUnfoldSet: state.currentUnfoldSet, currentUnfoldSet: state.currentUnfoldSet,
@@ -266,13 +273,8 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
type: state.proxyCardType, type: state.proxyCardType,
); );
final itemsOffset = _getItemHeightList(items, state.proxyCardType); final itemsOffset = _getItemHeightList(items, state.proxyCardType);
return Scrollbar( return CommonScrollBar(
controller: _controller, controller: _controller,
thumbVisibility: true,
trackVisibility: true,
thickness: 8,
radius: const Radius.circular(8),
interactive: true,
child: Stack( child: Stack(
children: [ children: [
Positioned.fill( Positioned.fill(
@@ -367,10 +369,13 @@ class _ListHeaderState extends State<ListHeader>
bool get isExpand => widget.isExpand; bool get isExpand => widget.isExpand;
_delayTest(List<Proxy> proxies) async { _delayTest() async {
if (isLock) return; if (isLock) return;
isLock = true; isLock = true;
await delayTest(proxies); await delayTest(
widget.group.all,
widget.group.testUrl,
);
isLock = false; isLock = false;
} }
@@ -563,9 +568,7 @@ class _ListHeaderState extends State<ListHeader>
), ),
), ),
IconButton( IconButton(
onPressed: () { onPressed: _delayTest,
_delayTest(widget.group.all);
},
icon: const Icon( icon: const Icon(
Icons.network_ping, Icons.network_ping,
), ),

View File

@@ -28,9 +28,7 @@ class _ProvidersState extends State<Providers> {
WidgetsBinding.instance.addPostFrameCallback( WidgetsBinding.instance.addPostFrameCallback(
(_) { (_) {
globalState.appController.updateProviders(); globalState.appController.updateProviders();
final commonScaffoldState = context.commonScaffoldState?.actions = [
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton( IconButton(
onPressed: () { onPressed: () {
_updateProviders(); _updateProviders();
@@ -47,21 +45,40 @@ class _ProvidersState extends State<Providers> {
_updateProviders() async { _updateProviders() async {
final appState = globalState.appController.appState; final appState = globalState.appController.appState;
final providers = globalState.appController.appState.providers; final providers = globalState.appController.appState.providers;
final messages = [];
final updateProviders = providers.map<Future>( final updateProviders = providers.map<Future>(
(provider) async { (provider) async {
appState.setProvider( appState.setProvider(
provider.copyWith(isUpdating: true), provider.copyWith(isUpdating: true),
); );
await clashCore.updateExternalProvider( final message = await clashCore.updateExternalProvider(
providerName: provider.name, providerName: provider.name,
); );
if (message.isNotEmpty) {
messages.add("${provider.name}: $message \n");
}
appState.setProvider( appState.setProvider(
await clashCore.getExternalProvider(provider.name), await clashCore.getExternalProvider(provider.name),
); );
}, },
); );
final titleMedium = context.textTheme.titleMedium;
await Future.wait(updateProviders); await Future.wait(updateProviders);
await globalState.appController.updateGroupsDebounce(); await globalState.appController.updateGroupsDebounce();
if (messages.isNotEmpty) {
globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(
children: [
for (final message in messages)
TextSpan(
text: message,
style: titleMedium,
)
],
),
);
}
} }
@override @override
@@ -107,10 +124,10 @@ class ProviderItem extends StatelessWidget {
}); });
_handleUpdateProvider() async { _handleUpdateProvider() async {
await globalState.safeRun<void>(() async { final appState = globalState.appController.appState;
final appState = globalState.appController.appState; if (provider.vehicleType != "HTTP") return;
if (provider.vehicleType != "HTTP") return; await globalState.safeRun(
await globalState.safeRun(() async { () async {
appState.setProvider( appState.setProvider(
provider.copyWith( provider.copyWith(
isUpdating: true, isUpdating: true,
@@ -120,11 +137,12 @@ class ProviderItem extends StatelessWidget {
providerName: provider.name, providerName: provider.name,
); );
if (message.isNotEmpty) throw message; if (message.isNotEmpty) throw message;
}); },
appState.setProvider( silence: false,
await clashCore.getExternalProvider(provider.name), );
); appState.setProvider(
}); await clashCore.getExternalProvider(provider.name),
);
await globalState.appController.updateGroupsDebounce(); await globalState.appController.updateGroupsDebounce();
} }

View File

@@ -21,10 +21,8 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
final GlobalKey<ProxiesTabFragmentState> _proxiesTabKey = GlobalKey(); final GlobalKey<ProxiesTabFragmentState> _proxiesTabKey = GlobalKey();
_initActions(ProxiesType proxiesType, bool hasProvider) { _initActions(ProxiesType proxiesType, bool hasProvider) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState = context.commonScaffoldState?.actions = [
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
if (hasProvider) ...[ if (hasProvider) ...[
IconButton( IconButton(
onPressed: () { onPressed: () {

View File

@@ -12,6 +12,7 @@ import 'card.dart';
import 'common.dart'; import 'common.dart';
List<Proxy> currentProxies = []; List<Proxy> currentProxies = [];
String? currentTestUrl;
typedef GroupNameKeyMap = Map<String, GlobalObjectKey<ProxyGroupViewState>>; typedef GroupNameKeyMap = Map<String, GlobalObjectKey<ProxyGroupViewState>>;
@@ -114,6 +115,7 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
} }
final currentGroup = currentGroups[index ?? _tabController!.index]; final currentGroup = currentGroups[index ?? _tabController!.index];
currentProxies = currentGroup.all; currentProxies = currentGroup.all;
currentTestUrl = currentGroup.testUrl;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
appController.config.updateCurrentGroupName( appController.config.updateCurrentGroupName(
currentGroup.name, currentGroup.name,
@@ -161,6 +163,11 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
return false; return false;
}, },
builder: (_, state, __) { builder: (_, state, __) {
if (state.groupNames.isEmpty) {
return NullStatus(
label: appLocalizations.nullProxies,
);
}
final index = state.groupNames.indexWhere( final index = state.groupNames.indexWhere(
(item) => item == state.currentGroupName, (item) => item == state.currentGroupName,
); );
@@ -273,7 +280,10 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
_delayTest() async { _delayTest() async {
if (isLock) return; if (isLock) return;
isLock = true; isLock = true;
await delayTest(currentProxies); await delayTest(
currentProxies,
currentTestUrl,
);
isLock = false; isLock = false;
} }
@@ -289,6 +299,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
} }
final sortedProxies = globalState.appController.getSortProxies( final sortedProxies = globalState.appController.getSortProxies(
currentProxies, currentProxies,
currentTestUrl,
); );
_controller.animateTo( _controller.animateTo(
min( min(
@@ -309,9 +320,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
return; return;
} }
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState = context.commonScaffoldState?.floatingActionButton = DelayTestButton(
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.floatingActionButton = DelayTestButton(
onClick: () async { onClick: () async {
await _delayTest(); await _delayTest();
}, },
@@ -334,6 +343,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
sortNum: appState.sortNum, sortNum: appState.sortNum,
proxies: group.all, proxies: group.all,
groupType: group.type, groupType: group.type,
testUrl: group.testUrl,
); );
}, },
builder: (_, state, __) { builder: (_, state, __) {
@@ -342,6 +352,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
final proxyCardType = state.proxyCardType; final proxyCardType = state.proxyCardType;
final sortedProxies = globalState.appController.getSortProxies( final sortedProxies = globalState.appController.getSortProxies(
proxies, proxies,
state.testUrl,
); );
return ActiveBuilder( return ActiveBuilder(
label: "proxies", label: "proxies",
@@ -369,6 +380,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
itemBuilder: (_, index) { itemBuilder: (_, index) {
final proxy = sortedProxies[index]; final proxy = sortedProxies[index];
return ProxyCard( return ProxyCard(
testUrl: state.testUrl,
groupType: state.groupType, groupType: state.groupType,
type: proxyCardType, type: proxyCardType,
key: ValueKey('$groupName.${proxy.name}'), key: ValueKey('$groupName.${proxy.name}'),

View File

@@ -1,317 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class RequestsFragment extends StatefulWidget {
const RequestsFragment({super.key});
@override
State<RequestsFragment> createState() => _RequestsFragmentState();
}
class _RequestsFragmentState extends State<RequestsFragment> {
final requestsNotifier =
ValueNotifier<ConnectionsAndKeywords>(const ConnectionsAndKeywords());
final ScrollController _scrollController = ScrollController(
keepScrollOffset: false,
);
Timer? timer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appController.appState;
requestsNotifier.value =
requestsNotifier.value.copyWith(connections: appState.requests);
if (timer != null) {
timer?.cancel();
timer = null;
}
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
final maxLength = Platform.isAndroid ? 1000 : 60;
final requests = appState.requests.safeSublist(
appState.requests.length - maxLength,
);
if (!connectionListEquality.equals(
requestsNotifier.value.connections,
requests,
)) {
requestsNotifier.value =
requestsNotifier.value.copyWith(connections: requests);
}
});
});
}
_initActions() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
showSearch(
context: context,
delegate: RequestsSearchDelegate(
state: requestsNotifier.value,
),
);
},
icon: const Icon(Icons.search),
),
];
},
);
}
_addKeyword(String keyword) {
final isContains = requestsNotifier.value.keywords.contains(keyword);
if (isContains) return;
final keywords = List<String>.from(requestsNotifier.value.keywords)
..add(keyword);
requestsNotifier.value = requestsNotifier.value.copyWith(
keywords: keywords,
);
}
_deleteKeyword(String keyword) {
final isContains = requestsNotifier.value.keywords.contains(keyword);
if (!isContains) return;
final keywords = List<String>.from(requestsNotifier.value.keywords)
..remove(keyword);
requestsNotifier.value = requestsNotifier.value.copyWith(
keywords: keywords,
);
}
@override
void dispose() {
timer?.cancel();
_scrollController.dispose();
timer = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return Selector<AppState, bool?>(
selector: (_, appState) =>
appState.currentLabel == 'requests' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions();
}
return child!;
},
child: ValueListenableBuilder<ConnectionsAndKeywords>(
valueListenable: requestsNotifier,
builder: (_, state, __) {
var connections = state.filteredConnections;
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullRequestsDesc,
);
}
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,
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: connections.length,
),
)
],
);
},
),
);
}
}
class RequestsSearchDelegate extends SearchDelegate {
ValueNotifier<ConnectionsAndKeywords> requestsNotifier;
RequestsSearchDelegate({
required ConnectionsAndKeywords state,
}) : requestsNotifier = ValueNotifier<ConnectionsAndKeywords>(state);
get state => requestsNotifier.value;
List<Connection> get _results {
final lowerQuery = query.toLowerCase().trim();
return requestsNotifier.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 = requestsNotifier.value.keywords.contains(keyword);
if (isContains) return;
final keywords = List<String>.from(requestsNotifier.value.keywords)
..add(keyword);
requestsNotifier.value = requestsNotifier.value.copyWith(
keywords: keywords,
);
}
_deleteKeyword(String keyword) {
final isContains = requestsNotifier.value.keywords.contains(keyword);
if (!isContains) return;
final keywords = List<String>.from(requestsNotifier.value.keywords)
..remove(keyword);
requestsNotifier.value = requestsNotifier.value.copyWith(
keywords: keywords,
);
}
@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() {
requestsNotifier.dispose();
super.dispose();
}
@override
Widget buildSuggestions(BuildContext context) {
return ValueListenableBuilder(
valueListenable: requestsNotifier,
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: (value) {
_addKeyword(value);
},
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: _results.length,
),
)
],
);
},
);
}
}

View File

@@ -112,7 +112,7 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
} }
Future<FileInfo> _getGeoFileLastModified(String fileName) async { Future<FileInfo> _getGeoFileLastModified(String fileName) async {
final homePath = await appPath.getHomeDirPath(); final homePath = await appPath.homeDirPath;
final file = File(join(homePath, fileName)); final file = File(join(homePath, fileName));
final lastModified = await file.lastModified(); final lastModified = await file.lastModified();
final size = await file.length(); final size = await file.length();
@@ -183,7 +183,12 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
} }
_handleUpdateGeoDataItem() async { _handleUpdateGeoDataItem() async {
await globalState.safeRun<void>(updateGeoDateItem); await globalState.safeRun<void>(
() async {
await updateGeoDateItem();
},
silence: false,
);
setState(() {}); setState(() {});
} }
@@ -191,9 +196,12 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
isUpdating.value = true; isUpdating.value = true;
try { try {
final message = await clashCore.updateGeoData( final message = await clashCore.updateGeoData(
geoName: geoItem.fileName, UpdateGeoDataParams(
geoType: geoItem.label, geoName: geoItem.fileName,
geoType: geoItem.label,
),
); );
print(message);
if (message.isNotEmpty) throw message; if (message.isNotEmpty) throw message;
} catch (e) { } catch (e) {
isUpdating.value = false; isUpdating.value = false;
@@ -249,12 +257,8 @@ class UpdateGeoUrlFormDialog extends StatefulWidget {
final String url; final String url;
final String? defaultValue; final String? defaultValue;
const UpdateGeoUrlFormDialog({ const UpdateGeoUrlFormDialog(
super.key, {super.key, required this.title, required this.url, this.defaultValue});
required this.title,
required this.url,
this.defaultValue
});
@override @override
State<UpdateGeoUrlFormDialog> createState() => _UpdateGeoUrlFormDialogState(); State<UpdateGeoUrlFormDialog> createState() => _UpdateGeoUrlFormDialogState();

View File

@@ -35,6 +35,7 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
delegate: OpenDelegate( delegate: OpenDelegate(
title: Intl.message(navigationItem.label), title: Intl.message(navigationItem.label),
widget: navigationItem.fragment, widget: navigationItem.fragment,
extendPageWidth: 360,
), ),
); );
} }
@@ -195,14 +196,17 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
Selector<AppState, MoreToolsSelectorState>( Selector<AppState, MoreToolsSelectorState>(
selector: (_, appState) { selector: (_, appState) {
return MoreToolsSelectorState( return MoreToolsSelectorState(
navigationItems: appState.viewMode == ViewMode.mobile navigationItems: appState.navigationItems.where((element) {
? appState.navigationItems.where( final isMore =
(element) { element.modes.contains(NavigationItemMode.more);
return element.modes final isDesktop =
.contains(NavigationItemMode.more); element.modes.contains(NavigationItemMode.desktop);
}, if (isMore && !isDesktop) return true;
).toList() if (appState.viewMode != ViewMode.mobile || !isMore) {
: [], return false;
}
return true;
}).toList(),
); );
}, },
builder: (_, state, __) { builder: (_, state, __) {

View File

@@ -333,5 +333,14 @@
"routeAddressDesc": "Config listen route address", "routeAddressDesc": "Config listen route address",
"pleaseInputAdminPassword": "Please enter the admin password", "pleaseInputAdminPassword": "Please enter the admin password",
"copyEnvVar": "Copying environment variables", "copyEnvVar": "Copying environment variables",
"memoryInfo": "Memory info" "memoryInfo": "Memory info",
"cancel": "Cancel",
"fileIsUpdate": "The file has been modified. Do you want to save the changes?",
"profileHasUpdate": "The profile has been modified. Do you want to disable auto update?",
"hasCacheChange": "Do you want to cache the changes?",
"nullProxies": "No proxies",
"copySuccess": "Copy success",
"copyLink": "Copy link",
"exportFile": "Export file",
"cacheCorrupt": "The cache is corrupt. Do you want to clear it?"
} }

View File

@@ -333,5 +333,14 @@
"routeAddressDesc": "配置监听路由地址", "routeAddressDesc": "配置监听路由地址",
"pleaseInputAdminPassword": "请输入管理员密码", "pleaseInputAdminPassword": "请输入管理员密码",
"copyEnvVar": "复制环境变量", "copyEnvVar": "复制环境变量",
"memoryInfo": "内存信息" "memoryInfo": "内存信息",
"cancel": "取消",
"fileIsUpdate": "文件有修改,是否保存修改",
"profileHasUpdate": "配置文件已经修改,是否关闭自动更新 ",
"hasCacheChange": "是否缓存修改",
"nullProxies": "暂无代理",
"copySuccess": "复制成功",
"copyLink": "复制链接",
"exportFile": "导出文件",
"cacheCorrupt": "缓存已损坏,是否清空?"
} }

View File

@@ -39,8 +39,10 @@ MessageLookupByLibrary? _findExact(String localeName) {
/// User programs should call this before using [localeName] for messages. /// User programs should call this before using [localeName] for messages.
Future<bool> initializeMessages(String localeName) { Future<bool> initializeMessages(String localeName) {
var availableLocale = Intl.verifiedLocale( var availableLocale = Intl.verifiedLocale(
localeName, (locale) => _deferredLibraries[locale] != null, localeName,
onFailure: (_) => null); (locale) => _deferredLibraries[locale] != null,
onFailure: (_) => null,
);
if (availableLocale == null) { if (availableLocale == null) {
return new SynchronousFuture(false); return new SynchronousFuture(false);
} }
@@ -60,8 +62,11 @@ bool _messagesExistFor(String locale) {
} }
MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
var actualLocale = var actualLocale = Intl.verifiedLocale(
Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); locale,
_messagesExistFor,
onFailure: (_) => null,
);
if (actualLocale == null) return null; if (actualLocale == null) return null;
return _findExact(actualLocale); return _findExact(actualLocale);
} }

File diff suppressed because it is too large Load Diff

View File

@@ -22,381 +22,391 @@ class MessageLookup extends MessageLookupByLibrary {
final messages = _notInlinedMessages(_notInlinedMessages); final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{ static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"about": MessageLookupByLibrary.simpleMessage("关于"), "about": MessageLookupByLibrary.simpleMessage("关于"),
"accessControl": MessageLookupByLibrary.simpleMessage("访问控制"), "accessControl": MessageLookupByLibrary.simpleMessage("访问控制"),
"accessControlAllowDesc": "accessControlAllowDesc": MessageLookupByLibrary.simpleMessage(
MessageLookupByLibrary.simpleMessage("只允许选中应用进入VPN"), "只允许选中应用进入VPN",
"accessControlDesc": MessageLookupByLibrary.simpleMessage("配置应用访问代理"), ),
"accessControlNotAllowDesc": "accessControlDesc": MessageLookupByLibrary.simpleMessage("配置应用访问代理"),
MessageLookupByLibrary.simpleMessage("选中应用将会被排除在VPN之外"), "accessControlNotAllowDesc": MessageLookupByLibrary.simpleMessage(
"account": MessageLookupByLibrary.simpleMessage("账号"), "选中应用将会被排除在VPN之外",
"accountTip": MessageLookupByLibrary.simpleMessage("账号不能为空"), ),
"action": MessageLookupByLibrary.simpleMessage("操作"), "account": MessageLookupByLibrary.simpleMessage("账号"),
"action_mode": MessageLookupByLibrary.simpleMessage("切换模式"), "accountTip": MessageLookupByLibrary.simpleMessage("账号不能为空"),
"action_proxy": MessageLookupByLibrary.simpleMessage("系统代理"), "action": MessageLookupByLibrary.simpleMessage("操作"),
"action_start": MessageLookupByLibrary.simpleMessage("启动/停止"), "action_mode": MessageLookupByLibrary.simpleMessage("切换模式"),
"action_tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"), "action_proxy": MessageLookupByLibrary.simpleMessage("系统代理"),
"action_view": MessageLookupByLibrary.simpleMessage("显示/隐藏"), "action_start": MessageLookupByLibrary.simpleMessage("启动/停止"),
"add": MessageLookupByLibrary.simpleMessage("添加"), "action_tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),
"address": MessageLookupByLibrary.simpleMessage("地址"), "action_view": MessageLookupByLibrary.simpleMessage("显示/隐藏"),
"addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"), "add": MessageLookupByLibrary.simpleMessage("添加"),
"addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"), "address": MessageLookupByLibrary.simpleMessage("地址"),
"adminAutoLaunch": MessageLookupByLibrary.simpleMessage("管理员自启动"), "addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"),
"adminAutoLaunchDesc": "addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"),
MessageLookupByLibrary.simpleMessage("使用管理员模式开机自启动"), "adminAutoLaunch": MessageLookupByLibrary.simpleMessage("管理员自启动"),
"ago": MessageLookupByLibrary.simpleMessage(""), "adminAutoLaunchDesc": MessageLookupByLibrary.simpleMessage("使用管理员模式开机自启动"),
"agree": MessageLookupByLibrary.simpleMessage("同意"), "ago": MessageLookupByLibrary.simpleMessage(""),
"allApps": MessageLookupByLibrary.simpleMessage("所有应用"), "agree": MessageLookupByLibrary.simpleMessage("同意"),
"allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"), "allApps": MessageLookupByLibrary.simpleMessage("所有应用"),
"allowBypassDesc": "allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"),
MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"), "allowBypassDesc": MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"),
"allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"), "allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"),
"allowLanDesc": MessageLookupByLibrary.simpleMessage("允许通过局域网访问代理"), "allowLanDesc": MessageLookupByLibrary.simpleMessage("允许通过局域网访问代理"),
"app": MessageLookupByLibrary.simpleMessage("应用"), "app": MessageLookupByLibrary.simpleMessage("应用"),
"appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"), "appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"),
"appDesc": MessageLookupByLibrary.simpleMessage("处理应用相关设置"), "appDesc": MessageLookupByLibrary.simpleMessage("处理应用相关设置"),
"application": MessageLookupByLibrary.simpleMessage("应用程序"), "application": MessageLookupByLibrary.simpleMessage("应用程序"),
"applicationDesc": MessageLookupByLibrary.simpleMessage("修改应用程序相关设置"), "applicationDesc": MessageLookupByLibrary.simpleMessage("修改应用程序相关设置"),
"auto": MessageLookupByLibrary.simpleMessage("自动"), "auto": MessageLookupByLibrary.simpleMessage("自动"),
"autoCheckUpdate": MessageLookupByLibrary.simpleMessage("自动检查更新"), "autoCheckUpdate": MessageLookupByLibrary.simpleMessage("自动检查更新"),
"autoCheckUpdateDesc": "autoCheckUpdateDesc": MessageLookupByLibrary.simpleMessage("应用启动时自动检查更新"),
MessageLookupByLibrary.simpleMessage("应用启动时自动检查更新"), "autoCloseConnections": MessageLookupByLibrary.simpleMessage("自动关闭连接"),
"autoCloseConnections": MessageLookupByLibrary.simpleMessage("自动关闭连接"), "autoCloseConnectionsDesc": MessageLookupByLibrary.simpleMessage(
"autoCloseConnectionsDesc": "切换节点后自动关闭连接",
MessageLookupByLibrary.simpleMessage("切换节点后自动关闭连接"), ),
"autoLaunch": MessageLookupByLibrary.simpleMessage("自启动"), "autoLaunch": MessageLookupByLibrary.simpleMessage("自启动"),
"autoLaunchDesc": MessageLookupByLibrary.simpleMessage("跟随系统自启动"), "autoLaunchDesc": MessageLookupByLibrary.simpleMessage("跟随系统自启动"),
"autoRun": MessageLookupByLibrary.simpleMessage("自动运行"), "autoRun": MessageLookupByLibrary.simpleMessage("自动运行"),
"autoRunDesc": MessageLookupByLibrary.simpleMessage("应用打开时自动运行"), "autoRunDesc": MessageLookupByLibrary.simpleMessage("应用打开时自动运行"),
"autoUpdate": MessageLookupByLibrary.simpleMessage("自动更新"), "autoUpdate": MessageLookupByLibrary.simpleMessage("自动更新"),
"autoUpdateInterval": "autoUpdateInterval": MessageLookupByLibrary.simpleMessage("自动更新间隔(分钟)"),
MessageLookupByLibrary.simpleMessage("自动更新间隔(分钟)"), "backup": MessageLookupByLibrary.simpleMessage("备份"),
"backup": MessageLookupByLibrary.simpleMessage("备份"), "backupAndRecovery": MessageLookupByLibrary.simpleMessage("备份与恢复"),
"backupAndRecovery": MessageLookupByLibrary.simpleMessage("备份与恢复"), "backupAndRecoveryDesc": MessageLookupByLibrary.simpleMessage(
"backupAndRecoveryDesc": "通过WebDAV或者文件同步数据",
MessageLookupByLibrary.simpleMessage("通过WebDAV或者文件同步数据"), ),
"backupSuccess": MessageLookupByLibrary.simpleMessage("备份成功"), "backupSuccess": MessageLookupByLibrary.simpleMessage("备份成功"),
"bind": MessageLookupByLibrary.simpleMessage("绑定"), "bind": MessageLookupByLibrary.simpleMessage("绑定"),
"blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"), "blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"),
"bypassDomain": MessageLookupByLibrary.simpleMessage("排除域名"), "bypassDomain": MessageLookupByLibrary.simpleMessage("排除域名"),
"bypassDomainDesc": MessageLookupByLibrary.simpleMessage("仅在系统代理启用时生效"), "bypassDomainDesc": MessageLookupByLibrary.simpleMessage("仅在系统代理启用时生效"),
"cancelFilterSystemApp": "cacheCorrupt": MessageLookupByLibrary.simpleMessage("缓存已损坏,是否清空?"),
MessageLookupByLibrary.simpleMessage("取消过滤系统应用"), "cancel": MessageLookupByLibrary.simpleMessage("取消"),
"cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全选"), "cancelFilterSystemApp": MessageLookupByLibrary.simpleMessage("取消过滤系统应用"),
"checkError": MessageLookupByLibrary.simpleMessage("检测失败"), "cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全选"),
"checkUpdate": MessageLookupByLibrary.simpleMessage("查更新"), "checkError": MessageLookupByLibrary.simpleMessage("测失败"),
"checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"), "checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"),
"checking": MessageLookupByLibrary.simpleMessage("检测中..."), "checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"),
"clipboardExport": MessageLookupByLibrary.simpleMessage("导出剪贴板"), "checking": MessageLookupByLibrary.simpleMessage("检测中..."),
"clipboardImport": MessageLookupByLibrary.simpleMessage("剪贴板导入"), "clipboardExport": MessageLookupByLibrary.simpleMessage("导出剪贴板"),
"columns": MessageLookupByLibrary.simpleMessage("列数"), "clipboardImport": MessageLookupByLibrary.simpleMessage("剪贴板导入"),
"compatible": MessageLookupByLibrary.simpleMessage("兼容模式"), "columns": MessageLookupByLibrary.simpleMessage("列数"),
"compatibleDesc": "compatible": MessageLookupByLibrary.simpleMessage("兼容模式"),
MessageLookupByLibrary.simpleMessage("开启将失去部分应用能力获得全量的Clash的支持"), "compatibleDesc": MessageLookupByLibrary.simpleMessage(
"confirm": MessageLookupByLibrary.simpleMessage("确定"), "开启将失去部分应用能力获得全量的Clash的支持",
"connections": MessageLookupByLibrary.simpleMessage("连接"), ),
"connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接数据"), "confirm": MessageLookupByLibrary.simpleMessage("确定"),
"connectivity": MessageLookupByLibrary.simpleMessage("通性:"), "connections": MessageLookupByLibrary.simpleMessage(""),
"copy": MessageLookupByLibrary.simpleMessage("复制"), "connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接数据"),
"copyEnvVar": MessageLookupByLibrary.simpleMessage("复制环境变量"), "connectivity": MessageLookupByLibrary.simpleMessage("连通性:"),
"core": MessageLookupByLibrary.simpleMessage("内核"), "copy": MessageLookupByLibrary.simpleMessage("复制"),
"coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"), "copyEnvVar": MessageLookupByLibrary.simpleMessage("复制环境变量"),
"country": MessageLookupByLibrary.simpleMessage("区域"), "copyLink": MessageLookupByLibrary.simpleMessage("复制链接"),
"create": MessageLookupByLibrary.simpleMessage("创建"), "copySuccess": MessageLookupByLibrary.simpleMessage("复制成功"),
"cut": MessageLookupByLibrary.simpleMessage("剪切"), "core": MessageLookupByLibrary.simpleMessage("内核"),
"dark": MessageLookupByLibrary.simpleMessage("深色"), "coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"),
"dashboard": MessageLookupByLibrary.simpleMessage("仪表盘"), "country": MessageLookupByLibrary.simpleMessage("区域"),
"days": MessageLookupByLibrary.simpleMessage(""), "create": MessageLookupByLibrary.simpleMessage("创建"),
"defaultNameserver": MessageLookupByLibrary.simpleMessage("默认域名服务器"), "cut": MessageLookupByLibrary.simpleMessage("剪切"),
"defaultNameserverDesc": "dark": MessageLookupByLibrary.simpleMessage("深色"),
MessageLookupByLibrary.simpleMessage("用于解析DNS服务器"), "dashboard": MessageLookupByLibrary.simpleMessage("仪表盘"),
"defaultSort": MessageLookupByLibrary.simpleMessage("按默认排序"), "days": MessageLookupByLibrary.simpleMessage(""),
"defaultText": MessageLookupByLibrary.simpleMessage("默认"), "defaultNameserver": MessageLookupByLibrary.simpleMessage("默认域名服务器"),
"delay": MessageLookupByLibrary.simpleMessage("延迟"), "defaultNameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析DNS服务器"),
"delaySort": MessageLookupByLibrary.simpleMessage("延迟排序"), "defaultSort": MessageLookupByLibrary.simpleMessage("默认排序"),
"delete": MessageLookupByLibrary.simpleMessage("删除"), "defaultText": MessageLookupByLibrary.simpleMessage("默认"),
"deleteProfileTip": MessageLookupByLibrary.simpleMessage("确定要删除当前配置吗?"), "delay": MessageLookupByLibrary.simpleMessage("延迟"),
"desc": MessageLookupByLibrary.simpleMessage( "delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"),
"基于ClashMeta的多平台代理客户端简单易用开源无广告。"), "delete": MessageLookupByLibrary.simpleMessage("删除"),
"direct": MessageLookupByLibrary.simpleMessage("直连"), "deleteProfileTip": MessageLookupByLibrary.simpleMessage("确定要删除当前配置吗?"),
"disclaimer": MessageLookupByLibrary.simpleMessage("免责声明"), "desc": MessageLookupByLibrary.simpleMessage(
"disclaimerDesc": MessageLookupByLibrary.simpleMessage( "基于ClashMeta的多平台代理客户端简单易用开源无广告。",
"本软件仅供学习交流、科研等非商业性质的用途,严禁将本软件用于商业目的。如有任何商业行为,均与本软件无关。"), ),
"discoverNewVersion": MessageLookupByLibrary.simpleMessage("发现新版本"), "direct": MessageLookupByLibrary.simpleMessage("直连"),
"discovery": MessageLookupByLibrary.simpleMessage("发现新版本"), "disclaimer": MessageLookupByLibrary.simpleMessage("免责声明"),
"dnsDesc": MessageLookupByLibrary.simpleMessage("更新DNS相关设置"), "disclaimerDesc": MessageLookupByLibrary.simpleMessage(
"dnsMode": MessageLookupByLibrary.simpleMessage("DNS模式"), "本软件仅供学习交流、科研等非商业性质的用途,严禁将本软件用于商业目的。如有任何商业行为,均与本软件无关。",
"doYouWantToPass": MessageLookupByLibrary.simpleMessage("是否要通过"), ),
"domain": MessageLookupByLibrary.simpleMessage("域名"), "discoverNewVersion": MessageLookupByLibrary.simpleMessage("发现新版本"),
"download": MessageLookupByLibrary.simpleMessage("下载"), "discovery": MessageLookupByLibrary.simpleMessage("发现新版本"),
"edit": MessageLookupByLibrary.simpleMessage("编辑"), "dnsDesc": MessageLookupByLibrary.simpleMessage("更新DNS相关设置"),
"en": MessageLookupByLibrary.simpleMessage("英语"), "dnsMode": MessageLookupByLibrary.simpleMessage("DNS模式"),
"entries": MessageLookupByLibrary.simpleMessage("个条目"), "doYouWantToPass": MessageLookupByLibrary.simpleMessage("是否要通过"),
"exclude": MessageLookupByLibrary.simpleMessage("从最近任务中隐藏"), "domain": MessageLookupByLibrary.simpleMessage("域名"),
"excludeDesc": "download": MessageLookupByLibrary.simpleMessage("下载"),
MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"), "edit": MessageLookupByLibrary.simpleMessage("编辑"),
"exit": MessageLookupByLibrary.simpleMessage("退出"), "en": MessageLookupByLibrary.simpleMessage("英语"),
"expand": MessageLookupByLibrary.simpleMessage("标准"), "entries": MessageLookupByLibrary.simpleMessage("个条目"),
"expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"), "exclude": MessageLookupByLibrary.simpleMessage("从最近任务中隐藏"),
"exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"), "excludeDesc": MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"),
"exportSuccess": MessageLookupByLibrary.simpleMessage("导出成功"), "exit": MessageLookupByLibrary.simpleMessage("退出"),
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"), "expand": MessageLookupByLibrary.simpleMessage("标准"),
"externalControllerDesc": "expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"),
MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制Clash内核"), "exportFile": MessageLookupByLibrary.simpleMessage("导出文件"),
"externalLink": MessageLookupByLibrary.simpleMessage("外部链接"), "exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"),
"externalResources": MessageLookupByLibrary.simpleMessage("外部资源"), "exportSuccess": MessageLookupByLibrary.simpleMessage("导出成功"),
"fakeipFilter": MessageLookupByLibrary.simpleMessage("Fakeip过滤"), "externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
"fakeipRange": MessageLookupByLibrary.simpleMessage("Fakeip范围"), "externalControllerDesc": MessageLookupByLibrary.simpleMessage(
"fallback": MessageLookupByLibrary.simpleMessage("Fallback"), "开启后将可以通过9090端口控制Clash内核",
"fallbackDesc": MessageLookupByLibrary.simpleMessage("一般情况下使用境外DNS"), ),
"fallbackFilter": MessageLookupByLibrary.simpleMessage("Fallback过滤"), "externalLink": MessageLookupByLibrary.simpleMessage("外部链接"),
"file": MessageLookupByLibrary.simpleMessage("文件"), "externalResources": MessageLookupByLibrary.simpleMessage("外部资源"),
"fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"), "fakeipFilter": MessageLookupByLibrary.simpleMessage("Fakeip过滤"),
"filterSystemApp": MessageLookupByLibrary.simpleMessage("过滤系统应用"), "fakeipRange": MessageLookupByLibrary.simpleMessage("Fakeip范围"),
"findProcessMode": MessageLookupByLibrary.simpleMessage("查找进程"), "fallback": MessageLookupByLibrary.simpleMessage("Fallback"),
"findProcessModeDesc": "fallbackDesc": MessageLookupByLibrary.simpleMessage("一般情况下使用境外DNS"),
MessageLookupByLibrary.simpleMessage("开启后存在闪退风险"), "fallbackFilter": MessageLookupByLibrary.simpleMessage("Fallback过滤"),
"fontFamily": MessageLookupByLibrary.simpleMessage("字体"), "file": MessageLookupByLibrary.simpleMessage("文件"),
"fourColumns": MessageLookupByLibrary.simpleMessage("四列"), "fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
"general": MessageLookupByLibrary.simpleMessage("基础"), "fileIsUpdate": MessageLookupByLibrary.simpleMessage("文件有修改,是否保存修改"),
"generalDesc": MessageLookupByLibrary.simpleMessage("覆写基础设置"), "filterSystemApp": MessageLookupByLibrary.simpleMessage("过滤系统应用"),
"geoData": MessageLookupByLibrary.simpleMessage("地理数据"), "findProcessMode": MessageLookupByLibrary.simpleMessage("查找进程"),
"geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"), "findProcessModeDesc": MessageLookupByLibrary.simpleMessage("开启后存在闪退风险"),
"geodataLoaderDesc": "fontFamily": MessageLookupByLibrary.simpleMessage("字体"),
MessageLookupByLibrary.simpleMessage("开启将使用Geo低内存加载器"), "fourColumns": MessageLookupByLibrary.simpleMessage("四列"),
"geoipCode": MessageLookupByLibrary.simpleMessage("Geoip代码"), "general": MessageLookupByLibrary.simpleMessage("基础"),
"global": MessageLookupByLibrary.simpleMessage("全局"), "generalDesc": MessageLookupByLibrary.simpleMessage("覆写基础设置"),
"go": MessageLookupByLibrary.simpleMessage("前往"), "geoData": MessageLookupByLibrary.simpleMessage("地理数据"),
"goDownload": MessageLookupByLibrary.simpleMessage("前往下载"), "geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"),
"hostsDesc": MessageLookupByLibrary.simpleMessage("追加Hosts"), "geodataLoaderDesc": MessageLookupByLibrary.simpleMessage("开启将使用Geo低内存加载器"),
"hotkeyConflict": MessageLookupByLibrary.simpleMessage("快捷键冲突"), "geoipCode": MessageLookupByLibrary.simpleMessage("Geoip代码"),
"hotkeyManagement": MessageLookupByLibrary.simpleMessage("快捷键管理"), "global": MessageLookupByLibrary.simpleMessage("全局"),
"hotkeyManagementDesc": "go": MessageLookupByLibrary.simpleMessage("前往"),
MessageLookupByLibrary.simpleMessage("使用键盘控制应用程序"), "goDownload": MessageLookupByLibrary.simpleMessage("前往下载"),
"hours": MessageLookupByLibrary.simpleMessage("小时"), "hasCacheChange": MessageLookupByLibrary.simpleMessage("是否缓存修改"),
"icon": MessageLookupByLibrary.simpleMessage("图片"), "hostsDesc": MessageLookupByLibrary.simpleMessage("追加Hosts"),
"iconConfiguration": MessageLookupByLibrary.simpleMessage("图片配置"), "hotkeyConflict": MessageLookupByLibrary.simpleMessage("快捷键冲突"),
"iconStyle": MessageLookupByLibrary.simpleMessage("图标样式"), "hotkeyManagement": MessageLookupByLibrary.simpleMessage("快捷键管理"),
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"), "hotkeyManagementDesc": MessageLookupByLibrary.simpleMessage("使用键盘控制应用程序"),
"infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"), "hours": MessageLookupByLibrary.simpleMessage("小时"),
"init": MessageLookupByLibrary.simpleMessage("初始化"), "icon": MessageLookupByLibrary.simpleMessage("图片"),
"inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("请输入正确的快捷键"), "iconConfiguration": MessageLookupByLibrary.simpleMessage("图片配置"),
"intelligentSelected": MessageLookupByLibrary.simpleMessage("智能选择"), "iconStyle": MessageLookupByLibrary.simpleMessage("图标样式"),
"intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"), "importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
"ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"), "infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"), "init": MessageLookupByLibrary.simpleMessage("初始化"),
"ipv6InboundDesc": MessageLookupByLibrary.simpleMessage("允许IPv6入站"), "inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("请输入正确的快捷键"),
"just": MessageLookupByLibrary.simpleMessage("刚刚"), "intelligentSelected": MessageLookupByLibrary.simpleMessage("智能选择"),
"keepAliveIntervalDesc": "intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"),
MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"), "ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"),
"key": MessageLookupByLibrary.simpleMessage(""), "ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"),
"language": MessageLookupByLibrary.simpleMessage("语言"), "ipv6InboundDesc": MessageLookupByLibrary.simpleMessage("允许IPv6入站"),
"layout": MessageLookupByLibrary.simpleMessage("布局"), "just": MessageLookupByLibrary.simpleMessage("刚刚"),
"light": MessageLookupByLibrary.simpleMessage("浅色"), "keepAliveIntervalDesc": MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"),
"list": MessageLookupByLibrary.simpleMessage("列表"), "key": MessageLookupByLibrary.simpleMessage(""),
"local": MessageLookupByLibrary.simpleMessage("本地"), "language": MessageLookupByLibrary.simpleMessage("语言"),
"localBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到本地"), "layout": MessageLookupByLibrary.simpleMessage("布局"),
"localRecoveryDesc": MessageLookupByLibrary.simpleMessage("通过文件恢复数据"), "light": MessageLookupByLibrary.simpleMessage("浅色"),
"logLevel": MessageLookupByLibrary.simpleMessage("日志等级"), "list": MessageLookupByLibrary.simpleMessage("列表"),
"logcat": MessageLookupByLibrary.simpleMessage("日志捕获"), "local": MessageLookupByLibrary.simpleMessage("本地"),
"logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"), "localBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到本地"),
"logs": MessageLookupByLibrary.simpleMessage("日志"), "localRecoveryDesc": MessageLookupByLibrary.simpleMessage("通过文件恢复数据"),
"logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"), "logLevel": MessageLookupByLibrary.simpleMessage("日志等级"),
"loopback": MessageLookupByLibrary.simpleMessage("回环解锁工具"), "logcat": MessageLookupByLibrary.simpleMessage("日志捕获"),
"loopbackDesc": MessageLookupByLibrary.simpleMessage("用于UWP回环解锁"), "logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"),
"loose": MessageLookupByLibrary.simpleMessage("宽松"), "logs": MessageLookupByLibrary.simpleMessage("日志"),
"memoryInfo": MessageLookupByLibrary.simpleMessage("内存信息"), "logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"),
"min": MessageLookupByLibrary.simpleMessage("最小"), "loopback": MessageLookupByLibrary.simpleMessage("回环解锁工具"),
"minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"), "loopbackDesc": MessageLookupByLibrary.simpleMessage("用于UWP回环解锁"),
"minimizeOnExitDesc": "loose": MessageLookupByLibrary.simpleMessage("宽松"),
MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"), "memoryInfo": MessageLookupByLibrary.simpleMessage("内存信息"),
"minutes": MessageLookupByLibrary.simpleMessage("分钟"), "min": MessageLookupByLibrary.simpleMessage("最小"),
"mode": MessageLookupByLibrary.simpleMessage("模式"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"),
"months": MessageLookupByLibrary.simpleMessage(""), "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"),
"more": MessageLookupByLibrary.simpleMessage("更多"), "minutes": MessageLookupByLibrary.simpleMessage("分钟"),
"name": MessageLookupByLibrary.simpleMessage("名称"), "mode": MessageLookupByLibrary.simpleMessage("模式"),
"nameSort": MessageLookupByLibrary.simpleMessage("按名称排序"), "months": MessageLookupByLibrary.simpleMessage(""),
"nameserver": MessageLookupByLibrary.simpleMessage("域名服务器"), "more": MessageLookupByLibrary.simpleMessage("更多"),
"nameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析域"), "name": MessageLookupByLibrary.simpleMessage(""),
"nameserverPolicy": MessageLookupByLibrary.simpleMessage("域名服务器策略"), "nameSort": MessageLookupByLibrary.simpleMessage("按名称排序"),
"nameserverPolicyDesc": "nameserver": MessageLookupByLibrary.simpleMessage("域名服务器"),
MessageLookupByLibrary.simpleMessage("指定对应域名服务器策略"), "nameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析域名"),
"network": MessageLookupByLibrary.simpleMessage("网络"), "nameserverPolicy": MessageLookupByLibrary.simpleMessage("域名服务器策略"),
"networkDesc": MessageLookupByLibrary.simpleMessage("修改网络相关设置"), "nameserverPolicyDesc": MessageLookupByLibrary.simpleMessage("指定对应域名服务器策略"),
"networkDetection": MessageLookupByLibrary.simpleMessage("网络检测"), "network": MessageLookupByLibrary.simpleMessage("网络"),
"networkSpeed": MessageLookupByLibrary.simpleMessage("网络速度"), "networkDesc": MessageLookupByLibrary.simpleMessage("修改网络相关设置"),
"noData": MessageLookupByLibrary.simpleMessage("暂无数据"), "networkDetection": MessageLookupByLibrary.simpleMessage("网络检测"),
"noHotKey": MessageLookupByLibrary.simpleMessage("暂无快捷键"), "networkSpeed": MessageLookupByLibrary.simpleMessage("网络速度"),
"noIcon": MessageLookupByLibrary.simpleMessage("无图标"), "noData": MessageLookupByLibrary.simpleMessage("暂无数据"),
"noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"), "noHotKey": MessageLookupByLibrary.simpleMessage("暂无快捷键"),
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暂无更多信息"), "noIcon": MessageLookupByLibrary.simpleMessage("无图标"),
"noNetwork": MessageLookupByLibrary.simpleMessage("无网络"), "noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"),
"noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"), "noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暂无更多信息"),
"noProxyDesc": "noNetwork": MessageLookupByLibrary.simpleMessage("无网络"),
MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"), "noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"),
"notEmpty": MessageLookupByLibrary.simpleMessage("不能为空"), "noProxyDesc": MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"),
"notSelectedTip": MessageLookupByLibrary.simpleMessage("当前代理组无法选中"), "notEmpty": MessageLookupByLibrary.simpleMessage("不能为空"),
"nullConnectionsDesc": MessageLookupByLibrary.simpleMessage("暂无连接"), "notSelectedTip": MessageLookupByLibrary.simpleMessage("当前代理组无法选中"),
"nullCoreInfoDesc": MessageLookupByLibrary.simpleMessage("无法获取内核信息"), "nullConnectionsDesc": MessageLookupByLibrary.simpleMessage("暂无连接"),
"nullLogsDesc": MessageLookupByLibrary.simpleMessage("暂无日志"), "nullCoreInfoDesc": MessageLookupByLibrary.simpleMessage("无法获取内核信息"),
"nullProfileDesc": "nullLogsDesc": MessageLookupByLibrary.simpleMessage("暂无日志"),
MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"), "nullProfileDesc": MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"),
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"), "nullProxies": MessageLookupByLibrary.simpleMessage("暂无代理"),
"oneColumn": MessageLookupByLibrary.simpleMessage("一列"), "nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"),
"onlyIcon": MessageLookupByLibrary.simpleMessage("仅图标"), "oneColumn": MessageLookupByLibrary.simpleMessage("一列"),
"onlyOtherApps": MessageLookupByLibrary.simpleMessage("第三方应用"), "onlyIcon": MessageLookupByLibrary.simpleMessage("图标"),
"onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("统计代理"), "onlyOtherApps": MessageLookupByLibrary.simpleMessage("第三方应用"),
"onlyStatisticsProxyDesc": "onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("仅统计代理"),
MessageLookupByLibrary.simpleMessage("开启后,将只统计代理流量"), "onlyStatisticsProxyDesc": MessageLookupByLibrary.simpleMessage(
"options": MessageLookupByLibrary.simpleMessage("选项"), "开启后,将只统计代理流量",
"other": MessageLookupByLibrary.simpleMessage("其他"), ),
"otherContributors": MessageLookupByLibrary.simpleMessage("其他贡献者"), "options": MessageLookupByLibrary.simpleMessage("选项"),
"outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"), "other": MessageLookupByLibrary.simpleMessage("其他"),
"override": MessageLookupByLibrary.simpleMessage("覆写"), "otherContributors": MessageLookupByLibrary.simpleMessage("其他贡献者"),
"overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"), "outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"),
"overrideDns": MessageLookupByLibrary.simpleMessage("覆写DNS"), "override": MessageLookupByLibrary.simpleMessage("覆写"),
"overrideDnsDesc": "overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"),
MessageLookupByLibrary.simpleMessage("开启后将覆盖配置中的DNS选项"), "overrideDns": MessageLookupByLibrary.simpleMessage("覆写DNS"),
"password": MessageLookupByLibrary.simpleMessage("密码"), "overrideDnsDesc": MessageLookupByLibrary.simpleMessage("开启后将覆盖配置中的DNS选项"),
"passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"), "password": MessageLookupByLibrary.simpleMessage("密码"),
"paste": MessageLookupByLibrary.simpleMessage("粘贴"), "passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"),
"pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage("请绑定WebDAV"), "paste": MessageLookupByLibrary.simpleMessage("粘贴"),
"pleaseInputAdminPassword": "pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage("请绑定WebDAV"),
MessageLookupByLibrary.simpleMessage("请输入管理员密码"), "pleaseInputAdminPassword": MessageLookupByLibrary.simpleMessage(
"pleaseUploadFile": MessageLookupByLibrary.simpleMessage("请上传文件"), "请输入管理员密码",
"pleaseUploadValidQrcode": ),
MessageLookupByLibrary.simpleMessage("请上传有效的二维码"), "pleaseUploadFile": MessageLookupByLibrary.simpleMessage("请上传文件"),
"port": MessageLookupByLibrary.simpleMessage("端口"), "pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage(
"preferH3Desc": MessageLookupByLibrary.simpleMessage("优先使用DOH的http/3"), "请上传有效的二维码",
"pressKeyboard": MessageLookupByLibrary.simpleMessage("请按下按键"), ),
"preview": MessageLookupByLibrary.simpleMessage("预览"), "port": MessageLookupByLibrary.simpleMessage("端口"),
"profile": MessageLookupByLibrary.simpleMessage("配置"), "preferH3Desc": MessageLookupByLibrary.simpleMessage("优先使用DOH的http/3"),
"profileAutoUpdateIntervalInvalidValidationDesc": "pressKeyboard": MessageLookupByLibrary.simpleMessage("请按下按键"),
MessageLookupByLibrary.simpleMessage("请输入有效间隔时间格式"), "preview": MessageLookupByLibrary.simpleMessage("预览"),
"profileAutoUpdateIntervalNullValidationDesc": "profile": MessageLookupByLibrary.simpleMessage("配置"),
MessageLookupByLibrary.simpleMessage("请输入自动更新间隔时间"), "profileAutoUpdateIntervalInvalidValidationDesc":
"profileNameNullValidationDesc": MessageLookupByLibrary.simpleMessage("请输入有效间隔时间格式"),
MessageLookupByLibrary.simpleMessage("请输入配置名称"), "profileAutoUpdateIntervalNullValidationDesc":
"profileParseErrorDesc": MessageLookupByLibrary.simpleMessage("请输入自动更新间隔时间"),
MessageLookupByLibrary.simpleMessage("配置文件解析错误"), "profileHasUpdate": MessageLookupByLibrary.simpleMessage(
"profileUrlInvalidValidationDesc": "配置文件已经修改,是否关闭自动更新 ",
MessageLookupByLibrary.simpleMessage("请输入有效配置URL"), ),
"profileUrlNullValidationDesc": "profileNameNullValidationDesc": MessageLookupByLibrary.simpleMessage(
MessageLookupByLibrary.simpleMessage("请输入配置URL"), "请输入配置名称",
"profiles": MessageLookupByLibrary.simpleMessage("配置"), ),
"profilesSort": MessageLookupByLibrary.simpleMessage("配置排序"), "profileParseErrorDesc": MessageLookupByLibrary.simpleMessage("配置文件解析错误"),
"project": MessageLookupByLibrary.simpleMessage("项目"), "profileUrlInvalidValidationDesc": MessageLookupByLibrary.simpleMessage(
"providers": MessageLookupByLibrary.simpleMessage("提供者"), "请输入有效配置URL",
"proxies": MessageLookupByLibrary.simpleMessage("代理"), ),
"proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"), "profileUrlNullValidationDesc": MessageLookupByLibrary.simpleMessage(
"proxyGroup": MessageLookupByLibrary.simpleMessage("代理组"), "请输入配置URL",
"proxyNameserver": MessageLookupByLibrary.simpleMessage("代理域名服务器"), ),
"proxyNameserverDesc": "profiles": MessageLookupByLibrary.simpleMessage("配置"),
MessageLookupByLibrary.simpleMessage("用于解析代理节点的域名"), "profilesSort": MessageLookupByLibrary.simpleMessage("配置排序"),
"proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"), "project": MessageLookupByLibrary.simpleMessage("项目"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"), "providers": MessageLookupByLibrary.simpleMessage("提供者"),
"proxyProviders": MessageLookupByLibrary.simpleMessage("代理提供者"), "proxies": MessageLookupByLibrary.simpleMessage("代理"),
"prueBlackMode": MessageLookupByLibrary.simpleMessage("纯黑模式"), "proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"),
"qrcode": MessageLookupByLibrary.simpleMessage("二维码"), "proxyGroup": MessageLookupByLibrary.simpleMessage("代理组"),
"qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"), "proxyNameserver": MessageLookupByLibrary.simpleMessage("代理域名服务器"),
"recovery": MessageLookupByLibrary.simpleMessage("恢复"), "proxyNameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析代理节点的域名"),
"recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"), "proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"),
"recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"), "proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"),
"recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"), "proxyProviders": MessageLookupByLibrary.simpleMessage("代理提供者"),
"regExp": MessageLookupByLibrary.simpleMessage("正则"), "prueBlackMode": MessageLookupByLibrary.simpleMessage("纯黑模式"),
"remote": MessageLookupByLibrary.simpleMessage("远程"), "qrcode": MessageLookupByLibrary.simpleMessage("二维码"),
"remoteBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"), "qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"),
"remoteRecoveryDesc": "recovery": MessageLookupByLibrary.simpleMessage("恢复"),
MessageLookupByLibrary.simpleMessage("通过WebDAV恢复数据"), "recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"),
"remove": MessageLookupByLibrary.simpleMessage("移除"), "recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"),
"requests": MessageLookupByLibrary.simpleMessage("请求"), "recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"),
"requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求记录"), "regExp": MessageLookupByLibrary.simpleMessage("正则"),
"reset": MessageLookupByLibrary.simpleMessage("重置"), "remote": MessageLookupByLibrary.simpleMessage("远程"),
"resetTip": MessageLookupByLibrary.simpleMessage("确定要重置吗?"), "remoteBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"),
"resources": MessageLookupByLibrary.simpleMessage("资源"), "remoteRecoveryDesc": MessageLookupByLibrary.simpleMessage("通过WebDAV恢复数据"),
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"), "remove": MessageLookupByLibrary.simpleMessage("移除"),
"respectRules": MessageLookupByLibrary.simpleMessage("遵守规则"), "requests": MessageLookupByLibrary.simpleMessage("请求"),
"respectRulesDesc": MessageLookupByLibrary.simpleMessage( "requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求记录"),
"DNS连接跟随rules,需配置proxy-server-nameserver"), "reset": MessageLookupByLibrary.simpleMessage("重置"),
"routeAddress": MessageLookupByLibrary.simpleMessage("路由地址"), "resetTip": MessageLookupByLibrary.simpleMessage("确定要重置吗?"),
"routeAddressDesc": MessageLookupByLibrary.simpleMessage("配置监听路由地址"), "resources": MessageLookupByLibrary.simpleMessage("资源"),
"routeMode": MessageLookupByLibrary.simpleMessage("路由模式"), "resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"),
"routeMode_bypassPrivate": "respectRules": MessageLookupByLibrary.simpleMessage("遵守规则"),
MessageLookupByLibrary.simpleMessage("绕过私有路由地址"), "respectRulesDesc": MessageLookupByLibrary.simpleMessage(
"routeMode_config": MessageLookupByLibrary.simpleMessage("使用配置"), "DNS连接跟随rules,需配置proxy-server-nameserver",
"rule": MessageLookupByLibrary.simpleMessage("规则"), ),
"ruleProviders": MessageLookupByLibrary.simpleMessage("规则提供者"), "routeAddress": MessageLookupByLibrary.simpleMessage("路由地址"),
"save": MessageLookupByLibrary.simpleMessage("保存"), "routeAddressDesc": MessageLookupByLibrary.simpleMessage("配置监听路由地址"),
"search": MessageLookupByLibrary.simpleMessage("搜索"), "routeMode": MessageLookupByLibrary.simpleMessage("路由模式"),
"seconds": MessageLookupByLibrary.simpleMessage(""), "routeMode_bypassPrivate": MessageLookupByLibrary.simpleMessage("绕过私有路由地址"),
"selectAll": MessageLookupByLibrary.simpleMessage("全选"), "routeMode_config": MessageLookupByLibrary.simpleMessage("使用配置"),
"selected": MessageLookupByLibrary.simpleMessage("已选择"), "rule": MessageLookupByLibrary.simpleMessage("规则"),
"settings": MessageLookupByLibrary.simpleMessage("设置"), "ruleProviders": MessageLookupByLibrary.simpleMessage("规则提供者"),
"show": MessageLookupByLibrary.simpleMessage("显示"), "save": MessageLookupByLibrary.simpleMessage("保存"),
"shrink": MessageLookupByLibrary.simpleMessage("紧凑"), "search": MessageLookupByLibrary.simpleMessage("搜索"),
"silentLaunch": MessageLookupByLibrary.simpleMessage("静默启动"), "seconds": MessageLookupByLibrary.simpleMessage(""),
"silentLaunchDesc": MessageLookupByLibrary.simpleMessage("后台启动"), "selectAll": MessageLookupByLibrary.simpleMessage("全选"),
"size": MessageLookupByLibrary.simpleMessage("尺寸"), "selected": MessageLookupByLibrary.simpleMessage("已选择"),
"sort": MessageLookupByLibrary.simpleMessage("排序"), "settings": MessageLookupByLibrary.simpleMessage("设置"),
"source": MessageLookupByLibrary.simpleMessage("来源"), "show": MessageLookupByLibrary.simpleMessage("显示"),
"stackMode": MessageLookupByLibrary.simpleMessage("栈模式"), "shrink": MessageLookupByLibrary.simpleMessage("紧凑"),
"standard": MessageLookupByLibrary.simpleMessage("标准"), "silentLaunch": MessageLookupByLibrary.simpleMessage("静默启动"),
"start": MessageLookupByLibrary.simpleMessage("启动"), "silentLaunchDesc": MessageLookupByLibrary.simpleMessage("后台启动"),
"startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."), "size": MessageLookupByLibrary.simpleMessage("尺寸"),
"status": MessageLookupByLibrary.simpleMessage("状态"), "sort": MessageLookupByLibrary.simpleMessage("排序"),
"statusDesc": MessageLookupByLibrary.simpleMessage("关闭后将使用系统DNS"), "source": MessageLookupByLibrary.simpleMessage("来源"),
"stop": MessageLookupByLibrary.simpleMessage("暂停"), "stackMode": MessageLookupByLibrary.simpleMessage("栈模式"),
"stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."), "standard": MessageLookupByLibrary.simpleMessage("标准"),
"style": MessageLookupByLibrary.simpleMessage("风格"), "start": MessageLookupByLibrary.simpleMessage("启动"),
"submit": MessageLookupByLibrary.simpleMessage("提交"), "startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."),
"sync": MessageLookupByLibrary.simpleMessage("同步"), "status": MessageLookupByLibrary.simpleMessage("状态"),
"system": MessageLookupByLibrary.simpleMessage("系统"), "statusDesc": MessageLookupByLibrary.simpleMessage("关闭后将使用系统DNS"),
"systemFont": MessageLookupByLibrary.simpleMessage("系统字体"), "stop": MessageLookupByLibrary.simpleMessage("暂停"),
"systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"), "stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage("设置系统代理"), "style": MessageLookupByLibrary.simpleMessage("风格"),
"tab": MessageLookupByLibrary.simpleMessage("标签页"), "submit": MessageLookupByLibrary.simpleMessage("提交"),
"tabAnimation": MessageLookupByLibrary.simpleMessage("选项卡动画"), "sync": MessageLookupByLibrary.simpleMessage("同步"),
"tabAnimationDesc": "system": MessageLookupByLibrary.simpleMessage("系统"),
MessageLookupByLibrary.simpleMessage("开启后,主页选项卡将添加切换动画"), "systemFont": MessageLookupByLibrary.simpleMessage("系统字体"),
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"), "systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许TCP并发"), "systemProxyDesc": MessageLookupByLibrary.simpleMessage("设置系统代理"),
"testUrl": MessageLookupByLibrary.simpleMessage("测速链接"), "tab": MessageLookupByLibrary.simpleMessage("标签页"),
"theme": MessageLookupByLibrary.simpleMessage("主题"), "tabAnimation": MessageLookupByLibrary.simpleMessage("选项卡动画"),
"themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"), "tabAnimationDesc": MessageLookupByLibrary.simpleMessage(
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"), "开启后,主页选项卡将添加切换动画",
"themeMode": MessageLookupByLibrary.simpleMessage("主题模式"), ),
"threeColumns": MessageLookupByLibrary.simpleMessage("三列"), "tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"),
"tight": MessageLookupByLibrary.simpleMessage("紧凑"), "tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许TCP并发"),
"time": MessageLookupByLibrary.simpleMessage("时间"), "testUrl": MessageLookupByLibrary.simpleMessage("测速链接"),
"tip": MessageLookupByLibrary.simpleMessage("提示"), "theme": MessageLookupByLibrary.simpleMessage("主题"),
"toggle": MessageLookupByLibrary.simpleMessage("切换"), "themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"),
"tools": MessageLookupByLibrary.simpleMessage("工具"), "themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"), "themeMode": MessageLookupByLibrary.simpleMessage("主题模式"),
"tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"), "threeColumns": MessageLookupByLibrary.simpleMessage("三列"),
"tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"), "tight": MessageLookupByLibrary.simpleMessage("紧凑"),
"twoColumns": MessageLookupByLibrary.simpleMessage("两列"), "time": MessageLookupByLibrary.simpleMessage("时间"),
"unableToUpdateCurrentProfileDesc": "tip": MessageLookupByLibrary.simpleMessage("提示"),
MessageLookupByLibrary.simpleMessage("无法更新当前配置文件"), "toggle": MessageLookupByLibrary.simpleMessage("切换"),
"unifiedDelay": MessageLookupByLibrary.simpleMessage("统一延迟"), "tools": MessageLookupByLibrary.simpleMessage("工具"),
"unifiedDelayDesc": MessageLookupByLibrary.simpleMessage("去除握手等额外延迟"), "trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"),
"unknown": MessageLookupByLibrary.simpleMessage("未知"), "tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),
"update": MessageLookupByLibrary.simpleMessage("更新"), "tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"),
"upload": MessageLookupByLibrary.simpleMessage("上传"), "twoColumns": MessageLookupByLibrary.simpleMessage("两列"),
"url": MessageLookupByLibrary.simpleMessage("URL"), "unableToUpdateCurrentProfileDesc": MessageLookupByLibrary.simpleMessage(
"urlDesc": MessageLookupByLibrary.simpleMessage("通过URL获取配置文件"), "无法更新当前配置文件",
"useHosts": MessageLookupByLibrary.simpleMessage("使用Hosts"), ),
"useSystemHosts": MessageLookupByLibrary.simpleMessage("使用系统Hosts"), "unifiedDelay": MessageLookupByLibrary.simpleMessage("统一延迟"),
"value": MessageLookupByLibrary.simpleMessage(""), "unifiedDelayDesc": MessageLookupByLibrary.simpleMessage("去除握手等额外延迟"),
"view": MessageLookupByLibrary.simpleMessage("查看"), "unknown": MessageLookupByLibrary.simpleMessage("未知"),
"vpnDesc": MessageLookupByLibrary.simpleMessage("修改VPN相关设置"), "update": MessageLookupByLibrary.simpleMessage("更新"),
"vpnEnableDesc": "upload": MessageLookupByLibrary.simpleMessage("上传"),
MessageLookupByLibrary.simpleMessage("通过VpnService自动路由系统所有流量"), "url": MessageLookupByLibrary.simpleMessage("URL"),
"vpnSystemProxyDesc": "urlDesc": MessageLookupByLibrary.simpleMessage("通过URL获取配置文件"),
MessageLookupByLibrary.simpleMessage("为VpnService附加HTTP代理"), "useHosts": MessageLookupByLibrary.simpleMessage("使用Hosts"),
"vpnTip": MessageLookupByLibrary.simpleMessage("重启VPN后改变生效"), "useSystemHosts": MessageLookupByLibrary.simpleMessage("使用系统Hosts"),
"webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"), "value": MessageLookupByLibrary.simpleMessage(""),
"whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"), "view": MessageLookupByLibrary.simpleMessage("查看"),
"years": MessageLookupByLibrary.simpleMessage(""), "vpnDesc": MessageLookupByLibrary.simpleMessage("修改VPN相关设置"),
"zh_CN": MessageLookupByLibrary.simpleMessage("中文简体") "vpnEnableDesc": MessageLookupByLibrary.simpleMessage(
}; "通过VpnService自动路由系统所有流量",
),
"vpnSystemProxyDesc": MessageLookupByLibrary.simpleMessage(
"为VpnService附加HTTP代理",
),
"vpnTip": MessageLookupByLibrary.simpleMessage("重启VPN后改变生效"),
"webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"),
"whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"),
"years": MessageLookupByLibrary.simpleMessage(""),
"zh_CN": MessageLookupByLibrary.simpleMessage("中文简体"),
};
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'dart:isolate';
import 'dart:ui';
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/plugins/tile.dart'; import 'package:fl_clash/plugins/tile.dart';
import 'package:fl_clash/plugins/vpn.dart'; import 'package:fl_clash/plugins/vpn.dart';
@@ -10,21 +14,24 @@ import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'application.dart'; import 'application.dart';
import 'clash/core.dart';
import 'clash/lib.dart';
import 'common/common.dart'; import 'common/common.dart';
import 'l10n/l10n.dart'; import 'l10n/l10n.dart';
import 'models/models.dart'; import 'models/models.dart';
Future<void> main() async { Future<void> main() async {
globalState.isService = false;
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
clashLib?.initMessage(); await clashCore.preload();
globalState.packageInfo = await PackageInfo.fromPlatform(); globalState.packageInfo = await PackageInfo.fromPlatform();
final version = await system.version; final version = await system.version;
final config = await preferences.getConfig() ?? Config(); final config = await preferences.getConfig() ?? Config();
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
await AppLocalizations.load( await AppLocalizations.load(
other.getLocaleForString(config.appSetting.locale) ?? other.getLocaleForString(config.appSetting.locale) ??
WidgetsBinding.instance.platformDispatcher.locale, WidgetsBinding.instance.platformDispatcher.locale,
); );
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
await android?.init(); await android?.init();
await window?.init(config.windowProps, version); await window?.init(config.windowProps, version);
final appState = AppState( final appState = AppState(
@@ -54,126 +61,125 @@ Future<void> main() async {
} }
@pragma('vm:entry-point') @pragma('vm:entry-point')
Future<void> vpnService() async { Future<void> _service(List<String> flags) async {
globalState.isService = true;
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
globalState.isVpnService = true; final quickStart = flags.contains("quick");
globalState.packageInfo = await PackageInfo.fromPlatform(); final clashLibHandler = ClashLibHandler();
final version = await system.version;
final config = await preferences.getConfig() ?? Config(); final config = await preferences.getConfig() ?? Config();
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
await AppLocalizations.load( await AppLocalizations.load(
other.getLocaleForString(config.appSetting.locale) ?? other.getLocaleForString(config.appSetting.locale) ??
WidgetsBinding.instance.platformDispatcher.locale, WidgetsBinding.instance.platformDispatcher.locale,
); );
final appState = AppState( tile?.addListener(
mode: clashConfig.mode, _TileListenerWithService(
selectedMap: config.currentSelectedMap, onStop: () async {
version: version, await app?.tip(appLocalizations.stopVpn);
); clashLibHandler.stopListener();
clashLibHandler.stopTun();
await globalState.init( await vpn?.stop();
appState: appState, exit(0);
config: config,
clashConfig: clashConfig,
);
await app?.tip(appLocalizations.startVpn);
globalState
.updateClashConfig(
appState: appState,
clashConfig: clashConfig,
config: config,
isPatch: false,
)
.then(
(_) async {
await globalState.handleStart();
tile?.addListener(
TileListenerWithVpn(
onStop: () async {
await app?.tip(appLocalizations.stopVpn);
await globalState.handleStop();
clashCore.shutdown();
exit(0);
},
),
);
globalState.updateTraffic(config: config);
globalState.updateFunctionLists = [
() {
globalState.updateTraffic(config: config);
}
];
},
);
vpn?.setServiceMessageHandler(
ServiceMessageHandler(
onProtect: (Fd fd) async {
await vpn?.setProtect(fd.value);
clashLib?.setFdMap(fd.id);
},
onProcess: (ProcessData process) async {
final packageName = await vpn?.resolverProcess(process);
clashLib?.setProcessMap(
ProcessMapItem(
id: process.id,
value: packageName ?? "",
),
);
},
onLoaded: (String groupName) {
final currentSelectedMap = config.currentSelectedMap;
final proxyName = currentSelectedMap[groupName];
if (proxyName == null) return;
globalState.changeProxy(
config: config,
groupName: groupName,
proxyName: proxyName,
);
}, },
), ),
); );
if (!quickStart) {
_handleMainIpc(clashLibHandler);
} else {
await ClashCore.initGeo();
globalState.packageInfo = await PackageInfo.fromPlatform();
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
final homeDirPath = await appPath.homeDirPath;
await app?.tip(appLocalizations.startVpn);
clashLibHandler
.quickStart(
homeDirPath,
globalState.getUpdateConfigParams(config, clashConfig, false),
globalState.getCoreState(config, clashConfig),
)
.then(
(res) async {
if (res.isNotEmpty) {
await vpn?.stop();
exit(0);
}
await vpn?.start(
clashLibHandler.getAndroidVpnOptions(),
);
clashLibHandler.startListener();
},
);
}
vpn?.handleGetStartForegroundParams = () {
final traffic = clashLibHandler.getTraffic();
return json.encode({
"title": clashLibHandler.getCurrentProfileName(),
"content": "$traffic"
});
};
vpn?.addListener(
_VpnListenerWithService(
onStarted: (int fd) {
clashLibHandler.startTun(fd);
},
onDnsChanged: (String dns) {
clashLibHandler.updateDns(dns);
},
),
);
final invokeReceiverPort = ReceivePort();
clashLibHandler.attachInvokePort(
invokeReceiverPort.sendPort.nativePort,
);
invokeReceiverPort.listen(
(message) async {
final invokeMessage = InvokeMessage.fromJson(json.decode(message));
switch (invokeMessage.type) {
case InvokeMessageType.protect:
final fd = Fd.fromJson(invokeMessage.data);
await vpn?.setProtect(fd.value);
clashLibHandler.setFdMap(fd.id);
case InvokeMessageType.process:
final process = ProcessData.fromJson(invokeMessage.data);
final processName = await vpn?.resolverProcess(process) ?? "";
clashLibHandler.setProcessMap(
ProcessMapItem(
id: process.id,
value: processName,
),
);
}
},
);
}
_handleMainIpc(ClashLibHandler clashLibHandler) {
final sendPort = IsolateNameServer.lookupPortByName(mainIsolate);
if (sendPort == null) {
return;
}
final serviceReceiverPort = ReceivePort();
serviceReceiverPort.listen((message) async {
final res = await clashLibHandler.invokeAction(message);
sendPort.send(res);
});
sendPort.send(serviceReceiverPort.sendPort);
final messageReceiverPort = ReceivePort();
clashLibHandler.attachMessagePort(
messageReceiverPort.sendPort.nativePort,
);
messageReceiverPort.listen((message) {
sendPort.send(message);
});
} }
@immutable @immutable
class ServiceMessageHandler with ServiceMessageListener { class _TileListenerWithService with TileListener {
final Function(Fd fd) _onProtect;
final Function(ProcessData process) _onProcess;
final Function(String providerName) _onLoaded;
const ServiceMessageHandler({
required Function(Fd fd) onProtect,
required Function(ProcessData process) onProcess,
required Function(String providerName) onLoaded,
}) : _onProtect = onProtect,
_onProcess = onProcess,
_onLoaded = onLoaded;
@override
onProtect(Fd fd) {
_onProtect(fd);
}
@override
onProcess(ProcessData process) {
_onProcess(process);
}
@override
onLoaded(String providerName) {
_onLoaded(providerName);
}
}
@immutable
class TileListenerWithVpn with TileListener {
final Function() _onStop; final Function() _onStop;
const TileListenerWithVpn({ const _TileListenerWithService({
required Function() onStop, required Function() onStop,
}) : _onStop = onStop; }) : _onStop = onStop;
@@ -182,3 +188,27 @@ class TileListenerWithVpn with TileListener {
_onStop(); _onStop();
} }
} }
@immutable
class _VpnListenerWithService with VpnListener {
final Function(int fd) _onStarted;
final Function(String dns) _onDnsChanged;
const _VpnListenerWithService({
required Function(int fd) onStarted,
required Function(String dns) onDnsChanged,
}) : _onStarted = onStarted,
_onDnsChanged = onDnsChanged;
@override
void onStarted(int fd) {
super.onStarted(fd);
_onStarted(fd);
}
@override
void onDnsChanged(String dns) {
super.onDnsChanged(dns);
_onDnsChanged(dns);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/clash/clash.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/state.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:provider/provider.dart';
@@ -26,17 +27,9 @@ class _AndroidContainerState extends State<AndroidManager> {
Widget _updateCoreState(Widget child) { Widget _updateCoreState(Widget child) {
return Selector2<Config, ClashConfig, CoreState>( return Selector2<Config, ClashConfig, CoreState>(
selector: (_, config, clashConfig) => CoreState( selector: (_, config, clashConfig) => globalState.getCoreState(
enable: config.vpnProps.enable, config,
accessControl: config.isAccessControl ? config.accessControl : null, clashConfig,
ipv6: config.vpnProps.ipv6,
allowBypass: config.vpnProps.allowBypass,
bypassDomain: config.networkProps.bypassDomain,
systemProxy: config.vpnProps.systemProxy,
onlyProxy: config.appSetting.onlyProxy,
currentProfileName:
config.currentProfile?.label ?? config.currentProfileId ?? "",
routeAddress: clashConfig.routeAddress,
), ),
builder: (__, state, child) { builder: (__, state, child) {
clashLib?.setState(state); clashLib?.setState(state);

View File

@@ -74,9 +74,12 @@ class _AppStateManagerState extends State<AppStateManager>
@override @override
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async { Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
final isPaused = state == AppLifecycleState.paused; if (state == AppLifecycleState.paused ||
if (isPaused) { state == AppLifecycleState.inactive) {
globalState.appController.savePreferencesDebounce(); globalState.appController.savePreferencesDebounce();
render?.pause();
} else {
render?.resume();
} }
} }
@@ -88,9 +91,14 @@ class _AppStateManagerState extends State<AppStateManager>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _cacheStateChange( return Listener(
_updateNavigationsContainer( onPointerHover: (_) {
widget.child, render?.resume();
},
child: _cacheStateChange(
_updateNavigationsContainer(
widget.child,
),
), ),
); );
} }

View File

@@ -99,14 +99,13 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
@override @override
Future<void> onDelay(Delay delay) async { Future<void> onDelay(Delay delay) async {
super.onDelay(delay);
final appController = globalState.appController; final appController = globalState.appController;
appController.setDelay(delay); appController.setDelay(delay);
super.onDelay(delay);
debouncer.call( debouncer.call(
DebounceTag.updateDelay, DebounceTag.updateDelay,
() async { () async {
await appController.updateGroupsDebounce(); await appController.updateGroupsDebounce();
// await appController.addCheckIpNumDebounce();
}, },
duration: const Duration(milliseconds: 5000), duration: const Duration(milliseconds: 5000),
); );
@@ -121,12 +120,6 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
super.onLog(log); super.onLog(log);
} }
@override
void onStarted(String runTime) {
super.onStarted(runTime);
globalState.appController.applyProfileDebounce();
}
@override @override
void onRequest(Connection connection) async { void onRequest(Connection connection) async {
globalState.appController.appState.addRequest(connection); globalState.appController.appState.addRequest(connection);

View File

@@ -19,45 +19,26 @@ class MessageManager extends StatefulWidget {
class MessageManagerState extends State<MessageManager> class MessageManagerState extends State<MessageManager>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
final _floatMessageKey = GlobalKey();
List<CommonMessage> bufferMessages = [];
final _messagesNotifier = ValueNotifier<List<CommonMessage>>([]); final _messagesNotifier = ValueNotifier<List<CommonMessage>>([]);
final _floatMessageNotifier = ValueNotifier<CommonMessage?>(null);
double maxWidth = 0; double maxWidth = 0;
Offset offset = Offset.zero;
late AnimationController _animationController; late AnimationController _animationController;
Completer? _animationCompleter;
late Animation<Offset> _floatOffsetAnimation;
late Animation<Offset> _commonOffsetAnimation;
final animationDuration = commonDuration * 2; final animationDuration = commonDuration * 2;
_initTransformState() {
_floatMessageNotifier.value = null;
_floatOffsetAnimation = Tween(
begin: Offset.zero,
end: Offset.zero,
).animate(_animationController);
_commonOffsetAnimation = _floatOffsetAnimation = Tween(
begin: Offset.zero,
end: Offset.zero,
).animate(_animationController);
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_animationController = AnimationController( _animationController = AnimationController(
vsync: this, vsync: this,
duration: Duration(milliseconds: 200), duration: Duration(milliseconds: 400),
); );
_initTransformState();
} }
@override @override
void dispose() { void dispose() {
_messagesNotifier.dispose(); _messagesNotifier.dispose();
_floatMessageNotifier.dispose();
_animationController.dispose(); _animationController.dispose();
super.dispose(); super.dispose();
} }
@@ -67,126 +48,13 @@ class MessageManagerState extends State<MessageManager>
id: other.uuidV4, id: other.uuidV4,
text: text, text: text,
); );
bufferMessages.add(commonMessage); _messagesNotifier.value = List.from(_messagesNotifier.value)
await _animationCompleter?.future; ..add(
_showMessage(); commonMessage,
}
_showMessage() {
final commonMessage = bufferMessages.removeAt(0);
_floatOffsetAnimation = Tween(
begin: Offset(-maxWidth, 0),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _animationController,
curve: Interval(
0.5,
1,
curve: Curves.easeInOut,
),
),
);
_floatMessageNotifier.value = commonMessage;
WidgetsBinding.instance.addPostFrameCallback((_) async {
final size = _floatMessageKey.currentContext?.size ?? Size.zero;
_commonOffsetAnimation = Tween(
begin: Offset.zero,
end: Offset(0, -size.height - 12),
).animate(
CurvedAnimation(
parent: _animationController,
curve: Interval(
0,
0.7,
curve: Curves.easeInOut,
),
),
); );
_animationCompleter = Completer();
_animationCompleter?.complete(_animationController.forward(from: 0));
await _animationCompleter?.future;
_initTransformState();
_messagesNotifier.value = List.from(_messagesNotifier.value)
..add(commonMessage);
Future.delayed(
commonMessage.duration,
() {
_removeMessage(commonMessage);
},
);
});
} }
Widget _wrapOffset(Widget child) { _handleRemove(CommonMessage commonMessage) async {
return AnimatedBuilder(
animation: _animationController.view,
builder: (context, child) {
return Transform.translate(
offset: _commonOffsetAnimation.value,
child: child!,
);
},
child: child,
);
}
Widget _wrapMessage(CommonMessage message) {
return Material(
elevation: 2,
borderRadius: BorderRadius.circular(8),
color: context.colorScheme.secondaryFixedDim,
clipBehavior: Clip.antiAlias,
child: Padding(
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 15),
child: Text(
message.text,
style: context.textTheme.bodyMedium?.copyWith(
color: context.colorScheme.onSecondaryFixedVariant,
),
maxLines: 5,
overflow: TextOverflow.ellipsis,
),
),
);
}
Widget _floatMessage() {
return ValueListenableBuilder(
valueListenable: _floatMessageNotifier,
builder: (_, message, ___) {
if (message == null) {
return SizedBox();
}
return AnimatedBuilder(
key: _floatMessageKey,
animation: _animationController.view,
builder: (_, child) {
if (!_animationController.isAnimating) {
return Opacity(
opacity: 0,
child: child,
);
}
return Transform.translate(
offset: _floatOffsetAnimation.value,
child: child,
);
},
child: _wrapMessage(
message,
),
);
},
);
}
_removeMessage(CommonMessage commonMessage) async {
final itemWrapState = GlobalObjectKey(commonMessage.id).currentState
as _MessageItemWrapState?;
await itemWrapState?.transform(
Offset(-maxWidth, 0),
);
_messagesNotifier.value = List<CommonMessage>.from(_messagesNotifier.value) _messagesNotifier.value = List<CommonMessage>.from(_messagesNotifier.value)
..remove(commonMessage); ..remove(commonMessage);
} }
@@ -204,6 +72,7 @@ class MessageManagerState extends State<MessageManager>
child: ValueListenableBuilder( child: ValueListenableBuilder(
valueListenable: globalState.safeMessageOffsetNotifier, valueListenable: globalState.safeMessageOffsetNotifier,
builder: (_, offset, child) { builder: (_, offset, child) {
this.offset = offset;
if (offset == Offset.zero) { if (offset == Offset.zero) {
return SizedBox(); return SizedBox();
} }
@@ -234,15 +103,14 @@ class MessageManagerState extends State<MessageManager>
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
for (final message in messages) ...[ for (final message in messages) ...[
if (message != messages.last) if (message != messages.first)
SizedBox( SizedBox(
height: 8, height: 12,
), ),
_MessageItemWrap( _MessageItem(
key: GlobalObjectKey(message.id), key: GlobalObjectKey(message.id),
child: _wrapOffset( message: message,
_wrapMessage(message), onRemove: _handleRemove,
),
), ),
], ],
], ],
@@ -250,7 +118,6 @@ class MessageManagerState extends State<MessageManager>
}, },
), ),
), ),
_floatMessage(),
], ],
), ),
), ),
@@ -263,22 +130,25 @@ class MessageManagerState extends State<MessageManager>
} }
} }
class _MessageItemWrap extends StatefulWidget { class _MessageItem extends StatefulWidget {
final Widget child; final CommonMessage message;
final Function(CommonMessage message) onRemove;
const _MessageItemWrap({ const _MessageItem({
super.key, super.key,
required this.child, required this.message,
required this.onRemove,
}); });
@override @override
State<_MessageItemWrap> createState() => _MessageItemWrapState(); State<_MessageItem> createState() => _MessageItemState();
} }
class _MessageItemWrapState extends State<_MessageItemWrap> class _MessageItemState extends State<_MessageItem>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late AnimationController _controller; late AnimationController _controller;
Offset _nextOffset = Offset.zero; late Animation<Offset> _offsetAnimation;
late Animation<double> _fadeAnimation;
@override @override
void initState() { void initState() {
@@ -287,11 +157,41 @@ class _MessageItemWrapState extends State<_MessageItemWrap>
vsync: this, vsync: this,
duration: commonDuration * 1.5, duration: commonDuration * 1.5,
); );
} _offsetAnimation = Tween<Offset>(
begin: Offset(-1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _controller,
curve: Interval(
0.0,
1,
curve: Curves.easeOut,
),
));
transform(Offset offset) async { _fadeAnimation = Tween<double>(
_nextOffset = offset; begin: 0.0,
await _controller.forward(from: 0); end: 1,
).animate(CurvedAnimation(
parent: _controller,
curve: Interval(
0.0,
0.2,
curve: Curves.easeIn,
),
));
_controller.forward();
Future.delayed(
widget.message.duration,
() async {
await _controller.reverse();
widget.onRemove(
widget.message,
);
},
);
} }
@override @override
@@ -305,26 +205,30 @@ class _MessageItemWrapState extends State<_MessageItemWrap>
return AnimatedBuilder( return AnimatedBuilder(
animation: _controller.view, animation: _controller.view,
builder: (_, child) { builder: (_, child) {
if (_nextOffset == Offset.zero) { return FadeTransition(
return child!; opacity: _fadeAnimation,
} child: SlideTransition(
final offset = Tween( position: _offsetAnimation,
begin: Offset.zero, child: Material(
end: _nextOffset, elevation: _controller.value * 12,
) borderRadius: BorderRadius.circular(8),
.animate( color: context.colorScheme.surfaceContainer,
CurvedAnimation( clipBehavior: Clip.none,
parent: _controller, child: Padding(
curve: Curves.easeOut, padding: EdgeInsets.symmetric(vertical: 16, horizontal: 16),
child: Text(
widget.message.text,
style: context.textTheme.bodyMedium?.copyWith(
color: context.colorScheme.onSurfaceVariant,
),
maxLines: 5,
overflow: TextOverflow.ellipsis,
),
), ),
) ),
.value; ),
return Transform.translate(
offset: offset,
child: child!,
); );
}, },
child: widget.child,
); );
} }
} }

View File

@@ -58,6 +58,12 @@ class _TrayContainerState extends State<TrayManager> with TrayListener {
trayManager.popUpContextMenu(); trayManager.popUpContextMenu();
} }
@override
void onTrayMenuItemClick(MenuItem menuItem) {
render?.active();
super.onTrayMenuItemClick(menuItem);
}
@override @override
onTrayIconMouseDown() { onTrayIconMouseDown() {
window?.show(); window?.show();

View File

@@ -64,6 +64,18 @@ class _WindowContainerState extends State<WindowManager>
super.onWindowClose(); super.onWindowClose();
} }
@override
void onWindowFocus() {
super.onWindowFocus();
render?.resume();
}
@override
void onWindowBlur() {
super.onWindowBlur();
render?.pause();
}
@override @override
Future<void> onShouldTerminate() async { Future<void> onShouldTerminate() async {
await globalState.appController.handleExit(); await globalState.appController.handleExit();
@@ -101,7 +113,6 @@ class _WindowContainerState extends State<WindowManager>
@override @override
Future<void> onTaskbarCreated() async { Future<void> onTaskbarCreated() async {
globalState.appController.updateTray(true); globalState.appController.updateTray(true);
await globalState.appController.restartCore();
super.onTaskbarCreated(); super.onTaskbarCreated();
} }

View File

@@ -6,7 +6,7 @@ import 'common.dart';
import 'core.dart'; import 'core.dart';
import 'profile.dart'; import 'profile.dart';
typedef DelayMap = Map<String, int?>; typedef DelayMap = Map<String, Map<String, int?>>;
class AppState with ChangeNotifier { class AppState with ChangeNotifier {
List<NavigationItem> _navigationItems; List<NavigationItem> _navigationItems;
@@ -20,7 +20,7 @@ class AppState with ChangeNotifier {
SelectedMap _selectedMap; SelectedMap _selectedMap;
List<Group> _groups; List<Group> _groups;
double _viewWidth; double _viewWidth;
List<Connection> _requests; final FixedList<Connection> _requests;
num _checkIpNum; num _checkIpNum;
List<ExternalProvider> _providers; List<ExternalProvider> _providers;
List<Package> _packages; List<Package> _packages;
@@ -31,14 +31,17 @@ class AppState with ChangeNotifier {
required Mode mode, required Mode mode,
required SelectedMap selectedMap, required SelectedMap selectedMap,
required int version, required int version,
}) : _navigationItems = [], })
: _navigationItems = [],
_isInit = false, _isInit = false,
_currentLabel = "dashboard", _currentLabel = "dashboard",
_viewWidth = other.getScreenSize().width, _viewWidth = other
.getScreenSize()
.width,
_selectedMap = selectedMap, _selectedMap = selectedMap,
_sortNum = 0, _sortNum = 0,
_checkIpNum = 0, _checkIpNum = 0,
_requests = [], _requests = FixedList(1000),
_mode = mode, _mode = mode,
_brightness = null, _brightness = null,
_delayMap = {}, _delayMap = {},
@@ -76,7 +79,7 @@ class AppState with ChangeNotifier {
return navigationItems return navigationItems
.where( .where(
(element) => element.modes.contains(navigationItemMode), (element) => element.modes.contains(navigationItemMode),
) )
.toList(); .toList();
} }
@@ -106,7 +109,7 @@ class AppState with ChangeNotifier {
if (index == -1) return proxyName; if (index == -1) return proxyName;
final group = groups[index]; final group = groups[index];
final currentSelectedName = final currentSelectedName =
group.getCurrentSelectedName(selectedMap[proxyName] ?? ''); group.getCurrentSelectedName(selectedMap[proxyName] ?? '');
if (currentSelectedName.isEmpty) return proxyName; if (currentSelectedName.isEmpty) return proxyName;
return getRealProxyName( return getRealProxyName(
currentSelectedName, currentSelectedName,
@@ -122,10 +125,6 @@ class AppState with ChangeNotifier {
return selectedMap[firstGroupName] ?? firstGroup.now; return selectedMap[firstGroupName] ?? firstGroup.now;
} }
int? getDelay(String proxyName) {
return _delayMap[getRealProxyName(proxyName)];
}
VersionInfo? get versionInfo => _versionInfo; VersionInfo? get versionInfo => _versionInfo;
set versionInfo(VersionInfo? value) { set versionInfo(VersionInfo? value) {
@@ -135,19 +134,10 @@ class AppState with ChangeNotifier {
} }
} }
List<Connection> get requests => _requests; List<Connection> get requests => _requests.list;
set requests(List<Connection> value) {
if (_requests != value) {
_requests = value;
notifyListeners();
}
}
addRequest(Connection value) { addRequest(Connection value) {
_requests = List.from(_requests)..add(value); _requests.add(value);
const maxLength = 1000;
_requests = _requests.safeSublist(_requests.length - maxLength);
notifyListeners(); notifyListeners();
} }
@@ -237,15 +227,20 @@ class AppState with ChangeNotifier {
} }
set delayMap(DelayMap value) { set delayMap(DelayMap value) {
if (!stringAndIntQMapEquality.equals(_delayMap, value)) { if (_delayMap != value) {
_delayMap = value; _delayMap = value;
notifyListeners(); notifyListeners();
} }
} }
setDelay(Delay delay) { setDelay(Delay delay) {
if (_delayMap[delay.name] != delay.value) { if (_delayMap[delay.url]?[delay.name] != delay.value) {
_delayMap = Map.from(_delayMap)..[delay.name] = delay.value; final DelayMap newDelayMap = Map.from(_delayMap);
if (newDelayMap[delay.url] == null) {
newDelayMap[delay.url] = {};
}
newDelayMap[delay.url]![delay.name] = delay.value;
_delayMap = newDelayMap;
notifyListeners(); notifyListeners();
} }
} }
@@ -272,13 +267,14 @@ class AppState with ChangeNotifier {
if (provider == null) return; if (provider == null) return;
final index = _providers.indexWhere((item) => item.name == provider.name); final index = _providers.indexWhere((item) => item.name == provider.name);
if (index == -1) return; if (index == -1) return;
_providers = List.from(_providers)..[index] = provider; _providers = List.from(_providers)
..[index] = provider;
notifyListeners(); notifyListeners();
} }
Group? getGroupWithName(String groupName) { Group? getGroupWithName(String groupName) {
final index = final index =
currentGroups.indexWhere((element) => element.name == groupName); currentGroups.indexWhere((element) => element.name == groupName);
return index != -1 ? currentGroups[index] : null; return index != -1 ? currentGroups[index] : null;
} }
@@ -303,13 +299,13 @@ class AppState with ChangeNotifier {
class AppFlowingState with ChangeNotifier { class AppFlowingState with ChangeNotifier {
int? _runTime; int? _runTime;
List<Log> _logs; final FixedList<Log> _logs;
List<Traffic> _traffics; List<Traffic> _traffics;
Traffic _totalTraffic; Traffic _totalTraffic;
String? _localIp; String? _localIp;
AppFlowingState() AppFlowingState()
: _logs = [], : _logs = FixedList(1000),
_traffics = [], _traffics = [],
_totalTraffic = Traffic(); _totalTraffic = Traffic();
@@ -324,19 +320,10 @@ class AppFlowingState with ChangeNotifier {
} }
} }
List<Log> get logs => _logs; List<Log> get logs => _logs.list;
set logs(List<Log> value) {
if (_logs != value) {
_logs = value;
notifyListeners();
}
}
addLog(Log log) { addLog(Log log) {
_logs = List.from(_logs)..add(log); _logs.add(log);
const maxLength = 1000;
_logs = _logs.safeSublist(_logs.length - maxLength);
notifyListeners(); notifyListeners();
} }
@@ -350,7 +337,8 @@ class AppFlowingState with ChangeNotifier {
} }
addTraffic(Traffic traffic) { addTraffic(Traffic traffic) {
_traffics = List.from(_traffics)..add(traffic); _traffics = List.from(_traffics)
..add(traffic);
const maxLength = 30; const maxLength = 30;
_traffics = _traffics.safeSublist(_traffics.length - maxLength); _traffics = _traffics.safeSublist(_traffics.length - maxLength);
notifyListeners(); notifyListeners();

View File

@@ -1,3 +1,5 @@
// ignore_for_file: invalid_annotation_target
import 'dart:math'; import 'dart:math';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
@@ -6,7 +8,6 @@ import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'generated/common.freezed.dart'; part 'generated/common.freezed.dart';
part 'generated/common.g.dart'; part 'generated/common.g.dart';
@freezed @freezed
@@ -29,7 +30,7 @@ class Package with _$Package {
required String packageName, required String packageName,
required String label, required String label,
required bool isSystem, required bool isSystem,
required int firstInstallTime, required int lastUpdateTime,
}) = _Package; }) = _Package;
factory Package.fromJson(Map<String, Object?> json) => factory Package.fromJson(Map<String, Object?> json) =>
@@ -69,6 +70,19 @@ class Connection with _$Connection {
_$ConnectionFromJson(json); _$ConnectionFromJson(json);
} }
extension ConnectionExt on Connection {
String get desc {
var text = "${metadata.network}://";
final ips = [
metadata.host,
metadata.destinationIP,
].where((ip) => ip.isNotEmpty);
text += ips.join("/");
text += ":${metadata.destinationPort}";
return text;
}
}
@JsonSerializable() @JsonSerializable()
class Log { class Log {
@JsonKey(name: "LogLevel") @JsonKey(name: "LogLevel")
@@ -99,42 +113,58 @@ class Log {
} }
@freezed @freezed
class LogsAndKeywords with _$LogsAndKeywords { class LogsState with _$LogsState {
const factory LogsAndKeywords({ const factory LogsState({
@Default([]) List<Log> logs, @Default([]) List<Log> logs,
@Default([]) List<String> keywords, @Default([]) List<String> keywords,
}) = _LogsAndKeywords; @Default("") String query,
}) = _LogsState;
factory LogsAndKeywords.fromJson(Map<String, Object?> json) =>
_$LogsAndKeywordsFromJson(json);
} }
extension LogsAndKeywordsExt on LogsAndKeywords { extension LogsStateExt on LogsState {
List<Log> get filteredLogs => logs List<Log> get list {
.where( final lowQuery = query.toLowerCase();
(log) => {log.logLevel.name}.containsAll(keywords), return logs.where(
) (log) {
.toList(); final payload = log.payload?.toLowerCase();
final logLevelName = log.logLevel.name;
return {logLevelName}.containsAll(keywords) &&
((payload?.contains(lowQuery) ?? false) ||
logLevelName.contains(lowQuery));
},
).toList();
}
} }
@freezed @freezed
class ConnectionsAndKeywords with _$ConnectionsAndKeywords { class ConnectionsState with _$ConnectionsState {
const factory ConnectionsAndKeywords({ const factory ConnectionsState({
@Default([]) List<Connection> connections, @Default([]) List<Connection> connections,
@Default([]) List<String> keywords, @Default([]) List<String> keywords,
}) = _ConnectionsAndKeywords; @Default("") String query,
}) = _ConnectionsState;
factory ConnectionsAndKeywords.fromJson(Map<String, Object?> json) =>
_$ConnectionsAndKeywordsFromJson(json);
} }
extension ConnectionsAndKeywordsExt on ConnectionsAndKeywords { extension ConnectionsStateExt on ConnectionsState {
List<Connection> get filteredConnections => connections List<Connection> get list {
.where((connection) => { final lowerQuery = query.toLowerCase().trim();
...connection.chains, final lowQuery = query.toLowerCase();
connection.metadata.process, return connections.where((connection) {
}.containsAll(keywords)) final chains = connection.chains;
.toList(); final process = connection.metadata.process;
final networkText = connection.metadata.network.toLowerCase();
final hostText = connection.metadata.host.toLowerCase();
final destinationIPText = connection.metadata.destinationIP.toLowerCase();
final processText = connection.metadata.process.toLowerCase();
final chainsText = chains.join("").toLowerCase();
return {...chains, process}.containsAll(keywords) &&
(networkText.contains(lowerQuery) ||
hostText.contains(lowerQuery) ||
destinationIPText.contains(lowQuery) ||
processText.contains(lowerQuery) ||
chainsText.contains(lowerQuery));
}).toList();
}
} }
const defaultDavFileName = "backup.zip"; const defaultDavFileName = "backup.zip";
@@ -291,6 +321,7 @@ class Group with _$Group {
@Default([]) List<Proxy> all, @Default([]) List<Proxy> all,
String? now, String? now,
bool? hidden, bool? hidden,
String? testUrl,
@Default("") String icon, @Default("") String icon,
required String name, required String name,
}) = _Group; }) = _Group;
@@ -441,3 +472,24 @@ class Field with _$Field {
Validator? validator, Validator? validator,
}) = _Field; }) = _Field;
} }
enum ActionType {
primary,
danger,
}
class ActionItemData {
const ActionItemData({
this.icon,
required this.label,
required this.onPressed,
this.type,
this.iconSize,
});
final double? iconSize;
final String label;
final VoidCallback onPressed;
final IconData? icon;
final ActionType? type;
}

View File

@@ -46,7 +46,7 @@ class AppSetting with _$AppSetting {
@JsonKey(fromJson: dashboardWidgetsRealFormJson) @JsonKey(fromJson: dashboardWidgetsRealFormJson)
@Default(defaultDashboardWidgets) @Default(defaultDashboardWidgets)
List<DashboardWidget> dashboardWidgets, List<DashboardWidget> dashboardWidgets,
@Default(false) bool onlyProxy, @Default(false) bool onlyStatisticsProxy,
@Default(false) bool autoLaunch, @Default(false) bool autoLaunch,
@Default(false) bool silentLaunch, @Default(false) bool silentLaunch,
@Default(false) bool autoRun, @Default(false) bool autoRun,

View File

@@ -1,12 +1,12 @@
// ignore_for_file: invalid_annotation_target // ignore_for_file: invalid_annotation_target
import 'dart:convert';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'generated/core.freezed.dart'; part 'generated/core.freezed.dart';
part 'generated/core.g.dart'; part 'generated/core.g.dart';
abstract mixin class AppMessageListener { abstract mixin class AppMessageListener {
@@ -16,8 +16,6 @@ abstract mixin class AppMessageListener {
void onRequest(Connection connection) {} void onRequest(Connection connection) {}
void onStarted(String runTime) {}
void onLoaded(String providerName) {} void onLoaded(String providerName) {}
} }
@@ -25,10 +23,6 @@ abstract mixin class ServiceMessageListener {
onProtect(Fd fd) {} onProtect(Fd fd) {}
onProcess(ProcessData process) {} onProcess(ProcessData process) {}
onStarted(String runTime) {}
onLoaded(String providerName) {}
} }
@freezed @freezed
@@ -42,7 +36,6 @@ class CoreState with _$CoreState {
required List<String> bypassDomain, required List<String> bypassDomain,
required List<String> routeAddress, required List<String> routeAddress,
required bool ipv6, required bool ipv6,
required bool onlyProxy,
}) = _CoreState; }) = _CoreState;
factory CoreState.fromJson(Map<String, Object?> json) => factory CoreState.fromJson(Map<String, Object?> json) =>
@@ -76,6 +69,7 @@ class ConfigExtendedParams with _$ConfigExtendedParams {
@JsonKey(name: "selected-map") required SelectedMap selectedMap, @JsonKey(name: "selected-map") required SelectedMap selectedMap,
@JsonKey(name: "override-dns") required bool overrideDns, @JsonKey(name: "override-dns") required bool overrideDns,
@JsonKey(name: "test-url") required String testUrl, @JsonKey(name: "test-url") required String testUrl,
@JsonKey(name: "only-statistics-proxy") required bool onlyStatisticsProxy,
}) = _ConfigExtendedParams; }) = _ConfigExtendedParams;
factory ConfigExtendedParams.fromJson(Map<String, Object?> json) => factory ConfigExtendedParams.fromJson(Map<String, Object?> json) =>
@@ -105,6 +99,17 @@ class ChangeProxyParams with _$ChangeProxyParams {
_$ChangeProxyParamsFromJson(json); _$ChangeProxyParamsFromJson(json);
} }
@freezed
class UpdateGeoDataParams with _$UpdateGeoDataParams {
const factory UpdateGeoDataParams({
@JsonKey(name: "geo-type") required String geoType,
@JsonKey(name: "geo-name") required String geoName,
}) = _UpdateGeoDataParams;
factory UpdateGeoDataParams.fromJson(Map<String, Object?> json) =>
_$UpdateGeoDataParamsFromJson(json);
}
@freezed @freezed
class AppMessage with _$AppMessage { class AppMessage with _$AppMessage {
const factory AppMessage({ const factory AppMessage({
@@ -117,20 +122,21 @@ class AppMessage with _$AppMessage {
} }
@freezed @freezed
class ServiceMessage with _$ServiceMessage { class InvokeMessage with _$InvokeMessage {
const factory ServiceMessage({ const factory InvokeMessage({
required ServiceMessageType type, required InvokeMessageType type,
dynamic data, dynamic data,
}) = _ServiceMessage; }) = _InvokeMessage;
factory ServiceMessage.fromJson(Map<String, Object?> json) => factory InvokeMessage.fromJson(Map<String, Object?> json) =>
_$ServiceMessageFromJson(json); _$InvokeMessageFromJson(json);
} }
@freezed @freezed
class Delay with _$Delay { class Delay with _$Delay {
const factory Delay({ const factory Delay({
required String name, required String name,
required String url,
int? value, int? value,
}) = _Delay; }) = _Delay;
@@ -150,7 +156,7 @@ class Now with _$Now {
@freezed @freezed
class ProcessData with _$ProcessData { class ProcessData with _$ProcessData {
const factory ProcessData({ const factory ProcessData({
required int id, required String id,
required Metadata metadata, required Metadata metadata,
}) = _ProcessData; }) = _ProcessData;
@@ -161,7 +167,7 @@ class ProcessData with _$ProcessData {
@freezed @freezed
class Fd with _$Fd { class Fd with _$Fd {
const factory Fd({ const factory Fd({
required int id, required String id,
required int value, required int value,
}) = _Fd; }) = _Fd;
@@ -171,7 +177,7 @@ class Fd with _$Fd {
@freezed @freezed
class ProcessMapItem with _$ProcessMapItem { class ProcessMapItem with _$ProcessMapItem {
const factory ProcessMapItem({ const factory ProcessMapItem({
required int id, required String id,
required String value, required String value,
}) = _ProcessMapItem; }) = _ProcessMapItem;
@@ -241,14 +247,21 @@ class Action with _$Action {
const factory Action({ const factory Action({
required ActionMethod method, required ActionMethod method,
required dynamic data, required dynamic data,
@JsonKey(name: "default-value") required dynamic defaultValue,
required String id, required String id,
}) = _Action; }) = _Action;
factory Action.fromJson(Map<String, Object?> json) => _$ActionFromJson(json); factory Action.fromJson(Map<String, Object?> json) => _$ActionFromJson(json);
} }
extension ActionExt on Action { @freezed
String get toJson { class ActionResult with _$ActionResult {
return json.encode(this); const factory ActionResult({
} required ActionMethod method,
required dynamic data,
String? id,
}) = _ActionResult;
factory ActionResult.fromJson(Map<String, Object?> json) =>
_$ActionResultFromJson(json);
} }

View File

@@ -290,7 +290,7 @@ mixin _$Package {
String get packageName => throw _privateConstructorUsedError; String get packageName => throw _privateConstructorUsedError;
String get label => throw _privateConstructorUsedError; String get label => throw _privateConstructorUsedError;
bool get isSystem => throw _privateConstructorUsedError; bool get isSystem => throw _privateConstructorUsedError;
int get firstInstallTime => throw _privateConstructorUsedError; int get lastUpdateTime => throw _privateConstructorUsedError;
/// Serializes this Package to a JSON map. /// Serializes this Package to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -307,7 +307,7 @@ abstract class $PackageCopyWith<$Res> {
_$PackageCopyWithImpl<$Res, Package>; _$PackageCopyWithImpl<$Res, Package>;
@useResult @useResult
$Res call( $Res call(
{String packageName, String label, bool isSystem, int firstInstallTime}); {String packageName, String label, bool isSystem, int lastUpdateTime});
} }
/// @nodoc /// @nodoc
@@ -328,7 +328,7 @@ class _$PackageCopyWithImpl<$Res, $Val extends Package>
Object? packageName = null, Object? packageName = null,
Object? label = null, Object? label = null,
Object? isSystem = null, Object? isSystem = null,
Object? firstInstallTime = null, Object? lastUpdateTime = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
packageName: null == packageName packageName: null == packageName
@@ -343,9 +343,9 @@ class _$PackageCopyWithImpl<$Res, $Val extends Package>
? _value.isSystem ? _value.isSystem
: isSystem // ignore: cast_nullable_to_non_nullable : isSystem // ignore: cast_nullable_to_non_nullable
as bool, as bool,
firstInstallTime: null == firstInstallTime lastUpdateTime: null == lastUpdateTime
? _value.firstInstallTime ? _value.lastUpdateTime
: firstInstallTime // ignore: cast_nullable_to_non_nullable : lastUpdateTime // ignore: cast_nullable_to_non_nullable
as int, as int,
) as $Val); ) as $Val);
} }
@@ -359,7 +359,7 @@ abstract class _$$PackageImplCopyWith<$Res> implements $PackageCopyWith<$Res> {
@override @override
@useResult @useResult
$Res call( $Res call(
{String packageName, String label, bool isSystem, int firstInstallTime}); {String packageName, String label, bool isSystem, int lastUpdateTime});
} }
/// @nodoc /// @nodoc
@@ -378,7 +378,7 @@ class __$$PackageImplCopyWithImpl<$Res>
Object? packageName = null, Object? packageName = null,
Object? label = null, Object? label = null,
Object? isSystem = null, Object? isSystem = null,
Object? firstInstallTime = null, Object? lastUpdateTime = null,
}) { }) {
return _then(_$PackageImpl( return _then(_$PackageImpl(
packageName: null == packageName packageName: null == packageName
@@ -393,9 +393,9 @@ class __$$PackageImplCopyWithImpl<$Res>
? _value.isSystem ? _value.isSystem
: isSystem // ignore: cast_nullable_to_non_nullable : isSystem // ignore: cast_nullable_to_non_nullable
as bool, as bool,
firstInstallTime: null == firstInstallTime lastUpdateTime: null == lastUpdateTime
? _value.firstInstallTime ? _value.lastUpdateTime
: firstInstallTime // ignore: cast_nullable_to_non_nullable : lastUpdateTime // ignore: cast_nullable_to_non_nullable
as int, as int,
)); ));
} }
@@ -408,7 +408,7 @@ class _$PackageImpl implements _Package {
{required this.packageName, {required this.packageName,
required this.label, required this.label,
required this.isSystem, required this.isSystem,
required this.firstInstallTime}); required this.lastUpdateTime});
factory _$PackageImpl.fromJson(Map<String, dynamic> json) => factory _$PackageImpl.fromJson(Map<String, dynamic> json) =>
_$$PackageImplFromJson(json); _$$PackageImplFromJson(json);
@@ -420,11 +420,11 @@ class _$PackageImpl implements _Package {
@override @override
final bool isSystem; final bool isSystem;
@override @override
final int firstInstallTime; final int lastUpdateTime;
@override @override
String toString() { String toString() {
return 'Package(packageName: $packageName, label: $label, isSystem: $isSystem, firstInstallTime: $firstInstallTime)'; return 'Package(packageName: $packageName, label: $label, isSystem: $isSystem, lastUpdateTime: $lastUpdateTime)';
} }
@override @override
@@ -437,14 +437,14 @@ class _$PackageImpl implements _Package {
(identical(other.label, label) || other.label == label) && (identical(other.label, label) || other.label == label) &&
(identical(other.isSystem, isSystem) || (identical(other.isSystem, isSystem) ||
other.isSystem == isSystem) && other.isSystem == isSystem) &&
(identical(other.firstInstallTime, firstInstallTime) || (identical(other.lastUpdateTime, lastUpdateTime) ||
other.firstInstallTime == firstInstallTime)); other.lastUpdateTime == lastUpdateTime));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => int get hashCode =>
Object.hash(runtimeType, packageName, label, isSystem, firstInstallTime); Object.hash(runtimeType, packageName, label, isSystem, lastUpdateTime);
/// Create a copy of Package /// Create a copy of Package
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -467,7 +467,7 @@ abstract class _Package implements Package {
{required final String packageName, {required final String packageName,
required final String label, required final String label,
required final bool isSystem, required final bool isSystem,
required final int firstInstallTime}) = _$PackageImpl; required final int lastUpdateTime}) = _$PackageImpl;
factory _Package.fromJson(Map<String, dynamic> json) = _$PackageImpl.fromJson; factory _Package.fromJson(Map<String, dynamic> json) = _$PackageImpl.fromJson;
@@ -478,7 +478,7 @@ abstract class _Package implements Package {
@override @override
bool get isSystem; bool get isSystem;
@override @override
int get firstInstallTime; int get lastUpdateTime;
/// Create a copy of Package /// Create a copy of Package
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -1092,51 +1092,45 @@ abstract class _Connection implements Connection {
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
LogsAndKeywords _$LogsAndKeywordsFromJson(Map<String, dynamic> json) {
return _LogsAndKeywords.fromJson(json);
}
/// @nodoc /// @nodoc
mixin _$LogsAndKeywords { mixin _$LogsState {
List<Log> get logs => throw _privateConstructorUsedError; List<Log> get logs => throw _privateConstructorUsedError;
List<String> get keywords => throw _privateConstructorUsedError; List<String> get keywords => throw _privateConstructorUsedError;
String get query => throw _privateConstructorUsedError;
/// Serializes this LogsAndKeywords to a JSON map. /// Create a copy of LogsState
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of LogsAndKeywords
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
$LogsAndKeywordsCopyWith<LogsAndKeywords> get copyWith => $LogsStateCopyWith<LogsState> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
/// @nodoc /// @nodoc
abstract class $LogsAndKeywordsCopyWith<$Res> { abstract class $LogsStateCopyWith<$Res> {
factory $LogsAndKeywordsCopyWith( factory $LogsStateCopyWith(LogsState value, $Res Function(LogsState) then) =
LogsAndKeywords value, $Res Function(LogsAndKeywords) then) = _$LogsStateCopyWithImpl<$Res, LogsState>;
_$LogsAndKeywordsCopyWithImpl<$Res, LogsAndKeywords>;
@useResult @useResult
$Res call({List<Log> logs, List<String> keywords}); $Res call({List<Log> logs, List<String> keywords, String query});
} }
/// @nodoc /// @nodoc
class _$LogsAndKeywordsCopyWithImpl<$Res, $Val extends LogsAndKeywords> class _$LogsStateCopyWithImpl<$Res, $Val extends LogsState>
implements $LogsAndKeywordsCopyWith<$Res> { implements $LogsStateCopyWith<$Res> {
_$LogsAndKeywordsCopyWithImpl(this._value, this._then); _$LogsStateCopyWithImpl(this._value, this._then);
// ignore: unused_field // ignore: unused_field
final $Val _value; final $Val _value;
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of LogsAndKeywords /// Create a copy of LogsState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
Object? logs = null, Object? logs = null,
Object? keywords = null, Object? keywords = null,
Object? query = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
logs: null == logs logs: null == logs
@@ -1147,38 +1141,43 @@ class _$LogsAndKeywordsCopyWithImpl<$Res, $Val extends LogsAndKeywords>
? _value.keywords ? _value.keywords
: keywords // ignore: cast_nullable_to_non_nullable : keywords // ignore: cast_nullable_to_non_nullable
as List<String>, as List<String>,
query: null == query
? _value.query
: query // ignore: cast_nullable_to_non_nullable
as String,
) as $Val); ) as $Val);
} }
} }
/// @nodoc /// @nodoc
abstract class _$$LogsAndKeywordsImplCopyWith<$Res> abstract class _$$LogsStateImplCopyWith<$Res>
implements $LogsAndKeywordsCopyWith<$Res> { implements $LogsStateCopyWith<$Res> {
factory _$$LogsAndKeywordsImplCopyWith(_$LogsAndKeywordsImpl value, factory _$$LogsStateImplCopyWith(
$Res Function(_$LogsAndKeywordsImpl) then) = _$LogsStateImpl value, $Res Function(_$LogsStateImpl) then) =
__$$LogsAndKeywordsImplCopyWithImpl<$Res>; __$$LogsStateImplCopyWithImpl<$Res>;
@override @override
@useResult @useResult
$Res call({List<Log> logs, List<String> keywords}); $Res call({List<Log> logs, List<String> keywords, String query});
} }
/// @nodoc /// @nodoc
class __$$LogsAndKeywordsImplCopyWithImpl<$Res> class __$$LogsStateImplCopyWithImpl<$Res>
extends _$LogsAndKeywordsCopyWithImpl<$Res, _$LogsAndKeywordsImpl> extends _$LogsStateCopyWithImpl<$Res, _$LogsStateImpl>
implements _$$LogsAndKeywordsImplCopyWith<$Res> { implements _$$LogsStateImplCopyWith<$Res> {
__$$LogsAndKeywordsImplCopyWithImpl( __$$LogsStateImplCopyWithImpl(
_$LogsAndKeywordsImpl _value, $Res Function(_$LogsAndKeywordsImpl) _then) _$LogsStateImpl _value, $Res Function(_$LogsStateImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of LogsAndKeywords /// Create a copy of LogsState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
Object? logs = null, Object? logs = null,
Object? keywords = null, Object? keywords = null,
Object? query = null,
}) { }) {
return _then(_$LogsAndKeywordsImpl( return _then(_$LogsStateImpl(
logs: null == logs logs: null == logs
? _value._logs ? _value._logs
: logs // ignore: cast_nullable_to_non_nullable : logs // ignore: cast_nullable_to_non_nullable
@@ -1187,21 +1186,24 @@ class __$$LogsAndKeywordsImplCopyWithImpl<$Res>
? _value._keywords ? _value._keywords
: keywords // ignore: cast_nullable_to_non_nullable : keywords // ignore: cast_nullable_to_non_nullable
as List<String>, as List<String>,
query: null == query
? _value.query
: query // ignore: cast_nullable_to_non_nullable
as String,
)); ));
} }
} }
/// @nodoc /// @nodoc
@JsonSerializable()
class _$LogsAndKeywordsImpl implements _LogsAndKeywords { class _$LogsStateImpl implements _LogsState {
const _$LogsAndKeywordsImpl( const _$LogsStateImpl(
{final List<Log> logs = const [], final List<String> keywords = const []}) {final List<Log> logs = const [],
final List<String> keywords = const [],
this.query = ""})
: _logs = logs, : _logs = logs,
_keywords = keywords; _keywords = keywords;
factory _$LogsAndKeywordsImpl.fromJson(Map<String, dynamic> json) =>
_$$LogsAndKeywordsImplFromJson(json);
final List<Log> _logs; final List<Log> _logs;
@override @override
@JsonKey() @JsonKey()
@@ -1220,112 +1222,103 @@ class _$LogsAndKeywordsImpl implements _LogsAndKeywords {
return EqualUnmodifiableListView(_keywords); return EqualUnmodifiableListView(_keywords);
} }
@override
@JsonKey()
final String query;
@override @override
String toString() { String toString() {
return 'LogsAndKeywords(logs: $logs, keywords: $keywords)'; return 'LogsState(logs: $logs, keywords: $keywords, query: $query)';
} }
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _$LogsAndKeywordsImpl && other is _$LogsStateImpl &&
const DeepCollectionEquality().equals(other._logs, _logs) && const DeepCollectionEquality().equals(other._logs, _logs) &&
const DeepCollectionEquality().equals(other._keywords, _keywords)); const DeepCollectionEquality().equals(other._keywords, _keywords) &&
(identical(other.query, query) || other.query == query));
} }
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(
runtimeType, runtimeType,
const DeepCollectionEquality().hash(_logs), const DeepCollectionEquality().hash(_logs),
const DeepCollectionEquality().hash(_keywords)); const DeepCollectionEquality().hash(_keywords),
query);
/// Create a copy of LogsAndKeywords /// Create a copy of LogsState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$LogsAndKeywordsImplCopyWith<_$LogsAndKeywordsImpl> get copyWith => _$$LogsStateImplCopyWith<_$LogsStateImpl> get copyWith =>
__$$LogsAndKeywordsImplCopyWithImpl<_$LogsAndKeywordsImpl>( __$$LogsStateImplCopyWithImpl<_$LogsStateImpl>(this, _$identity);
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$LogsAndKeywordsImplToJson(
this,
);
}
} }
abstract class _LogsAndKeywords implements LogsAndKeywords { abstract class _LogsState implements LogsState {
const factory _LogsAndKeywords( const factory _LogsState(
{final List<Log> logs, {final List<Log> logs,
final List<String> keywords}) = _$LogsAndKeywordsImpl; final List<String> keywords,
final String query}) = _$LogsStateImpl;
factory _LogsAndKeywords.fromJson(Map<String, dynamic> json) =
_$LogsAndKeywordsImpl.fromJson;
@override @override
List<Log> get logs; List<Log> get logs;
@override @override
List<String> get keywords; List<String> get keywords;
@override
String get query;
/// Create a copy of LogsAndKeywords /// Create a copy of LogsState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
_$$LogsAndKeywordsImplCopyWith<_$LogsAndKeywordsImpl> get copyWith => _$$LogsStateImplCopyWith<_$LogsStateImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
ConnectionsAndKeywords _$ConnectionsAndKeywordsFromJson(
Map<String, dynamic> json) {
return _ConnectionsAndKeywords.fromJson(json);
}
/// @nodoc /// @nodoc
mixin _$ConnectionsAndKeywords { mixin _$ConnectionsState {
List<Connection> get connections => throw _privateConstructorUsedError; List<Connection> get connections => throw _privateConstructorUsedError;
List<String> get keywords => throw _privateConstructorUsedError; List<String> get keywords => throw _privateConstructorUsedError;
String get query => throw _privateConstructorUsedError;
/// Serializes this ConnectionsAndKeywords to a JSON map. /// Create a copy of ConnectionsState
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of ConnectionsAndKeywords
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
$ConnectionsAndKeywordsCopyWith<ConnectionsAndKeywords> get copyWith => $ConnectionsStateCopyWith<ConnectionsState> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
/// @nodoc /// @nodoc
abstract class $ConnectionsAndKeywordsCopyWith<$Res> { abstract class $ConnectionsStateCopyWith<$Res> {
factory $ConnectionsAndKeywordsCopyWith(ConnectionsAndKeywords value, factory $ConnectionsStateCopyWith(
$Res Function(ConnectionsAndKeywords) then) = ConnectionsState value, $Res Function(ConnectionsState) then) =
_$ConnectionsAndKeywordsCopyWithImpl<$Res, ConnectionsAndKeywords>; _$ConnectionsStateCopyWithImpl<$Res, ConnectionsState>;
@useResult @useResult
$Res call({List<Connection> connections, List<String> keywords}); $Res call(
{List<Connection> connections, List<String> keywords, String query});
} }
/// @nodoc /// @nodoc
class _$ConnectionsAndKeywordsCopyWithImpl<$Res, class _$ConnectionsStateCopyWithImpl<$Res, $Val extends ConnectionsState>
$Val extends ConnectionsAndKeywords> implements $ConnectionsStateCopyWith<$Res> {
implements $ConnectionsAndKeywordsCopyWith<$Res> { _$ConnectionsStateCopyWithImpl(this._value, this._then);
_$ConnectionsAndKeywordsCopyWithImpl(this._value, this._then);
// ignore: unused_field // ignore: unused_field
final $Val _value; final $Val _value;
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of ConnectionsAndKeywords /// Create a copy of ConnectionsState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
Object? connections = null, Object? connections = null,
Object? keywords = null, Object? keywords = null,
Object? query = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
connections: null == connections connections: null == connections
@@ -1336,41 +1329,44 @@ class _$ConnectionsAndKeywordsCopyWithImpl<$Res,
? _value.keywords ? _value.keywords
: keywords // ignore: cast_nullable_to_non_nullable : keywords // ignore: cast_nullable_to_non_nullable
as List<String>, as List<String>,
query: null == query
? _value.query
: query // ignore: cast_nullable_to_non_nullable
as String,
) as $Val); ) as $Val);
} }
} }
/// @nodoc /// @nodoc
abstract class _$$ConnectionsAndKeywordsImplCopyWith<$Res> abstract class _$$ConnectionsStateImplCopyWith<$Res>
implements $ConnectionsAndKeywordsCopyWith<$Res> { implements $ConnectionsStateCopyWith<$Res> {
factory _$$ConnectionsAndKeywordsImplCopyWith( factory _$$ConnectionsStateImplCopyWith(_$ConnectionsStateImpl value,
_$ConnectionsAndKeywordsImpl value, $Res Function(_$ConnectionsStateImpl) then) =
$Res Function(_$ConnectionsAndKeywordsImpl) then) = __$$ConnectionsStateImplCopyWithImpl<$Res>;
__$$ConnectionsAndKeywordsImplCopyWithImpl<$Res>;
@override @override
@useResult @useResult
$Res call({List<Connection> connections, List<String> keywords}); $Res call(
{List<Connection> connections, List<String> keywords, String query});
} }
/// @nodoc /// @nodoc
class __$$ConnectionsAndKeywordsImplCopyWithImpl<$Res> class __$$ConnectionsStateImplCopyWithImpl<$Res>
extends _$ConnectionsAndKeywordsCopyWithImpl<$Res, extends _$ConnectionsStateCopyWithImpl<$Res, _$ConnectionsStateImpl>
_$ConnectionsAndKeywordsImpl> implements _$$ConnectionsStateImplCopyWith<$Res> {
implements _$$ConnectionsAndKeywordsImplCopyWith<$Res> { __$$ConnectionsStateImplCopyWithImpl(_$ConnectionsStateImpl _value,
__$$ConnectionsAndKeywordsImplCopyWithImpl( $Res Function(_$ConnectionsStateImpl) _then)
_$ConnectionsAndKeywordsImpl _value,
$Res Function(_$ConnectionsAndKeywordsImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of ConnectionsAndKeywords /// Create a copy of ConnectionsState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
Object? connections = null, Object? connections = null,
Object? keywords = null, Object? keywords = null,
Object? query = null,
}) { }) {
return _then(_$ConnectionsAndKeywordsImpl( return _then(_$ConnectionsStateImpl(
connections: null == connections connections: null == connections
? _value._connections ? _value._connections
: connections // ignore: cast_nullable_to_non_nullable : connections // ignore: cast_nullable_to_non_nullable
@@ -1379,22 +1375,24 @@ class __$$ConnectionsAndKeywordsImplCopyWithImpl<$Res>
? _value._keywords ? _value._keywords
: keywords // ignore: cast_nullable_to_non_nullable : keywords // ignore: cast_nullable_to_non_nullable
as List<String>, as List<String>,
query: null == query
? _value.query
: query // ignore: cast_nullable_to_non_nullable
as String,
)); ));
} }
} }
/// @nodoc /// @nodoc
@JsonSerializable()
class _$ConnectionsAndKeywordsImpl implements _ConnectionsAndKeywords { class _$ConnectionsStateImpl implements _ConnectionsState {
const _$ConnectionsAndKeywordsImpl( const _$ConnectionsStateImpl(
{final List<Connection> connections = const [], {final List<Connection> connections = const [],
final List<String> keywords = const []}) final List<String> keywords = const [],
this.query = ""})
: _connections = connections, : _connections = connections,
_keywords = keywords; _keywords = keywords;
factory _$ConnectionsAndKeywordsImpl.fromJson(Map<String, dynamic> json) =>
_$$ConnectionsAndKeywordsImplFromJson(json);
final List<Connection> _connections; final List<Connection> _connections;
@override @override
@JsonKey() @JsonKey()
@@ -1413,64 +1411,62 @@ class _$ConnectionsAndKeywordsImpl implements _ConnectionsAndKeywords {
return EqualUnmodifiableListView(_keywords); return EqualUnmodifiableListView(_keywords);
} }
@override
@JsonKey()
final String query;
@override @override
String toString() { String toString() {
return 'ConnectionsAndKeywords(connections: $connections, keywords: $keywords)'; return 'ConnectionsState(connections: $connections, keywords: $keywords, query: $query)';
} }
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _$ConnectionsAndKeywordsImpl && other is _$ConnectionsStateImpl &&
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other._connections, _connections) && .equals(other._connections, _connections) &&
const DeepCollectionEquality().equals(other._keywords, _keywords)); const DeepCollectionEquality().equals(other._keywords, _keywords) &&
(identical(other.query, query) || other.query == query));
} }
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(
runtimeType, runtimeType,
const DeepCollectionEquality().hash(_connections), const DeepCollectionEquality().hash(_connections),
const DeepCollectionEquality().hash(_keywords)); const DeepCollectionEquality().hash(_keywords),
query);
/// Create a copy of ConnectionsAndKeywords /// Create a copy of ConnectionsState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$ConnectionsAndKeywordsImplCopyWith<_$ConnectionsAndKeywordsImpl> _$$ConnectionsStateImplCopyWith<_$ConnectionsStateImpl> get copyWith =>
get copyWith => __$$ConnectionsAndKeywordsImplCopyWithImpl< __$$ConnectionsStateImplCopyWithImpl<_$ConnectionsStateImpl>(
_$ConnectionsAndKeywordsImpl>(this, _$identity); this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ConnectionsAndKeywordsImplToJson(
this,
);
}
} }
abstract class _ConnectionsAndKeywords implements ConnectionsAndKeywords { abstract class _ConnectionsState implements ConnectionsState {
const factory _ConnectionsAndKeywords( const factory _ConnectionsState(
{final List<Connection> connections, {final List<Connection> connections,
final List<String> keywords}) = _$ConnectionsAndKeywordsImpl; final List<String> keywords,
final String query}) = _$ConnectionsStateImpl;
factory _ConnectionsAndKeywords.fromJson(Map<String, dynamic> json) =
_$ConnectionsAndKeywordsImpl.fromJson;
@override @override
List<Connection> get connections; List<Connection> get connections;
@override @override
List<String> get keywords; List<String> get keywords;
@override
String get query;
/// Create a copy of ConnectionsAndKeywords /// Create a copy of ConnectionsState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
_$$ConnectionsAndKeywordsImplCopyWith<_$ConnectionsAndKeywordsImpl> _$$ConnectionsStateImplCopyWith<_$ConnectionsStateImpl> get copyWith =>
get copyWith => throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
DAV _$DAVFromJson(Map<String, dynamic> json) { DAV _$DAVFromJson(Map<String, dynamic> json) {
@@ -1998,6 +1994,7 @@ mixin _$Group {
List<Proxy> get all => throw _privateConstructorUsedError; List<Proxy> get all => throw _privateConstructorUsedError;
String? get now => throw _privateConstructorUsedError; String? get now => throw _privateConstructorUsedError;
bool? get hidden => throw _privateConstructorUsedError; bool? get hidden => throw _privateConstructorUsedError;
String? get testUrl => throw _privateConstructorUsedError;
String get icon => throw _privateConstructorUsedError; String get icon => throw _privateConstructorUsedError;
String get name => throw _privateConstructorUsedError; String get name => throw _privateConstructorUsedError;
@@ -2020,6 +2017,7 @@ abstract class $GroupCopyWith<$Res> {
List<Proxy> all, List<Proxy> all,
String? now, String? now,
bool? hidden, bool? hidden,
String? testUrl,
String icon, String icon,
String name}); String name});
} }
@@ -2043,6 +2041,7 @@ class _$GroupCopyWithImpl<$Res, $Val extends Group>
Object? all = null, Object? all = null,
Object? now = freezed, Object? now = freezed,
Object? hidden = freezed, Object? hidden = freezed,
Object? testUrl = freezed,
Object? icon = null, Object? icon = null,
Object? name = null, Object? name = null,
}) { }) {
@@ -2063,6 +2062,10 @@ class _$GroupCopyWithImpl<$Res, $Val extends Group>
? _value.hidden ? _value.hidden
: hidden // ignore: cast_nullable_to_non_nullable : hidden // ignore: cast_nullable_to_non_nullable
as bool?, as bool?,
testUrl: freezed == testUrl
? _value.testUrl
: testUrl // ignore: cast_nullable_to_non_nullable
as String?,
icon: null == icon icon: null == icon
? _value.icon ? _value.icon
: icon // ignore: cast_nullable_to_non_nullable : icon // ignore: cast_nullable_to_non_nullable
@@ -2087,6 +2090,7 @@ abstract class _$$GroupImplCopyWith<$Res> implements $GroupCopyWith<$Res> {
List<Proxy> all, List<Proxy> all,
String? now, String? now,
bool? hidden, bool? hidden,
String? testUrl,
String icon, String icon,
String name}); String name});
} }
@@ -2108,6 +2112,7 @@ class __$$GroupImplCopyWithImpl<$Res>
Object? all = null, Object? all = null,
Object? now = freezed, Object? now = freezed,
Object? hidden = freezed, Object? hidden = freezed,
Object? testUrl = freezed,
Object? icon = null, Object? icon = null,
Object? name = null, Object? name = null,
}) { }) {
@@ -2128,6 +2133,10 @@ class __$$GroupImplCopyWithImpl<$Res>
? _value.hidden ? _value.hidden
: hidden // ignore: cast_nullable_to_non_nullable : hidden // ignore: cast_nullable_to_non_nullable
as bool?, as bool?,
testUrl: freezed == testUrl
? _value.testUrl
: testUrl // ignore: cast_nullable_to_non_nullable
as String?,
icon: null == icon icon: null == icon
? _value.icon ? _value.icon
: icon // ignore: cast_nullable_to_non_nullable : icon // ignore: cast_nullable_to_non_nullable
@@ -2148,6 +2157,7 @@ class _$GroupImpl implements _Group {
final List<Proxy> all = const [], final List<Proxy> all = const [],
this.now, this.now,
this.hidden, this.hidden,
this.testUrl,
this.icon = "", this.icon = "",
required this.name}) required this.name})
: _all = all; : _all = all;
@@ -2171,6 +2181,8 @@ class _$GroupImpl implements _Group {
@override @override
final bool? hidden; final bool? hidden;
@override @override
final String? testUrl;
@override
@JsonKey() @JsonKey()
final String icon; final String icon;
@override @override
@@ -2178,7 +2190,7 @@ class _$GroupImpl implements _Group {
@override @override
String toString() { String toString() {
return 'Group(type: $type, all: $all, now: $now, hidden: $hidden, icon: $icon, name: $name)'; return 'Group(type: $type, all: $all, now: $now, hidden: $hidden, testUrl: $testUrl, icon: $icon, name: $name)';
} }
@override @override
@@ -2190,14 +2202,22 @@ class _$GroupImpl implements _Group {
const DeepCollectionEquality().equals(other._all, _all) && const DeepCollectionEquality().equals(other._all, _all) &&
(identical(other.now, now) || other.now == now) && (identical(other.now, now) || other.now == now) &&
(identical(other.hidden, hidden) || other.hidden == hidden) && (identical(other.hidden, hidden) || other.hidden == hidden) &&
(identical(other.testUrl, testUrl) || other.testUrl == testUrl) &&
(identical(other.icon, icon) || other.icon == icon) && (identical(other.icon, icon) || other.icon == icon) &&
(identical(other.name, name) || other.name == name)); (identical(other.name, name) || other.name == name));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, type, int get hashCode => Object.hash(
const DeepCollectionEquality().hash(_all), now, hidden, icon, name); runtimeType,
type,
const DeepCollectionEquality().hash(_all),
now,
hidden,
testUrl,
icon,
name);
/// Create a copy of Group /// Create a copy of Group
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -2221,6 +2241,7 @@ abstract class _Group implements Group {
final List<Proxy> all, final List<Proxy> all,
final String? now, final String? now,
final bool? hidden, final bool? hidden,
final String? testUrl,
final String icon, final String icon,
required final String name}) = _$GroupImpl; required final String name}) = _$GroupImpl;
@@ -2235,6 +2256,8 @@ abstract class _Group implements Group {
@override @override
bool? get hidden; bool? get hidden;
@override @override
String? get testUrl;
@override
String get icon; String get icon;
@override @override
String get name; String get name;

View File

@@ -29,7 +29,7 @@ _$PackageImpl _$$PackageImplFromJson(Map<String, dynamic> json) =>
packageName: json['packageName'] as String, packageName: json['packageName'] as String,
label: json['label'] as String, label: json['label'] as String,
isSystem: json['isSystem'] as bool, isSystem: json['isSystem'] as bool,
firstInstallTime: (json['firstInstallTime'] as num).toInt(), lastUpdateTime: (json['lastUpdateTime'] as num).toInt(),
); );
Map<String, dynamic> _$$PackageImplToJson(_$PackageImpl instance) => Map<String, dynamic> _$$PackageImplToJson(_$PackageImpl instance) =>
@@ -37,7 +37,7 @@ Map<String, dynamic> _$$PackageImplToJson(_$PackageImpl instance) =>
'packageName': instance.packageName, 'packageName': instance.packageName,
'label': instance.label, 'label': instance.label,
'isSystem': instance.isSystem, 'isSystem': instance.isSystem,
'firstInstallTime': instance.firstInstallTime, 'lastUpdateTime': instance.lastUpdateTime,
}; };
_$MetadataImpl _$$MetadataImplFromJson(Map<String, dynamic> json) => _$MetadataImpl _$$MetadataImplFromJson(Map<String, dynamic> json) =>
@@ -87,46 +87,6 @@ Map<String, dynamic> _$$ConnectionImplToJson(_$ConnectionImpl instance) =>
'chains': instance.chains, 'chains': instance.chains,
}; };
_$LogsAndKeywordsImpl _$$LogsAndKeywordsImplFromJson(
Map<String, dynamic> json) =>
_$LogsAndKeywordsImpl(
logs: (json['logs'] as List<dynamic>?)
?.map((e) => Log.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
keywords: (json['keywords'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
);
Map<String, dynamic> _$$LogsAndKeywordsImplToJson(
_$LogsAndKeywordsImpl instance) =>
<String, dynamic>{
'logs': instance.logs,
'keywords': instance.keywords,
};
_$ConnectionsAndKeywordsImpl _$$ConnectionsAndKeywordsImplFromJson(
Map<String, dynamic> json) =>
_$ConnectionsAndKeywordsImpl(
connections: (json['connections'] as List<dynamic>?)
?.map((e) => Connection.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
keywords: (json['keywords'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
);
Map<String, dynamic> _$$ConnectionsAndKeywordsImplToJson(
_$ConnectionsAndKeywordsImpl instance) =>
<String, dynamic>{
'connections': instance.connections,
'keywords': instance.keywords,
};
_$DAVImpl _$$DAVImplFromJson(Map<String, dynamic> json) => _$DAVImpl( _$DAVImpl _$$DAVImplFromJson(Map<String, dynamic> json) => _$DAVImpl(
uri: json['uri'] as String, uri: json['uri'] as String,
user: json['user'] as String, user: json['user'] as String,
@@ -161,6 +121,7 @@ _$GroupImpl _$$GroupImplFromJson(Map<String, dynamic> json) => _$GroupImpl(
const [], const [],
now: json['now'] as String?, now: json['now'] as String?,
hidden: json['hidden'] as bool?, hidden: json['hidden'] as bool?,
testUrl: json['testUrl'] as String?,
icon: json['icon'] as String? ?? "", icon: json['icon'] as String? ?? "",
name: json['name'] as String, name: json['name'] as String,
); );
@@ -171,6 +132,7 @@ Map<String, dynamic> _$$GroupImplToJson(_$GroupImpl instance) =>
'all': instance.all, 'all': instance.all,
'now': instance.now, 'now': instance.now,
'hidden': instance.hidden, 'hidden': instance.hidden,
'testUrl': instance.testUrl,
'icon': instance.icon, 'icon': instance.icon,
'name': instance.name, 'name': instance.name,
}; };

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