Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6266b7917 | ||
|
|
6c27f2e2f1 | ||
|
|
e04a0094b1 | ||
|
|
683e6a58ea | ||
|
|
b340feeb49 | ||
|
|
6a39b7ef5a |
32
CHANGELOG.md
32
CHANGELOG.md
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.follow.clash;
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
class FlClashApplication : Application() {
|
||||||
|
companion object {
|
||||||
|
private lateinit var instance: FlClashApplication
|
||||||
|
fun getAppContext(): Context {
|
||||||
|
return instance.applicationContext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
instance = this
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,7 +31,7 @@ object GlobalState {
|
|||||||
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
|
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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Submodule core/Clash.Meta updated: 3175efe8c0...0c03d8e4b4
171
core/action.go
171
core/action.go
@@ -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), ¶ms)
|
||||||
|
if err != nil {
|
||||||
|
send(action.wrapMessage(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
geoType := params["geo-type"]
|
||||||
|
geoName := params["geo-name"]
|
||||||
|
handleUpdateGeoData(geoType, geoName, func(value string) {
|
||||||
|
send(action.wrapMessage(value))
|
||||||
|
})
|
||||||
|
return
|
||||||
|
case updateExternalProviderMethod:
|
||||||
|
providerName := action.Data.(string)
|
||||||
|
handleUpdateExternalProvider(providerName, func(value string) {
|
||||||
|
send(action.wrapMessage(value))
|
||||||
|
})
|
||||||
|
return
|
||||||
|
case sideLoadExternalProviderMethod:
|
||||||
|
paramsString := action.Data.(string)
|
||||||
|
var params = map[string]string{}
|
||||||
|
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||||
|
if err != nil {
|
||||||
|
send(action.wrapMessage(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
providerName := params["providerName"]
|
||||||
|
data := params["data"]
|
||||||
|
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
|
||||||
|
send(action.wrapMessage(value))
|
||||||
|
})
|
||||||
|
return
|
||||||
|
case startLogMethod:
|
||||||
|
handleStartLog()
|
||||||
|
send(action.wrapMessage(true))
|
||||||
|
return
|
||||||
|
case stopLogMethod:
|
||||||
|
handleStopLog()
|
||||||
|
send(action.wrapMessage(true))
|
||||||
|
return
|
||||||
|
case startListenerMethod:
|
||||||
|
send(action.wrapMessage(handleStartListener()))
|
||||||
|
return
|
||||||
|
case stopListenerMethod:
|
||||||
|
send(action.wrapMessage(handleStopListener()))
|
||||||
|
return
|
||||||
|
case getCountryCodeMethod:
|
||||||
|
ip := action.Data.(string)
|
||||||
|
handleGetCountryCode(ip, func(value string) {
|
||||||
|
send(action.wrapMessage(value))
|
||||||
|
})
|
||||||
|
return
|
||||||
|
case getMemoryMethod:
|
||||||
|
handleGetMemory(func(value string) {
|
||||||
|
send(action.wrapMessage(value))
|
||||||
|
})
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
handle := nextHandle(action, send)
|
||||||
|
if handle {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
send(action.wrapMessage(action.DefaultValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
44
core/hub.go
44
core/hub.go
@@ -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,
|
||||||
})
|
})
|
||||||
|
|||||||
205
core/lib.go
205
core/lib.go
@@ -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()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
11
core/lib_no_android.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build !android && cgo
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
func nextHandle(action *Action) {
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextHandle(action *Action, send func([]byte)) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
//go:build !cgo
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
func SendMessage(message Message) {
|
|
||||||
s, err := message.Json()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Action{
|
|
||||||
Method: messageMethod,
|
|
||||||
}.callback(s)
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
//go:build cgo
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
bridge "core/dart-bridge"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
Port int64 = -1
|
|
||||||
ServicePort int64 = -1
|
|
||||||
)
|
|
||||||
|
|
||||||
func SendMessage(message Message) {
|
|
||||||
s, err := message.Json()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if handler, ok := messageHandlers[message.Type]; ok {
|
|
||||||
handler(s)
|
|
||||||
} else {
|
|
||||||
sendToPort(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var messageHandlers = map[MessageType]func(string) bool{
|
|
||||||
ProtectMessage: sendToServicePort,
|
|
||||||
ProcessMessage: sendToServicePort,
|
|
||||||
StartedMessage: conditionalSend,
|
|
||||||
LoadedMessage: conditionalSend,
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendToPort(s string) bool {
|
|
||||||
return bridge.SendToPort(Port, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendToServicePort(s string) bool {
|
|
||||||
return bridge.SendToPort(ServicePort, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func conditionalSend(s string) bool {
|
|
||||||
isSuccess := sendToPort(s)
|
|
||||||
if !isSuccess {
|
|
||||||
return sendToServicePort(s)
|
|
||||||
}
|
|
||||||
return isSuccess
|
|
||||||
}
|
|
||||||
151
core/server.go
151
core/server.go
@@ -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), ¶ms)
|
|
||||||
if err != nil {
|
|
||||||
action.callback(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
geoType := params["geoType"]
|
|
||||||
geoName := params["geoName"]
|
|
||||||
handleUpdateGeoData(geoType, geoName, func(value string) {
|
|
||||||
action.callback(value)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
case updateExternalProviderMethod:
|
|
||||||
providerName := action.Data.(string)
|
|
||||||
handleUpdateExternalProvider(providerName, func(value string) {
|
|
||||||
action.callback(value)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
case sideLoadExternalProviderMethod:
|
|
||||||
paramsString := action.Data.(string)
|
|
||||||
var params = map[string]string{}
|
|
||||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
|
||||||
if err != nil {
|
|
||||||
action.callback(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
providerName := params["providerName"]
|
|
||||||
data := params["data"]
|
|
||||||
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
|
|
||||||
action.callback(value)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
case startLogMethod:
|
|
||||||
handleStartLog()
|
|
||||||
return
|
|
||||||
case stopLogMethod:
|
|
||||||
handleStopLog()
|
|
||||||
return
|
|
||||||
case startListenerMethod:
|
|
||||||
action.callback(handleStartListener())
|
|
||||||
return
|
|
||||||
case stopListenerMethod:
|
|
||||||
action.callback(handleStopListener())
|
|
||||||
return
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
57
lib/common/render.dart
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
|
class Render {
|
||||||
|
static Render? _instance;
|
||||||
|
bool _isPaused = false;
|
||||||
|
final _dispatcher = SchedulerBinding.instance.platformDispatcher;
|
||||||
|
FrameCallback? _beginFrame;
|
||||||
|
VoidCallback? _drawFrame;
|
||||||
|
|
||||||
|
Render._internal();
|
||||||
|
|
||||||
|
factory Render() {
|
||||||
|
_instance ??= Render._internal();
|
||||||
|
return _instance!;
|
||||||
|
}
|
||||||
|
|
||||||
|
active() {
|
||||||
|
resume();
|
||||||
|
pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
pause() {
|
||||||
|
debouncer.call(
|
||||||
|
"render_pause",
|
||||||
|
_pause,
|
||||||
|
duration: Duration(seconds: 5),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resume() {
|
||||||
|
debouncer.cancel("render_pause");
|
||||||
|
_resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _pause() {
|
||||||
|
if (_isPaused) return;
|
||||||
|
_isPaused = true;
|
||||||
|
_beginFrame = _dispatcher.onBeginFrame;
|
||||||
|
_drawFrame = _dispatcher.onDrawFrame;
|
||||||
|
_dispatcher.onBeginFrame = null;
|
||||||
|
_dispatcher.onDrawFrame = null;
|
||||||
|
debugPrint("[App] pause");
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resume() {
|
||||||
|
if (!_isPaused) return;
|
||||||
|
_isPaused = false;
|
||||||
|
_dispatcher.onBeginFrame = _beginFrame;
|
||||||
|
_dispatcher.onDrawFrame = _drawFrame;
|
||||||
|
_dispatcher.scheduleFrame();
|
||||||
|
debugPrint("[App] resume");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final render = system.isDesktop ? Render() : null;
|
||||||
@@ -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
20
lib/common/view.dart
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'context.dart';
|
||||||
|
|
||||||
|
mixin ViewMixin<T extends StatefulWidget> on State<T> {
|
||||||
|
List<Widget> get actions => [];
|
||||||
|
|
||||||
|
Widget? get floatingActionButton => null;
|
||||||
|
|
||||||
|
initViewState() {
|
||||||
|
final commonScaffoldState = context.commonScaffoldState;
|
||||||
|
commonScaffoldState?.actions = actions;
|
||||||
|
commonScaffoldState?.floatingActionButton = floatingActionButton;
|
||||||
|
commonScaffoldState?.onSearch = onSearch;
|
||||||
|
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
Function(String)? get onSearch => null;
|
||||||
|
|
||||||
|
Function(List<String>)? get onKeywordsUpdate => null;
|
||||||
|
}
|
||||||
@@ -63,6 +63,7 @@ class Window {
|
|||||||
await windowManager.show();
|
await windowManager.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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
155
lib/fragments/connection/connections.dart
Normal file
155
lib/fragments/connection/connections.dart
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:fl_clash/clash/clash.dart';
|
||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'item.dart';
|
||||||
|
|
||||||
|
class ConnectionsFragment extends StatefulWidget {
|
||||||
|
const ConnectionsFragment({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ConnectionsFragment> createState() => _ConnectionsFragmentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConnectionsFragmentState extends State<ConnectionsFragment>
|
||||||
|
with ViewMixin {
|
||||||
|
final _connectionsStateNotifier = ValueNotifier<ConnectionsState>(
|
||||||
|
const ConnectionsState(),
|
||||||
|
);
|
||||||
|
final ScrollController _scrollController = ScrollController(
|
||||||
|
keepScrollOffset: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
Timer? timer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Widget> get actions => [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
clashCore.closeConnections();
|
||||||
|
_connectionsStateNotifier.value =
|
||||||
|
_connectionsStateNotifier.value.copyWith(
|
||||||
|
connections: await clashCore.getConnections(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.delete_sweep_outlined),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
get onSearch => (value) {
|
||||||
|
_connectionsStateNotifier.value =
|
||||||
|
_connectionsStateNotifier.value.copyWith(
|
||||||
|
query: value,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
get onKeywordsUpdate => (keywords) {
|
||||||
|
_connectionsStateNotifier.value =
|
||||||
|
_connectionsStateNotifier.value.copyWith(keywords: keywords);
|
||||||
|
};
|
||||||
|
|
||||||
|
_updateConnections() async {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
_connectionsStateNotifier.value =
|
||||||
|
_connectionsStateNotifier.value.copyWith(
|
||||||
|
connections: await clashCore.getConnections(),
|
||||||
|
);
|
||||||
|
timer = Timer(Duration(seconds: 1), () async {
|
||||||
|
_updateConnections();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_updateConnections();
|
||||||
|
}
|
||||||
|
|
||||||
|
_initActions() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback(
|
||||||
|
(_) {
|
||||||
|
initViewState();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleBlockConnection(String id) async {
|
||||||
|
clashCore.closeConnection(id);
|
||||||
|
_connectionsStateNotifier.value = _connectionsStateNotifier.value.copyWith(
|
||||||
|
connections: await clashCore.getConnections(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
timer?.cancel();
|
||||||
|
_connectionsStateNotifier.dispose();
|
||||||
|
_scrollController.dispose();
|
||||||
|
timer = null;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Selector<AppState, bool?>(
|
||||||
|
selector: (_, appState) =>
|
||||||
|
appState.currentLabel == 'connections' ||
|
||||||
|
appState.viewMode == ViewMode.mobile &&
|
||||||
|
appState.currentLabel == "tools",
|
||||||
|
builder: (_, isCurrent, child) {
|
||||||
|
if (isCurrent == null || isCurrent) {
|
||||||
|
_initActions();
|
||||||
|
}
|
||||||
|
return child!;
|
||||||
|
},
|
||||||
|
child: ValueListenableBuilder<ConnectionsState>(
|
||||||
|
valueListenable: _connectionsStateNotifier,
|
||||||
|
builder: (_, state, __) {
|
||||||
|
final connections = state.list;
|
||||||
|
if (connections.isEmpty) {
|
||||||
|
return NullStatus(
|
||||||
|
label: appLocalizations.nullConnectionsDesc,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return CommonScrollBar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: ListView.separated(
|
||||||
|
controller: _scrollController,
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
final connection = connections[index];
|
||||||
|
return ConnectionItem(
|
||||||
|
key: Key(connection.id),
|
||||||
|
connection: connection,
|
||||||
|
onClick: (value) {
|
||||||
|
context.commonScaffoldState?.addKeyword(value);
|
||||||
|
},
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.block),
|
||||||
|
onPressed: () {
|
||||||
|
_handleBlockConnection(connection.id);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (BuildContext context, int index) {
|
||||||
|
return const Divider(
|
||||||
|
height: 0,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: connections.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
150
lib/fragments/connection/item.dart
Normal file
150
lib/fragments/connection/item.dart
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/plugins/app.dart';
|
||||||
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class FindProcessBuilder extends StatelessWidget {
|
||||||
|
final Widget Function(bool value) builder;
|
||||||
|
|
||||||
|
const FindProcessBuilder({
|
||||||
|
super.key,
|
||||||
|
required this.builder,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Selector<ClashConfig, bool>(
|
||||||
|
selector: (_, clashConfig) =>
|
||||||
|
clashConfig.findProcessMode == FindProcessMode.always &&
|
||||||
|
Platform.isAndroid,
|
||||||
|
builder: (_, value, __) {
|
||||||
|
return builder(value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectionItem extends StatelessWidget {
|
||||||
|
final Connection connection;
|
||||||
|
final Function(String)? onClick;
|
||||||
|
final Widget? trailing;
|
||||||
|
|
||||||
|
const ConnectionItem({
|
||||||
|
super.key,
|
||||||
|
required this.connection,
|
||||||
|
this.onClick,
|
||||||
|
this.trailing,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<ImageProvider?> _getPackageIcon(Connection connection) async {
|
||||||
|
return await app?.getPackageIcon(connection.metadata.process);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getSourceText(Connection connection) {
|
||||||
|
final metadata = connection.metadata;
|
||||||
|
if (metadata.process.isEmpty) {
|
||||||
|
return connection.start.lastUpdateTimeDesc;
|
||||||
|
}
|
||||||
|
return "${metadata.process} · ${connection.start.lastUpdateTimeDesc}";
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final title = Text(
|
||||||
|
connection.desc,
|
||||||
|
style: context.textTheme.bodyLarge,
|
||||||
|
);
|
||||||
|
final subTitle = Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
_getSourceText(connection),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
Wrap(
|
||||||
|
runSpacing: 6,
|
||||||
|
spacing: 6,
|
||||||
|
children: [
|
||||||
|
for (final chain in connection.chains)
|
||||||
|
CommonChip(
|
||||||
|
label: chain,
|
||||||
|
onPressed: () {
|
||||||
|
if (onClick == null) return;
|
||||||
|
onClick!(chain);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
if (!Platform.isAndroid) {
|
||||||
|
return ListItem(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
|
||||||
|
title: title,
|
||||||
|
subtitle: subTitle,
|
||||||
|
trailing: trailing,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return FindProcessBuilder(
|
||||||
|
builder: (bool value) {
|
||||||
|
final leading = value
|
||||||
|
? GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (onClick == null) return;
|
||||||
|
final process = connection.metadata.process;
|
||||||
|
if (process.isEmpty) return;
|
||||||
|
onClick!(process);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(top: 4),
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
child: FutureBuilder<ImageProvider?>(
|
||||||
|
future: _getPackageIcon(connection),
|
||||||
|
builder: (_, snapshot) {
|
||||||
|
if (!snapshot.hasData && snapshot.data == null) {
|
||||||
|
return Container();
|
||||||
|
} else {
|
||||||
|
return Image(
|
||||||
|
image: snapshot.data!,
|
||||||
|
gaplessPlayback: true,
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
return ListItem(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
|
||||||
|
leading: leading,
|
||||||
|
title: title,
|
||||||
|
subtitle: subTitle,
|
||||||
|
trailing: trailing,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
239
lib/fragments/connection/requests.dart
Normal file
239
lib/fragments/connection/requests.dart
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/state.dart';
|
||||||
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'item.dart';
|
||||||
|
|
||||||
|
double _preOffset = 0;
|
||||||
|
|
||||||
|
class RequestsFragment extends StatefulWidget {
|
||||||
|
const RequestsFragment({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RequestsFragment> createState() => _RequestsFragmentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
|
||||||
|
final _requestsStateNotifier =
|
||||||
|
ValueNotifier<ConnectionsState>(const ConnectionsState());
|
||||||
|
List<Connection> _requests = [];
|
||||||
|
|
||||||
|
final ScrollController _scrollController = ScrollController(
|
||||||
|
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
|
||||||
|
);
|
||||||
|
|
||||||
|
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
|
||||||
|
|
||||||
|
double _currentMaxWidth = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
get onSearch => (value) {
|
||||||
|
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
|
||||||
|
query: value,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
get onKeywordsUpdate => (keywords) {
|
||||||
|
_requestsStateNotifier.value =
|
||||||
|
_requestsStateNotifier.value.copyWith(keywords: keywords);
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final appController = globalState.appController;
|
||||||
|
final appState = appController.appState;
|
||||||
|
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
|
||||||
|
connections: appState.requests,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_initActions() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback(
|
||||||
|
(_) {
|
||||||
|
initViewState();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _calcCacheHeight(Connection item) {
|
||||||
|
final cacheHeight = _cacheDynamicHeightMap.get(item.id);
|
||||||
|
if (cacheHeight != null) {
|
||||||
|
return cacheHeight;
|
||||||
|
}
|
||||||
|
final size = globalState.measure.computeTextSize(
|
||||||
|
Text(
|
||||||
|
item.desc,
|
||||||
|
style: context.textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
maxWidth: _currentMaxWidth,
|
||||||
|
);
|
||||||
|
final chainsText = item.chains.join("");
|
||||||
|
final length = item.chains.length;
|
||||||
|
final chainSize = globalState.measure.computeTextSize(
|
||||||
|
Text(
|
||||||
|
chainsText,
|
||||||
|
style: context.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
maxWidth: (_currentMaxWidth - (length - 1) * 6 - length * 24),
|
||||||
|
);
|
||||||
|
final baseHeight = globalState.measure.bodyMediumHeight;
|
||||||
|
final lines = (chainSize.height / baseHeight).round();
|
||||||
|
final computerHeight =
|
||||||
|
size.height + chainSize.height + 24 + 24 * (lines - 1);
|
||||||
|
_cacheDynamicHeightMap.put(item.id, computerHeight);
|
||||||
|
return computerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleTryClearCache(double maxWidth) {
|
||||||
|
if (_currentMaxWidth != maxWidth) {
|
||||||
|
_currentMaxWidth = maxWidth;
|
||||||
|
_cacheDynamicHeightMap.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_requestsStateNotifier.dispose();
|
||||||
|
_scrollController.dispose();
|
||||||
|
_currentMaxWidth = 0;
|
||||||
|
_cacheDynamicHeightMap.clear();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _wrapPage(Widget child) {
|
||||||
|
return Selector<AppState, bool?>(
|
||||||
|
selector: (_, appState) =>
|
||||||
|
appState.currentLabel == 'requests' ||
|
||||||
|
appState.viewMode == ViewMode.mobile &&
|
||||||
|
appState.currentLabel == "tools",
|
||||||
|
builder: (_, isCurrent, child) {
|
||||||
|
if (isCurrent == null || isCurrent) {
|
||||||
|
_initActions();
|
||||||
|
}
|
||||||
|
return child!;
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRequestsThrottler() {
|
||||||
|
throttler.call("request", () {
|
||||||
|
final isEquality = connectionListEquality.equals(
|
||||||
|
_requests,
|
||||||
|
_requestsStateNotifier.value.connections,
|
||||||
|
);
|
||||||
|
if (isEquality) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
|
||||||
|
connections: _requests,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, duration: commonDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _wrapRequestsUpdate(Widget child) {
|
||||||
|
return Selector<AppState, List<Connection>>(
|
||||||
|
selector: (_, appState) => appState.requests,
|
||||||
|
shouldRebuild: (prev, next) {
|
||||||
|
final isEquality = connectionListEquality.equals(prev, next);
|
||||||
|
if (!isEquality) {
|
||||||
|
_requests = next;
|
||||||
|
updateRequestsThrottler();
|
||||||
|
}
|
||||||
|
return !isEquality;
|
||||||
|
},
|
||||||
|
builder: (_, next, child) {
|
||||||
|
return child!;
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (_, constraints) {
|
||||||
|
return FindProcessBuilder(builder: (value) {
|
||||||
|
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0));
|
||||||
|
return _wrapPage(
|
||||||
|
_wrapRequestsUpdate(
|
||||||
|
ValueListenableBuilder<ConnectionsState>(
|
||||||
|
valueListenable: _requestsStateNotifier,
|
||||||
|
builder: (_, state, __) {
|
||||||
|
final connections = state.list;
|
||||||
|
if (connections.isEmpty) {
|
||||||
|
return NullStatus(
|
||||||
|
label: appLocalizations.nullRequestsDesc,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final items = connections
|
||||||
|
.map<Widget>(
|
||||||
|
(connection) => ConnectionItem(
|
||||||
|
key: Key(connection.id),
|
||||||
|
connection: connection,
|
||||||
|
onClick: (value) {
|
||||||
|
context.commonScaffoldState?.addKeyword(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.separated(
|
||||||
|
const Divider(
|
||||||
|
height: 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: NotificationListener<ScrollEndNotification>(
|
||||||
|
onNotification: (details) {
|
||||||
|
_preOffset = details.metrics.pixels;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: CommonScrollBar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: ListView.builder(
|
||||||
|
reverse: true,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: NextClampingScrollPhysics(),
|
||||||
|
controller: _scrollController,
|
||||||
|
itemExtentBuilder: (index, __) {
|
||||||
|
final widget = items[index];
|
||||||
|
if (widget.runtimeType == Divider) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
final measure = globalState.measure;
|
||||||
|
final bodyMediumHeight = measure.bodyMediumHeight;
|
||||||
|
final connection = connections[(index / 2).floor()];
|
||||||
|
final height = _calcCacheHeight(connection);
|
||||||
|
return height + bodyMediumHeight + 32;
|
||||||
|
},
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
return items[index];
|
||||||
|
},
|
||||||
|
itemCount: items.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,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,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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; // 禁用过度滚动效果
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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: () {
|
||||||
|
|||||||
@@ -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}'),
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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, __) {
|
||||||
|
|||||||
@@ -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?"
|
||||||
}
|
}
|
||||||
@@ -333,5 +333,14 @@
|
|||||||
"routeAddressDesc": "配置监听路由地址",
|
"routeAddressDesc": "配置监听路由地址",
|
||||||
"pleaseInputAdminPassword": "请输入管理员密码",
|
"pleaseInputAdminPassword": "请输入管理员密码",
|
||||||
"copyEnvVar": "复制环境变量",
|
"copyEnvVar": "复制环境变量",
|
||||||
"memoryInfo": "内存信息"
|
"memoryInfo": "内存信息",
|
||||||
|
"cancel": "取消",
|
||||||
|
"fileIsUpdate": "文件有修改,是否保存修改",
|
||||||
|
"profileHasUpdate": "配置文件已经修改,是否关闭自动更新 ",
|
||||||
|
"hasCacheChange": "是否缓存修改",
|
||||||
|
"nullProxies": "暂无代理",
|
||||||
|
"copySuccess": "复制成功",
|
||||||
|
"copyLink": "复制链接",
|
||||||
|
"exportFile": "导出文件",
|
||||||
|
"cacheCorrupt": "缓存已损坏,是否清空?"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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("中文简体"),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
1208
lib/l10n/l10n.dart
1208
lib/l10n/l10n.dart
File diff suppressed because it is too large
Load Diff
246
lib/main.dart
246
lib/main.dart
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user