Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e04a0094b1 | ||
|
|
683e6a58ea | ||
|
|
b340feeb49 | ||
|
|
6a39b7ef5a | ||
|
|
35f89fea90 | ||
|
|
58acd9c1ab |
4
.github/workflows/build.yaml
vendored
4
.github/workflows/build.yaml
vendored
@@ -67,8 +67,8 @@ jobs:
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.x'
|
||||
channel: 'stable'
|
||||
flutter-version: 3.24.5
|
||||
channel: stable
|
||||
cache: true
|
||||
|
||||
- name: Get Flutter Dependency
|
||||
|
||||
38
CHANGELOG.md
38
CHANGELOG.md
@@ -1,3 +1,41 @@
|
||||
## v0.8.73
|
||||
|
||||
- Update popup menu
|
||||
|
||||
- Add file editor
|
||||
|
||||
- Fix android service issues
|
||||
|
||||
- Optimize desktop background performance
|
||||
|
||||
- Optimize android main process performance
|
||||
|
||||
- Optimize delay test
|
||||
|
||||
- Optimize vpn protect
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.72
|
||||
|
||||
- Update core
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.71
|
||||
|
||||
- Remake dashboard
|
||||
|
||||
- Optimize theme
|
||||
|
||||
- Optimize more details
|
||||
|
||||
- Update flutter version
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.70
|
||||
|
||||
- Support better window position memory
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<application
|
||||
android:name="${applicationName}"
|
||||
android:name=".FlClashApplication"
|
||||
android:hardwareAccelerated="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="FlClash">
|
||||
@@ -64,9 +64,7 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- <meta-data-->
|
||||
<!-- android:name="io.flutter.embedding.android.EnableImpeller"-->
|
||||
<!-- android:value="true" />-->
|
||||
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false" />
|
||||
|
||||
<activity
|
||||
android:name=".TempActivity"
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.follow.clash;
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context;
|
||||
|
||||
class FlClashApplication : Application() {
|
||||
companion object {
|
||||
private lateinit var instance: FlClashApplication
|
||||
fun getAppContext(): Context {
|
||||
return instance.applicationContext
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ object GlobalState {
|
||||
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
|
||||
}
|
||||
|
||||
fun getText(text: String): String {
|
||||
suspend fun getText(text: String): String {
|
||||
return getCurrentAppPlugin()?.getText(text) ?: ""
|
||||
}
|
||||
|
||||
@@ -44,14 +44,14 @@ object GlobalState {
|
||||
return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
|
||||
}
|
||||
|
||||
fun handleToggle(context: Context) {
|
||||
val starting = handleStart(context)
|
||||
fun handleToggle() {
|
||||
val starting = handleStart()
|
||||
if (!starting) {
|
||||
handleStop()
|
||||
}
|
||||
}
|
||||
|
||||
fun handleStart(context: Context): Boolean {
|
||||
fun handleStart(): Boolean {
|
||||
if (runState.value == RunState.STOP) {
|
||||
runState.value = RunState.PENDING
|
||||
runLock.lock()
|
||||
@@ -59,7 +59,7 @@ object GlobalState {
|
||||
if (tilePlugin != null) {
|
||||
tilePlugin.handleStart()
|
||||
} else {
|
||||
initServiceEngine(context)
|
||||
initServiceEngine()
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -74,6 +74,12 @@ object GlobalState {
|
||||
}
|
||||
}
|
||||
|
||||
fun handleTryDestroy() {
|
||||
if (flutterEngine == null) {
|
||||
destroyServiceEngine()
|
||||
}
|
||||
}
|
||||
|
||||
fun destroyServiceEngine() {
|
||||
runLock.withLock {
|
||||
serviceEngine?.destroy()
|
||||
@@ -81,21 +87,21 @@ object GlobalState {
|
||||
}
|
||||
}
|
||||
|
||||
fun initServiceEngine(context: Context) {
|
||||
fun initServiceEngine() {
|
||||
if (serviceEngine != null) return
|
||||
destroyServiceEngine()
|
||||
runLock.withLock {
|
||||
serviceEngine = FlutterEngine(context)
|
||||
serviceEngine?.plugins?.add(VpnPlugin())
|
||||
serviceEngine = FlutterEngine(FlClashApplication.getAppContext())
|
||||
serviceEngine?.plugins?.add(VpnPlugin)
|
||||
serviceEngine?.plugins?.add(AppPlugin())
|
||||
serviceEngine?.plugins?.add(TilePlugin())
|
||||
serviceEngine?.plugins?.add(ServicePlugin())
|
||||
val vpnService = DartExecutor.DartEntrypoint(
|
||||
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
|
||||
"vpnService"
|
||||
"_service"
|
||||
)
|
||||
serviceEngine?.dartExecutor?.executeDartEntrypoint(
|
||||
vpnService,
|
||||
if (flutterEngine == null) listOf("quick") else null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package com.follow.clash
|
||||
|
||||
|
||||
import com.follow.clash.plugins.AppPlugin
|
||||
import com.follow.clash.plugins.ServicePlugin
|
||||
import com.follow.clash.plugins.TilePlugin
|
||||
import com.follow.clash.plugins.VpnPlugin
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
|
||||
@@ -12,8 +10,7 @@ class MainActivity : FlutterActivity() {
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
flutterEngine.plugins.add(AppPlugin())
|
||||
flutterEngine.plugins.add(VpnPlugin())
|
||||
flutterEngine.plugins.add(ServicePlugin())
|
||||
flutterEngine.plugins.add(ServicePlugin)
|
||||
flutterEngine.plugins.add(TilePlugin())
|
||||
GlobalState.flutterEngine = flutterEngine
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ class TempActivity : Activity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
when (intent.action) {
|
||||
wrapAction("START") -> {
|
||||
GlobalState.handleStart(applicationContext)
|
||||
GlobalState.handleStart()
|
||||
}
|
||||
|
||||
wrapAction("STOP") -> {
|
||||
@@ -17,7 +17,7 @@ class TempActivity : Activity() {
|
||||
}
|
||||
|
||||
wrapAction("CHANGE") -> {
|
||||
GlobalState.handleToggle(applicationContext)
|
||||
GlobalState.handleToggle()
|
||||
}
|
||||
}
|
||||
finishAndRemoveTask()
|
||||
|
||||
@@ -97,7 +97,6 @@ fun String.toCIDR(): CIDR {
|
||||
return CIDR(address, prefixLength)
|
||||
}
|
||||
|
||||
|
||||
fun ConnectivityManager.resolveDns(network: Network?): List<String> {
|
||||
val properties = getLinkProperties(network) ?: return listOf()
|
||||
return properties.dnsServers.map { it.asSocketAddressText(53) }
|
||||
@@ -143,7 +142,6 @@ fun Context.getActionPendingIntent(action: String): PendingIntent {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun numericToTextFormat(src: ByteArray): String {
|
||||
val sb = StringBuilder(39)
|
||||
for (i in 0 until 8) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.follow.clash.models
|
||||
|
||||
data class Process(
|
||||
val id: Int,
|
||||
val id: String,
|
||||
val metadata: Metadata,
|
||||
)
|
||||
|
||||
|
||||
@@ -25,4 +25,9 @@ data class VpnOptions(
|
||||
val ipv4Address: String,
|
||||
val ipv6Address: String,
|
||||
val dnsServerAddress: String,
|
||||
)
|
||||
|
||||
data class StartForegroundParams(
|
||||
val title: String,
|
||||
val content: String,
|
||||
)
|
||||
@@ -3,7 +3,6 @@ package com.follow.clash.plugins
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.ComponentInfo
|
||||
@@ -19,6 +18,7 @@ import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
|
||||
import com.follow.clash.FlClashApplication
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.R
|
||||
import com.follow.clash.extensions.awaitResult
|
||||
@@ -40,13 +40,12 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
|
||||
|
||||
private var activity: Activity? = null
|
||||
|
||||
private lateinit var context: Context
|
||||
private var activityRef: WeakReference<Activity>? = null
|
||||
|
||||
private lateinit var channel: MethodChannel
|
||||
|
||||
@@ -121,21 +120,27 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
scope = CoroutineScope(Dispatchers.Default)
|
||||
context = flutterPluginBinding.applicationContext
|
||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
|
||||
channel.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
private fun initShortcuts(label: String) {
|
||||
val shortcut = ShortcutInfoCompat.Builder(context, "toggle")
|
||||
val shortcut = ShortcutInfoCompat.Builder(FlClashApplication.getAppContext(), "toggle")
|
||||
.setShortLabel(label)
|
||||
.setIcon(IconCompat.createWithResource(context, R.mipmap.ic_launcher_round))
|
||||
.setIntent(context.getActionIntent("CHANGE"))
|
||||
.setIcon(
|
||||
IconCompat.createWithResource(
|
||||
FlClashApplication.getAppContext(),
|
||||
R.mipmap.ic_launcher_round
|
||||
)
|
||||
)
|
||||
.setIntent(FlClashApplication.getAppContext().getActionIntent("CHANGE"))
|
||||
.build()
|
||||
ShortcutManagerCompat.setDynamicShortcuts(context, listOf(shortcut))
|
||||
ShortcutManagerCompat.setDynamicShortcuts(
|
||||
FlClashApplication.getAppContext(),
|
||||
listOf(shortcut)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel.setMethodCallHandler(null)
|
||||
scope.cancel()
|
||||
@@ -143,14 +148,14 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
|
||||
private fun tip(message: String?) {
|
||||
if (GlobalState.flutterEngine == null) {
|
||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(FlClashApplication.getAppContext(), message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: Result) {
|
||||
when (call.method) {
|
||||
"moveTaskToBack" -> {
|
||||
activity?.moveTaskToBack(true)
|
||||
activityRef?.get()?.moveTaskToBack(true)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
@@ -192,7 +197,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
if (iconMap["default"] == null) {
|
||||
iconMap["default"] =
|
||||
context.packageManager?.defaultActivityIcon?.getBase64()
|
||||
FlClashApplication.getAppContext().packageManager?.defaultActivityIcon?.getBase64()
|
||||
}
|
||||
result.success(iconMap["default"])
|
||||
return@launch
|
||||
@@ -221,8 +226,8 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
private fun openFile(path: String) {
|
||||
val file = File(path)
|
||||
val uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileProvider",
|
||||
FlClashApplication.getAppContext(),
|
||||
"${FlClashApplication.getAppContext().packageName}.fileProvider",
|
||||
file
|
||||
)
|
||||
|
||||
@@ -234,13 +239,13 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
val flags =
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
|
||||
val resInfoList = context.packageManager.queryIntentActivities(
|
||||
val resInfoList = FlClashApplication.getAppContext().packageManager.queryIntentActivities(
|
||||
intent, PackageManager.MATCH_DEFAULT_ONLY
|
||||
)
|
||||
|
||||
for (resolveInfo in resInfoList) {
|
||||
val packageName = resolveInfo.activityInfo.packageName
|
||||
context.grantUriPermission(
|
||||
FlClashApplication.getAppContext().grantUriPermission(
|
||||
packageName,
|
||||
uri,
|
||||
flags
|
||||
@@ -248,19 +253,19 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
|
||||
try {
|
||||
activity?.startActivity(intent)
|
||||
activityRef?.get()?.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
println(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateExcludeFromRecents(value: Boolean?) {
|
||||
val am = getSystemService(context, ActivityManager::class.java)
|
||||
val am = getSystemService(FlClashApplication.getAppContext(), ActivityManager::class.java)
|
||||
val task = am?.appTasks?.firstOrNull {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
it.taskInfo.taskId == activity?.taskId
|
||||
it.taskInfo.taskId == activityRef?.get()?.taskId
|
||||
} else {
|
||||
it.taskInfo.id == activity?.taskId
|
||||
it.taskInfo.id == activityRef?.get()?.taskId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,7 +277,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
|
||||
private suspend fun getPackageIcon(packageName: String): String? {
|
||||
val packageManager = context.packageManager
|
||||
val packageManager = FlClashApplication.getAppContext().packageManager
|
||||
if (iconMap[packageName] == null) {
|
||||
iconMap[packageName] = try {
|
||||
packageManager?.getApplicationIcon(packageName)?.getBase64()
|
||||
@@ -285,10 +290,10 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
|
||||
private fun getPackages(): List<Package> {
|
||||
val packageManager = context.packageManager
|
||||
val packageManager = FlClashApplication.getAppContext().packageManager
|
||||
if (packages.isNotEmpty()) return packages
|
||||
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
|
||||
it.packageName != context.packageName
|
||||
it.packageName != FlClashApplication.getAppContext().packageName
|
||||
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||
|| it.packageName == "android"
|
||||
|
||||
@@ -317,43 +322,45 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
}
|
||||
|
||||
fun requestVpnPermission(context: Context, callBack: () -> Unit) {
|
||||
fun requestVpnPermission(callBack: () -> Unit) {
|
||||
vpnCallBack = callBack
|
||||
val intent = VpnService.prepare(context)
|
||||
val intent = VpnService.prepare(FlClashApplication.getAppContext())
|
||||
if (intent != null) {
|
||||
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
|
||||
activityRef?.get()?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
|
||||
return
|
||||
}
|
||||
vpnCallBack?.invoke()
|
||||
}
|
||||
|
||||
fun requestNotificationsPermission(context: Context) {
|
||||
fun requestNotificationsPermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val permission = ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
FlClashApplication.getAppContext(),
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
)
|
||||
if (permission != PackageManager.PERMISSION_GRANTED) {
|
||||
if (isBlockNotification) return
|
||||
if (activity == null) return
|
||||
ActivityCompat.requestPermissions(
|
||||
activity!!,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
NOTIFICATION_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
return
|
||||
if (activityRef?.get() == null) return
|
||||
activityRef?.get()?.let {
|
||||
ActivityCompat.requestPermissions(
|
||||
it,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
NOTIFICATION_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getText(text: String): String? {
|
||||
return runBlocking {
|
||||
suspend fun getText(text: String): String? {
|
||||
return withContext(Dispatchers.Default){
|
||||
channel.awaitResult<String>("getText", text)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isChinaPackage(packageName: String): Boolean {
|
||||
val packageManager = context.packageManager ?: return false
|
||||
val packageManager = FlClashApplication.getAppContext().packageManager ?: return false
|
||||
skipPrefixList.forEach {
|
||||
if (packageName == it || packageName.startsWith("$it.")) return false
|
||||
}
|
||||
@@ -373,7 +380,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION") packageManager.getPackageInfo(
|
||||
packageManager.getPackageInfo(
|
||||
packageName, packageManagerFlags
|
||||
)
|
||||
}
|
||||
@@ -420,28 +427,28 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
activityRef = WeakReference(binding.activity)
|
||||
binding.addActivityResultListener(::onActivityResult)
|
||||
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
activity = null
|
||||
activityRef = null
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
activityRef = WeakReference(binding.activity)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
channel.invokeMethod("exit", null)
|
||||
activity = null
|
||||
activityRef = null
|
||||
}
|
||||
|
||||
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
|
||||
if (resultCode == FlutterActivity.RESULT_OK) {
|
||||
GlobalState.initServiceEngine(context)
|
||||
GlobalState.initServiceEngine()
|
||||
vpnCallBack?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import android.content.Context
|
||||
import com.follow.clash.FlClashApplication
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import com.google.gson.Gson
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
|
||||
class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
data object ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
|
||||
private lateinit var flutterMethodChannel: MethodChannel
|
||||
|
||||
private lateinit var context: Context
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
context = flutterPluginBinding.applicationContext
|
||||
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "service")
|
||||
flutterMethodChannel.setMethodCallHandler(this)
|
||||
}
|
||||
@@ -24,9 +23,22 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
|
||||
"startVpn" -> {
|
||||
val data = call.argument<String>("data")
|
||||
val options = Gson().fromJson(data, VpnOptions::class.java)
|
||||
GlobalState.getCurrentVPNPlugin()?.handleStart(options)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"stopVpn" -> {
|
||||
GlobalState.getCurrentVPNPlugin()?.handleStop()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"init" -> {
|
||||
GlobalState.getCurrentAppPlugin()?.requestNotificationsPermission(context)
|
||||
GlobalState.initServiceEngine(context)
|
||||
GlobalState.getCurrentAppPlugin()
|
||||
?.requestNotificationsPermission()
|
||||
GlobalState.initServiceEngine()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
@@ -41,7 +53,7 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
private fun handleDestroy() {
|
||||
GlobalState.getCurrentVPNPlugin()?.stop()
|
||||
GlobalState.getCurrentVPNPlugin()?.handleStop()
|
||||
GlobalState.destroyServiceEngine()
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class TilePlugin(private val onStart: (() -> Unit)? = null, private val onStop: (() -> Unit)? = null) : FlutterPlugin,
|
||||
MethodChannel.MethodCallHandler {
|
||||
class TilePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
|
||||
private lateinit var channel: MethodChannel
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "tile")
|
||||
channel.setMethodCallHandler(this)
|
||||
@@ -20,13 +19,11 @@ class TilePlugin(private val onStart: (() -> Unit)? = null, private val onStop:
|
||||
}
|
||||
|
||||
fun handleStart() {
|
||||
onStart?.let { it() }
|
||||
channel.invokeMethod("start", null)
|
||||
}
|
||||
|
||||
fun handleStop() {
|
||||
channel.invokeMethod("stop", null)
|
||||
onStop?.let { it() }
|
||||
}
|
||||
|
||||
private fun handleDetached() {
|
||||
|
||||
@@ -11,13 +11,16 @@ import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.content.getSystemService
|
||||
import com.follow.clash.BaseServiceInterface
|
||||
import com.follow.clash.FlClashApplication
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.RunState
|
||||
import com.follow.clash.extensions.awaitResult
|
||||
import com.follow.clash.extensions.getProtocol
|
||||
import com.follow.clash.extensions.resolveDns
|
||||
import com.follow.clash.models.Process
|
||||
import com.follow.clash.models.StartForegroundParams
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import com.follow.clash.services.BaseServiceInterface
|
||||
import com.follow.clash.services.FlClashService
|
||||
import com.follow.clash.services.FlClashVpnService
|
||||
import com.google.gson.Gson
|
||||
@@ -26,21 +29,24 @@ import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.InetSocketAddress
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
|
||||
class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
private lateinit var flutterMethodChannel: MethodChannel
|
||||
private lateinit var context: Context
|
||||
private var flClashService: BaseServiceInterface? = null
|
||||
private lateinit var options: VpnOptions
|
||||
private lateinit var scope: CoroutineScope
|
||||
private var lastStartForegroundParams: StartForegroundParams? = null
|
||||
private var timerJob: Job? = null
|
||||
|
||||
private val connectivity by lazy {
|
||||
context.getSystemService<ConnectivityManager>()
|
||||
FlClashApplication.getAppContext().getSystemService<ConnectivityManager>()
|
||||
}
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
@@ -50,7 +56,7 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
is FlClashService.LocalBinder -> service.getService()
|
||||
else -> throw Exception("invalid binder")
|
||||
}
|
||||
start()
|
||||
handleStartService()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(arg: ComponentName) {
|
||||
@@ -60,7 +66,6 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
scope = CoroutineScope(Dispatchers.Default)
|
||||
context = flutterPluginBinding.applicationContext
|
||||
scope.launch {
|
||||
registerNetworkCallback()
|
||||
}
|
||||
@@ -77,16 +82,11 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
when (call.method) {
|
||||
"start" -> {
|
||||
val data = call.argument<String>("data")
|
||||
options = Gson().fromJson(data, VpnOptions::class.java)
|
||||
when (options.enable) {
|
||||
true -> handleStartVpn()
|
||||
false -> start()
|
||||
}
|
||||
result.success(true)
|
||||
result.success(handleStart(Gson().fromJson(data, VpnOptions::class.java)))
|
||||
}
|
||||
|
||||
"stop" -> {
|
||||
stop()
|
||||
handleStop()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
@@ -102,13 +102,6 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
}
|
||||
|
||||
"startForeground" -> {
|
||||
val title = call.argument<String>("title") as String
|
||||
val content = call.argument<String>("content") as String
|
||||
startForeground(title, content)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"resolverProcess" -> {
|
||||
val data = call.argument<String>("data")
|
||||
val process = if (data != null) Gson().fromJson(
|
||||
@@ -144,7 +137,8 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
result.success(null)
|
||||
return@withContext
|
||||
}
|
||||
val packages = context.packageManager?.getPackagesForUid(uid)
|
||||
val packages =
|
||||
FlClashApplication.getAppContext().packageManager?.getPackagesForUid(uid)
|
||||
result.success(packages?.first())
|
||||
}
|
||||
}
|
||||
@@ -156,10 +150,20 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleStartVpn() {
|
||||
GlobalState.getCurrentAppPlugin()?.requestVpnPermission(context) {
|
||||
start()
|
||||
fun handleStart(options: VpnOptions): Boolean {
|
||||
this.options = options
|
||||
when (options.enable) {
|
||||
true -> handleStartVpn()
|
||||
false -> handleStartService()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun handleStartVpn() {
|
||||
GlobalState.getCurrentAppPlugin()
|
||||
?.requestVpnPermission {
|
||||
handleStartService()
|
||||
}
|
||||
}
|
||||
|
||||
fun requestGc() {
|
||||
@@ -177,16 +181,6 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
flutterMethodChannel.invokeMethod("dnsChanged", dns)
|
||||
}
|
||||
}
|
||||
// if (flClashService is FlClashVpnService) {
|
||||
// val network = networks.maxByOrNull { net ->
|
||||
// connectivity?.getNetworkCapabilities(net)?.let { cap ->
|
||||
// TRANSPORT_PRIORITY.indexOfFirst { cap.hasTransport(it) }
|
||||
// } ?: -1
|
||||
// }
|
||||
// network?.let {
|
||||
// (flClashService as FlClashVpnService).updateUnderlyingNetworks(arrayOf(network))
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private val callback = object : ConnectivityManager.NetworkCallback() {
|
||||
@@ -218,14 +212,41 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
onUpdateNetwork()
|
||||
}
|
||||
|
||||
private fun startForeground(title: String, content: String) {
|
||||
GlobalState.runLock.withLock {
|
||||
private suspend fun startForeground() {
|
||||
GlobalState.runLock.lock()
|
||||
try {
|
||||
if (GlobalState.runState.value != RunState.START) return
|
||||
flClashService?.startForeground(title, content)
|
||||
val data = flutterMethodChannel.awaitResult<String>("getStartForegroundParams")
|
||||
val startForegroundParams = Gson().fromJson(
|
||||
data, StartForegroundParams::class.java
|
||||
)
|
||||
if (lastStartForegroundParams != startForegroundParams) {
|
||||
lastStartForegroundParams = startForegroundParams
|
||||
flClashService?.startForeground(
|
||||
startForegroundParams.title,
|
||||
startForegroundParams.content,
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
GlobalState.runLock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
private fun start() {
|
||||
private fun startForegroundJob() {
|
||||
timerJob = CoroutineScope(Dispatchers.Main).launch {
|
||||
while (isActive) {
|
||||
startForeground()
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopForegroundJob() {
|
||||
timerJob?.cancel()
|
||||
timerJob = null
|
||||
}
|
||||
|
||||
private fun handleStartService() {
|
||||
if (flClashService == null) {
|
||||
bindService()
|
||||
return
|
||||
@@ -237,24 +258,25 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
flutterMethodChannel.invokeMethod(
|
||||
"started", fd
|
||||
)
|
||||
startForegroundJob();
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
fun handleStop() {
|
||||
GlobalState.runLock.withLock {
|
||||
if (GlobalState.runState.value == RunState.STOP) return
|
||||
GlobalState.runState.value = RunState.STOP
|
||||
stopForegroundJob()
|
||||
flClashService?.stop()
|
||||
GlobalState.handleTryDestroy()
|
||||
}
|
||||
GlobalState.destroyServiceEngine()
|
||||
}
|
||||
|
||||
private fun bindService() {
|
||||
val intent = when (options.enable) {
|
||||
true -> Intent(context, FlClashVpnService::class.java)
|
||||
false -> Intent(context, FlClashService::class.java)
|
||||
true -> Intent(FlClashApplication.getAppContext(), FlClashVpnService::class.java)
|
||||
false -> Intent(FlClashApplication.getAppContext(), FlClashService::class.java)
|
||||
}
|
||||
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
FlClashApplication.getAppContext().bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.follow.clash
|
||||
|
||||
package com.follow.clash.services
|
||||
|
||||
import com.follow.clash.models.VpnOptions
|
||||
|
||||
interface BaseServiceInterface {
|
||||
|
||||
fun start(options: VpnOptions): Int
|
||||
|
||||
fun stop()
|
||||
fun startForeground(title: String, content: String)
|
||||
|
||||
suspend fun startForeground(title: String, content: String)
|
||||
}
|
||||
@@ -12,14 +12,14 @@ import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.follow.clash.BaseServiceInterface
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.MainActivity
|
||||
import com.follow.clash.extensions.getActionPendingIntent
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.async
|
||||
|
||||
|
||||
class FlClashService : Service(), BaseServiceInterface {
|
||||
@@ -42,44 +42,54 @@ class FlClashService : Service(), BaseServiceInterface {
|
||||
|
||||
private val notificationId: Int = 1
|
||||
|
||||
private val notificationBuilder: NotificationCompat.Builder by lazy {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy {
|
||||
CoroutineScope(Dispatchers.Main).async {
|
||||
val stopText = GlobalState.getText("stop")
|
||||
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
val intent = Intent(
|
||||
this@FlClashService, MainActivity::class.java
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
with(NotificationCompat.Builder(this, CHANNEL)) {
|
||||
setSmallIcon(com.follow.clash.R.drawable.ic_stat_name)
|
||||
setContentTitle("FlClash")
|
||||
setContentIntent(pendingIntent)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
priority = NotificationCompat.PRIORITY_MIN
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this@FlClashService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this@FlClashService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
|
||||
with(NotificationCompat.Builder(this@FlClashService, CHANNEL)) {
|
||||
setSmallIcon(com.follow.clash.R.drawable.ic_stat_name)
|
||||
setContentTitle("FlClash")
|
||||
setContentIntent(pendingIntent)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
priority = NotificationCompat.PRIORITY_MIN
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||
}
|
||||
addAction(
|
||||
0,
|
||||
stopText, // 使用 suspend 函数获取的文本
|
||||
getActionPendingIntent("STOP")
|
||||
)
|
||||
setOngoing(true)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
addAction(
|
||||
0,
|
||||
GlobalState.getText("stop"),
|
||||
getActionPendingIntent("STOP")
|
||||
)
|
||||
setOngoing(true)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
}
|
||||
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
|
||||
return notificationBuilderDeferred.await()
|
||||
}
|
||||
|
||||
override fun start(options: VpnOptions) = 0
|
||||
|
||||
@@ -91,24 +101,24 @@ class FlClashService : Service(), BaseServiceInterface {
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
||||
override fun startForeground(title: String, content: String) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
var channel = manager?.getNotificationChannel(CHANNEL)
|
||||
if (channel == null) {
|
||||
channel =
|
||||
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
||||
manager?.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
val notification =
|
||||
notificationBuilder.setContentTitle(title).setContentText(content).build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(notificationId, notification)
|
||||
override suspend fun startForeground(title: String, content: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
var channel = manager?.getNotificationChannel(CHANNEL)
|
||||
if (channel == null) {
|
||||
channel =
|
||||
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
||||
manager?.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
val notification =
|
||||
getNotificationBuilder()
|
||||
.setContentTitle(title)
|
||||
.setContentText(content).build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ class FlClashTileService : TileService() {
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
activityTransfer()
|
||||
GlobalState.handleToggle(applicationContext)
|
||||
GlobalState.handleToggle()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
import android.net.Network
|
||||
import android.net.ProxyInfo
|
||||
import android.net.VpnService
|
||||
import android.os.Binder
|
||||
@@ -17,7 +16,6 @@ import android.os.Parcel
|
||||
import android.os.RemoteException
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.follow.clash.BaseServiceInterface
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.MainActivity
|
||||
import com.follow.clash.R
|
||||
@@ -28,14 +26,16 @@ import com.follow.clash.extensions.toCIDR
|
||||
import com.follow.clash.models.AccessControlMode
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
GlobalState.initServiceEngine(applicationContext)
|
||||
GlobalState.initServiceEngine()
|
||||
}
|
||||
|
||||
override fun start(options: VpnOptions): Int {
|
||||
@@ -105,12 +105,6 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateUnderlyingNetworks(networks: Array<Network>) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
this.setUnderlyingNetworks(networks)
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
stopSelf()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
@@ -122,69 +116,74 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
|
||||
private val notificationId: Int = 1
|
||||
|
||||
private val notificationBuilder: NotificationCompat.Builder by lazy {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy {
|
||||
CoroutineScope(Dispatchers.Main).async {
|
||||
val stopText = GlobalState.getText("stop")
|
||||
val intent = Intent(this@FlClashVpnService, MainActivity::class.java)
|
||||
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
|
||||
with(NotificationCompat.Builder(this, CHANNEL)) {
|
||||
setSmallIcon(R.drawable.ic_stat_name)
|
||||
setContentTitle("FlClash")
|
||||
setContentIntent(pendingIntent)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
priority = NotificationCompat.PRIORITY_MIN
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this@FlClashVpnService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this@FlClashVpnService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
|
||||
with(NotificationCompat.Builder(this@FlClashVpnService, CHANNEL)) {
|
||||
setSmallIcon(R.drawable.ic_stat_name)
|
||||
setContentTitle("FlClash")
|
||||
setContentIntent(pendingIntent)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
priority = NotificationCompat.PRIORITY_MIN
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||
}
|
||||
setOngoing(true)
|
||||
addAction(
|
||||
0,
|
||||
stopText,
|
||||
getActionPendingIntent("STOP")
|
||||
)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
setOngoing(true)
|
||||
addAction(
|
||||
0,
|
||||
GlobalState.getText("stop"),
|
||||
getActionPendingIntent("STOP")
|
||||
)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
|
||||
return notificationBuilderDeferred.await()
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
||||
override fun startForeground(title: String, content: String) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
var channel = manager?.getNotificationChannel(CHANNEL)
|
||||
if (channel == null) {
|
||||
channel =
|
||||
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
||||
manager?.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
val notification =
|
||||
notificationBuilder
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(notificationId, notification)
|
||||
override suspend fun startForeground(title: String, content: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
var channel = manager?.getNotificationChannel(CHANNEL)
|
||||
if (channel == null) {
|
||||
channel =
|
||||
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
||||
manager?.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
val notification =
|
||||
getNotificationBuilder()
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
|
||||
Submodule core/Clash.Meta updated: f7c61f885c...0c03d8e4b4
171
core/action.go
171
core/action.go
@@ -1,32 +1,167 @@
|
||||
//go:build !cgo
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
func (action Action) Json() ([]byte, error) {
|
||||
data, err := json.Marshal(action)
|
||||
type Action struct {
|
||||
Id string `json:"id"`
|
||||
Method Method `json:"method"`
|
||||
Data interface{} `json:"data"`
|
||||
DefaultValue interface{} `json:"default-value"`
|
||||
}
|
||||
|
||||
type ActionResult struct {
|
||||
Id string `json:"id"`
|
||||
Method Method `json:"method"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func (result ActionResult) Json() ([]byte, error) {
|
||||
data, err := json.Marshal(result)
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (action Action) callback(data interface{}) bool {
|
||||
if conn == nil {
|
||||
return false
|
||||
}
|
||||
sendAction := Action{
|
||||
func (action Action) wrapMessage(data interface{}) []byte {
|
||||
sendAction := ActionResult{
|
||||
Id: action.Id,
|
||||
Method: action.Method,
|
||||
Data: data,
|
||||
}
|
||||
res, err := sendAction.Json()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, err = conn.Write(append(res, []byte("\n")...))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
res, _ := sendAction.Json()
|
||||
return res
|
||||
}
|
||||
|
||||
func handleAction(action *Action, send func([]byte)) {
|
||||
switch action.Method {
|
||||
case initClashMethod:
|
||||
data := action.Data.(string)
|
||||
send(action.wrapMessage(handleInitClash(data)))
|
||||
return
|
||||
case getIsInitMethod:
|
||||
send(action.wrapMessage(handleGetIsInit()))
|
||||
return
|
||||
case forceGcMethod:
|
||||
handleForceGc()
|
||||
send(action.wrapMessage(true))
|
||||
return
|
||||
case shutdownMethod:
|
||||
send(action.wrapMessage(handleShutdown()))
|
||||
return
|
||||
case validateConfigMethod:
|
||||
data := []byte(action.Data.(string))
|
||||
send(action.wrapMessage(handleValidateConfig(data)))
|
||||
return
|
||||
case updateConfigMethod:
|
||||
data := []byte(action.Data.(string))
|
||||
send(action.wrapMessage(handleUpdateConfig(data)))
|
||||
return
|
||||
case getProxiesMethod:
|
||||
send(action.wrapMessage(handleGetProxies()))
|
||||
return
|
||||
case changeProxyMethod:
|
||||
data := action.Data.(string)
|
||||
handleChangeProxy(data, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
})
|
||||
return
|
||||
case getTrafficMethod:
|
||||
send(action.wrapMessage(handleGetTraffic()))
|
||||
return
|
||||
case getTotalTrafficMethod:
|
||||
send(action.wrapMessage(handleGetTotalTraffic()))
|
||||
return
|
||||
case resetTrafficMethod:
|
||||
handleResetTraffic()
|
||||
send(action.wrapMessage(true))
|
||||
return
|
||||
case asyncTestDelayMethod:
|
||||
data := action.Data.(string)
|
||||
handleAsyncTestDelay(data, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
})
|
||||
return
|
||||
case getConnectionsMethod:
|
||||
send(action.wrapMessage(handleGetConnections()))
|
||||
return
|
||||
case closeConnectionsMethod:
|
||||
send(action.wrapMessage(handleCloseConnections()))
|
||||
return
|
||||
case closeConnectionMethod:
|
||||
id := action.Data.(string)
|
||||
send(action.wrapMessage(handleCloseConnection(id)))
|
||||
return
|
||||
case getExternalProvidersMethod:
|
||||
send(action.wrapMessage(handleGetExternalProviders()))
|
||||
return
|
||||
case getExternalProviderMethod:
|
||||
externalProviderName := action.Data.(string)
|
||||
send(action.wrapMessage(handleGetExternalProvider(externalProviderName)))
|
||||
case updateGeoDataMethod:
|
||||
paramsString := action.Data.(string)
|
||||
var params = map[string]string{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
send(action.wrapMessage(err.Error()))
|
||||
return
|
||||
}
|
||||
geoType := params["geo-type"]
|
||||
geoName := params["geo-name"]
|
||||
handleUpdateGeoData(geoType, geoName, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
})
|
||||
return
|
||||
case updateExternalProviderMethod:
|
||||
providerName := action.Data.(string)
|
||||
handleUpdateExternalProvider(providerName, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
})
|
||||
return
|
||||
case sideLoadExternalProviderMethod:
|
||||
paramsString := action.Data.(string)
|
||||
var params = map[string]string{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
send(action.wrapMessage(err.Error()))
|
||||
return
|
||||
}
|
||||
providerName := params["providerName"]
|
||||
data := params["data"]
|
||||
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
})
|
||||
return
|
||||
case startLogMethod:
|
||||
handleStartLog()
|
||||
send(action.wrapMessage(true))
|
||||
return
|
||||
case stopLogMethod:
|
||||
handleStopLog()
|
||||
send(action.wrapMessage(true))
|
||||
return
|
||||
case startListenerMethod:
|
||||
send(action.wrapMessage(handleStartListener()))
|
||||
return
|
||||
case stopListenerMethod:
|
||||
send(action.wrapMessage(handleStopListener()))
|
||||
return
|
||||
case getCountryCodeMethod:
|
||||
ip := action.Data.(string)
|
||||
handleGetCountryCode(ip, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
})
|
||||
return
|
||||
case getMemoryMethod:
|
||||
handleGetMemory(func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
})
|
||||
return
|
||||
default:
|
||||
handle := nextHandle(action, send)
|
||||
if handle {
|
||||
return
|
||||
} else {
|
||||
send(action.wrapMessage(action.DefaultValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/metacubex/mihomo/adapter"
|
||||
@@ -25,7 +24,6 @@ import (
|
||||
"github.com/samber/lo"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
@@ -43,11 +41,6 @@ func (a ExternalProviders) Len() int { return len(a) }
|
||||
func (a ExternalProviders) Less(i, j int) bool { return a[i].Name < a[j].Name }
|
||||
func (a ExternalProviders) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
|
||||
func (message *Message) Json() (string, error) {
|
||||
data, err := json.Marshal(message)
|
||||
return string(data), err
|
||||
}
|
||||
|
||||
func readFile(path string) ([]byte, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil, err
|
||||
@@ -86,16 +79,6 @@ func getRawConfigWithId(id string) *config.RawConfig {
|
||||
continue
|
||||
}
|
||||
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
|
||||
if configParams.TestURL != nil {
|
||||
if mapping["health-check"] != nil {
|
||||
hc := mapping["health-check"].(map[string]any)
|
||||
if hc != nil {
|
||||
if hc["url"] != nil {
|
||||
hc["url"] = *configParams.TestURL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, mapping := range prof.RuleProvider {
|
||||
value, exist := mapping["path"].(string)
|
||||
@@ -352,8 +335,6 @@ func applyConfig(rawConfig *config.RawConfig) error {
|
||||
if configParams.IsPatch {
|
||||
patchConfig()
|
||||
} else {
|
||||
handleCloseConnectionsUnLock()
|
||||
runtime.GC()
|
||||
hub.ApplyConfig(currentConfig)
|
||||
patchSelectGroup()
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/metacubex/mihomo/adapter/provider"
|
||||
"github.com/metacubex/mihomo/config"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigExtendedParams struct {
|
||||
IsPatch bool `json:"is-patch"`
|
||||
IsCompatible bool `json:"is-compatible"`
|
||||
SelectedMap map[string]string `json:"selected-map"`
|
||||
TestURL *string `json:"test-url"`
|
||||
OverrideDns bool `json:"override-dns"`
|
||||
IsPatch bool `json:"is-patch"`
|
||||
IsCompatible bool `json:"is-compatible"`
|
||||
SelectedMap map[string]string `json:"selected-map"`
|
||||
TestURL *string `json:"test-url"`
|
||||
OverrideDns bool `json:"override-dns"`
|
||||
OnlyStatisticsProxy bool `json:"only-statistics-proxy"`
|
||||
}
|
||||
|
||||
type GenerateConfigParams struct {
|
||||
@@ -28,14 +29,10 @@ type ChangeProxyParams struct {
|
||||
|
||||
type TestDelayParams struct {
|
||||
ProxyName string `json:"proxy-name"`
|
||||
TestUrl string `json:"test-url"`
|
||||
Timeout int64 `json:"timeout"`
|
||||
}
|
||||
|
||||
type ProcessMapItem struct {
|
||||
Id int64 `json:"id"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type ExternalProvider struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
@@ -74,19 +71,23 @@ const (
|
||||
stopLogMethod Method = "stopLog"
|
||||
startListenerMethod Method = "startListener"
|
||||
stopListenerMethod Method = "stopListener"
|
||||
startTunMethod Method = "startTun"
|
||||
stopTunMethod Method = "stopTun"
|
||||
updateDnsMethod Method = "updateDns"
|
||||
setProcessMapMethod Method = "setProcessMap"
|
||||
setFdMapMethod Method = "setFdMap"
|
||||
setStateMethod Method = "setState"
|
||||
getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions"
|
||||
getRunTimeMethod Method = "getRunTime"
|
||||
getCurrentProfileNameMethod Method = "getCurrentProfileName"
|
||||
)
|
||||
|
||||
type Method string
|
||||
|
||||
type Action struct {
|
||||
Id string `json:"id"`
|
||||
Method Method `json:"method"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type MessageType string
|
||||
|
||||
type Delay struct {
|
||||
Url string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
Value int32 `json:"value"`
|
||||
}
|
||||
@@ -96,17 +97,31 @@ type Message struct {
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type Process struct {
|
||||
Id int64 `json:"id"`
|
||||
Metadata *constant.Metadata `json:"metadata"`
|
||||
}
|
||||
|
||||
const (
|
||||
LogMessage MessageType = "log"
|
||||
ProtectMessage MessageType = "protect"
|
||||
DelayMessage MessageType = "delay"
|
||||
ProcessMessage MessageType = "process"
|
||||
RequestMessage MessageType = "request"
|
||||
StartedMessage MessageType = "started"
|
||||
LoadedMessage MessageType = "loaded"
|
||||
)
|
||||
|
||||
func (message *Message) Json() (string, error) {
|
||||
data, err := json.Marshal(message)
|
||||
return string(data), err
|
||||
}
|
||||
|
||||
type InvokeMessage struct {
|
||||
Type InvokeType `json:"type"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type InvokeType string
|
||||
|
||||
const (
|
||||
ProtectInvoke InvokeType = "protect"
|
||||
ProcessInvoke InvokeType = "process"
|
||||
)
|
||||
|
||||
func (message *InvokeMessage) Json() string {
|
||||
data, _ := json.Marshal(message)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
64
core/go.mod
64
core/go.mod
@@ -1,21 +1,14 @@
|
||||
module core
|
||||
|
||||
go 1.21
|
||||
go 1.20
|
||||
|
||||
replace github.com/metacubex/mihomo => ./Clash.Meta
|
||||
|
||||
require github.com/metacubex/mihomo v1.17.1
|
||||
|
||||
require (
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
|
||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
|
||||
github.com/sagernet/cors v1.2.1 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000
|
||||
github.com/samber/lo v1.47.0
|
||||
)
|
||||
|
||||
replace github.com/sagernet/sing => github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297
|
||||
|
||||
require (
|
||||
github.com/3andne/restls-client-go v0.1.6 // indirect
|
||||
github.com/RyuaNerin/go-krypto v1.2.4 // indirect
|
||||
@@ -27,13 +20,15 @@ require (
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/coreos/go-iptables v0.8.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/ebitengine/purego v0.8.1 // indirect
|
||||
github.com/enfein/mieru/v3 v3.10.0 // indirect
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
|
||||
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gaukas/godicttls v0.0.4 // indirect
|
||||
github.com/go-chi/chi/v5 v5.1.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.0 // indirect
|
||||
github.com/go-chi/render v1.0.3 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
@@ -41,33 +36,35 @@ require (
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.3.0 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475 // indirect
|
||||
github.com/hashicorp/yamux v0.1.2 // indirect
|
||||
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
|
||||
github.com/metacubex/chacha v0.1.0 // indirect
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
|
||||
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec // indirect
|
||||
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4 // indirect
|
||||
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a // indirect
|
||||
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da // indirect
|
||||
github.com/metacubex/randv2 v0.2.0 // indirect
|
||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 // indirect
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1 // indirect
|
||||
github.com/metacubex/sing-tun v0.4.5 // indirect
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 // indirect
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3 // indirect
|
||||
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa // indirect
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect
|
||||
github.com/metacubex/utls v1.6.6 // indirect
|
||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
|
||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
|
||||
@@ -79,16 +76,15 @@ require (
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||
github.com/sagernet/cors v1.2.1 // indirect
|
||||
github.com/sagernet/fswatch v0.1.1 // indirect
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
|
||||
github.com/sagernet/sing v0.5.0-alpha.13 // indirect
|
||||
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 // indirect
|
||||
github.com/sagernet/sing-shadowtls v0.1.4 // indirect
|
||||
github.com/sagernet/sing v0.5.1 // indirect
|
||||
github.com/sagernet/sing-mux v0.2.1 // indirect
|
||||
github.com/sagernet/sing-shadowtls v0.1.5 // indirect
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
|
||||
github.com/samber/lo v1.47.0
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.24.11 // indirect
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
|
||||
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
|
||||
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
|
||||
@@ -97,22 +93,24 @@ require (
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // indirect
|
||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/crypto v0.28.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect; indirect`
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
)
|
||||
|
||||
109
core/go.sum
109
core/go.sum
@@ -26,12 +26,15 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
|
||||
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/enfein/mieru/v3 v3.10.0 h1:KMnAtY4s8MB74sUg4GbvF9R9v3jkXPQTSkxPxl1emxQ=
|
||||
github.com/enfein/mieru/v3 v3.10.0/go.mod h1:jH2nXzJSNUn6UWuzD8E8AsRVa9Ca0CqcTcr9Z+CJO1o=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po=
|
||||
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg=
|
||||
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c/go.mod h1:ETASDWf/FmEb6Ysrtd1QhjNedUU/ZQxBCRLh60bQ/UI=
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY=
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4=
|
||||
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA=
|
||||
@@ -40,12 +43,11 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
|
||||
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
|
||||
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
@@ -61,29 +63,27 @@ github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk=
|
||||
github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
|
||||
github.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7spovjlY=
|
||||
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
|
||||
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475 h1:hxST5pwMBEOWmxpkX20w9oZG+hXdhKmAIPQ3NGGAxas=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d h1:VkCNWh6tuQLgDBc6KrUOz/L1mCUQGnR1Ujj8uTgpwwk=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
|
||||
@@ -102,28 +102,26 @@ github.com/metacubex/chacha v0.1.0 h1:tg9RSJ18NvL38cCWNyYH1eiG6qDCyyXIaTLQthon0s
|
||||
github.com/metacubex/chacha v0.1.0/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
|
||||
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec h1:HxreOiFTUrJXJautEo8rnE1uKTVGY8wtZepY1Tii/Nc=
|
||||
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec/go.mod h1:8BVmQ+3cxjqzWElafm24rb2Ae4jRI6vAXNXWqWjfrXw=
|
||||
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4 h1:CgdUBRxmNlxEGkp35HwvgQ10jwOOUJKWdOxpi8yWi8o=
|
||||
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4/go.mod h1:Y7yRGqFE6UQL/3aKPYmiYdjfVkeujJaStP4+jiZMcN8=
|
||||
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a h1:cZ6oNVrsmsi3SNlnSnRio4zOgtQq+/XidwsaNgKICcg=
|
||||
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a/go.mod h1:xBw/SYJPgUMPQ1tklV/brGn2nxhfr3BnvBzNlyi4Nic=
|
||||
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da h1:Mq6cbHbPTLLTUfA9scrwBmOGkvl6y99E3WmtMIMqo30=
|
||||
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da/go.mod h1:AiZ+UPgrkO1DTnmiAX4b+kRoV1Vfc65UkYD7RbFlIZA=
|
||||
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
||||
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
||||
github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297 h1:YG/JkwGPbca5rUtEMHIu8ZuqzR7BSVm1iqY8hNoMeMA=
|
||||
github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 h1:HobpULaPK6OoxrHMmgcwLkwwIduXVmwdcznwUfH1GQM=
|
||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1 h1:ypfofGDZbP8p3Y4P/m74JYu7sQViesi3c8nbmT6cS0Y=
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1/go.mod h1:olbEx9yVcaw5tHTNlRamRoxmMKcvDvcVS1YLnQGzvWE=
|
||||
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0=
|
||||
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3 h1:xg71VmzLS6ByAzi/57phwDvjE+dLLs+ozH00k4DnOns=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3/go.mod h1:6nitcmzPDL3MXnLdhu6Hm126Zk4S1fBbX3P7jxUxSFw=
|
||||
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa h1:9mcjV+RGZVC3reJBNDjjNPyS8PmFG97zq56X7WNaFO4=
|
||||
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa/go.mod h1:4tLB5c8U0CxpkFM+AJJB77jEaVDbLH5XQvy42vAGsWw=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc=
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
|
||||
github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8=
|
||||
github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo=
|
||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
|
||||
@@ -137,7 +135,6 @@ github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:U
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
|
||||
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||
github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0=
|
||||
github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo=
|
||||
github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0=
|
||||
@@ -166,20 +163,19 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
|
||||
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
|
||||
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 h1:5bCAkvDDzSMITiHFjolBwpdqYsvycdTu71FsMEFXQ14=
|
||||
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ=
|
||||
github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
|
||||
github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4=
|
||||
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
|
||||
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
|
||||
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
|
||||
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
|
||||
github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
|
||||
github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4=
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
|
||||
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8=
|
||||
github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8=
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=
|
||||
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk=
|
||||
@@ -189,17 +185,10 @@ github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
@@ -229,8 +218,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
@@ -239,11 +228,11 @@ golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -255,20 +244,18 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
|
||||
44
core/hub.go
44
core/hub.go
@@ -26,8 +26,10 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
isInit = false
|
||||
configParams = ConfigExtendedParams{}
|
||||
isInit = false
|
||||
configParams = ConfigExtendedParams{
|
||||
OnlyStatisticsProxy: false,
|
||||
}
|
||||
externalProviders = map[string]cp.Provider{}
|
||||
logSubscriber observable.Subscription[log.Event]
|
||||
currentConfig *config.Config
|
||||
@@ -149,8 +151,8 @@ func handleChangeProxy(data string, fn func(string string)) {
|
||||
}()
|
||||
}
|
||||
|
||||
func handleGetTraffic(onlyProxy bool) string {
|
||||
up, down := statistic.DefaultManager.Current(onlyProxy)
|
||||
func handleGetTraffic() string {
|
||||
up, down := statistic.DefaultManager.Current(configParams.OnlyStatisticsProxy)
|
||||
traffic := map[string]int64{
|
||||
"up": up,
|
||||
"down": down,
|
||||
@@ -163,8 +165,8 @@ func handleGetTraffic(onlyProxy bool) string {
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func handleGetTotalTraffic(onlyProxy bool) string {
|
||||
up, down := statistic.DefaultManager.Total(onlyProxy)
|
||||
func handleGetTotalTraffic() string {
|
||||
up, down := statistic.DefaultManager.Total(configParams.OnlyStatisticsProxy)
|
||||
traffic := map[string]int64{
|
||||
"up": up,
|
||||
"down": down,
|
||||
@@ -213,7 +215,13 @@ func handleAsyncTestDelay(paramsString string, fn func(string)) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus)
|
||||
testUrl := constant.DefaultTestURL
|
||||
|
||||
if params.TestUrl != "" {
|
||||
testUrl = params.TestUrl
|
||||
}
|
||||
|
||||
delay, err := proxy.URLTest(ctx, testUrl, expectedStatus)
|
||||
if err != nil || delay == 0 {
|
||||
delayData.Value = -1
|
||||
data, _ := json.Marshal(delayData)
|
||||
@@ -240,17 +248,6 @@ func handleGetConnections() string {
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func handleCloseConnectionsUnLock() bool {
|
||||
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
|
||||
err := c.Close()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
func handleCloseConnections() bool {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
@@ -395,7 +392,7 @@ func handleStartLog() {
|
||||
Type: LogMessage,
|
||||
Data: logData,
|
||||
}
|
||||
SendMessage(*message)
|
||||
sendMessage(*message)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -427,8 +424,9 @@ func handleGetMemory(fn func(value string)) {
|
||||
}
|
||||
|
||||
func init() {
|
||||
adapter.UrlTestHook = func(name string, delay uint16) {
|
||||
adapter.UrlTestHook = func(url string, name string, delay uint16) {
|
||||
delayData := &Delay{
|
||||
Url: url,
|
||||
Name: name,
|
||||
}
|
||||
if delay == 0 {
|
||||
@@ -436,19 +434,19 @@ func init() {
|
||||
} else {
|
||||
delayData.Value = int32(delay)
|
||||
}
|
||||
SendMessage(Message{
|
||||
sendMessage(Message{
|
||||
Type: DelayMessage,
|
||||
Data: delayData,
|
||||
})
|
||||
}
|
||||
statistic.DefaultRequestNotify = func(c statistic.Tracker) {
|
||||
SendMessage(Message{
|
||||
sendMessage(Message{
|
||||
Type: RequestMessage,
|
||||
Data: c,
|
||||
})
|
||||
}
|
||||
executor.DefaultProviderLoadedHook = func(providerName string) {
|
||||
SendMessage(Message{
|
||||
sendMessage(Message{
|
||||
Type: LoadedMessage,
|
||||
Data: providerName,
|
||||
})
|
||||
|
||||
205
core/lib.go
205
core/lib.go
@@ -8,18 +8,30 @@ package main
|
||||
import "C"
|
||||
import (
|
||||
bridge "core/dart-bridge"
|
||||
"encoding/json"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var messagePort int64 = -1
|
||||
|
||||
//export initNativeApiBridge
|
||||
func initNativeApiBridge(api unsafe.Pointer) {
|
||||
bridge.InitDartApi(api)
|
||||
}
|
||||
|
||||
//export initMessage
|
||||
func initMessage(port C.longlong) {
|
||||
i := int64(port)
|
||||
Port = i
|
||||
//export attachMessagePort
|
||||
func attachMessagePort(mPort C.longlong) {
|
||||
messagePort = int64(mPort)
|
||||
}
|
||||
|
||||
//export getTraffic
|
||||
func getTraffic() *C.char {
|
||||
return C.CString(handleGetTraffic())
|
||||
}
|
||||
|
||||
//export getTotalTraffic
|
||||
func getTotalTraffic() *C.char {
|
||||
return C.CString(handleGetTotalTraffic())
|
||||
}
|
||||
|
||||
//export freeCString
|
||||
@@ -27,9 +39,32 @@ func freeCString(s *C.char) {
|
||||
C.free(unsafe.Pointer(s))
|
||||
}
|
||||
|
||||
//export initClash
|
||||
func initClash(homeDirStr *C.char) bool {
|
||||
return handleInitClash(C.GoString(homeDirStr))
|
||||
//export invokeAction
|
||||
func invokeAction(paramsChar *C.char, port C.longlong) {
|
||||
params := C.GoString(paramsChar)
|
||||
i := int64(port)
|
||||
var action = &Action{}
|
||||
err := json.Unmarshal([]byte(params), action)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
go handleAction(action, func(bytes []byte) {
|
||||
bridge.SendToPort(i, string(bytes))
|
||||
})
|
||||
}
|
||||
|
||||
func sendMessage(message Message) {
|
||||
if messagePort == -1 {
|
||||
return
|
||||
}
|
||||
res, err := message.Json()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
bridge.SendToPort(messagePort, string(Action{
|
||||
Method: messageMethod,
|
||||
}.wrapMessage(res)))
|
||||
}
|
||||
|
||||
//export startListener
|
||||
@@ -41,159 +76,3 @@ func startListener() {
|
||||
func stopListener() {
|
||||
handleStopListener()
|
||||
}
|
||||
|
||||
//export getIsInit
|
||||
func getIsInit() bool {
|
||||
return handleGetIsInit()
|
||||
}
|
||||
|
||||
//export shutdownClash
|
||||
func shutdownClash() bool {
|
||||
return handleShutdown()
|
||||
}
|
||||
|
||||
//export forceGc
|
||||
func forceGc() {
|
||||
handleForceGc()
|
||||
}
|
||||
|
||||
//export validateConfig
|
||||
func validateConfig(s *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
bytes := []byte(C.GoString(s))
|
||||
go func() {
|
||||
bridge.SendToPort(i, handleValidateConfig(bytes))
|
||||
}()
|
||||
}
|
||||
|
||||
//export updateConfig
|
||||
func updateConfig(s *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
bytes := []byte(C.GoString(s))
|
||||
go func() {
|
||||
bridge.SendToPort(i, handleUpdateConfig(bytes))
|
||||
}()
|
||||
}
|
||||
|
||||
//export getProxies
|
||||
func getProxies() *C.char {
|
||||
return C.CString(handleGetProxies())
|
||||
}
|
||||
|
||||
//export changeProxy
|
||||
func changeProxy(s *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
paramsString := C.GoString(s)
|
||||
handleChangeProxy(paramsString, func(value string) {
|
||||
bridge.SendToPort(i, value)
|
||||
})
|
||||
}
|
||||
|
||||
//export getTraffic
|
||||
func getTraffic(port C.int) *C.char {
|
||||
onlyProxy := int(port) == 1
|
||||
return C.CString(handleGetTraffic(onlyProxy))
|
||||
}
|
||||
|
||||
//export getTotalTraffic
|
||||
func getTotalTraffic(port C.int) *C.char {
|
||||
onlyProxy := int(port) == 1
|
||||
return C.CString(handleGetTotalTraffic(onlyProxy))
|
||||
}
|
||||
|
||||
//export resetTraffic
|
||||
func resetTraffic() {
|
||||
handleResetTraffic()
|
||||
}
|
||||
|
||||
//export asyncTestDelay
|
||||
func asyncTestDelay(s *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
paramsString := C.GoString(s)
|
||||
handleAsyncTestDelay(paramsString, func(value string) {
|
||||
bridge.SendToPort(i, value)
|
||||
})
|
||||
}
|
||||
|
||||
//export getConnections
|
||||
func getConnections() *C.char {
|
||||
return C.CString(handleGetConnections())
|
||||
}
|
||||
|
||||
//export 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 (
|
||||
bridge "core/dart-bridge"
|
||||
"core/platform"
|
||||
"core/state"
|
||||
t "core/tun"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
"github.com/metacubex/mihomo/component/process"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
@@ -19,123 +21,165 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ProcessMap struct {
|
||||
m sync.Map
|
||||
}
|
||||
|
||||
type FdMap struct {
|
||||
m sync.Map
|
||||
}
|
||||
|
||||
type Fd struct {
|
||||
Id int64 `json:"id"`
|
||||
Value int64 `json:"value"`
|
||||
Id string `json:"id"`
|
||||
Value int64 `json:"value"`
|
||||
}
|
||||
|
||||
type Process struct {
|
||||
Id string `json:"id"`
|
||||
Metadata *constant.Metadata `json:"metadata"`
|
||||
}
|
||||
|
||||
type ProcessMapItem struct {
|
||||
Id string `json:"id"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type InvokeManager struct {
|
||||
invokeMap sync.Map
|
||||
chanMap map[string]chan struct{}
|
||||
chanLock sync.Mutex
|
||||
}
|
||||
|
||||
func NewInvokeManager() *InvokeManager {
|
||||
return &InvokeManager{
|
||||
chanMap: make(map[string]chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *InvokeManager) load(id string) string {
|
||||
res, ok := m.invokeMap.Load(id)
|
||||
if ok {
|
||||
return res.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *InvokeManager) delete(id string) {
|
||||
m.invokeMap.Delete(id)
|
||||
}
|
||||
|
||||
func (m *InvokeManager) completer(id string, value string) {
|
||||
m.invokeMap.Store(id, value)
|
||||
m.chanLock.Lock()
|
||||
if ch, ok := m.chanMap[id]; ok {
|
||||
close(ch)
|
||||
delete(m.chanMap, id)
|
||||
}
|
||||
m.chanLock.Unlock()
|
||||
}
|
||||
|
||||
func (m *InvokeManager) await(id string) {
|
||||
m.chanLock.Lock()
|
||||
if _, ok := m.chanMap[id]; !ok {
|
||||
m.chanMap[id] = make(chan struct{})
|
||||
}
|
||||
ch := m.chanMap[id]
|
||||
m.chanLock.Unlock()
|
||||
|
||||
timeout := time.After(500 * time.Millisecond)
|
||||
select {
|
||||
case <-ch:
|
||||
return
|
||||
case <-timeout:
|
||||
m.completer(id, "")
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
tunListener *sing_tun.Listener
|
||||
fdMap FdMap
|
||||
fdCounter int64 = 0
|
||||
counter int64 = 0
|
||||
processMap ProcessMap
|
||||
tunLock sync.Mutex
|
||||
runTime *time.Time
|
||||
errBlocked = errors.New("blocked")
|
||||
invokePort int64 = -1
|
||||
tunListener *sing_tun.Listener
|
||||
fdInvokeMap = NewInvokeManager()
|
||||
processInvokeMap = NewInvokeManager()
|
||||
tunLock sync.Mutex
|
||||
runTime *time.Time
|
||||
errBlocked = errors.New("blocked")
|
||||
)
|
||||
|
||||
func (cm *ProcessMap) Store(key int64, value string) {
|
||||
cm.m.Store(key, value)
|
||||
}
|
||||
|
||||
func (cm *ProcessMap) Load(key int64) (string, bool) {
|
||||
value, ok := cm.m.Load(key)
|
||||
if !ok || value == nil {
|
||||
return "", false
|
||||
}
|
||||
return value.(string), true
|
||||
}
|
||||
|
||||
func (cm *FdMap) Store(key int64) {
|
||||
cm.m.Store(key, struct{}{})
|
||||
}
|
||||
|
||||
func (cm *FdMap) Load(key int64) bool {
|
||||
_, ok := cm.m.Load(key)
|
||||
return ok
|
||||
}
|
||||
|
||||
//export startTUN
|
||||
func startTUN(fd C.int, port C.longlong) {
|
||||
i := int64(port)
|
||||
ServicePort = i
|
||||
func handleStartTun(fd int) string {
|
||||
handleStopTun()
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
if fd == 0 {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
now := time.Now()
|
||||
runTime = &now
|
||||
SendMessage(Message{
|
||||
Type: StartedMessage,
|
||||
Data: strconv.FormatInt(runTime.UnixMilli(), 10),
|
||||
})
|
||||
return
|
||||
}
|
||||
initSocketHook()
|
||||
go func() {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
f := int(fd)
|
||||
tunListener, _ = t.Start(f, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
|
||||
} else {
|
||||
initSocketHook()
|
||||
tunListener, _ = t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
|
||||
if tunListener != nil {
|
||||
log.Infoln("TUN address: %v", tunListener.Address())
|
||||
}
|
||||
now := time.Now()
|
||||
runTime = &now
|
||||
}()
|
||||
}
|
||||
|
||||
//export getRunTime
|
||||
func getRunTime() *C.char {
|
||||
if runTime == nil {
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(strconv.FormatInt(runTime.UnixMilli(), 10))
|
||||
return handleGetRunTime()
|
||||
}
|
||||
|
||||
//export stopTun
|
||||
func stopTun() {
|
||||
func handleStopTun() {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
removeSocketHook()
|
||||
go func() {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
|
||||
runTime = nil
|
||||
|
||||
if tunListener != nil {
|
||||
_ = tunListener.Close()
|
||||
}
|
||||
}()
|
||||
runTime = nil
|
||||
if tunListener != nil {
|
||||
log.Infoln("TUN close")
|
||||
_ = tunListener.Close()
|
||||
}
|
||||
}
|
||||
|
||||
//export setFdMap
|
||||
func setFdMap(fd C.long) {
|
||||
fdInt := int64(fd)
|
||||
go func() {
|
||||
fdMap.Store(fdInt)
|
||||
}()
|
||||
func handleGetRunTime() string {
|
||||
if runTime == nil {
|
||||
return ""
|
||||
}
|
||||
return strconv.FormatInt(runTime.UnixMilli(), 10)
|
||||
}
|
||||
|
||||
func markSocket(fd Fd) {
|
||||
SendMessage(Message{
|
||||
Type: ProtectMessage,
|
||||
func handleSetProcessMap(params string) {
|
||||
var processMapItem = &ProcessMapItem{}
|
||||
err := json.Unmarshal([]byte(params), processMapItem)
|
||||
if err == nil {
|
||||
processInvokeMap.completer(processMapItem.Id, processMapItem.Value)
|
||||
}
|
||||
}
|
||||
|
||||
//export attachInvokePort
|
||||
func attachInvokePort(mPort C.longlong) {
|
||||
invokePort = int64(mPort)
|
||||
}
|
||||
|
||||
func sendInvokeMessage(message InvokeMessage) {
|
||||
if invokePort == -1 {
|
||||
return
|
||||
}
|
||||
bridge.SendToPort(invokePort, message.Json())
|
||||
}
|
||||
|
||||
func handleMarkSocket(fd Fd) {
|
||||
sendInvokeMessage(InvokeMessage{
|
||||
Type: ProtectInvoke,
|
||||
Data: fd,
|
||||
})
|
||||
}
|
||||
|
||||
func handleParseProcess(process Process) {
|
||||
sendInvokeMessage(InvokeMessage{
|
||||
Type: ProcessInvoke,
|
||||
Data: process,
|
||||
})
|
||||
}
|
||||
|
||||
func handleSetFdMap(id string) {
|
||||
go func() {
|
||||
fdInvokeMap.completer(id, "")
|
||||
}()
|
||||
}
|
||||
|
||||
func initSocketHook() {
|
||||
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
|
||||
if platform.ShouldBlockConnection() {
|
||||
@@ -143,26 +187,15 @@ func initSocketHook() {
|
||||
}
|
||||
return conn.Control(func(fd uintptr) {
|
||||
fdInt := int64(fd)
|
||||
timeout := time.After(500 * time.Millisecond)
|
||||
id := atomic.AddInt64(&fdCounter, 1)
|
||||
id := utils.NewUUIDV1().String()
|
||||
|
||||
markSocket(Fd{
|
||||
handleMarkSocket(Fd{
|
||||
Id: id,
|
||||
Value: fdInt,
|
||||
})
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timeout:
|
||||
return
|
||||
default:
|
||||
exists := fdMap.Load(id)
|
||||
if exists {
|
||||
return
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
fdInvokeMap.await(id)
|
||||
fdInvokeMap.delete(id)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -176,58 +209,19 @@ func init() {
|
||||
if metadata == nil {
|
||||
return "", process.ErrInvalidNetwork
|
||||
}
|
||||
id := atomic.AddInt64(&counter, 1)
|
||||
|
||||
timeout := time.After(200 * time.Millisecond)
|
||||
|
||||
SendMessage(Message{
|
||||
Type: ProcessMessage,
|
||||
Data: Process{
|
||||
Id: id,
|
||||
Metadata: metadata,
|
||||
},
|
||||
id := utils.NewUUIDV1().String()
|
||||
handleParseProcess(Process{
|
||||
Id: id,
|
||||
Metadata: metadata,
|
||||
})
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timeout:
|
||||
return "", errors.New("package resolver timeout")
|
||||
default:
|
||||
value, exists := processMap.Load(id)
|
||||
if exists {
|
||||
return value, nil
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
processInvokeMap.await(id)
|
||||
res := processInvokeMap.load(id)
|
||||
processInvokeMap.delete(id)
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
//export setProcessMap
|
||||
func setProcessMap(s *C.char) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
paramsString := C.GoString(s)
|
||||
go func() {
|
||||
var processMapItem = &ProcessMapItem{}
|
||||
err := json.Unmarshal([]byte(paramsString), processMapItem)
|
||||
if err == nil {
|
||||
processMap.Store(processMapItem.Id, processMapItem.Value)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
//export getCurrentProfileName
|
||||
func getCurrentProfileName() *C.char {
|
||||
if state.CurrentState == nil {
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(state.CurrentState.CurrentProfileName)
|
||||
}
|
||||
|
||||
//export getAndroidVpnOptions
|
||||
func getAndroidVpnOptions() *C.char {
|
||||
func handleGetAndroidVpnOptions() string {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
options := state.AndroidVpnOptions{
|
||||
@@ -245,26 +239,140 @@ func getAndroidVpnOptions() *C.char {
|
||||
data, err := json.Marshal(options)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return C.CString("")
|
||||
return ""
|
||||
}
|
||||
return C.CString(string(data))
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func handleSetState(params string) {
|
||||
_ = json.Unmarshal([]byte(params), state.CurrentState)
|
||||
}
|
||||
|
||||
func handleUpdateDns(value string) {
|
||||
go func() {
|
||||
log.Infoln("[DNS] updateDns %s", value)
|
||||
dns.UpdateSystemDNS(strings.Split(value, ","))
|
||||
dns.FlushCacheWithDefaultResolver()
|
||||
}()
|
||||
}
|
||||
|
||||
func handleGetCurrentProfileName() string {
|
||||
if state.CurrentState == nil {
|
||||
return ""
|
||||
}
|
||||
return state.CurrentState.CurrentProfileName
|
||||
}
|
||||
|
||||
func nextHandle(action *Action, send func([]byte)) bool {
|
||||
switch action.Method {
|
||||
case startTunMethod:
|
||||
data := action.Data.(string)
|
||||
var fd int
|
||||
_ = json.Unmarshal([]byte(data), &fd)
|
||||
send(action.wrapMessage(handleStartTun(fd)))
|
||||
return true
|
||||
case stopTunMethod:
|
||||
handleStopTun()
|
||||
send(action.wrapMessage(true))
|
||||
return true
|
||||
case setStateMethod:
|
||||
data := action.Data.(string)
|
||||
handleSetState(data)
|
||||
send(action.wrapMessage(true))
|
||||
return true
|
||||
case getAndroidVpnOptionsMethod:
|
||||
send(action.wrapMessage(handleGetAndroidVpnOptions()))
|
||||
return true
|
||||
case updateDnsMethod:
|
||||
data := action.Data.(string)
|
||||
handleUpdateDns(data)
|
||||
send(action.wrapMessage(true))
|
||||
return true
|
||||
case setFdMapMethod:
|
||||
fdId := action.Data.(string)
|
||||
handleSetFdMap(fdId)
|
||||
send(action.wrapMessage(true))
|
||||
return true
|
||||
case setProcessMapMethod:
|
||||
data := action.Data.(string)
|
||||
handleSetProcessMap(data)
|
||||
send(action.wrapMessage(true))
|
||||
return true
|
||||
case getRunTimeMethod:
|
||||
send(action.wrapMessage(handleGetRunTime()))
|
||||
return true
|
||||
case getCurrentProfileNameMethod:
|
||||
send(action.wrapMessage(handleGetCurrentProfileName()))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//export quickStart
|
||||
func quickStart(dirChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
dir := C.GoString(dirChar)
|
||||
bytes := []byte(C.GoString(paramsChar))
|
||||
stateParams := C.GoString(stateParamsChar)
|
||||
go func() {
|
||||
res := handleInitClash(dir)
|
||||
if res == false {
|
||||
bridge.SendToPort(i, "init error")
|
||||
}
|
||||
handleSetState(stateParams)
|
||||
bridge.SendToPort(i, handleUpdateConfig(bytes))
|
||||
}()
|
||||
}
|
||||
|
||||
//export startTUN
|
||||
func startTUN(fd C.int) *C.char {
|
||||
f := int(fd)
|
||||
return C.CString(handleStartTun(f))
|
||||
}
|
||||
|
||||
//export getRunTime
|
||||
func getRunTime() *C.char {
|
||||
return C.CString(handleGetRunTime())
|
||||
}
|
||||
|
||||
//export stopTun
|
||||
func stopTun() {
|
||||
handleStopTun()
|
||||
}
|
||||
|
||||
//export setFdMap
|
||||
func setFdMap(fdIdChar *C.char) {
|
||||
fdId := C.GoString(fdIdChar)
|
||||
handleSetFdMap(fdId)
|
||||
}
|
||||
|
||||
//export getCurrentProfileName
|
||||
func getCurrentProfileName() *C.char {
|
||||
return C.CString(handleGetCurrentProfileName())
|
||||
}
|
||||
|
||||
//export getAndroidVpnOptions
|
||||
func getAndroidVpnOptions() *C.char {
|
||||
return C.CString(handleGetAndroidVpnOptions())
|
||||
}
|
||||
|
||||
//export setState
|
||||
func setState(s *C.char) {
|
||||
paramsString := C.GoString(s)
|
||||
err := json.Unmarshal([]byte(paramsString), state.CurrentState)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
handleSetState(paramsString)
|
||||
}
|
||||
|
||||
//export updateDns
|
||||
func updateDns(s *C.char) {
|
||||
dnsList := C.GoString(s)
|
||||
go func() {
|
||||
log.Infoln("[DNS] updateDns %s", dnsList)
|
||||
dns.UpdateSystemDNS(strings.Split(dnsList, ","))
|
||||
dns.FlushCacheWithDefaultResolver()
|
||||
}()
|
||||
handleUpdateDns(dnsList)
|
||||
}
|
||||
|
||||
//export setProcessMap
|
||||
func setProcessMap(s *C.char) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
paramsString := C.GoString(s)
|
||||
handleSetProcessMap(paramsString)
|
||||
}
|
||||
|
||||
11
core/lib_no_android.go
Normal file
11
core/lib_no_android.go
Normal file
@@ -0,0 +1,11 @@
|
||||
//go:build !android && cgo
|
||||
|
||||
package main
|
||||
|
||||
func nextHandle(action *Action) {
|
||||
return action
|
||||
}
|
||||
|
||||
func nextHandle(action *Action, send func([]byte)) bool {
|
||||
return false
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
//go:build !cgo
|
||||
|
||||
package main
|
||||
|
||||
func SendMessage(message Message) {
|
||||
s, err := message.Json()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
Action{
|
||||
Method: messageMethod,
|
||||
}.callback(s)
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
//go:build cgo
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
bridge "core/dart-bridge"
|
||||
)
|
||||
|
||||
var (
|
||||
Port int64 = -1
|
||||
ServicePort int64 = -1
|
||||
)
|
||||
|
||||
func SendMessage(message Message) {
|
||||
s, err := message.Json()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if handler, ok := messageHandlers[message.Type]; ok {
|
||||
handler(s)
|
||||
} else {
|
||||
sendToPort(s)
|
||||
}
|
||||
}
|
||||
|
||||
var messageHandlers = map[MessageType]func(string) bool{
|
||||
ProtectMessage: sendToServicePort,
|
||||
ProcessMessage: sendToServicePort,
|
||||
StartedMessage: conditionalSend,
|
||||
LoadedMessage: conditionalSend,
|
||||
}
|
||||
|
||||
func sendToPort(s string) bool {
|
||||
return bridge.SendToPort(Port, s)
|
||||
}
|
||||
|
||||
func sendToServicePort(s string) bool {
|
||||
return bridge.SendToPort(ServicePort, s)
|
||||
}
|
||||
|
||||
func conditionalSend(s string) bool {
|
||||
isSuccess := sendToPort(s)
|
||||
if !isSuccess {
|
||||
return sendToServicePort(s)
|
||||
}
|
||||
return isSuccess
|
||||
}
|
||||
151
core/server.go
151
core/server.go
@@ -10,10 +10,29 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var conn net.Conn = nil
|
||||
var conn net.Conn
|
||||
|
||||
func sendMessage(message Message) {
|
||||
res, err := message.Json()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
send(Action{
|
||||
Method: messageMethod,
|
||||
}.wrapMessage(res))
|
||||
}
|
||||
|
||||
func send(data []byte) {
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
_, _ = conn.Write(append(data, []byte("\n")...))
|
||||
}
|
||||
|
||||
func startServer(arg string) {
|
||||
|
||||
_, err := strconv.Atoi(arg)
|
||||
|
||||
if err != nil {
|
||||
conn, err = net.Dial("unix", arg)
|
||||
} else {
|
||||
@@ -42,132 +61,12 @@ func startServer(arg string) {
|
||||
return
|
||||
}
|
||||
|
||||
go handleAction(action)
|
||||
go handleAction(action, func(bytes []byte) {
|
||||
send(bytes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func handleAction(action *Action) {
|
||||
switch action.Method {
|
||||
case initClashMethod:
|
||||
data := action.Data.(string)
|
||||
action.callback(handleInitClash(data))
|
||||
return
|
||||
case getIsInitMethod:
|
||||
action.callback(handleGetIsInit())
|
||||
return
|
||||
case forceGcMethod:
|
||||
handleForceGc()
|
||||
return
|
||||
case shutdownMethod:
|
||||
action.callback(handleShutdown())
|
||||
return
|
||||
case validateConfigMethod:
|
||||
data := []byte(action.Data.(string))
|
||||
action.callback(handleValidateConfig(data))
|
||||
return
|
||||
case updateConfigMethod:
|
||||
data := []byte(action.Data.(string))
|
||||
action.callback(handleUpdateConfig(data))
|
||||
return
|
||||
case getProxiesMethod:
|
||||
action.callback(handleGetProxies())
|
||||
return
|
||||
case changeProxyMethod:
|
||||
data := action.Data.(string)
|
||||
handleChangeProxy(data, func(value string) {
|
||||
action.callback(value)
|
||||
})
|
||||
return
|
||||
case getTrafficMethod:
|
||||
data := action.Data.(bool)
|
||||
action.callback(handleGetTraffic(data))
|
||||
return
|
||||
case getTotalTrafficMethod:
|
||||
data := action.Data.(bool)
|
||||
action.callback(handleGetTotalTraffic(data))
|
||||
return
|
||||
case resetTrafficMethod:
|
||||
handleResetTraffic()
|
||||
return
|
||||
case asyncTestDelayMethod:
|
||||
data := action.Data.(string)
|
||||
handleAsyncTestDelay(data, func(value string) {
|
||||
action.callback(value)
|
||||
})
|
||||
return
|
||||
case getConnectionsMethod:
|
||||
action.callback(handleGetConnections())
|
||||
return
|
||||
case closeConnectionsMethod:
|
||||
action.callback(handleCloseConnections())
|
||||
return
|
||||
case closeConnectionMethod:
|
||||
id := action.Data.(string)
|
||||
action.callback(handleCloseConnection(id))
|
||||
return
|
||||
case getExternalProvidersMethod:
|
||||
action.callback(handleGetExternalProviders())
|
||||
return
|
||||
case getExternalProviderMethod:
|
||||
externalProviderName := action.Data.(string)
|
||||
action.callback(handleGetExternalProvider(externalProviderName))
|
||||
case updateGeoDataMethod:
|
||||
paramsString := action.Data.(string)
|
||||
var params = map[string]string{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
action.callback(err.Error())
|
||||
return
|
||||
}
|
||||
geoType := params["geoType"]
|
||||
geoName := params["geoName"]
|
||||
handleUpdateGeoData(geoType, geoName, func(value string) {
|
||||
action.callback(value)
|
||||
})
|
||||
return
|
||||
case updateExternalProviderMethod:
|
||||
providerName := action.Data.(string)
|
||||
handleUpdateExternalProvider(providerName, func(value string) {
|
||||
action.callback(value)
|
||||
})
|
||||
return
|
||||
case sideLoadExternalProviderMethod:
|
||||
paramsString := action.Data.(string)
|
||||
var params = map[string]string{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
action.callback(err.Error())
|
||||
return
|
||||
}
|
||||
providerName := params["providerName"]
|
||||
data := params["data"]
|
||||
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
|
||||
action.callback(value)
|
||||
})
|
||||
return
|
||||
case startLogMethod:
|
||||
handleStartLog()
|
||||
return
|
||||
case stopLogMethod:
|
||||
handleStopLog()
|
||||
return
|
||||
case startListenerMethod:
|
||||
action.callback(handleStartListener())
|
||||
return
|
||||
case stopListenerMethod:
|
||||
action.callback(handleStopListener())
|
||||
return
|
||||
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
|
||||
}
|
||||
|
||||
func nextHandle(action *Action, send func([]byte)) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ class ApplicationState extends State<Application> {
|
||||
}
|
||||
|
||||
_autoUpdateProfilesTask() {
|
||||
_autoUpdateProfilesTaskTimer = Timer(const Duration(seconds: 5), () async {
|
||||
_autoUpdateProfilesTaskTimer = Timer(const Duration(minutes: 20), () async {
|
||||
await globalState.appController.autoUpdateProfiles();
|
||||
_autoUpdateProfilesTask();
|
||||
});
|
||||
@@ -153,7 +153,10 @@ class ApplicationState extends State<Application> {
|
||||
return AppStateManager(
|
||||
child: ClashManager(
|
||||
child: ConnectivityManager(
|
||||
onConnectivityChanged: globalState.appController.updateLocalIp,
|
||||
onConnectivityChanged: () {
|
||||
globalState.appController.updateLocalIp();
|
||||
globalState.appController.addCheckIpNumDebounce();
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
@@ -175,8 +178,8 @@ class ApplicationState extends State<Application> {
|
||||
|
||||
@override
|
||||
Widget build(context) {
|
||||
return _buildWrap(
|
||||
_buildPlatformWrap(
|
||||
return _buildPlatformWrap(
|
||||
_buildWrap(
|
||||
Selector2<AppState, Config, ApplicationSelectorState>(
|
||||
selector: (_, appState, config) => ApplicationSelectorState(
|
||||
locale: config.appSetting.locale,
|
||||
@@ -252,7 +255,7 @@ class ApplicationState extends State<Application> {
|
||||
linkManager.destroy();
|
||||
_autoUpdateGroupTaskTimer?.cancel();
|
||||
_autoUpdateProfilesTaskTimer?.cancel();
|
||||
await clashService?.destroy();
|
||||
await clashCore.destroy();
|
||||
await globalState.appController.savePreferences();
|
||||
await globalState.appController.handleExit();
|
||||
super.dispose();
|
||||
|
||||
@@ -13,7 +13,7 @@ import 'package:path/path.dart';
|
||||
|
||||
class ClashCore {
|
||||
static ClashCore? _instance;
|
||||
late ClashInterface clashInterface;
|
||||
late ClashHandlerInterface clashInterface;
|
||||
|
||||
ClashCore._internal() {
|
||||
if (Platform.isAndroid) {
|
||||
@@ -28,7 +28,11 @@ class ClashCore {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<void> _initGeo() async {
|
||||
Future<bool> preload() {
|
||||
return clashInterface.preload();
|
||||
}
|
||||
|
||||
static Future<void> initGeo() async {
|
||||
final homePath = await appPath.getHomeDirPath();
|
||||
final homeDir = Directory(homePath);
|
||||
final isExists = await homeDir.exists();
|
||||
@@ -63,7 +67,7 @@ class ClashCore {
|
||||
required ClashConfig clashConfig,
|
||||
required Config config,
|
||||
}) async {
|
||||
await _initGeo();
|
||||
await initGeo();
|
||||
final homeDirPath = await appPath.getHomeDirPath();
|
||||
return await clashInterface.init(homeDirPath);
|
||||
}
|
||||
@@ -135,6 +139,9 @@ class ClashCore {
|
||||
Future<List<ExternalProvider>> getExternalProviders() async {
|
||||
final externalProvidersRawString =
|
||||
await clashInterface.getExternalProviders();
|
||||
if (externalProvidersRawString.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
return Isolate.run<List<ExternalProvider>>(
|
||||
() {
|
||||
final externalProviders =
|
||||
@@ -152,7 +159,7 @@ class ClashCore {
|
||||
String externalProviderName) async {
|
||||
final externalProvidersRawString =
|
||||
await clashInterface.getExternalProvider(externalProviderName);
|
||||
if (externalProvidersRawString == null) {
|
||||
if (externalProvidersRawString.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (externalProvidersRawString.isEmpty) {
|
||||
@@ -161,11 +168,8 @@ class ClashCore {
|
||||
return ExternalProvider.fromJson(json.decode(externalProvidersRawString));
|
||||
}
|
||||
|
||||
Future<String> updateGeoData({
|
||||
required String geoType,
|
||||
required String geoName,
|
||||
}) {
|
||||
return clashInterface.updateGeoData(geoType: geoType, geoName: geoName);
|
||||
Future<String> updateGeoData(UpdateGeoDataParams params) {
|
||||
return clashInterface.updateGeoData(params);
|
||||
}
|
||||
|
||||
Future<String> sideLoadExternalProvider({
|
||||
@@ -190,13 +194,16 @@ class ClashCore {
|
||||
await clashInterface.stopListener();
|
||||
}
|
||||
|
||||
Future<Delay> getDelay(String proxyName) async {
|
||||
final data = await clashInterface.asyncTestDelay(proxyName);
|
||||
Future<Delay> getDelay(String url, String proxyName) async {
|
||||
final data = await clashInterface.asyncTestDelay(url, proxyName);
|
||||
return Delay.fromJson(json.decode(data));
|
||||
}
|
||||
|
||||
Future<Traffic> getTraffic(bool value) async {
|
||||
final trafficString = await clashInterface.getTraffic(value);
|
||||
Future<Traffic> getTraffic() async {
|
||||
final trafficString = await clashInterface.getTraffic();
|
||||
if (trafficString.isEmpty) {
|
||||
return Traffic();
|
||||
}
|
||||
return Traffic.fromMap(json.decode(trafficString));
|
||||
}
|
||||
|
||||
@@ -211,13 +218,19 @@ class ClashCore {
|
||||
);
|
||||
}
|
||||
|
||||
Future<Traffic> getTotalTraffic(bool value) async {
|
||||
final totalTrafficString = await clashInterface.getTotalTraffic(value);
|
||||
Future<Traffic> getTotalTraffic() async {
|
||||
final totalTrafficString = await clashInterface.getTotalTraffic();
|
||||
if (totalTrafficString.isEmpty) {
|
||||
return Traffic();
|
||||
}
|
||||
return Traffic.fromMap(json.decode(totalTrafficString));
|
||||
}
|
||||
|
||||
Future<int> getMemory() async {
|
||||
final value = await clashInterface.getMemory();
|
||||
if (value.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
return int.parse(value);
|
||||
}
|
||||
|
||||
@@ -236,6 +249,10 @@ class ClashCore {
|
||||
requestGc() {
|
||||
clashInterface.forceGc();
|
||||
}
|
||||
|
||||
destroy() async {
|
||||
await clashInterface.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
final clashCore = ClashCore();
|
||||
|
||||
@@ -2362,18 +2362,39 @@ class ClashFFI {
|
||||
late final _initNativeApiBridge = _initNativeApiBridgePtr
|
||||
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
|
||||
|
||||
void initMessage(
|
||||
int port,
|
||||
void attachMessagePort(
|
||||
int mPort,
|
||||
) {
|
||||
return _initMessage(
|
||||
port,
|
||||
return _attachMessagePort(
|
||||
mPort,
|
||||
);
|
||||
}
|
||||
|
||||
late final _initMessagePtr =
|
||||
late final _attachMessagePortPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
|
||||
'initMessage');
|
||||
late final _initMessage = _initMessagePtr.asFunction<void Function(int)>();
|
||||
'attachMessagePort');
|
||||
late final _attachMessagePort =
|
||||
_attachMessagePortPtr.asFunction<void Function(int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getTraffic() {
|
||||
return _getTraffic();
|
||||
}
|
||||
|
||||
late final _getTrafficPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
||||
'getTraffic');
|
||||
late final _getTraffic =
|
||||
_getTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getTotalTraffic() {
|
||||
return _getTotalTraffic();
|
||||
}
|
||||
|
||||
late final _getTotalTrafficPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
||||
'getTotalTraffic');
|
||||
late final _getTotalTraffic =
|
||||
_getTotalTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
void freeCString(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
@@ -2389,19 +2410,22 @@ class ClashFFI {
|
||||
late final _freeCString =
|
||||
_freeCStringPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
int initClash(
|
||||
ffi.Pointer<ffi.Char> homeDirStr,
|
||||
void invokeAction(
|
||||
ffi.Pointer<ffi.Char> paramsChar,
|
||||
int port,
|
||||
) {
|
||||
return _initClash(
|
||||
homeDirStr,
|
||||
return _invokeAction(
|
||||
paramsChar,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _initClashPtr =
|
||||
_lookup<ffi.NativeFunction<GoUint8 Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'initClash');
|
||||
late final _initClash =
|
||||
_initClashPtr.asFunction<int Function(ffi.Pointer<ffi.Char>)>();
|
||||
late final _invokeActionPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('invokeAction');
|
||||
late final _invokeAction =
|
||||
_invokeActionPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
void startListener() {
|
||||
return _startListener();
|
||||
@@ -2419,317 +2443,55 @@ class ClashFFI {
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopListener');
|
||||
late final _stopListener = _stopListenerPtr.asFunction<void Function()>();
|
||||
|
||||
int getIsInit() {
|
||||
return _getIsInit();
|
||||
void attachInvokePort(
|
||||
int mPort,
|
||||
) {
|
||||
return _attachInvokePort(
|
||||
mPort,
|
||||
);
|
||||
}
|
||||
|
||||
late final _getIsInitPtr =
|
||||
_lookup<ffi.NativeFunction<GoUint8 Function()>>('getIsInit');
|
||||
late final _getIsInit = _getIsInitPtr.asFunction<int Function()>();
|
||||
late final _attachInvokePortPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
|
||||
'attachInvokePort');
|
||||
late final _attachInvokePort =
|
||||
_attachInvokePortPtr.asFunction<void Function(int)>();
|
||||
|
||||
int shutdownClash() {
|
||||
return _shutdownClash();
|
||||
}
|
||||
|
||||
late final _shutdownClashPtr =
|
||||
_lookup<ffi.NativeFunction<GoUint8 Function()>>('shutdownClash');
|
||||
late final _shutdownClash = _shutdownClashPtr.asFunction<int Function()>();
|
||||
|
||||
void forceGc() {
|
||||
return _forceGc();
|
||||
}
|
||||
|
||||
late final _forceGcPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('forceGc');
|
||||
late final _forceGc = _forceGcPtr.asFunction<void Function()>();
|
||||
|
||||
void validateConfig(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
void quickStart(
|
||||
ffi.Pointer<ffi.Char> dirChar,
|
||||
ffi.Pointer<ffi.Char> paramsChar,
|
||||
ffi.Pointer<ffi.Char> stateParamsChar,
|
||||
int port,
|
||||
) {
|
||||
return _validateConfig(
|
||||
s,
|
||||
return _quickStart(
|
||||
dirChar,
|
||||
paramsChar,
|
||||
stateParamsChar,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _validateConfigPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('validateConfig');
|
||||
late final _validateConfig = _validateConfigPtr
|
||||
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
void updateConfig(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
int port,
|
||||
) {
|
||||
return _updateConfig(
|
||||
s,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _updateConfigPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('updateConfig');
|
||||
late final _updateConfig =
|
||||
_updateConfigPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getProxies() {
|
||||
return _getProxies();
|
||||
}
|
||||
|
||||
late final _getProxiesPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
||||
'getProxies');
|
||||
late final _getProxies =
|
||||
_getProxiesPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
void changeProxy(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
int port,
|
||||
) {
|
||||
return _changeProxy(
|
||||
s,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _changeProxyPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('changeProxy');
|
||||
late final _changeProxy =
|
||||
_changeProxyPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getTraffic(
|
||||
int port,
|
||||
) {
|
||||
return _getTraffic(
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _getTrafficPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
|
||||
'getTraffic');
|
||||
late final _getTraffic =
|
||||
_getTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getTotalTraffic(
|
||||
int port,
|
||||
) {
|
||||
return _getTotalTraffic(
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _getTotalTrafficPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
|
||||
'getTotalTraffic');
|
||||
late final _getTotalTraffic =
|
||||
_getTotalTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
|
||||
|
||||
void resetTraffic() {
|
||||
return _resetTraffic();
|
||||
}
|
||||
|
||||
late final _resetTrafficPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('resetTraffic');
|
||||
late final _resetTraffic = _resetTrafficPtr.asFunction<void Function()>();
|
||||
|
||||
void asyncTestDelay(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
int port,
|
||||
) {
|
||||
return _asyncTestDelay(
|
||||
s,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _asyncTestDelayPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('asyncTestDelay');
|
||||
late final _asyncTestDelay = _asyncTestDelayPtr
|
||||
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getConnections() {
|
||||
return _getConnections();
|
||||
}
|
||||
|
||||
late final _getConnectionsPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
||||
'getConnections');
|
||||
late final _getConnections =
|
||||
_getConnectionsPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
void 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<
|
||||
late final _quickStartPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
|
||||
ffi.LongLong)>>('updateGeoData');
|
||||
late final _updateGeoData = _updateGeoDataPtr.asFunction<
|
||||
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('quickStart');
|
||||
late final _quickStart = _quickStartPtr.asFunction<
|
||||
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
|
||||
ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
void updateExternalProvider(
|
||||
ffi.Pointer<ffi.Char> providerNameChar,
|
||||
int port,
|
||||
) {
|
||||
return _updateExternalProvider(
|
||||
providerNameChar,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _updateExternalProviderPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('updateExternalProvider');
|
||||
late final _updateExternalProvider = _updateExternalProviderPtr
|
||||
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
void 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(
|
||||
ffi.Pointer<ffi.Char> startTUN(
|
||||
int fd,
|
||||
int port,
|
||||
) {
|
||||
return _startTUN(
|
||||
fd,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _startTUNPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int, ffi.LongLong)>>(
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
|
||||
'startTUN');
|
||||
late final _startTUN = _startTUNPtr.asFunction<void Function(int, int)>();
|
||||
late final _startTUN =
|
||||
_startTUNPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getRunTime() {
|
||||
return _getRunTime();
|
||||
@@ -2750,30 +2512,18 @@ class ClashFFI {
|
||||
late final _stopTun = _stopTunPtr.asFunction<void Function()>();
|
||||
|
||||
void setFdMap(
|
||||
int fd,
|
||||
ffi.Pointer<ffi.Char> fdIdChar,
|
||||
) {
|
||||
return _setFdMap(
|
||||
fd,
|
||||
fdIdChar,
|
||||
);
|
||||
}
|
||||
|
||||
late final _setFdMapPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Long)>>('setFdMap');
|
||||
late final _setFdMap = _setFdMapPtr.asFunction<void Function(int)>();
|
||||
|
||||
void setProcessMap(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
) {
|
||||
return _setProcessMap(
|
||||
s,
|
||||
);
|
||||
}
|
||||
|
||||
late final _setProcessMapPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'setProcessMap');
|
||||
late final _setProcessMap =
|
||||
_setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
'setFdMap');
|
||||
late final _setFdMap =
|
||||
_setFdMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getCurrentProfileName() {
|
||||
return _getCurrentProfileName();
|
||||
@@ -2822,6 +2572,20 @@ class ClashFFI {
|
||||
'updateDns');
|
||||
late final _updateDns =
|
||||
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
void setProcessMap(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
) {
|
||||
return _setProcessMap(
|
||||
s,
|
||||
);
|
||||
}
|
||||
|
||||
late final _setProcessMapPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'setProcessMap');
|
||||
late final _setProcessMap =
|
||||
_setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
}
|
||||
|
||||
final class __mbstate_t extends ffi.Union {
|
||||
@@ -3994,8 +3758,6 @@ final class GoSlice extends ffi.Struct {
|
||||
typedef GoInt = GoInt64;
|
||||
typedef GoInt64 = ffi.LongLong;
|
||||
typedef DartGoInt64 = int;
|
||||
typedef GoUint8 = ffi.UnsignedChar;
|
||||
typedef DartGoUint8 = int;
|
||||
|
||||
const int __has_safe_buffers = 1;
|
||||
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_clash/clash/message.dart';
|
||||
import 'package:fl_clash/common/constant.dart';
|
||||
import 'package:fl_clash/common/future.dart';
|
||||
import 'package:fl_clash/common/other.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:flutter/material.dart' hide Action;
|
||||
|
||||
mixin ClashInterface {
|
||||
FutureOr<bool> init(String homeDir);
|
||||
Future<bool> init(String homeDir);
|
||||
|
||||
FutureOr<void> shutdown();
|
||||
Future<bool> preload();
|
||||
|
||||
FutureOr<bool> get isInit;
|
||||
Future<bool> shutdown();
|
||||
|
||||
forceGc();
|
||||
Future<bool> get isInit;
|
||||
|
||||
Future<bool> forceGc();
|
||||
|
||||
FutureOr<String> validateConfig(String data);
|
||||
|
||||
Future<String> asyncTestDelay(String proxyName);
|
||||
Future<String> asyncTestDelay(String url, String proxyName);
|
||||
|
||||
FutureOr<String> updateConfig(UpdateConfigParams updateConfigParams);
|
||||
|
||||
@@ -29,10 +38,7 @@ mixin ClashInterface {
|
||||
|
||||
FutureOr<String>? getExternalProvider(String externalProviderName);
|
||||
|
||||
Future<String> updateGeoData({
|
||||
required String geoType,
|
||||
required String geoName,
|
||||
});
|
||||
Future<String> updateGeoData(UpdateGeoDataParams params);
|
||||
|
||||
Future<String> sideLoadExternalProvider({
|
||||
required String providerName,
|
||||
@@ -41,9 +47,9 @@ mixin ClashInterface {
|
||||
|
||||
Future<String> updateExternalProvider(String providerName);
|
||||
|
||||
FutureOr<String> getTraffic(bool value);
|
||||
FutureOr<String> getTraffic();
|
||||
|
||||
FutureOr<String> getTotalTraffic(bool value);
|
||||
FutureOr<String> getTotalTraffic();
|
||||
|
||||
FutureOr<String> getCountryCode(String ip);
|
||||
|
||||
@@ -61,3 +67,338 @@ mixin ClashInterface {
|
||||
|
||||
FutureOr<bool> closeConnections();
|
||||
}
|
||||
|
||||
mixin AndroidClashInterface {
|
||||
Future<bool> setFdMap(int fd);
|
||||
|
||||
Future<bool> setProcessMap(ProcessMapItem item);
|
||||
|
||||
Future<bool> setState(CoreState state);
|
||||
|
||||
Future<bool> stopTun();
|
||||
|
||||
Future<bool> updateDns(String value);
|
||||
|
||||
Future<DateTime?> startTun(int fd);
|
||||
|
||||
Future<AndroidVpnOptions?> getAndroidVpnOptions();
|
||||
|
||||
Future<String> getCurrentProfileName();
|
||||
|
||||
Future<DateTime?> getRunTime();
|
||||
}
|
||||
|
||||
abstract class ClashHandlerInterface with ClashInterface {
|
||||
Map<String, Completer> callbackCompleterMap = {};
|
||||
|
||||
Future<bool> nextHandleResult(ActionResult result, Completer? completer) =>
|
||||
Future.value(false);
|
||||
|
||||
handleResult(ActionResult result) async {
|
||||
final completer = callbackCompleterMap[result.id];
|
||||
try {
|
||||
switch (result.method) {
|
||||
case ActionMethod.initClash:
|
||||
case ActionMethod.shutdown:
|
||||
case ActionMethod.getIsInit:
|
||||
case ActionMethod.startListener:
|
||||
case ActionMethod.resetTraffic:
|
||||
case ActionMethod.closeConnections:
|
||||
case ActionMethod.closeConnection:
|
||||
case ActionMethod.stopListener:
|
||||
completer?.complete(result.data as bool);
|
||||
return;
|
||||
case ActionMethod.changeProxy:
|
||||
case ActionMethod.getProxies:
|
||||
case ActionMethod.getTraffic:
|
||||
case ActionMethod.getTotalTraffic:
|
||||
case ActionMethod.asyncTestDelay:
|
||||
case ActionMethod.getConnections:
|
||||
case ActionMethod.getExternalProviders:
|
||||
case ActionMethod.getExternalProvider:
|
||||
case ActionMethod.validateConfig:
|
||||
case ActionMethod.updateConfig:
|
||||
case ActionMethod.updateGeoData:
|
||||
case ActionMethod.updateExternalProvider:
|
||||
case ActionMethod.sideLoadExternalProvider:
|
||||
case ActionMethod.getCountryCode:
|
||||
case ActionMethod.getMemory:
|
||||
completer?.complete(result.data as String);
|
||||
return;
|
||||
case ActionMethod.message:
|
||||
clashMessage.controller.add(result.data as String);
|
||||
completer?.complete(true);
|
||||
return;
|
||||
default:
|
||||
final isHandled = await nextHandleResult(result, completer);
|
||||
if (isHandled) {
|
||||
return;
|
||||
}
|
||||
completer?.complete(result.data);
|
||||
}
|
||||
} catch (_) {
|
||||
debugPrint(result.id);
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(String message);
|
||||
|
||||
reStart();
|
||||
|
||||
FutureOr<bool> destroy();
|
||||
|
||||
Future<T> invoke<T>({
|
||||
required ActionMethod method,
|
||||
dynamic data,
|
||||
Duration? timeout,
|
||||
FutureOr<T> Function()? onTimeout,
|
||||
}) async {
|
||||
final id = "${method.name}#${other.id}";
|
||||
|
||||
callbackCompleterMap[id] = Completer<T>();
|
||||
|
||||
dynamic defaultValue;
|
||||
|
||||
if (T == String) {
|
||||
defaultValue = "";
|
||||
}
|
||||
if (T == bool) {
|
||||
defaultValue = false;
|
||||
}
|
||||
|
||||
sendMessage(
|
||||
json.encode(
|
||||
Action(
|
||||
id: id,
|
||||
method: method,
|
||||
data: data,
|
||||
defaultValue: defaultValue,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return (callbackCompleterMap[id] as Completer<T>).safeFuture(
|
||||
timeout: timeout,
|
||||
onLast: () {
|
||||
callbackCompleterMap.remove(id);
|
||||
},
|
||||
onTimeout: onTimeout ??
|
||||
() {
|
||||
return defaultValue;
|
||||
},
|
||||
functionName: id,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> init(String homeDir) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.initClash,
|
||||
data: homeDir,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
shutdown() async {
|
||||
return await invoke<bool>(
|
||||
method: ActionMethod.shutdown,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> get isInit {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.getIsInit,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> forceGc() {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.forceGc,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> validateConfig(String data) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.validateConfig,
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
|
||||
return await invoke<String>(
|
||||
method: ActionMethod.updateConfig,
|
||||
data: json.encode(updateConfigParams),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getProxies() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getProxies,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.changeProxy,
|
||||
data: json.encode(changeProxyParams),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getExternalProviders() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getExternalProviders,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getExternalProvider(String externalProviderName) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getExternalProvider,
|
||||
data: externalProviderName,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> updateGeoData(UpdateGeoDataParams params) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.updateGeoData,
|
||||
data: json.encode(params),
|
||||
);
|
||||
}
|
||||
|
||||
@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() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getTotalTraffic,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getTraffic() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getTraffic,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
resetTraffic() {
|
||||
invoke(method: ActionMethod.resetTraffic);
|
||||
}
|
||||
|
||||
@override
|
||||
startLog() {
|
||||
invoke(method: ActionMethod.startLog);
|
||||
}
|
||||
|
||||
@override
|
||||
stopLog() {
|
||||
invoke<bool>(
|
||||
method: ActionMethod.stopLog,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> startListener() {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.startListener,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
stopListener() {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.stopListener,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> asyncTestDelay(String url, String proxyName) {
|
||||
final delayParams = {
|
||||
"proxy-name": proxyName,
|
||||
"timeout": httpTimeoutDuration.inMilliseconds,
|
||||
"test-url": url,
|
||||
};
|
||||
return invoke<String>(
|
||||
method: ActionMethod.asyncTestDelay,
|
||||
data: json.encode(delayParams),
|
||||
timeout: Duration(
|
||||
milliseconds: 6000,
|
||||
),
|
||||
onTimeout: () {
|
||||
return json.encode(
|
||||
Delay(
|
||||
name: proxyName,
|
||||
value: -1,
|
||||
url: url,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getCountryCode(String ip) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getCountryCode,
|
||||
data: ip,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getMemory() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getMemory,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,28 +3,58 @@ import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:fl_clash/common/constant.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/plugins/service.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
|
||||
import 'generated/clash_ffi.dart';
|
||||
import 'interface.dart';
|
||||
|
||||
class ClashLib with ClashInterface {
|
||||
class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
|
||||
static ClashLib? _instance;
|
||||
final receiver = ReceivePort();
|
||||
|
||||
late final ClashFFI clashFFI;
|
||||
|
||||
late final DynamicLibrary lib;
|
||||
Completer<bool> _canSendCompleter = Completer();
|
||||
SendPort? sendPort;
|
||||
final receiverPort = ReceivePort();
|
||||
|
||||
ClashLib._internal() {
|
||||
lib = DynamicLibrary.open("libclash.so");
|
||||
clashFFI = ClashFFI(lib);
|
||||
clashFFI.initNativeApiBridge(
|
||||
NativeApi.initializeApiDLData,
|
||||
);
|
||||
_initService();
|
||||
}
|
||||
|
||||
@override
|
||||
preload() {
|
||||
return _canSendCompleter.future;
|
||||
}
|
||||
|
||||
_initService() async {
|
||||
await service?.destroy();
|
||||
_registerMainPort(receiverPort.sendPort);
|
||||
receiverPort.listen((message) {
|
||||
if (message is SendPort) {
|
||||
if (_canSendCompleter.isCompleted) {
|
||||
sendPort = null;
|
||||
_canSendCompleter = Completer();
|
||||
}
|
||||
sendPort = message;
|
||||
_canSendCompleter.complete(true);
|
||||
} else {
|
||||
handleResult(
|
||||
ActionResult.fromJson(json.decode(
|
||||
message,
|
||||
)),
|
||||
);
|
||||
}
|
||||
});
|
||||
await service?.init();
|
||||
}
|
||||
|
||||
_registerMainPort(SendPort sendPort) {
|
||||
IsolateNameServer.removePortNameMapping(mainIsolate);
|
||||
IsolateNameServer.registerPortWithName(sendPort, mainIsolate);
|
||||
}
|
||||
|
||||
factory ClashLib() {
|
||||
@@ -32,227 +62,154 @@ class ClashLib with ClashInterface {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
initMessage() {
|
||||
clashFFI.initMessage(
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
@override
|
||||
Future<bool> nextHandleResult(result, completer) async {
|
||||
switch (result.method) {
|
||||
case ActionMethod.setFdMap:
|
||||
case ActionMethod.setProcessMap:
|
||||
case ActionMethod.setState:
|
||||
case ActionMethod.stopTun:
|
||||
case ActionMethod.updateDns:
|
||||
completer?.complete(result.data as bool);
|
||||
return true;
|
||||
case ActionMethod.getRunTime:
|
||||
case ActionMethod.startTun:
|
||||
case ActionMethod.getAndroidVpnOptions:
|
||||
case ActionMethod.getCurrentProfileName:
|
||||
completer?.complete(result.data as String);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool init(String homeDir) {
|
||||
final homeDirChar = homeDir.toNativeUtf8().cast<Char>();
|
||||
final isInit = clashFFI.initClash(homeDirChar) == 1;
|
||||
malloc.free(homeDirChar);
|
||||
return isInit;
|
||||
}
|
||||
|
||||
@override
|
||||
shutdown() async {
|
||||
clashFFI.shutdownClash();
|
||||
lib.close();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isInit => clashFFI.getIsInit() == 1;
|
||||
|
||||
@override
|
||||
Future<String> validateConfig(String data) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final dataChar = data.toNativeUtf8().cast<Char>();
|
||||
clashFFI.validateConfig(
|
||||
dataChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(dataChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> updateConfig(UpdateConfigParams updateConfigParams) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final params = json.encode(updateConfigParams);
|
||||
final paramsChar = params.toNativeUtf8().cast<Char>();
|
||||
clashFFI.updateConfig(
|
||||
paramsChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(paramsChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
String getProxies() {
|
||||
final proxiesRaw = clashFFI.getProxies();
|
||||
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(proxiesRaw);
|
||||
return proxiesRawString;
|
||||
}
|
||||
|
||||
@override
|
||||
String getExternalProviders() {
|
||||
final externalProvidersRaw = clashFFI.getExternalProviders();
|
||||
final externalProvidersRawString =
|
||||
externalProvidersRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(externalProvidersRaw);
|
||||
return externalProvidersRawString;
|
||||
}
|
||||
|
||||
@override
|
||||
String getExternalProvider(String externalProviderName) {
|
||||
final externalProviderNameChar =
|
||||
externalProviderName.toNativeUtf8().cast<Char>();
|
||||
final externalProviderRaw =
|
||||
clashFFI.getExternalProvider(externalProviderNameChar);
|
||||
malloc.free(externalProviderNameChar);
|
||||
final externalProviderRawString =
|
||||
externalProviderRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(externalProviderRaw);
|
||||
return externalProviderRawString;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> updateGeoData({
|
||||
required String geoType,
|
||||
required String geoName,
|
||||
}) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final geoTypeChar = geoType.toNativeUtf8().cast<Char>();
|
||||
final geoNameChar = geoName.toNativeUtf8().cast<Char>();
|
||||
clashFFI.updateGeoData(
|
||||
geoTypeChar,
|
||||
geoNameChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(geoTypeChar);
|
||||
malloc.free(geoNameChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> sideLoadExternalProvider({
|
||||
required String providerName,
|
||||
required String data,
|
||||
}) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
|
||||
final dataChar = data.toNativeUtf8().cast<Char>();
|
||||
clashFFI.sideLoadExternalProvider(
|
||||
providerNameChar,
|
||||
dataChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(providerNameChar);
|
||||
malloc.free(dataChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> updateExternalProvider(String providerName) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
|
||||
clashFFI.updateExternalProvider(
|
||||
providerNameChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(providerNameChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> changeProxy(ChangeProxyParams changeProxyParams) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final params = json.encode(changeProxyParams);
|
||||
final paramsChar = params.toNativeUtf8().cast<Char>();
|
||||
clashFFI.changeProxy(
|
||||
paramsChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(paramsChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
String getConnections() {
|
||||
final connectionsDataRaw = clashFFI.getConnections();
|
||||
final connectionsString = connectionsDataRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(connectionsDataRaw);
|
||||
return connectionsString;
|
||||
}
|
||||
|
||||
@override
|
||||
closeConnection(String id) {
|
||||
final idChar = id.toNativeUtf8().cast<Char>();
|
||||
clashFFI.closeConnection(idChar);
|
||||
malloc.free(idChar);
|
||||
destroy() async {
|
||||
await service?.destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
closeConnections() {
|
||||
clashFFI.closeConnections();
|
||||
reStart() {
|
||||
_initService();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> shutdown() async {
|
||||
await super.shutdown();
|
||||
destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
startListener() async {
|
||||
clashFFI.startListener();
|
||||
return true;
|
||||
sendMessage(String message) async {
|
||||
await _canSendCompleter.future;
|
||||
sendPort?.send(message);
|
||||
}
|
||||
|
||||
@override
|
||||
stopListener() async {
|
||||
clashFFI.stopListener();
|
||||
return true;
|
||||
Future<bool> setFdMap(int fd) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.setFdMap,
|
||||
data: json.encode(fd),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> asyncTestDelay(String proxyName) {
|
||||
final delayParams = {
|
||||
"proxy-name": proxyName,
|
||||
"timeout": httpTimeoutDuration.inMilliseconds,
|
||||
};
|
||||
Future<bool> setProcessMap(item) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.setProcessMap,
|
||||
data: item,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> setState(CoreState state) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.setState,
|
||||
data: json.encode(state),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DateTime?> startTun(int fd) async {
|
||||
final res = await invoke<String>(
|
||||
method: ActionMethod.startTun,
|
||||
data: json.encode(fd),
|
||||
);
|
||||
|
||||
if (res.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return DateTime.fromMillisecondsSinceEpoch(int.parse(res));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> stopTun() {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.stopTun,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AndroidVpnOptions?> getAndroidVpnOptions() async {
|
||||
final res = await invoke<String>(
|
||||
method: ActionMethod.getAndroidVpnOptions,
|
||||
);
|
||||
if (res.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return AndroidVpnOptions.fromJson(json.decode(res));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> updateDns(String value) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.updateDns,
|
||||
data: value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DateTime?> getRunTime() async {
|
||||
final runTimeString = await invoke<String>(
|
||||
method: ActionMethod.getRunTime,
|
||||
);
|
||||
if (runTimeString.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getCurrentProfileName() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getCurrentProfileName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ClashLibHandler {
|
||||
static ClashLibHandler? _instance;
|
||||
|
||||
late final ClashFFI clashFFI;
|
||||
|
||||
late final DynamicLibrary lib;
|
||||
|
||||
ClashLibHandler._internal() {
|
||||
lib = DynamicLibrary.open("libclash.so");
|
||||
clashFFI = ClashFFI(lib);
|
||||
clashFFI.initNativeApiBridge(
|
||||
NativeApi.initializeApiDLData,
|
||||
);
|
||||
}
|
||||
|
||||
factory ClashLibHandler() {
|
||||
_instance ??= ClashLibHandler._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<String> invokeAction(String actionParams) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
@@ -261,89 +218,33 @@ class ClashLib with ClashInterface {
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final delayParamsChar =
|
||||
json.encode(delayParams).toNativeUtf8().cast<Char>();
|
||||
clashFFI.asyncTestDelay(
|
||||
delayParamsChar,
|
||||
final actionParamsChar = actionParams.toNativeUtf8().cast<Char>();
|
||||
clashFFI.invokeAction(
|
||||
actionParamsChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(delayParamsChar);
|
||||
malloc.free(actionParamsChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
String getTraffic(bool value) {
|
||||
final trafficRaw = clashFFI.getTraffic(value ? 1 : 0);
|
||||
final trafficString = trafficRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(trafficRaw);
|
||||
return trafficString;
|
||||
}
|
||||
|
||||
@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,
|
||||
attachMessagePort(int messagePort) {
|
||||
clashFFI.attachMessagePort(
|
||||
messagePort,
|
||||
);
|
||||
malloc.free(ipChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getMemory() {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
clashFFI.getMemory(receiver.sendPort.nativePort);
|
||||
return completer.future;
|
||||
attachInvokePort(int invokePort) {
|
||||
clashFFI.attachInvokePort(
|
||||
invokePort,
|
||||
);
|
||||
}
|
||||
|
||||
/// Android
|
||||
|
||||
startTun(int fd, int port) {
|
||||
if (!Platform.isAndroid) return;
|
||||
clashFFI.startTUN(fd, port);
|
||||
DateTime? startTun(int fd) {
|
||||
final runTimeRaw = clashFFI.startTUN(fd);
|
||||
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(runTimeRaw);
|
||||
if (runTimeString.isEmpty) return null;
|
||||
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
|
||||
}
|
||||
|
||||
stopTun() {
|
||||
@@ -351,7 +252,6 @@ class ClashLib with ClashInterface {
|
||||
}
|
||||
|
||||
updateDns(String dns) {
|
||||
if (!Platform.isAndroid) return;
|
||||
final dnsChar = dns.toNativeUtf8().cast<Char>();
|
||||
clashFFI.updateDns(dnsChar);
|
||||
malloc.free(dnsChar);
|
||||
@@ -384,8 +284,70 @@ class ClashLib with ClashInterface {
|
||||
return AndroidVpnOptions.fromJson(vpnOptions);
|
||||
}
|
||||
|
||||
setFdMap(int fd) {
|
||||
clashFFI.setFdMap(fd);
|
||||
Traffic getTraffic() {
|
||||
final trafficRaw = clashFFI.getTraffic();
|
||||
final trafficString = trafficRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(trafficRaw);
|
||||
if (trafficString.isEmpty) {
|
||||
return Traffic();
|
||||
}
|
||||
return Traffic.fromMap(json.decode(trafficString));
|
||||
}
|
||||
|
||||
Traffic getTotalTraffic(bool value) {
|
||||
final trafficRaw = clashFFI.getTotalTraffic();
|
||||
final trafficString = trafficRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(trafficRaw);
|
||||
if (trafficString.isEmpty) {
|
||||
return Traffic();
|
||||
}
|
||||
return Traffic.fromMap(json.decode(trafficString));
|
||||
}
|
||||
|
||||
startListener() async {
|
||||
clashFFI.startListener();
|
||||
return true;
|
||||
}
|
||||
|
||||
stopListener() async {
|
||||
clashFFI.stopListener();
|
||||
return true;
|
||||
}
|
||||
|
||||
setFdMap(String id) {
|
||||
final idChar = id.toNativeUtf8().cast<Char>();
|
||||
clashFFI.setFdMap(idChar);
|
||||
malloc.free(idChar);
|
||||
}
|
||||
|
||||
Future<String> quickStart(
|
||||
String homeDir,
|
||||
UpdateConfigParams updateConfigParams,
|
||||
CoreState state,
|
||||
) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final params = json.encode(updateConfigParams);
|
||||
final stateParams = json.encode(state);
|
||||
final homeChar = homeDir.toNativeUtf8().cast<Char>();
|
||||
final paramsChar = params.toNativeUtf8().cast<Char>();
|
||||
final stateParamsChar = stateParams.toNativeUtf8().cast<Char>();
|
||||
clashFFI.quickStart(
|
||||
homeChar,
|
||||
paramsChar,
|
||||
stateParamsChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(homeChar);
|
||||
malloc.free(paramsChar);
|
||||
malloc.free(stateParamsChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
DateTime? getRunTime() {
|
||||
@@ -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:convert';
|
||||
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class ClashMessage {
|
||||
final controller = StreamController();
|
||||
final controller = StreamController<String>();
|
||||
|
||||
ClashMessage._() {
|
||||
clashLib?.receiver.listen(controller.add);
|
||||
controller.stream.listen(
|
||||
(message) {
|
||||
if(message.isEmpty){
|
||||
return;
|
||||
}
|
||||
final m = AppMessage.fromJson(json.decode(message));
|
||||
for (final AppMessageListener listener in _listeners) {
|
||||
switch (m.type) {
|
||||
@@ -25,9 +26,6 @@ class ClashMessage {
|
||||
case AppMessageType.request:
|
||||
listener.onRequest(Connection.fromJson(m.data));
|
||||
break;
|
||||
case AppMessageType.started:
|
||||
listener.onStarted(m.data);
|
||||
break;
|
||||
case AppMessageType.loaded:
|
||||
listener.onLoaded(m.data);
|
||||
break;
|
||||
|
||||
@@ -3,21 +3,17 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/clash/interface.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/core.dart';
|
||||
|
||||
class ClashService with ClashInterface {
|
||||
class ClashService extends ClashHandlerInterface {
|
||||
static ClashService? _instance;
|
||||
|
||||
Completer<ServerSocket> serverCompleter = Completer();
|
||||
|
||||
Completer<Socket> socketCompleter = Completer();
|
||||
|
||||
Map<String, Completer> callbackCompleterMap = {};
|
||||
|
||||
Process? process;
|
||||
|
||||
factory ClashService() {
|
||||
@@ -26,11 +22,11 @@ class ClashService with ClashInterface {
|
||||
}
|
||||
|
||||
ClashService._internal() {
|
||||
_createServer();
|
||||
startCore();
|
||||
_initServer();
|
||||
reStart();
|
||||
}
|
||||
|
||||
_createServer() async {
|
||||
_initServer() async {
|
||||
final address = !Platform.isWindows
|
||||
? InternetAddress(
|
||||
unixSocketPath,
|
||||
@@ -61,8 +57,8 @@ class ClashService with ClashInterface {
|
||||
.transform(LineSplitter())
|
||||
.listen(
|
||||
(data) {
|
||||
_handleAction(
|
||||
Action.fromJson(
|
||||
handleResult(
|
||||
ActionResult.fromJson(
|
||||
json.decode(data.trim()),
|
||||
),
|
||||
);
|
||||
@@ -71,7 +67,8 @@ class ClashService with ClashInterface {
|
||||
}
|
||||
}
|
||||
|
||||
startCore() async {
|
||||
@override
|
||||
reStart() async {
|
||||
if (process != null) {
|
||||
await shutdown();
|
||||
}
|
||||
@@ -95,6 +92,20 @@ class ClashService with ClashInterface {
|
||||
process!.stdout.listen((_) {});
|
||||
}
|
||||
|
||||
@override
|
||||
destroy() async {
|
||||
final server = await serverCompleter.future;
|
||||
await server.close();
|
||||
await _deleteSocketFile();
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
sendMessage(String message) async {
|
||||
final socket = await socketCompleter.future;
|
||||
socket.writeln(message);
|
||||
}
|
||||
|
||||
_deleteSocketFile() async {
|
||||
if (!Platform.isWindows) {
|
||||
final file = File(unixSocketPath);
|
||||
@@ -112,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
|
||||
shutdown() async {
|
||||
await _invoke<bool>(
|
||||
method: ActionMethod.shutdown,
|
||||
);
|
||||
await super.shutdown();
|
||||
if (Platform.isWindows) {
|
||||
await request.stopCoreByHelper();
|
||||
}
|
||||
await _destroySocket();
|
||||
process?.kill();
|
||||
process = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> get isInit {
|
||||
return _invoke<bool>(
|
||||
method: ActionMethod.getIsInit,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
forceGc() {
|
||||
_prueInvoke(method: ActionMethod.forceGc);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> validateConfig(String data) {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.validateConfig,
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
|
||||
return await _invoke<String>(
|
||||
method: ActionMethod.updateConfig,
|
||||
data: json.encode(updateConfigParams),
|
||||
timeout: const Duration(seconds: 20),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getProxies() {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.getProxies,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.changeProxy,
|
||||
data: json.encode(changeProxyParams),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getExternalProviders() {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.getExternalProviders,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getExternalProvider(String externalProviderName) {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.getExternalProvider,
|
||||
data: externalProviderName,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> updateGeoData({
|
||||
required String geoType,
|
||||
required String geoName,
|
||||
}) {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.updateGeoData,
|
||||
data: json.encode(
|
||||
{
|
||||
"geoType": geoType,
|
||||
"geoName": geoName,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> sideLoadExternalProvider({
|
||||
required String providerName,
|
||||
required String data,
|
||||
}) {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.sideLoadExternalProvider,
|
||||
data: json.encode({
|
||||
"providerName": providerName,
|
||||
"data": data,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> updateExternalProvider(String providerName) {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.updateExternalProvider,
|
||||
data: providerName,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getConnections() {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.getConnections,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> closeConnections() {
|
||||
return _invoke<bool>(
|
||||
method: ActionMethod.closeConnections,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> closeConnection(String id) {
|
||||
return _invoke<bool>(
|
||||
method: ActionMethod.closeConnection,
|
||||
data: id,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getTotalTraffic(bool value) {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.getTotalTraffic,
|
||||
data: value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getTraffic(bool value) {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.getTraffic,
|
||||
data: value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
resetTraffic() {
|
||||
_prueInvoke(method: ActionMethod.resetTraffic);
|
||||
}
|
||||
|
||||
@override
|
||||
startLog() {
|
||||
_prueInvoke(method: ActionMethod.startLog);
|
||||
}
|
||||
|
||||
@override
|
||||
stopLog() {
|
||||
_prueInvoke(method: ActionMethod.stopLog);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> startListener() {
|
||||
return _invoke<bool>(
|
||||
method: ActionMethod.startListener,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
stopListener() {
|
||||
return _invoke<bool>(
|
||||
method: ActionMethod.stopListener,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> asyncTestDelay(String proxyName) {
|
||||
final delayParams = {
|
||||
"proxy-name": proxyName,
|
||||
"timeout": httpTimeoutDuration.inMilliseconds,
|
||||
};
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.asyncTestDelay,
|
||||
data: json.encode(delayParams),
|
||||
timeout: Duration(
|
||||
milliseconds: 6000,
|
||||
),
|
||||
onTimeout: () {
|
||||
return json.encode(
|
||||
Delay(
|
||||
name: proxyName,
|
||||
value: -1,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
destroy() async {
|
||||
final server = await serverCompleter.future;
|
||||
await server.close();
|
||||
await _deleteSocketFile();
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getCountryCode(String ip) {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.getCountryCode,
|
||||
data: ip,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getMemory() {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.getMemory,
|
||||
);
|
||||
Future<bool> preload() async {
|
||||
await serverCompleter.future;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,3 +34,4 @@ export 'text.dart';
|
||||
export 'tray.dart';
|
||||
export 'window.dart';
|
||||
export 'windows.dart';
|
||||
export 'render.dart';
|
||||
@@ -73,7 +73,7 @@ const hotKeyActionListEquality = ListEquality<HotKeyAction>();
|
||||
const stringAndStringMapEquality = MapEquality<String, String>();
|
||||
const stringAndStringMapEntryIterableEquality =
|
||||
IterableEquality<MapEntry<String, String>>();
|
||||
const stringAndIntQMapEquality = MapEquality<String, int?>();
|
||||
const delayMapEquality = MapEquality<String, Map<String, int?>>();
|
||||
const stringSetEquality = SetEquality<String>();
|
||||
const keyboardModifierListEquality = SetEquality<KeyboardModifier>();
|
||||
|
||||
@@ -88,3 +88,7 @@ const defaultPrimaryColor = Colors.brown;
|
||||
double getWidgetHeight(num lines) {
|
||||
return max(lines * 84 + (lines - 1) * 16, 0);
|
||||
}
|
||||
|
||||
final mainIsolate = "FlClashMainIsolate";
|
||||
|
||||
final serviceIsolate = "FlClashServiceIsolate";
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
|
||||
extension CompleterExt<T> on Completer<T> {
|
||||
safeFuture({
|
||||
Duration? timeout,
|
||||
@@ -8,8 +10,8 @@ extension CompleterExt<T> on Completer<T> {
|
||||
FutureOr<T> Function()? onTimeout,
|
||||
required String functionName,
|
||||
}) {
|
||||
final realTimeout = timeout ?? const Duration(seconds: 6);
|
||||
Timer(realTimeout + Duration(milliseconds: 1000), () {
|
||||
final realTimeout = timeout ?? const Duration(seconds: 1);
|
||||
Timer(realTimeout + commonDuration, () {
|
||||
if (onLast != null) {
|
||||
onLast();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BaseNavigator {
|
||||
static Future<T?> push<T>(BuildContext context, Widget child) async {
|
||||
if (!globalState.appController.isMobileView) {
|
||||
return await Navigator.of(context).push<T>(
|
||||
CommonDesktopRoute(
|
||||
builder: (context) => child,
|
||||
),
|
||||
);
|
||||
}
|
||||
return await Navigator.of(context).push<T>(
|
||||
CommonRoute(
|
||||
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> {
|
||||
CommonRoute({
|
||||
required super.builder,
|
||||
|
||||
56
lib/common/render.dart
Normal file
56
lib/common/render.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
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;
|
||||
debugPrint("[App] resume");
|
||||
}
|
||||
}
|
||||
|
||||
final render = system.isDesktop ? Render() : null;
|
||||
@@ -63,6 +63,7 @@ class Window {
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
await windowManager.setSkipTaskbar(false);
|
||||
render?.resume();
|
||||
}
|
||||
|
||||
Future<bool> isVisible() async {
|
||||
@@ -76,6 +77,7 @@ class Window {
|
||||
hide() async {
|
||||
await windowManager.hide();
|
||||
await windowManager.setSkipTaskbar(true);
|
||||
render?.pause();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,13 +76,10 @@ class AppController {
|
||||
|
||||
updateStatus(bool isStart) async {
|
||||
if (isStart) {
|
||||
await globalState.handleStart();
|
||||
updateRunTime();
|
||||
updateTraffic();
|
||||
globalState.updateFunctionLists = [
|
||||
await globalState.handleStart([
|
||||
updateRunTime,
|
||||
updateTraffic,
|
||||
];
|
||||
]);
|
||||
final currentLastModified =
|
||||
await config.getCurrentProfile()?.profileLastModified;
|
||||
if (currentLastModified == null ||
|
||||
@@ -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 {
|
||||
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
||||
if (commonScaffoldState?.mounted != true) return;
|
||||
@@ -279,8 +283,9 @@ class AppController {
|
||||
await clashService?.destroy();
|
||||
await proxy?.stopProxy();
|
||||
await savePreferences();
|
||||
} catch (_) {}
|
||||
system.exit();
|
||||
} finally {
|
||||
system.exit();
|
||||
}
|
||||
}
|
||||
|
||||
autoCheckUpdate() async {
|
||||
@@ -298,7 +303,7 @@ class AppController {
|
||||
final body = data['body'];
|
||||
final submits = other.parseReleaseBody(body);
|
||||
final textTheme = context.textTheme;
|
||||
globalState.showMessage(
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.discoverNewVersion,
|
||||
message: TextSpan(
|
||||
text: "$tagName \n",
|
||||
@@ -315,13 +320,14 @@ class AppController {
|
||||
),
|
||||
],
|
||||
),
|
||||
onTab: () {
|
||||
launchUrl(
|
||||
Uri.parse("https://github.com/$repository/releases/latest"),
|
||||
);
|
||||
},
|
||||
confirmText: appLocalizations.goDownload,
|
||||
);
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
launchUrl(
|
||||
Uri.parse("https://github.com/$repository/releases/latest"),
|
||||
);
|
||||
} else if (handleError) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.checkUpdate,
|
||||
@@ -337,9 +343,6 @@ class AppController {
|
||||
if (!isDisclaimerAccepted) {
|
||||
handleExit();
|
||||
}
|
||||
if (!config.appSetting.silentLaunch) {
|
||||
window?.show();
|
||||
}
|
||||
await globalState.initCore(
|
||||
appState: appState,
|
||||
clashConfig: clashConfig,
|
||||
@@ -351,11 +354,16 @@ class AppController {
|
||||
);
|
||||
autoUpdateProfiles();
|
||||
autoCheckUpdate();
|
||||
if (!config.appSetting.silentLaunch) {
|
||||
window?.show();
|
||||
} else {
|
||||
window?.hide();
|
||||
}
|
||||
}
|
||||
|
||||
_initStatus() async {
|
||||
if (Platform.isAndroid) {
|
||||
globalState.updateStartTime();
|
||||
await globalState.updateStartTime();
|
||||
}
|
||||
final status =
|
||||
globalState.isStart == true ? true : config.appSetting.autoRun;
|
||||
@@ -370,7 +378,10 @@ class AppController {
|
||||
appState.setDelay(delay);
|
||||
}
|
||||
|
||||
toPage(int index, {bool hasAnimate = false}) {
|
||||
toPage(
|
||||
int index, {
|
||||
bool hasAnimate = false,
|
||||
}) {
|
||||
if (index > appState.currentNavigationItems.length - 1) {
|
||||
return;
|
||||
}
|
||||
@@ -397,8 +408,8 @@ class AppController {
|
||||
|
||||
initLink() {
|
||||
linkManager.initAppLinksListen(
|
||||
(url) {
|
||||
globalState.showMessage(
|
||||
(url) async {
|
||||
final res = await globalState.showMessage(
|
||||
title: "${appLocalizations.add}${appLocalizations.profile}",
|
||||
message: TextSpan(
|
||||
children: [
|
||||
@@ -416,10 +427,12 @@ class AppController {
|
||||
"${appLocalizations.create}${appLocalizations.profile}"),
|
||||
],
|
||||
),
|
||||
onTab: () {
|
||||
addProfileFormURL(url);
|
||||
},
|
||||
);
|
||||
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
addProfileFormURL(url);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -522,6 +535,18 @@ class AppController {
|
||||
});
|
||||
}
|
||||
|
||||
int? getDelay(String proxyName, [String? url]) {
|
||||
final currentDelayMap = appState.delayMap[getRealTestUrl(url)];
|
||||
return currentDelayMap?[appState.getRealProxyName(proxyName)];
|
||||
}
|
||||
|
||||
String getRealTestUrl(String? url) {
|
||||
if (url == null || url.isEmpty) {
|
||||
return config.appSetting.testUrl;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
List<Proxy> _sortOfName(List<Proxy> proxies) {
|
||||
return List.of(proxies)
|
||||
..sort(
|
||||
@@ -532,12 +557,12 @@ class AppController {
|
||||
);
|
||||
}
|
||||
|
||||
List<Proxy> _sortOfDelay(List<Proxy> proxies) {
|
||||
return proxies = List.of(proxies)
|
||||
List<Proxy> _sortOfDelay(String url, List<Proxy> proxies) {
|
||||
return List.of(proxies)
|
||||
..sort(
|
||||
(a, b) {
|
||||
final aDelay = appState.getDelay(a.name);
|
||||
final bDelay = appState.getDelay(b.name);
|
||||
final aDelay = getDelay(a.name, url);
|
||||
final bDelay = getDelay(b.name, url);
|
||||
if (aDelay == null && bDelay == null) {
|
||||
return 0;
|
||||
}
|
||||
@@ -552,10 +577,10 @@ class AppController {
|
||||
);
|
||||
}
|
||||
|
||||
List<Proxy> getSortProxies(List<Proxy> proxies) {
|
||||
List<Proxy> getSortProxies(List<Proxy> proxies, [String? url]) {
|
||||
return switch (config.proxiesStyle.sortType) {
|
||||
ProxiesSortType.none => proxies,
|
||||
ProxiesSortType.delay => _sortOfDelay(proxies),
|
||||
ProxiesSortType.delay => _sortOfDelay(getRealTestUrl(url), proxies),
|
||||
ProxiesSortType.name => _sortOfName(proxies),
|
||||
};
|
||||
}
|
||||
@@ -580,6 +605,10 @@ class AppController {
|
||||
});
|
||||
}
|
||||
|
||||
bool get isMobileView {
|
||||
return appState.viewMode == ViewMode.mobile;
|
||||
}
|
||||
|
||||
updateTun() {
|
||||
clashConfig.tun = clashConfig.tun.copyWith(
|
||||
enable: !clashConfig.tun.enable,
|
||||
|
||||
@@ -100,15 +100,12 @@ enum AppMessageType {
|
||||
log,
|
||||
delay,
|
||||
request,
|
||||
started,
|
||||
loaded,
|
||||
}
|
||||
|
||||
enum ServiceMessageType {
|
||||
enum InvokeMessageType {
|
||||
protect,
|
||||
process,
|
||||
started,
|
||||
loaded,
|
||||
}
|
||||
|
||||
enum FindProcessMode { always, off }
|
||||
@@ -241,6 +238,17 @@ enum ActionMethod {
|
||||
stopListener,
|
||||
getCountryCode,
|
||||
getMemory,
|
||||
|
||||
///Android,
|
||||
setFdMap,
|
||||
setProcessMap,
|
||||
setState,
|
||||
startTun,
|
||||
stopTun,
|
||||
getRunTime,
|
||||
updateDns,
|
||||
getAndroidVpnOptions,
|
||||
getCurrentProfileName,
|
||||
}
|
||||
|
||||
enum AuthorizeCode { none, success, error }
|
||||
|
||||
@@ -40,7 +40,7 @@ class UsageSwitch extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.onlyProxy,
|
||||
selector: (_, config) => config.appSetting.onlyStatisticsProxy,
|
||||
builder: (_, onlyProxy, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.onlyStatisticsProxy),
|
||||
@@ -50,7 +50,7 @@ class UsageSwitch extends StatelessWidget {
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
onlyProxy: value,
|
||||
onlyStatisticsProxy: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -225,7 +225,7 @@ class FakeIpFilterItem extends StatelessWidget {
|
||||
title: appLocalizations.fakeipFilter,
|
||||
items: fakeIpFilter,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -260,7 +260,7 @@ class DefaultNameserverItem extends StatelessWidget {
|
||||
title: appLocalizations.defaultNameserver,
|
||||
items: defaultNameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -295,7 +295,7 @@ class NameserverItem extends StatelessWidget {
|
||||
title: "域名服务器",
|
||||
items: nameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -384,7 +384,7 @@ class NameserverPolicyItem extends StatelessWidget {
|
||||
items: nameserverPolicy.entries,
|
||||
titleBuilder: (item) => Text(item.key),
|
||||
subtitleBuilder: (item) => Text(item.value),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -419,7 +419,7 @@ class ProxyServerNameserverItem extends StatelessWidget {
|
||||
title: appLocalizations.proxyNameserver,
|
||||
items: proxyServerNameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -454,7 +454,7 @@ class FallbackItem extends StatelessWidget {
|
||||
title: appLocalizations.fallback,
|
||||
items: fallback,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -555,7 +555,7 @@ class GeositeItem extends StatelessWidget {
|
||||
title: "Geosite",
|
||||
items: geosite,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -591,7 +591,7 @@ class IpcidrItem extends StatelessWidget {
|
||||
title: appLocalizations.ipcidr,
|
||||
items: ipcidr,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -627,7 +627,7 @@ class DomainItem extends StatelessWidget {
|
||||
title: appLocalizations.domain,
|
||||
items: domain,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -709,16 +709,17 @@ class DnsListView extends StatelessWidget {
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.reset,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.resetTip,
|
||||
),
|
||||
onTab: () {
|
||||
globalState.appController.clashConfig.dns = defaultDns;
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
onPressed: () async {
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.reset,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.resetTip,
|
||||
),
|
||||
);
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
globalState.appController.clashConfig.dns = defaultDns;
|
||||
},
|
||||
tooltip: appLocalizations.reset,
|
||||
icon: const Icon(
|
||||
|
||||
@@ -210,19 +210,19 @@ class BypassDomainItem extends StatelessWidget {
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
globalState.showMessage(
|
||||
onPressed: () async {
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.reset,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.resetTip,
|
||||
),
|
||||
onTab: () {
|
||||
final config = globalState.appController.config;
|
||||
config.networkProps = config.networkProps.copyWith(
|
||||
bypassDomain: defaultBypassDomain,
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
final config = globalState.appController.config;
|
||||
config.networkProps = config.networkProps.copyWith(
|
||||
bypassDomain: defaultBypassDomain,
|
||||
);
|
||||
},
|
||||
tooltip: appLocalizations.reset,
|
||||
@@ -382,19 +382,19 @@ class NetworkListView extends StatelessWidget {
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
globalState.showMessage(
|
||||
onPressed: () async {
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.reset,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.resetTip,
|
||||
),
|
||||
onTab: () {
|
||||
final appController = globalState.appController;
|
||||
appController.config.vpnProps = defaultVpnProps;
|
||||
appController.clashConfig.tun = defaultTun;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
appController.config.vpnProps = defaultVpnProps;
|
||||
appController.clashConfig.tun = defaultTun;
|
||||
},
|
||||
tooltip: appLocalizations.reset,
|
||||
icon: const Icon(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/clash/clash.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:flutter/material.dart';
|
||||
|
||||
final _memoryInfoStateNotifier =
|
||||
ValueNotifier<TrafficValue>(TrafficValue(value: 0));
|
||||
final _memoryInfoStateNotifier = ValueNotifier<TrafficValue>(
|
||||
TrafficValue(value: 0),
|
||||
);
|
||||
|
||||
class MemoryInfo extends StatefulWidget {
|
||||
const MemoryInfo({super.key});
|
||||
@@ -22,10 +24,7 @@ class _MemoryInfoState extends State<MemoryInfo> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
clashCore.getMemory().then((memory) {
|
||||
_memoryInfoStateNotifier.value = TrafficValue(value: memory);
|
||||
});
|
||||
_updateMemoryData();
|
||||
_updateMemory();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -34,11 +33,15 @@ class _MemoryInfoState extends State<MemoryInfo> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_updateMemoryData() {
|
||||
timer = Timer(Duration(seconds: 2), () async {
|
||||
final memory = await clashCore.getMemory();
|
||||
_memoryInfoStateNotifier.value = TrafficValue(value: memory);
|
||||
_updateMemoryData();
|
||||
_updateMemory() async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final rss = ProcessInfo.currentRss;
|
||||
_memoryInfoStateNotifier.value = TrafficValue(
|
||||
value: clashLib != null ? rss : await clashCore.getMemory() + rss,
|
||||
);
|
||||
timer = Timer(Duration(seconds: 5), () async {
|
||||
_updateMemory();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,7 +54,9 @@ class _MemoryInfoState extends State<MemoryInfo> {
|
||||
iconData: Icons.memory,
|
||||
label: appLocalizations.memoryInfo,
|
||||
),
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
clashCore.requestGc();
|
||||
},
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _memoryInfoStateNotifier,
|
||||
builder: (_, trafficValue, __) {
|
||||
|
||||
@@ -76,41 +76,11 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
|
||||
-16,
|
||||
-20,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Text(
|
||||
"${_getLastTraffic(traffics).up}↑ ${_getLastTraffic(traffics).down}↓",
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/pages/editor.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class EditProfile extends StatefulWidget {
|
||||
final Profile profile;
|
||||
@@ -30,10 +30,13 @@ class _EditProfileState extends State<EditProfile> {
|
||||
late TextEditingController urlController;
|
||||
late TextEditingController autoUpdateDurationController;
|
||||
late bool autoUpdate;
|
||||
String? rawText;
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
final fileInfoNotifier = ValueNotifier<FileInfo?>(null);
|
||||
Uint8List? fileData;
|
||||
|
||||
Profile get profile => widget.profile;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -51,28 +54,43 @@ class _EditProfileState extends State<EditProfile> {
|
||||
|
||||
_handleConfirm() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
final config = widget.context.read<Config>();
|
||||
var profile = widget.profile.copyWith(
|
||||
url: urlController.text,
|
||||
label: labelController.text,
|
||||
autoUpdate: autoUpdate,
|
||||
autoUpdateDuration: Duration(
|
||||
minutes: int.parse(
|
||||
autoUpdateDurationController.text,
|
||||
),
|
||||
),
|
||||
);
|
||||
final appController = globalState.appController;
|
||||
Profile profile = this.profile.copyWith(
|
||||
url: urlController.text,
|
||||
label: labelController.text,
|
||||
autoUpdate: autoUpdate,
|
||||
autoUpdateDuration: Duration(
|
||||
minutes: int.parse(
|
||||
autoUpdateDurationController.text,
|
||||
),
|
||||
),
|
||||
);
|
||||
final hasUpdate = widget.profile.url != profile.url;
|
||||
if (fileData != null) {
|
||||
config.setProfile(await profile.saveFile(fileData!));
|
||||
if (profile.type == ProfileType.url && autoUpdate) {
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.profileHasUpdate,
|
||||
),
|
||||
);
|
||||
if (res == true) {
|
||||
profile = profile.copyWith(
|
||||
autoUpdate: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
appController.setProfile(await profile.saveFile(fileData!));
|
||||
} else if (!hasUpdate) {
|
||||
appController.setProfile(profile);
|
||||
} else {
|
||||
config.setProfile(profile);
|
||||
}
|
||||
if (hasUpdate) {
|
||||
globalState.homeScaffoldKey.currentState?.loadingRun(
|
||||
() async {
|
||||
await Future.delayed(
|
||||
commonDuration,
|
||||
);
|
||||
if (hasUpdate) {
|
||||
await globalState.appController.updateProfile(profile);
|
||||
await appController.updateProfile(profile);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -102,22 +120,69 @@ class _EditProfileState extends State<EditProfile> {
|
||||
);
|
||||
}
|
||||
|
||||
_editProfileFile() async {
|
||||
final profilePath = await appPath.getProfilePath(widget.profile.id);
|
||||
if (profilePath == null) return;
|
||||
globalState.safeRun(() async {
|
||||
if (Platform.isAndroid) {
|
||||
await app?.openFile(
|
||||
profilePath,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await launchUrl(
|
||||
Uri.file(
|
||||
profilePath,
|
||||
),
|
||||
_handleSaveEdit(BuildContext context, String data) async {
|
||||
final message = await globalState.safeRun<String>(
|
||||
() async {
|
||||
final message = await clashCore.validateConfig(data);
|
||||
return message;
|
||||
},
|
||||
silence: false,
|
||||
);
|
||||
if (message?.isNotEmpty == true) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(text: message),
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop(data);
|
||||
}
|
||||
}
|
||||
|
||||
_editProfileFile() async {
|
||||
if (rawText == null) {
|
||||
final profilePath = await appPath.getProfilePath(widget.profile.id);
|
||||
if (profilePath == null) return;
|
||||
final file = File(profilePath);
|
||||
rawText = await file.readAsString();
|
||||
}
|
||||
if (!mounted) return;
|
||||
final title = widget.profile.label ?? widget.profile.id;
|
||||
final data = await BaseNavigator.push<String>(
|
||||
globalState.homeScaffoldKey.currentContext!,
|
||||
EditorPage(
|
||||
title: title,
|
||||
content: rawText!,
|
||||
onSave: _handleSaveEdit,
|
||||
onPop: (context, data) async {
|
||||
if (data == rawText) {
|
||||
return true;
|
||||
}
|
||||
final res = await globalState.showMessage(
|
||||
title: title,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.hasCacheChange,
|
||||
),
|
||||
);
|
||||
if (res == true && context.mounted) {
|
||||
_handleSaveEdit(context, data);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
),
|
||||
);
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
rawText = data;
|
||||
fileData = Uint8List.fromList(utf8.encode(data));
|
||||
fileInfoNotifier.value = fileInfoNotifier.value?.copyWith(
|
||||
size: fileData?.length ?? 0,
|
||||
lastModified: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
_uploadProfileFile() async {
|
||||
@@ -130,6 +195,20 @@ class _EditProfileState extends State<EditProfile> {
|
||||
);
|
||||
}
|
||||
|
||||
_handleBack() async {
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(text: appLocalizations.fileIsUpdate),
|
||||
);
|
||||
if (res == true) {
|
||||
_handleConfirm();
|
||||
} else {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = [
|
||||
@@ -245,34 +324,45 @@ class _EditProfileState extends State<EditProfile> {
|
||||
},
|
||||
),
|
||||
];
|
||||
return FloatLayout(
|
||||
floatingWidget: FloatWrapper(
|
||||
child: FloatingActionButton.extended(
|
||||
heroTag: null,
|
||||
onPressed: _handleConfirm,
|
||||
label: Text(appLocalizations.save),
|
||||
icon: const Icon(Icons.save),
|
||||
),
|
||||
),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, __) {
|
||||
if (didPop) return;
|
||||
if (fileData == null) {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
_handleBack();
|
||||
},
|
||||
child: FloatLayout(
|
||||
floatingWidget: FloatWrapper(
|
||||
child: FloatingActionButton.extended(
|
||||
heroTag: null,
|
||||
onPressed: _handleConfirm,
|
||||
label: Text(appLocalizations.save),
|
||||
icon: const Icon(Icons.save),
|
||||
),
|
||||
child: ListView.separated(
|
||||
padding: kMaterialListPadding.copyWith(
|
||||
bottom: 72,
|
||||
),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
),
|
||||
child: ListView.separated(
|
||||
padding: kMaterialListPadding.copyWith(
|
||||
bottom: 72,
|
||||
),
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
separatorBuilder: (_, __) {
|
||||
return const SizedBox(
|
||||
height: 24,
|
||||
);
|
||||
},
|
||||
itemCount: items.length,
|
||||
),
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
separatorBuilder: (_, __) {
|
||||
return const SizedBox(
|
||||
height: 24,
|
||||
);
|
||||
},
|
||||
itemCount: items.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -7,18 +7,11 @@ import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'add_profile.dart';
|
||||
|
||||
enum PopupMenuItemEnum { delete, edit }
|
||||
|
||||
enum ProfileActions {
|
||||
edit,
|
||||
update,
|
||||
delete,
|
||||
}
|
||||
|
||||
class ProfilesFragment extends StatefulWidget {
|
||||
const ProfilesFragment({super.key});
|
||||
|
||||
@@ -185,18 +178,16 @@ class ProfileItem extends StatelessWidget {
|
||||
});
|
||||
|
||||
_handleDeleteProfile(BuildContext context) async {
|
||||
globalState.showMessage(
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.deleteProfileTip,
|
||||
),
|
||||
onTab: () async {
|
||||
await globalState.appController.deleteProfile(profile.id);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
);
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
await globalState.appController.deleteProfile(profile.id);
|
||||
}
|
||||
|
||||
_handleUpdateProfile() async {
|
||||
@@ -207,7 +198,7 @@ class ProfileItem extends StatelessWidget {
|
||||
final appController = globalState.appController;
|
||||
final config = appController.config;
|
||||
if (profile.type == ProfileType.file) return;
|
||||
await globalState.safeRun(() async {
|
||||
await globalState.safeRun(silence: false, () async {
|
||||
try {
|
||||
config.setProfile(
|
||||
profile.copyWith(
|
||||
@@ -266,8 +257,39 @@ class ProfileItem extends StatelessWidget {
|
||||
];
|
||||
}
|
||||
|
||||
_handleCopyLink(BuildContext context) async {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: profile.url,
|
||||
),
|
||||
);
|
||||
if (context.mounted) {
|
||||
context.showNotifier(appLocalizations.copySuccess);
|
||||
}
|
||||
}
|
||||
|
||||
_handleExportFile(BuildContext context) async {
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
final res = await commonScaffoldState?.loadingRun<bool>(
|
||||
() async {
|
||||
final file = await profile.getFile();
|
||||
final value = await picker.saveFile(
|
||||
profile.label ?? profile.id,
|
||||
file.readAsBytesSync(),
|
||||
);
|
||||
if (value == null) return false;
|
||||
return true;
|
||||
},
|
||||
title: appLocalizations.tip,
|
||||
);
|
||||
if (res == true && context.mounted) {
|
||||
context.showNotifier(appLocalizations.exportSuccess);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final key = GlobalKey<CommonPopupBoxState>();
|
||||
return CommonCard(
|
||||
isSelected: profile.id == groupValue,
|
||||
onPressed: () {
|
||||
@@ -286,46 +308,62 @@ class ProfileItem extends StatelessWidget {
|
||||
padding: EdgeInsets.all(8),
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: CommonPopupMenu<ProfileActions>(
|
||||
icon: Icon(Icons.more_vert),
|
||||
items: [
|
||||
CommonPopupMenuItem(
|
||||
action: ProfileActions.edit,
|
||||
label: appLocalizations.edit,
|
||||
iconData: Icons.edit,
|
||||
),
|
||||
if (profile.type == ProfileType.url)
|
||||
CommonPopupMenuItem(
|
||||
action: ProfileActions.update,
|
||||
label: appLocalizations.update,
|
||||
iconData: Icons.sync,
|
||||
: CommonPopupBox(
|
||||
key: key,
|
||||
popup: CommonPopupMenu(
|
||||
items: [
|
||||
ActionItemData(
|
||||
icon: Icons.edit_outlined,
|
||||
label: appLocalizations.edit,
|
||||
onPressed: () {
|
||||
_handleShowEditExtendPage(context);
|
||||
},
|
||||
),
|
||||
CommonPopupMenuItem(
|
||||
action: ProfileActions.delete,
|
||||
label: appLocalizations.delete,
|
||||
iconData: Icons.delete,
|
||||
),
|
||||
],
|
||||
onSelected: (ProfileActions? action) async {
|
||||
switch (action) {
|
||||
case ProfileActions.edit:
|
||||
_handleShowEditExtendPage(context);
|
||||
break;
|
||||
case ProfileActions.delete:
|
||||
_handleDeleteProfile(context);
|
||||
break;
|
||||
case ProfileActions.update:
|
||||
_handleUpdateProfile();
|
||||
break;
|
||||
case null:
|
||||
break;
|
||||
}
|
||||
},
|
||||
if (profile.type == ProfileType.url) ...[
|
||||
ActionItemData(
|
||||
icon: Icons.sync_alt_sharp,
|
||||
label: appLocalizations.sync,
|
||||
onPressed: () {
|
||||
_handleUpdateProfile();
|
||||
},
|
||||
),
|
||||
ActionItemData(
|
||||
icon: Icons.copy,
|
||||
label: appLocalizations.copyLink,
|
||||
onPressed: () {
|
||||
_handleCopyLink(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
ActionItemData(
|
||||
icon: Icons.file_copy_outlined,
|
||||
label: appLocalizations.exportFile,
|
||||
onPressed: () {
|
||||
_handleExportFile(context);
|
||||
},
|
||||
),
|
||||
ActionItemData(
|
||||
icon: Icons.delete_outlined,
|
||||
iconSize: 20,
|
||||
label: appLocalizations.delete,
|
||||
onPressed: () {
|
||||
_handleDeleteProfile(context);
|
||||
},
|
||||
type: ActionType.danger,
|
||||
),
|
||||
],
|
||||
),
|
||||
target: IconButton(
|
||||
onPressed: () {
|
||||
key.currentState?.pop();
|
||||
},
|
||||
icon: Icon(Icons.more_vert),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/scaffold.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:re_editor/re_editor.dart';
|
||||
import 'package:re_highlight/languages/yaml.dart';
|
||||
import 'package:re_highlight/styles/atom-one-light.dart';
|
||||
|
||||
class ViewProfile extends StatefulWidget {
|
||||
final Profile profile;
|
||||
|
||||
const ViewProfile({
|
||||
super.key,
|
||||
required this.profile,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ViewProfile> createState() => _ViewProfileState();
|
||||
}
|
||||
|
||||
class _ViewProfileState extends State<ViewProfile> {
|
||||
bool readOnly = true;
|
||||
final CodeLineEditingController _controller = CodeLineEditingController();
|
||||
final key = GlobalKey<CommonScaffoldState>();
|
||||
final _focusNode = FocusNode();
|
||||
String? rawText;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
appPath.getProfilePath(widget.profile.id).then((path) async {
|
||||
if (path == null) return;
|
||||
final file = File(path);
|
||||
rawText = await file.readAsString();
|
||||
_controller.text = rawText ?? "";
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_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 GroupType groupType;
|
||||
final ProxyCardType type;
|
||||
final String? testUrl;
|
||||
|
||||
const ProxyCard({
|
||||
super.key,
|
||||
required this.groupName,
|
||||
required this.testUrl,
|
||||
required this.proxy,
|
||||
required this.groupType,
|
||||
required this.type,
|
||||
@@ -24,16 +26,18 @@ class ProxyCard extends StatelessWidget {
|
||||
Measure get measure => globalState.measure;
|
||||
|
||||
_handleTestCurrentDelay() {
|
||||
proxyDelayTest(proxy);
|
||||
proxyDelayTest(
|
||||
proxy,
|
||||
testUrl,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDelayText() {
|
||||
return SizedBox(
|
||||
height: measure.labelSmallHeight,
|
||||
child: Selector<AppState, int?>(
|
||||
selector: (context, appState) => appState.getDelay(
|
||||
proxy.name,
|
||||
),
|
||||
selector: (context, appState) =>
|
||||
globalState.appController.getDelay(proxy.name,testUrl),
|
||||
builder: (context, delay, __) {
|
||||
return FadeBox(
|
||||
child: Builder(
|
||||
|
||||
@@ -38,33 +38,48 @@ double getItemHeight(ProxyCardType proxyCardType) {
|
||||
};
|
||||
}
|
||||
|
||||
proxyDelayTest(Proxy proxy) async {
|
||||
proxyDelayTest(Proxy proxy, [String? testUrl]) async {
|
||||
final appController = globalState.appController;
|
||||
final proxyName = appController.appState.getRealProxyName(proxy.name);
|
||||
final url = appController.getRealTestUrl(testUrl);
|
||||
globalState.appController.setDelay(
|
||||
Delay(
|
||||
url: url,
|
||||
name: proxyName,
|
||||
value: 0,
|
||||
),
|
||||
);
|
||||
globalState.appController.setDelay(await clashCore.getDelay(proxyName));
|
||||
globalState.appController.setDelay(
|
||||
await clashCore.getDelay(
|
||||
url,
|
||||
proxyName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
delayTest(List<Proxy> proxies) async {
|
||||
delayTest(List<Proxy> proxies, [String? testUrl]) async {
|
||||
final appController = globalState.appController;
|
||||
final proxyNames = proxies
|
||||
.map((proxy) => appController.appState.getRealProxyName(proxy.name))
|
||||
.toSet()
|
||||
.toList();
|
||||
|
||||
final url = appController.getRealTestUrl(testUrl);
|
||||
|
||||
final delayProxies = proxyNames.map<Future>((proxyName) async {
|
||||
globalState.appController.setDelay(
|
||||
Delay(
|
||||
url: url,
|
||||
name: proxyName,
|
||||
value: 0,
|
||||
),
|
||||
);
|
||||
globalState.appController.setDelay(await clashCore.getDelay(proxyName));
|
||||
globalState.appController.setDelay(
|
||||
await clashCore.getDelay(
|
||||
url,
|
||||
proxyName,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final batchesDelayProxies = delayProxies.batch(100);
|
||||
@@ -86,7 +101,7 @@ double getScrollToSelectedOffset({
|
||||
final proxyCardType = appController.config.proxiesStyle.cardType;
|
||||
final selectedName = appController.getCurrentSelectedName(groupName);
|
||||
final findSelectedIndex = proxies.indexWhere(
|
||||
(proxy) => proxy.name == selectedName,
|
||||
(proxy) => proxy.name == selectedName,
|
||||
);
|
||||
final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0;
|
||||
final rows = (selectedIndex / columns).floor();
|
||||
|
||||
@@ -134,6 +134,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
if (isExpand) {
|
||||
final sortedProxies = globalState.appController.getSortProxies(
|
||||
group.all,
|
||||
group.testUrl,
|
||||
);
|
||||
groupNameProxiesMap[groupName] = sortedProxies;
|
||||
final chunks = sortedProxies.chunks(columns);
|
||||
@@ -142,6 +143,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
.map<Widget>(
|
||||
(proxy) => Flexible(
|
||||
child: ProxyCard(
|
||||
testUrl: group.testUrl,
|
||||
type: type,
|
||||
groupType: group.type,
|
||||
key: ValueKey('$groupName.${proxy.name}'),
|
||||
@@ -259,6 +261,11 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
return prev != next;
|
||||
},
|
||||
builder: (_, state, __) {
|
||||
if (state.groupNames.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullProxies,
|
||||
);
|
||||
}
|
||||
final items = _buildItems(
|
||||
groupNames: state.groupNames,
|
||||
currentUnfoldSet: state.currentUnfoldSet,
|
||||
@@ -367,10 +374,13 @@ class _ListHeaderState extends State<ListHeader>
|
||||
|
||||
bool get isExpand => widget.isExpand;
|
||||
|
||||
_delayTest(List<Proxy> proxies) async {
|
||||
_delayTest() async {
|
||||
if (isLock) return;
|
||||
isLock = true;
|
||||
await delayTest(proxies);
|
||||
await delayTest(
|
||||
widget.group.all,
|
||||
widget.group.testUrl,
|
||||
);
|
||||
isLock = false;
|
||||
}
|
||||
|
||||
@@ -563,9 +573,7 @@ class _ListHeaderState extends State<ListHeader>
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_delayTest(widget.group.all);
|
||||
},
|
||||
onPressed: _delayTest,
|
||||
icon: const Icon(
|
||||
Icons.network_ping,
|
||||
),
|
||||
|
||||
@@ -47,21 +47,40 @@ class _ProvidersState extends State<Providers> {
|
||||
_updateProviders() async {
|
||||
final appState = globalState.appController.appState;
|
||||
final providers = globalState.appController.appState.providers;
|
||||
final messages = [];
|
||||
final updateProviders = providers.map<Future>(
|
||||
(provider) async {
|
||||
appState.setProvider(
|
||||
provider.copyWith(isUpdating: true),
|
||||
);
|
||||
await clashCore.updateExternalProvider(
|
||||
final message = await clashCore.updateExternalProvider(
|
||||
providerName: provider.name,
|
||||
);
|
||||
if (message.isNotEmpty) {
|
||||
messages.add("${provider.name}: $message \n");
|
||||
}
|
||||
appState.setProvider(
|
||||
await clashCore.getExternalProvider(provider.name),
|
||||
);
|
||||
},
|
||||
);
|
||||
final titleMedium = context.textTheme.titleMedium;
|
||||
await Future.wait(updateProviders);
|
||||
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
|
||||
@@ -107,10 +126,10 @@ class ProviderItem extends StatelessWidget {
|
||||
});
|
||||
|
||||
_handleUpdateProvider() async {
|
||||
await globalState.safeRun<void>(() async {
|
||||
final appState = globalState.appController.appState;
|
||||
if (provider.vehicleType != "HTTP") return;
|
||||
await globalState.safeRun(() async {
|
||||
final appState = globalState.appController.appState;
|
||||
if (provider.vehicleType != "HTTP") return;
|
||||
await globalState.safeRun(
|
||||
() async {
|
||||
appState.setProvider(
|
||||
provider.copyWith(
|
||||
isUpdating: true,
|
||||
@@ -120,11 +139,12 @@ class ProviderItem extends StatelessWidget {
|
||||
providerName: provider.name,
|
||||
);
|
||||
if (message.isNotEmpty) throw message;
|
||||
});
|
||||
appState.setProvider(
|
||||
await clashCore.getExternalProvider(provider.name),
|
||||
);
|
||||
});
|
||||
},
|
||||
silence: false,
|
||||
);
|
||||
appState.setProvider(
|
||||
await clashCore.getExternalProvider(provider.name),
|
||||
);
|
||||
await globalState.appController.updateGroupsDebounce();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'card.dart';
|
||||
import 'common.dart';
|
||||
|
||||
List<Proxy> currentProxies = [];
|
||||
String? currentTestUrl;
|
||||
|
||||
typedef GroupNameKeyMap = Map<String, GlobalObjectKey<ProxyGroupViewState>>;
|
||||
|
||||
@@ -114,6 +115,7 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
||||
}
|
||||
final currentGroup = currentGroups[index ?? _tabController!.index];
|
||||
currentProxies = currentGroup.all;
|
||||
currentTestUrl = currentGroup.testUrl;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
appController.config.updateCurrentGroupName(
|
||||
currentGroup.name,
|
||||
@@ -161,6 +163,11 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
||||
return false;
|
||||
},
|
||||
builder: (_, state, __) {
|
||||
if (state.groupNames.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullProxies,
|
||||
);
|
||||
}
|
||||
final index = state.groupNames.indexWhere(
|
||||
(item) => item == state.currentGroupName,
|
||||
);
|
||||
@@ -273,7 +280,10 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
|
||||
_delayTest() async {
|
||||
if (isLock) return;
|
||||
isLock = true;
|
||||
await delayTest(currentProxies);
|
||||
await delayTest(
|
||||
currentProxies,
|
||||
currentTestUrl,
|
||||
);
|
||||
isLock = false;
|
||||
}
|
||||
|
||||
@@ -289,6 +299,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
|
||||
}
|
||||
final sortedProxies = globalState.appController.getSortProxies(
|
||||
currentProxies,
|
||||
currentTestUrl,
|
||||
);
|
||||
_controller.animateTo(
|
||||
min(
|
||||
@@ -334,6 +345,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
|
||||
sortNum: appState.sortNum,
|
||||
proxies: group.all,
|
||||
groupType: group.type,
|
||||
testUrl: group.testUrl,
|
||||
);
|
||||
},
|
||||
builder: (_, state, __) {
|
||||
@@ -342,6 +354,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
|
||||
final proxyCardType = state.proxyCardType;
|
||||
final sortedProxies = globalState.appController.getSortProxies(
|
||||
proxies,
|
||||
state.testUrl,
|
||||
);
|
||||
return ActiveBuilder(
|
||||
label: "proxies",
|
||||
@@ -369,6 +382,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
|
||||
itemBuilder: (_, index) {
|
||||
final proxy = sortedProxies[index];
|
||||
return ProxyCard(
|
||||
testUrl: state.testUrl,
|
||||
groupType: state.groupType,
|
||||
type: proxyCardType,
|
||||
key: ValueKey('$groupName.${proxy.name}'),
|
||||
|
||||
@@ -191,8 +191,10 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
||||
isUpdating.value = true;
|
||||
try {
|
||||
final message = await clashCore.updateGeoData(
|
||||
geoName: geoItem.fileName,
|
||||
geoType: geoItem.label,
|
||||
UpdateGeoDataParams(
|
||||
geoName: geoItem.fileName,
|
||||
geoType: geoItem.label,
|
||||
),
|
||||
);
|
||||
if (message.isNotEmpty) throw message;
|
||||
} catch (e) {
|
||||
@@ -249,12 +251,8 @@ class UpdateGeoUrlFormDialog extends StatefulWidget {
|
||||
final String url;
|
||||
final String? defaultValue;
|
||||
|
||||
const UpdateGeoUrlFormDialog({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.url,
|
||||
this.defaultValue
|
||||
});
|
||||
const UpdateGeoUrlFormDialog(
|
||||
{super.key, required this.title, required this.url, this.defaultValue});
|
||||
|
||||
@override
|
||||
State<UpdateGeoUrlFormDialog> createState() => _UpdateGeoUrlFormDialogState();
|
||||
|
||||
@@ -333,5 +333,13 @@
|
||||
"routeAddressDesc": "Config listen route address",
|
||||
"pleaseInputAdminPassword": "Please enter the admin password",
|
||||
"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"
|
||||
}
|
||||
@@ -333,5 +333,13 @@
|
||||
"routeAddressDesc": "配置监听路由地址",
|
||||
"pleaseInputAdminPassword": "请输入管理员密码",
|
||||
"copyEnvVar": "复制环境变量",
|
||||
"memoryInfo": "内存信息"
|
||||
"memoryInfo": "内存信息",
|
||||
"cancel": "取消",
|
||||
"fileIsUpdate": "文件有修改,是否保存修改",
|
||||
"profileHasUpdate": "配置文件已经修改,是否关闭自动更新 ",
|
||||
"hasCacheChange": "是否缓存修改",
|
||||
"nullProxies": "暂无代理",
|
||||
"copySuccess": "复制成功",
|
||||
"copyLink": "复制链接",
|
||||
"exportFile": "导出文件"
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"bypassDomain": MessageLookupByLibrary.simpleMessage("Bypass domain"),
|
||||
"bypassDomainDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Only takes effect when the system proxy is enabled"),
|
||||
"cancel": MessageLookupByLibrary.simpleMessage("Cancel"),
|
||||
"cancelFilterSystemApp":
|
||||
MessageLookupByLibrary.simpleMessage("Cancel filter system app"),
|
||||
"cancelSelectAll":
|
||||
@@ -123,6 +124,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"copy": MessageLookupByLibrary.simpleMessage("Copy"),
|
||||
"copyEnvVar": MessageLookupByLibrary.simpleMessage(
|
||||
"Copying environment variables"),
|
||||
"copyLink": MessageLookupByLibrary.simpleMessage("Copy link"),
|
||||
"copySuccess": MessageLookupByLibrary.simpleMessage("Copy success"),
|
||||
"core": MessageLookupByLibrary.simpleMessage("Core"),
|
||||
"coreInfo": MessageLookupByLibrary.simpleMessage("Core info"),
|
||||
"country": MessageLookupByLibrary.simpleMessage("Country"),
|
||||
@@ -170,6 +173,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"expand": MessageLookupByLibrary.simpleMessage("Standard"),
|
||||
"expirationTime":
|
||||
MessageLookupByLibrary.simpleMessage("Expiration time"),
|
||||
"exportFile": MessageLookupByLibrary.simpleMessage("Export file"),
|
||||
"exportLogs": MessageLookupByLibrary.simpleMessage("Export logs"),
|
||||
"exportSuccess": MessageLookupByLibrary.simpleMessage("Export Success"),
|
||||
"externalController":
|
||||
@@ -189,6 +193,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"file": MessageLookupByLibrary.simpleMessage("File"),
|
||||
"fileDesc":
|
||||
MessageLookupByLibrary.simpleMessage("Directly upload profile"),
|
||||
"fileIsUpdate": MessageLookupByLibrary.simpleMessage(
|
||||
"The file has been modified. Do you want to save the changes?"),
|
||||
"filterSystemApp":
|
||||
MessageLookupByLibrary.simpleMessage("Filter system app"),
|
||||
"findProcessMode": MessageLookupByLibrary.simpleMessage("Find process"),
|
||||
@@ -208,6 +214,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"global": MessageLookupByLibrary.simpleMessage("Global"),
|
||||
"go": MessageLookupByLibrary.simpleMessage("Go"),
|
||||
"goDownload": MessageLookupByLibrary.simpleMessage("Go to download"),
|
||||
"hasCacheChange": MessageLookupByLibrary.simpleMessage(
|
||||
"Do you want to cache the changes?"),
|
||||
"hostsDesc": MessageLookupByLibrary.simpleMessage("Add Hosts"),
|
||||
"hotkeyConflict":
|
||||
MessageLookupByLibrary.simpleMessage("Hotkey conflict"),
|
||||
@@ -303,6 +311,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"nullLogsDesc": MessageLookupByLibrary.simpleMessage("No logs"),
|
||||
"nullProfileDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"No profile, Please add a profile"),
|
||||
"nullProxies": MessageLookupByLibrary.simpleMessage("No proxies"),
|
||||
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"),
|
||||
"oneColumn": MessageLookupByLibrary.simpleMessage("One column"),
|
||||
"onlyIcon": MessageLookupByLibrary.simpleMessage("Icon"),
|
||||
@@ -348,6 +357,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"profileAutoUpdateIntervalNullValidationDesc":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Please enter the auto update interval time"),
|
||||
"profileHasUpdate": MessageLookupByLibrary.simpleMessage(
|
||||
"The profile has been modified. Do you want to disable auto update?"),
|
||||
"profileNameNullValidationDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Please input the profile name"),
|
||||
"profileParseErrorDesc":
|
||||
|
||||
@@ -80,6 +80,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"),
|
||||
"bypassDomain": MessageLookupByLibrary.simpleMessage("排除域名"),
|
||||
"bypassDomainDesc": MessageLookupByLibrary.simpleMessage("仅在系统代理启用时生效"),
|
||||
"cancel": MessageLookupByLibrary.simpleMessage("取消"),
|
||||
"cancelFilterSystemApp":
|
||||
MessageLookupByLibrary.simpleMessage("取消过滤系统应用"),
|
||||
"cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全选"),
|
||||
@@ -99,6 +100,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"connectivity": MessageLookupByLibrary.simpleMessage("连通性:"),
|
||||
"copy": MessageLookupByLibrary.simpleMessage("复制"),
|
||||
"copyEnvVar": MessageLookupByLibrary.simpleMessage("复制环境变量"),
|
||||
"copyLink": MessageLookupByLibrary.simpleMessage("复制链接"),
|
||||
"copySuccess": MessageLookupByLibrary.simpleMessage("复制成功"),
|
||||
"core": MessageLookupByLibrary.simpleMessage("内核"),
|
||||
"coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"),
|
||||
"country": MessageLookupByLibrary.simpleMessage("区域"),
|
||||
@@ -138,6 +141,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"exit": MessageLookupByLibrary.simpleMessage("退出"),
|
||||
"expand": MessageLookupByLibrary.simpleMessage("标准"),
|
||||
"expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"),
|
||||
"exportFile": MessageLookupByLibrary.simpleMessage("导出文件"),
|
||||
"exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"),
|
||||
"exportSuccess": MessageLookupByLibrary.simpleMessage("导出成功"),
|
||||
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
|
||||
@@ -152,6 +156,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"fallbackFilter": MessageLookupByLibrary.simpleMessage("Fallback过滤"),
|
||||
"file": MessageLookupByLibrary.simpleMessage("文件"),
|
||||
"fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
|
||||
"fileIsUpdate": MessageLookupByLibrary.simpleMessage("文件有修改,是否保存修改"),
|
||||
"filterSystemApp": MessageLookupByLibrary.simpleMessage("过滤系统应用"),
|
||||
"findProcessMode": MessageLookupByLibrary.simpleMessage("查找进程"),
|
||||
"findProcessModeDesc":
|
||||
@@ -168,6 +173,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"global": MessageLookupByLibrary.simpleMessage("全局"),
|
||||
"go": MessageLookupByLibrary.simpleMessage("前往"),
|
||||
"goDownload": MessageLookupByLibrary.simpleMessage("前往下载"),
|
||||
"hasCacheChange": MessageLookupByLibrary.simpleMessage("是否缓存修改"),
|
||||
"hostsDesc": MessageLookupByLibrary.simpleMessage("追加Hosts"),
|
||||
"hotkeyConflict": MessageLookupByLibrary.simpleMessage("快捷键冲突"),
|
||||
"hotkeyManagement": MessageLookupByLibrary.simpleMessage("快捷键管理"),
|
||||
@@ -241,6 +247,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"nullLogsDesc": MessageLookupByLibrary.simpleMessage("暂无日志"),
|
||||
"nullProfileDesc":
|
||||
MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"),
|
||||
"nullProxies": MessageLookupByLibrary.simpleMessage("暂无代理"),
|
||||
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"),
|
||||
"oneColumn": MessageLookupByLibrary.simpleMessage("一列"),
|
||||
"onlyIcon": MessageLookupByLibrary.simpleMessage("仅图标"),
|
||||
@@ -275,6 +282,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("请输入有效间隔时间格式"),
|
||||
"profileAutoUpdateIntervalNullValidationDesc":
|
||||
MessageLookupByLibrary.simpleMessage("请输入自动更新间隔时间"),
|
||||
"profileHasUpdate":
|
||||
MessageLookupByLibrary.simpleMessage("配置文件已经修改,是否关闭自动更新 "),
|
||||
"profileNameNullValidationDesc":
|
||||
MessageLookupByLibrary.simpleMessage("请输入配置名称"),
|
||||
"profileParseErrorDesc":
|
||||
|
||||
@@ -3399,6 +3399,86 @@ class AppLocalizations {
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Cancel`
|
||||
String get cancel {
|
||||
return Intl.message(
|
||||
'Cancel',
|
||||
name: 'cancel',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `The file has been modified. Do you want to save the changes?`
|
||||
String get fileIsUpdate {
|
||||
return Intl.message(
|
||||
'The file has been modified. Do you want to save the changes?',
|
||||
name: 'fileIsUpdate',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `The profile has been modified. Do you want to disable auto update?`
|
||||
String get profileHasUpdate {
|
||||
return Intl.message(
|
||||
'The profile has been modified. Do you want to disable auto update?',
|
||||
name: 'profileHasUpdate',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Do you want to cache the changes?`
|
||||
String get hasCacheChange {
|
||||
return Intl.message(
|
||||
'Do you want to cache the changes?',
|
||||
name: 'hasCacheChange',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `No proxies`
|
||||
String get nullProxies {
|
||||
return Intl.message(
|
||||
'No proxies',
|
||||
name: 'nullProxies',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Copy success`
|
||||
String get copySuccess {
|
||||
return Intl.message(
|
||||
'Copy success',
|
||||
name: 'copySuccess',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Copy link`
|
||||
String get copyLink {
|
||||
return Intl.message(
|
||||
'Copy link',
|
||||
name: 'copyLink',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Export file`
|
||||
String get exportFile {
|
||||
return Intl.message(
|
||||
'Export file',
|
||||
name: 'exportFile',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {
|
||||
|
||||
244
lib/main.dart
244
lib/main.dart
@@ -1,7 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
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/tile.dart';
|
||||
import 'package:fl_clash/plugins/vpn.dart';
|
||||
@@ -10,13 +14,16 @@ import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
import 'application.dart';
|
||||
import 'clash/core.dart';
|
||||
import 'clash/lib.dart';
|
||||
import 'common/common.dart';
|
||||
import 'l10n/l10n.dart';
|
||||
import 'models/models.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
globalState.isService = false;
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
clashLib?.initMessage();
|
||||
await clashCore.preload();
|
||||
globalState.packageInfo = await PackageInfo.fromPlatform();
|
||||
final version = await system.version;
|
||||
final config = await preferences.getConfig() ?? Config();
|
||||
@@ -54,126 +61,125 @@ Future<void> main() async {
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> vpnService() async {
|
||||
Future<void> _service(List<String> flags) async {
|
||||
globalState.isService = true;
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
globalState.isVpnService = true;
|
||||
globalState.packageInfo = await PackageInfo.fromPlatform();
|
||||
final version = await system.version;
|
||||
final quickStart = flags.contains("quick");
|
||||
final clashLibHandler = ClashLibHandler();
|
||||
final config = await preferences.getConfig() ?? Config();
|
||||
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
|
||||
await AppLocalizations.load(
|
||||
other.getLocaleForString(config.appSetting.locale) ??
|
||||
WidgetsBinding.instance.platformDispatcher.locale,
|
||||
);
|
||||
|
||||
final appState = AppState(
|
||||
mode: clashConfig.mode,
|
||||
selectedMap: config.currentSelectedMap,
|
||||
version: version,
|
||||
);
|
||||
|
||||
await globalState.init(
|
||||
appState: appState,
|
||||
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,
|
||||
);
|
||||
tile?.addListener(
|
||||
_TileListenerWithService(
|
||||
onStop: () async {
|
||||
await app?.tip(appLocalizations.stopVpn);
|
||||
clashLibHandler.stopListener();
|
||||
clashLibHandler.stopTun();
|
||||
await vpn?.stop();
|
||||
exit(0);
|
||||
},
|
||||
),
|
||||
);
|
||||
if (!quickStart) {
|
||||
_handleMainIpc(clashLibHandler);
|
||||
} else {
|
||||
await ClashCore.initGeo();
|
||||
globalState.packageInfo = await PackageInfo.fromPlatform();
|
||||
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
|
||||
final homeDirPath = await appPath.getHomeDirPath();
|
||||
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
|
||||
class ServiceMessageHandler with ServiceMessageListener {
|
||||
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 {
|
||||
class _TileListenerWithService with TileListener {
|
||||
final Function() _onStop;
|
||||
|
||||
const TileListenerWithVpn({
|
||||
const _TileListenerWithService({
|
||||
required Function() onStop,
|
||||
}) : _onStop = onStop;
|
||||
|
||||
@@ -182,3 +188,27 @@ class TileListenerWithVpn with TileListener {
|
||||
_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/models/models.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -26,17 +27,9 @@ class _AndroidContainerState extends State<AndroidManager> {
|
||||
|
||||
Widget _updateCoreState(Widget child) {
|
||||
return Selector2<Config, ClashConfig, CoreState>(
|
||||
selector: (_, config, clashConfig) => CoreState(
|
||||
enable: config.vpnProps.enable,
|
||||
accessControl: config.isAccessControl ? config.accessControl : null,
|
||||
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,
|
||||
selector: (_, config, clashConfig) => globalState.getCoreState(
|
||||
config,
|
||||
clashConfig,
|
||||
),
|
||||
builder: (__, state, child) {
|
||||
clashLib?.setState(state);
|
||||
|
||||
@@ -21,11 +21,10 @@ class _AppStateManagerState extends State<AppStateManager>
|
||||
_updateNavigationsContainer(Widget child) {
|
||||
return Selector2<AppState, Config, UpdateNavigationsSelector>(
|
||||
selector: (_, appState, config) {
|
||||
final group = appState.currentGroups;
|
||||
final hasProfile = config.profiles.isNotEmpty;
|
||||
return UpdateNavigationsSelector(
|
||||
openLogs: config.appSetting.openLogs,
|
||||
hasProxies: group.isNotEmpty && hasProfile,
|
||||
hasProxies: hasProfile && config.currentProfileId != null,
|
||||
);
|
||||
},
|
||||
builder: (context, state, child) {
|
||||
@@ -74,9 +73,12 @@ class _AppStateManagerState extends State<AppStateManager>
|
||||
|
||||
@override
|
||||
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
|
||||
final isPaused = state == AppLifecycleState.paused;
|
||||
if (isPaused) {
|
||||
if (state == AppLifecycleState.paused ||
|
||||
state == AppLifecycleState.inactive) {
|
||||
globalState.appController.savePreferencesDebounce();
|
||||
render?.pause();
|
||||
} else {
|
||||
render?.resume();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,9 +90,14 @@ class _AppStateManagerState extends State<AppStateManager>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _cacheStateChange(
|
||||
_updateNavigationsContainer(
|
||||
widget.child,
|
||||
return Listener(
|
||||
onPointerDown: (_) {
|
||||
render?.resume();
|
||||
},
|
||||
child: _cacheStateChange(
|
||||
_updateNavigationsContainer(
|
||||
widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,14 +99,13 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
|
||||
|
||||
@override
|
||||
Future<void> onDelay(Delay delay) async {
|
||||
super.onDelay(delay);
|
||||
final appController = globalState.appController;
|
||||
appController.setDelay(delay);
|
||||
super.onDelay(delay);
|
||||
debouncer.call(
|
||||
DebounceTag.updateDelay,
|
||||
() async {
|
||||
await appController.updateGroupsDebounce();
|
||||
// await appController.addCheckIpNumDebounce();
|
||||
},
|
||||
duration: const Duration(milliseconds: 5000),
|
||||
);
|
||||
@@ -121,12 +120,6 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
|
||||
super.onLog(log);
|
||||
}
|
||||
|
||||
@override
|
||||
void onStarted(String runTime) {
|
||||
super.onStarted(runTime);
|
||||
globalState.appController.applyProfileDebounce();
|
||||
}
|
||||
|
||||
@override
|
||||
void onRequest(Connection connection) async {
|
||||
globalState.appController.appState.addRequest(connection);
|
||||
|
||||
@@ -19,45 +19,26 @@ class MessageManager extends StatefulWidget {
|
||||
|
||||
class MessageManagerState extends State<MessageManager>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final _floatMessageKey = GlobalKey();
|
||||
List<CommonMessage> bufferMessages = [];
|
||||
final _messagesNotifier = ValueNotifier<List<CommonMessage>>([]);
|
||||
final _floatMessageNotifier = ValueNotifier<CommonMessage?>(null);
|
||||
double maxWidth = 0;
|
||||
Offset offset = Offset.zero;
|
||||
|
||||
late AnimationController _animationController;
|
||||
|
||||
Completer? _animationCompleter;
|
||||
late Animation<Offset> _floatOffsetAnimation;
|
||||
late Animation<Offset> _commonOffsetAnimation;
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: Duration(milliseconds: 200),
|
||||
duration: Duration(milliseconds: 400),
|
||||
);
|
||||
_initTransformState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_messagesNotifier.dispose();
|
||||
_floatMessageNotifier.dispose();
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -67,126 +48,13 @@ class MessageManagerState extends State<MessageManager>
|
||||
id: other.uuidV4,
|
||||
text: text,
|
||||
);
|
||||
bufferMessages.add(commonMessage);
|
||||
await _animationCompleter?.future;
|
||||
_showMessage();
|
||||
}
|
||||
|
||||
_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,
|
||||
),
|
||||
),
|
||||
_messagesNotifier.value = List.from(_messagesNotifier.value)
|
||||
..add(
|
||||
commonMessage,
|
||||
);
|
||||
_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) {
|
||||
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.all(16),
|
||||
child: Text(
|
||||
message.text,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSecondaryFixedVariant,
|
||||
),
|
||||
maxLines: 2,
|
||||
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),
|
||||
);
|
||||
_handleRemove(CommonMessage commonMessage) async {
|
||||
_messagesNotifier.value = List<CommonMessage>.from(_messagesNotifier.value)
|
||||
..remove(commonMessage);
|
||||
}
|
||||
@@ -204,6 +72,7 @@ class MessageManagerState extends State<MessageManager>
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: globalState.safeMessageOffsetNotifier,
|
||||
builder: (_, offset, child) {
|
||||
this.offset = offset;
|
||||
if (offset == Offset.zero) {
|
||||
return SizedBox();
|
||||
}
|
||||
@@ -232,21 +101,23 @@ class MessageManagerState extends State<MessageManager>
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 12,
|
||||
children: [
|
||||
for (final message in messages)
|
||||
_MessageItemWrap(
|
||||
key: GlobalObjectKey(message.id),
|
||||
child: _wrapOffset(
|
||||
_wrapMessage(message),
|
||||
for (final message in messages) ...[
|
||||
if (message != messages.first)
|
||||
SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
_MessageItem(
|
||||
key: GlobalObjectKey(message.id),
|
||||
message: message,
|
||||
onRemove: _handleRemove,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
_floatMessage(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -259,22 +130,25 @@ class MessageManagerState extends State<MessageManager>
|
||||
}
|
||||
}
|
||||
|
||||
class _MessageItemWrap extends StatefulWidget {
|
||||
final Widget child;
|
||||
class _MessageItem extends StatefulWidget {
|
||||
final CommonMessage message;
|
||||
final Function(CommonMessage message) onRemove;
|
||||
|
||||
const _MessageItemWrap({
|
||||
const _MessageItem({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.message,
|
||||
required this.onRemove,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_MessageItemWrap> createState() => _MessageItemWrapState();
|
||||
State<_MessageItem> createState() => _MessageItemState();
|
||||
}
|
||||
|
||||
class _MessageItemWrapState extends State<_MessageItemWrap>
|
||||
class _MessageItemState extends State<_MessageItem>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
Offset _nextOffset = Offset.zero;
|
||||
late Animation<Offset> _offsetAnimation;
|
||||
late Animation<double> _fadeAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -283,11 +157,41 @@ class _MessageItemWrapState extends State<_MessageItemWrap>
|
||||
vsync: this,
|
||||
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 {
|
||||
_nextOffset = offset;
|
||||
await _controller.forward(from: 0);
|
||||
_fadeAnimation = Tween<double>(
|
||||
begin: 0.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
|
||||
@@ -301,26 +205,30 @@ class _MessageItemWrapState extends State<_MessageItemWrap>
|
||||
return AnimatedBuilder(
|
||||
animation: _controller.view,
|
||||
builder: (_, child) {
|
||||
if (_nextOffset == Offset.zero) {
|
||||
return child!;
|
||||
}
|
||||
final offset = Tween(
|
||||
begin: Offset.zero,
|
||||
end: _nextOffset,
|
||||
)
|
||||
.animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOut,
|
||||
return FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: SlideTransition(
|
||||
position: _offsetAnimation,
|
||||
child: Material(
|
||||
elevation: _controller.value * 12,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
clipBehavior: Clip.none,
|
||||
child: Padding(
|
||||
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();
|
||||
}
|
||||
|
||||
@override
|
||||
void onTrayMenuItemClick(MenuItem menuItem) {
|
||||
render?.active();
|
||||
super.onTrayMenuItemClick(menuItem);
|
||||
}
|
||||
|
||||
@override
|
||||
onTrayIconMouseDown() {
|
||||
window?.show();
|
||||
|
||||
@@ -64,6 +64,18 @@ class _WindowContainerState extends State<WindowManager>
|
||||
super.onWindowClose();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowFocus() {
|
||||
super.onWindowFocus();
|
||||
render?.resume();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowBlur() {
|
||||
super.onWindowBlur();
|
||||
render?.pause();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onShouldTerminate() async {
|
||||
await globalState.appController.handleExit();
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'common.dart';
|
||||
import 'core.dart';
|
||||
import 'profile.dart';
|
||||
|
||||
typedef DelayMap = Map<String, int?>;
|
||||
typedef DelayMap = Map<String, Map<String, int?>>;
|
||||
|
||||
class AppState with ChangeNotifier {
|
||||
List<NavigationItem> _navigationItems;
|
||||
@@ -122,10 +122,6 @@ class AppState with ChangeNotifier {
|
||||
return selectedMap[firstGroupName] ?? firstGroup.now;
|
||||
}
|
||||
|
||||
int? getDelay(String proxyName) {
|
||||
return _delayMap[getRealProxyName(proxyName)];
|
||||
}
|
||||
|
||||
VersionInfo? get versionInfo => _versionInfo;
|
||||
|
||||
set versionInfo(VersionInfo? value) {
|
||||
@@ -237,15 +233,20 @@ class AppState with ChangeNotifier {
|
||||
}
|
||||
|
||||
set delayMap(DelayMap value) {
|
||||
if (!stringAndIntQMapEquality.equals(_delayMap, value)) {
|
||||
if (_delayMap != value) {
|
||||
_delayMap = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
setDelay(Delay delay) {
|
||||
if (_delayMap[delay.name] != delay.value) {
|
||||
_delayMap = Map.from(_delayMap)..[delay.name] = delay.value;
|
||||
if (_delayMap[delay.url]?[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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// ignore_for_file: invalid_annotation_target
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
@@ -291,6 +293,7 @@ class Group with _$Group {
|
||||
@Default([]) List<Proxy> all,
|
||||
String? now,
|
||||
bool? hidden,
|
||||
String? testUrl,
|
||||
@Default("") String icon,
|
||||
required String name,
|
||||
}) = _Group;
|
||||
@@ -441,3 +444,24 @@ class Field with _$Field {
|
||||
Validator? validator,
|
||||
}) = _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)
|
||||
@Default(defaultDashboardWidgets)
|
||||
List<DashboardWidget> dashboardWidgets,
|
||||
@Default(false) bool onlyProxy,
|
||||
@Default(false) bool onlyStatisticsProxy,
|
||||
@Default(false) bool autoLaunch,
|
||||
@Default(false) bool silentLaunch,
|
||||
@Default(false) bool autoRun,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// ignore_for_file: invalid_annotation_target
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'generated/core.freezed.dart';
|
||||
|
||||
part 'generated/core.g.dart';
|
||||
|
||||
abstract mixin class AppMessageListener {
|
||||
@@ -16,8 +16,6 @@ abstract mixin class AppMessageListener {
|
||||
|
||||
void onRequest(Connection connection) {}
|
||||
|
||||
void onStarted(String runTime) {}
|
||||
|
||||
void onLoaded(String providerName) {}
|
||||
}
|
||||
|
||||
@@ -25,10 +23,6 @@ abstract mixin class ServiceMessageListener {
|
||||
onProtect(Fd fd) {}
|
||||
|
||||
onProcess(ProcessData process) {}
|
||||
|
||||
onStarted(String runTime) {}
|
||||
|
||||
onLoaded(String providerName) {}
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -42,7 +36,6 @@ class CoreState with _$CoreState {
|
||||
required List<String> bypassDomain,
|
||||
required List<String> routeAddress,
|
||||
required bool ipv6,
|
||||
required bool onlyProxy,
|
||||
}) = _CoreState;
|
||||
|
||||
factory CoreState.fromJson(Map<String, Object?> json) =>
|
||||
@@ -76,6 +69,7 @@ class ConfigExtendedParams with _$ConfigExtendedParams {
|
||||
@JsonKey(name: "selected-map") required SelectedMap selectedMap,
|
||||
@JsonKey(name: "override-dns") required bool overrideDns,
|
||||
@JsonKey(name: "test-url") required String testUrl,
|
||||
@JsonKey(name: "only-statistics-proxy") required bool onlyStatisticsProxy,
|
||||
}) = _ConfigExtendedParams;
|
||||
|
||||
factory ConfigExtendedParams.fromJson(Map<String, Object?> json) =>
|
||||
@@ -105,6 +99,17 @@ class ChangeProxyParams with _$ChangeProxyParams {
|
||||
_$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
|
||||
class AppMessage with _$AppMessage {
|
||||
const factory AppMessage({
|
||||
@@ -117,20 +122,21 @@ class AppMessage with _$AppMessage {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ServiceMessage with _$ServiceMessage {
|
||||
const factory ServiceMessage({
|
||||
required ServiceMessageType type,
|
||||
class InvokeMessage with _$InvokeMessage {
|
||||
const factory InvokeMessage({
|
||||
required InvokeMessageType type,
|
||||
dynamic data,
|
||||
}) = _ServiceMessage;
|
||||
}) = _InvokeMessage;
|
||||
|
||||
factory ServiceMessage.fromJson(Map<String, Object?> json) =>
|
||||
_$ServiceMessageFromJson(json);
|
||||
factory InvokeMessage.fromJson(Map<String, Object?> json) =>
|
||||
_$InvokeMessageFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Delay with _$Delay {
|
||||
const factory Delay({
|
||||
required String name,
|
||||
required String url,
|
||||
int? value,
|
||||
}) = _Delay;
|
||||
|
||||
@@ -150,7 +156,7 @@ class Now with _$Now {
|
||||
@freezed
|
||||
class ProcessData with _$ProcessData {
|
||||
const factory ProcessData({
|
||||
required int id,
|
||||
required String id,
|
||||
required Metadata metadata,
|
||||
}) = _ProcessData;
|
||||
|
||||
@@ -161,7 +167,7 @@ class ProcessData with _$ProcessData {
|
||||
@freezed
|
||||
class Fd with _$Fd {
|
||||
const factory Fd({
|
||||
required int id,
|
||||
required String id,
|
||||
required int value,
|
||||
}) = _Fd;
|
||||
|
||||
@@ -171,7 +177,7 @@ class Fd with _$Fd {
|
||||
@freezed
|
||||
class ProcessMapItem with _$ProcessMapItem {
|
||||
const factory ProcessMapItem({
|
||||
required int id,
|
||||
required String id,
|
||||
required String value,
|
||||
}) = _ProcessMapItem;
|
||||
|
||||
@@ -241,14 +247,21 @@ class Action with _$Action {
|
||||
const factory Action({
|
||||
required ActionMethod method,
|
||||
required dynamic data,
|
||||
@JsonKey(name: "default-value") required dynamic defaultValue,
|
||||
required String id,
|
||||
}) = _Action;
|
||||
|
||||
factory Action.fromJson(Map<String, Object?> json) => _$ActionFromJson(json);
|
||||
}
|
||||
|
||||
extension ActionExt on Action {
|
||||
String get toJson {
|
||||
return json.encode(this);
|
||||
}
|
||||
@freezed
|
||||
class ActionResult with _$ActionResult {
|
||||
const factory ActionResult({
|
||||
required ActionMethod method,
|
||||
required dynamic data,
|
||||
String? id,
|
||||
}) = _ActionResult;
|
||||
|
||||
factory ActionResult.fromJson(Map<String, Object?> json) =>
|
||||
_$ActionResultFromJson(json);
|
||||
}
|
||||
|
||||
@@ -1998,6 +1998,7 @@ mixin _$Group {
|
||||
List<Proxy> get all => throw _privateConstructorUsedError;
|
||||
String? get now => throw _privateConstructorUsedError;
|
||||
bool? get hidden => throw _privateConstructorUsedError;
|
||||
String? get testUrl => throw _privateConstructorUsedError;
|
||||
String get icon => throw _privateConstructorUsedError;
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
|
||||
@@ -2020,6 +2021,7 @@ abstract class $GroupCopyWith<$Res> {
|
||||
List<Proxy> all,
|
||||
String? now,
|
||||
bool? hidden,
|
||||
String? testUrl,
|
||||
String icon,
|
||||
String name});
|
||||
}
|
||||
@@ -2043,6 +2045,7 @@ class _$GroupCopyWithImpl<$Res, $Val extends Group>
|
||||
Object? all = null,
|
||||
Object? now = freezed,
|
||||
Object? hidden = freezed,
|
||||
Object? testUrl = freezed,
|
||||
Object? icon = null,
|
||||
Object? name = null,
|
||||
}) {
|
||||
@@ -2063,6 +2066,10 @@ class _$GroupCopyWithImpl<$Res, $Val extends Group>
|
||||
? _value.hidden
|
||||
: hidden // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,
|
||||
testUrl: freezed == testUrl
|
||||
? _value.testUrl
|
||||
: testUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
icon: null == icon
|
||||
? _value.icon
|
||||
: icon // ignore: cast_nullable_to_non_nullable
|
||||
@@ -2087,6 +2094,7 @@ abstract class _$$GroupImplCopyWith<$Res> implements $GroupCopyWith<$Res> {
|
||||
List<Proxy> all,
|
||||
String? now,
|
||||
bool? hidden,
|
||||
String? testUrl,
|
||||
String icon,
|
||||
String name});
|
||||
}
|
||||
@@ -2108,6 +2116,7 @@ class __$$GroupImplCopyWithImpl<$Res>
|
||||
Object? all = null,
|
||||
Object? now = freezed,
|
||||
Object? hidden = freezed,
|
||||
Object? testUrl = freezed,
|
||||
Object? icon = null,
|
||||
Object? name = null,
|
||||
}) {
|
||||
@@ -2128,6 +2137,10 @@ class __$$GroupImplCopyWithImpl<$Res>
|
||||
? _value.hidden
|
||||
: hidden // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,
|
||||
testUrl: freezed == testUrl
|
||||
? _value.testUrl
|
||||
: testUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
icon: null == icon
|
||||
? _value.icon
|
||||
: icon // ignore: cast_nullable_to_non_nullable
|
||||
@@ -2148,6 +2161,7 @@ class _$GroupImpl implements _Group {
|
||||
final List<Proxy> all = const [],
|
||||
this.now,
|
||||
this.hidden,
|
||||
this.testUrl,
|
||||
this.icon = "",
|
||||
required this.name})
|
||||
: _all = all;
|
||||
@@ -2171,6 +2185,8 @@ class _$GroupImpl implements _Group {
|
||||
@override
|
||||
final bool? hidden;
|
||||
@override
|
||||
final String? testUrl;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String icon;
|
||||
@override
|
||||
@@ -2178,7 +2194,7 @@ class _$GroupImpl implements _Group {
|
||||
|
||||
@override
|
||||
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
|
||||
@@ -2190,14 +2206,22 @@ class _$GroupImpl implements _Group {
|
||||
const DeepCollectionEquality().equals(other._all, _all) &&
|
||||
(identical(other.now, now) || other.now == now) &&
|
||||
(identical(other.hidden, hidden) || other.hidden == hidden) &&
|
||||
(identical(other.testUrl, testUrl) || other.testUrl == testUrl) &&
|
||||
(identical(other.icon, icon) || other.icon == icon) &&
|
||||
(identical(other.name, name) || other.name == name));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, type,
|
||||
const DeepCollectionEquality().hash(_all), now, hidden, icon, name);
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
type,
|
||||
const DeepCollectionEquality().hash(_all),
|
||||
now,
|
||||
hidden,
|
||||
testUrl,
|
||||
icon,
|
||||
name);
|
||||
|
||||
/// Create a copy of Group
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -2221,6 +2245,7 @@ abstract class _Group implements Group {
|
||||
final List<Proxy> all,
|
||||
final String? now,
|
||||
final bool? hidden,
|
||||
final String? testUrl,
|
||||
final String icon,
|
||||
required final String name}) = _$GroupImpl;
|
||||
|
||||
@@ -2235,6 +2260,8 @@ abstract class _Group implements Group {
|
||||
@override
|
||||
bool? get hidden;
|
||||
@override
|
||||
String? get testUrl;
|
||||
@override
|
||||
String get icon;
|
||||
@override
|
||||
String get name;
|
||||
|
||||
@@ -161,6 +161,7 @@ _$GroupImpl _$$GroupImplFromJson(Map<String, dynamic> json) => _$GroupImpl(
|
||||
const [],
|
||||
now: json['now'] as String?,
|
||||
hidden: json['hidden'] as bool?,
|
||||
testUrl: json['testUrl'] as String?,
|
||||
icon: json['icon'] as String? ?? "",
|
||||
name: json['name'] as String,
|
||||
);
|
||||
@@ -171,6 +172,7 @@ Map<String, dynamic> _$$GroupImplToJson(_$GroupImpl instance) =>
|
||||
'all': instance.all,
|
||||
'now': instance.now,
|
||||
'hidden': instance.hidden,
|
||||
'testUrl': instance.testUrl,
|
||||
'icon': instance.icon,
|
||||
'name': instance.name,
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ mixin _$AppSetting {
|
||||
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
|
||||
List<DashboardWidget> get dashboardWidgets =>
|
||||
throw _privateConstructorUsedError;
|
||||
bool get onlyProxy => throw _privateConstructorUsedError;
|
||||
bool get onlyStatisticsProxy => throw _privateConstructorUsedError;
|
||||
bool get autoLaunch => throw _privateConstructorUsedError;
|
||||
bool get silentLaunch => throw _privateConstructorUsedError;
|
||||
bool get autoRun => throw _privateConstructorUsedError;
|
||||
@@ -58,7 +58,7 @@ abstract class $AppSettingCopyWith<$Res> {
|
||||
{String? locale,
|
||||
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
|
||||
List<DashboardWidget> dashboardWidgets,
|
||||
bool onlyProxy,
|
||||
bool onlyStatisticsProxy,
|
||||
bool autoLaunch,
|
||||
bool silentLaunch,
|
||||
bool autoRun,
|
||||
@@ -90,7 +90,7 @@ class _$AppSettingCopyWithImpl<$Res, $Val extends AppSetting>
|
||||
$Res call({
|
||||
Object? locale = freezed,
|
||||
Object? dashboardWidgets = null,
|
||||
Object? onlyProxy = null,
|
||||
Object? onlyStatisticsProxy = null,
|
||||
Object? autoLaunch = null,
|
||||
Object? silentLaunch = null,
|
||||
Object? autoRun = null,
|
||||
@@ -113,9 +113,9 @@ class _$AppSettingCopyWithImpl<$Res, $Val extends AppSetting>
|
||||
? _value.dashboardWidgets
|
||||
: dashboardWidgets // ignore: cast_nullable_to_non_nullable
|
||||
as List<DashboardWidget>,
|
||||
onlyProxy: null == onlyProxy
|
||||
? _value.onlyProxy
|
||||
: onlyProxy // ignore: cast_nullable_to_non_nullable
|
||||
onlyStatisticsProxy: null == onlyStatisticsProxy
|
||||
? _value.onlyStatisticsProxy
|
||||
: onlyStatisticsProxy // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
autoLaunch: null == autoLaunch
|
||||
? _value.autoLaunch
|
||||
@@ -181,7 +181,7 @@ abstract class _$$AppSettingImplCopyWith<$Res>
|
||||
{String? locale,
|
||||
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
|
||||
List<DashboardWidget> dashboardWidgets,
|
||||
bool onlyProxy,
|
||||
bool onlyStatisticsProxy,
|
||||
bool autoLaunch,
|
||||
bool silentLaunch,
|
||||
bool autoRun,
|
||||
@@ -211,7 +211,7 @@ class __$$AppSettingImplCopyWithImpl<$Res>
|
||||
$Res call({
|
||||
Object? locale = freezed,
|
||||
Object? dashboardWidgets = null,
|
||||
Object? onlyProxy = null,
|
||||
Object? onlyStatisticsProxy = null,
|
||||
Object? autoLaunch = null,
|
||||
Object? silentLaunch = null,
|
||||
Object? autoRun = null,
|
||||
@@ -234,9 +234,9 @@ class __$$AppSettingImplCopyWithImpl<$Res>
|
||||
? _value._dashboardWidgets
|
||||
: dashboardWidgets // ignore: cast_nullable_to_non_nullable
|
||||
as List<DashboardWidget>,
|
||||
onlyProxy: null == onlyProxy
|
||||
? _value.onlyProxy
|
||||
: onlyProxy // ignore: cast_nullable_to_non_nullable
|
||||
onlyStatisticsProxy: null == onlyStatisticsProxy
|
||||
? _value.onlyStatisticsProxy
|
||||
: onlyStatisticsProxy // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
autoLaunch: null == autoLaunch
|
||||
? _value.autoLaunch
|
||||
@@ -297,7 +297,7 @@ class _$AppSettingImpl implements _AppSetting {
|
||||
{this.locale,
|
||||
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
|
||||
final List<DashboardWidget> dashboardWidgets = defaultDashboardWidgets,
|
||||
this.onlyProxy = false,
|
||||
this.onlyStatisticsProxy = false,
|
||||
this.autoLaunch = false,
|
||||
this.silentLaunch = false,
|
||||
this.autoRun = false,
|
||||
@@ -329,7 +329,7 @@ class _$AppSettingImpl implements _AppSetting {
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool onlyProxy;
|
||||
final bool onlyStatisticsProxy;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool autoLaunch;
|
||||
@@ -369,7 +369,7 @@ class _$AppSettingImpl implements _AppSetting {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppSetting(locale: $locale, dashboardWidgets: $dashboardWidgets, onlyProxy: $onlyProxy, autoLaunch: $autoLaunch, silentLaunch: $silentLaunch, autoRun: $autoRun, openLogs: $openLogs, closeConnections: $closeConnections, testUrl: $testUrl, isAnimateToPage: $isAnimateToPage, autoCheckUpdate: $autoCheckUpdate, showLabel: $showLabel, disclaimerAccepted: $disclaimerAccepted, minimizeOnExit: $minimizeOnExit, hidden: $hidden)';
|
||||
return 'AppSetting(locale: $locale, dashboardWidgets: $dashboardWidgets, onlyStatisticsProxy: $onlyStatisticsProxy, autoLaunch: $autoLaunch, silentLaunch: $silentLaunch, autoRun: $autoRun, openLogs: $openLogs, closeConnections: $closeConnections, testUrl: $testUrl, isAnimateToPage: $isAnimateToPage, autoCheckUpdate: $autoCheckUpdate, showLabel: $showLabel, disclaimerAccepted: $disclaimerAccepted, minimizeOnExit: $minimizeOnExit, hidden: $hidden)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -380,8 +380,8 @@ class _$AppSettingImpl implements _AppSetting {
|
||||
(identical(other.locale, locale) || other.locale == locale) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._dashboardWidgets, _dashboardWidgets) &&
|
||||
(identical(other.onlyProxy, onlyProxy) ||
|
||||
other.onlyProxy == onlyProxy) &&
|
||||
(identical(other.onlyStatisticsProxy, onlyStatisticsProxy) ||
|
||||
other.onlyStatisticsProxy == onlyStatisticsProxy) &&
|
||||
(identical(other.autoLaunch, autoLaunch) ||
|
||||
other.autoLaunch == autoLaunch) &&
|
||||
(identical(other.silentLaunch, silentLaunch) ||
|
||||
@@ -411,7 +411,7 @@ class _$AppSettingImpl implements _AppSetting {
|
||||
runtimeType,
|
||||
locale,
|
||||
const DeepCollectionEquality().hash(_dashboardWidgets),
|
||||
onlyProxy,
|
||||
onlyStatisticsProxy,
|
||||
autoLaunch,
|
||||
silentLaunch,
|
||||
autoRun,
|
||||
@@ -446,7 +446,7 @@ abstract class _AppSetting implements AppSetting {
|
||||
{final String? locale,
|
||||
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
|
||||
final List<DashboardWidget> dashboardWidgets,
|
||||
final bool onlyProxy,
|
||||
final bool onlyStatisticsProxy,
|
||||
final bool autoLaunch,
|
||||
final bool silentLaunch,
|
||||
final bool autoRun,
|
||||
@@ -469,7 +469,7 @@ abstract class _AppSetting implements AppSetting {
|
||||
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
|
||||
List<DashboardWidget> get dashboardWidgets;
|
||||
@override
|
||||
bool get onlyProxy;
|
||||
bool get onlyStatisticsProxy;
|
||||
@override
|
||||
bool get autoLaunch;
|
||||
@override
|
||||
|
||||
@@ -57,7 +57,7 @@ _$AppSettingImpl _$$AppSettingImplFromJson(Map<String, dynamic> json) =>
|
||||
dashboardWidgets: json['dashboardWidgets'] == null
|
||||
? defaultDashboardWidgets
|
||||
: dashboardWidgetsRealFormJson(json['dashboardWidgets'] as List?),
|
||||
onlyProxy: json['onlyProxy'] as bool? ?? false,
|
||||
onlyStatisticsProxy: json['onlyStatisticsProxy'] as bool? ?? false,
|
||||
autoLaunch: json['autoLaunch'] as bool? ?? false,
|
||||
silentLaunch: json['silentLaunch'] as bool? ?? false,
|
||||
autoRun: json['autoRun'] as bool? ?? false,
|
||||
@@ -78,7 +78,7 @@ Map<String, dynamic> _$$AppSettingImplToJson(_$AppSettingImpl instance) =>
|
||||
'dashboardWidgets': instance.dashboardWidgets
|
||||
.map((e) => _$DashboardWidgetEnumMap[e]!)
|
||||
.toList(),
|
||||
'onlyProxy': instance.onlyProxy,
|
||||
'onlyStatisticsProxy': instance.onlyStatisticsProxy,
|
||||
'autoLaunch': instance.autoLaunch,
|
||||
'silentLaunch': instance.silentLaunch,
|
||||
'autoRun': instance.autoRun,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,6 @@ _$CoreStateImpl _$$CoreStateImplFromJson(Map<String, dynamic> json) =>
|
||||
.map((e) => e as String)
|
||||
.toList(),
|
||||
ipv6: json['ipv6'] as bool,
|
||||
onlyProxy: json['onlyProxy'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$CoreStateImplToJson(_$CoreStateImpl instance) =>
|
||||
@@ -36,7 +35,6 @@ Map<String, dynamic> _$$CoreStateImplToJson(_$CoreStateImpl instance) =>
|
||||
'bypassDomain': instance.bypassDomain,
|
||||
'routeAddress': instance.routeAddress,
|
||||
'ipv6': instance.ipv6,
|
||||
'onlyProxy': instance.onlyProxy,
|
||||
};
|
||||
|
||||
_$AndroidVpnOptionsImpl _$$AndroidVpnOptionsImplFromJson(
|
||||
@@ -84,6 +82,7 @@ _$ConfigExtendedParamsImpl _$$ConfigExtendedParamsImplFromJson(
|
||||
selectedMap: Map<String, String>.from(json['selected-map'] as Map),
|
||||
overrideDns: json['override-dns'] as bool,
|
||||
testUrl: json['test-url'] as String,
|
||||
onlyStatisticsProxy: json['only-statistics-proxy'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$ConfigExtendedParamsImplToJson(
|
||||
@@ -94,6 +93,7 @@ Map<String, dynamic> _$$ConfigExtendedParamsImplToJson(
|
||||
'selected-map': instance.selectedMap,
|
||||
'override-dns': instance.overrideDns,
|
||||
'test-url': instance.testUrl,
|
||||
'only-statistics-proxy': instance.onlyStatisticsProxy,
|
||||
};
|
||||
|
||||
_$UpdateConfigParamsImpl _$$UpdateConfigParamsImplFromJson(
|
||||
@@ -127,6 +127,20 @@ Map<String, dynamic> _$$ChangeProxyParamsImplToJson(
|
||||
'proxy-name': instance.proxyName,
|
||||
};
|
||||
|
||||
_$UpdateGeoDataParamsImpl _$$UpdateGeoDataParamsImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$UpdateGeoDataParamsImpl(
|
||||
geoType: json['geo-type'] as String,
|
||||
geoName: json['geo-name'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$UpdateGeoDataParamsImplToJson(
|
||||
_$UpdateGeoDataParamsImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'geo-type': instance.geoType,
|
||||
'geo-name': instance.geoName,
|
||||
};
|
||||
|
||||
_$AppMessageImpl _$$AppMessageImplFromJson(Map<String, dynamic> json) =>
|
||||
_$AppMessageImpl(
|
||||
type: $enumDecode(_$AppMessageTypeEnumMap, json['type']),
|
||||
@@ -143,38 +157,36 @@ const _$AppMessageTypeEnumMap = {
|
||||
AppMessageType.log: 'log',
|
||||
AppMessageType.delay: 'delay',
|
||||
AppMessageType.request: 'request',
|
||||
AppMessageType.started: 'started',
|
||||
AppMessageType.loaded: 'loaded',
|
||||
};
|
||||
|
||||
_$ServiceMessageImpl _$$ServiceMessageImplFromJson(Map<String, dynamic> json) =>
|
||||
_$ServiceMessageImpl(
|
||||
type: $enumDecode(_$ServiceMessageTypeEnumMap, json['type']),
|
||||
_$InvokeMessageImpl _$$InvokeMessageImplFromJson(Map<String, dynamic> json) =>
|
||||
_$InvokeMessageImpl(
|
||||
type: $enumDecode(_$InvokeMessageTypeEnumMap, json['type']),
|
||||
data: json['data'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$ServiceMessageImplToJson(
|
||||
_$ServiceMessageImpl instance) =>
|
||||
Map<String, dynamic> _$$InvokeMessageImplToJson(_$InvokeMessageImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'type': _$ServiceMessageTypeEnumMap[instance.type]!,
|
||||
'type': _$InvokeMessageTypeEnumMap[instance.type]!,
|
||||
'data': instance.data,
|
||||
};
|
||||
|
||||
const _$ServiceMessageTypeEnumMap = {
|
||||
ServiceMessageType.protect: 'protect',
|
||||
ServiceMessageType.process: 'process',
|
||||
ServiceMessageType.started: 'started',
|
||||
ServiceMessageType.loaded: 'loaded',
|
||||
const _$InvokeMessageTypeEnumMap = {
|
||||
InvokeMessageType.protect: 'protect',
|
||||
InvokeMessageType.process: 'process',
|
||||
};
|
||||
|
||||
_$DelayImpl _$$DelayImplFromJson(Map<String, dynamic> json) => _$DelayImpl(
|
||||
name: json['name'] as String,
|
||||
url: json['url'] as String,
|
||||
value: (json['value'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$DelayImplToJson(_$DelayImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'name': instance.name,
|
||||
'url': instance.url,
|
||||
'value': instance.value,
|
||||
};
|
||||
|
||||
@@ -190,7 +202,7 @@ Map<String, dynamic> _$$NowImplToJson(_$NowImpl instance) => <String, dynamic>{
|
||||
|
||||
_$ProcessDataImpl _$$ProcessDataImplFromJson(Map<String, dynamic> json) =>
|
||||
_$ProcessDataImpl(
|
||||
id: (json['id'] as num).toInt(),
|
||||
id: json['id'] as String,
|
||||
metadata: Metadata.fromJson(json['metadata'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
@@ -201,7 +213,7 @@ Map<String, dynamic> _$$ProcessDataImplToJson(_$ProcessDataImpl instance) =>
|
||||
};
|
||||
|
||||
_$FdImpl _$$FdImplFromJson(Map<String, dynamic> json) => _$FdImpl(
|
||||
id: (json['id'] as num).toInt(),
|
||||
id: json['id'] as String,
|
||||
value: (json['value'] as num).toInt(),
|
||||
);
|
||||
|
||||
@@ -212,7 +224,7 @@ Map<String, dynamic> _$$FdImplToJson(_$FdImpl instance) => <String, dynamic>{
|
||||
|
||||
_$ProcessMapItemImpl _$$ProcessMapItemImplFromJson(Map<String, dynamic> json) =>
|
||||
_$ProcessMapItemImpl(
|
||||
id: (json['id'] as num).toInt(),
|
||||
id: json['id'] as String,
|
||||
value: json['value'] as String,
|
||||
);
|
||||
|
||||
@@ -293,6 +305,7 @@ Map<String, dynamic> _$$TunPropsImplToJson(_$TunPropsImpl instance) =>
|
||||
_$ActionImpl _$$ActionImplFromJson(Map<String, dynamic> json) => _$ActionImpl(
|
||||
method: $enumDecode(_$ActionMethodEnumMap, json['method']),
|
||||
data: json['data'],
|
||||
defaultValue: json['default-value'],
|
||||
id: json['id'] as String,
|
||||
);
|
||||
|
||||
@@ -300,6 +313,7 @@ Map<String, dynamic> _$$ActionImplToJson(_$ActionImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'method': _$ActionMethodEnumMap[instance.method]!,
|
||||
'data': instance.data,
|
||||
'default-value': instance.defaultValue,
|
||||
'id': instance.id,
|
||||
};
|
||||
|
||||
@@ -331,4 +345,27 @@ const _$ActionMethodEnumMap = {
|
||||
ActionMethod.stopListener: 'stopListener',
|
||||
ActionMethod.getCountryCode: 'getCountryCode',
|
||||
ActionMethod.getMemory: 'getMemory',
|
||||
ActionMethod.setFdMap: 'setFdMap',
|
||||
ActionMethod.setProcessMap: 'setProcessMap',
|
||||
ActionMethod.setState: 'setState',
|
||||
ActionMethod.startTun: 'startTun',
|
||||
ActionMethod.stopTun: 'stopTun',
|
||||
ActionMethod.getRunTime: 'getRunTime',
|
||||
ActionMethod.updateDns: 'updateDns',
|
||||
ActionMethod.getAndroidVpnOptions: 'getAndroidVpnOptions',
|
||||
ActionMethod.getCurrentProfileName: 'getCurrentProfileName',
|
||||
};
|
||||
|
||||
_$ActionResultImpl _$$ActionResultImplFromJson(Map<String, dynamic> json) =>
|
||||
_$ActionResultImpl(
|
||||
method: $enumDecode(_$ActionMethodEnumMap, json['method']),
|
||||
data: json['data'],
|
||||
id: json['id'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$ActionResultImplToJson(_$ActionResultImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'method': _$ActionMethodEnumMap[instance.method]!,
|
||||
'data': instance.data,
|
||||
'id': instance.id,
|
||||
};
|
||||
|
||||
@@ -2298,6 +2298,7 @@ abstract class _ProxiesListSelectorState implements ProxiesListSelectorState {
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ProxyGroupSelectorState {
|
||||
String? get testUrl => throw _privateConstructorUsedError;
|
||||
ProxiesSortType get proxiesSortType => throw _privateConstructorUsedError;
|
||||
ProxyCardType get proxyCardType => throw _privateConstructorUsedError;
|
||||
num get sortNum => throw _privateConstructorUsedError;
|
||||
@@ -2319,7 +2320,8 @@ abstract class $ProxyGroupSelectorStateCopyWith<$Res> {
|
||||
_$ProxyGroupSelectorStateCopyWithImpl<$Res, ProxyGroupSelectorState>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{ProxiesSortType proxiesSortType,
|
||||
{String? testUrl,
|
||||
ProxiesSortType proxiesSortType,
|
||||
ProxyCardType proxyCardType,
|
||||
num sortNum,
|
||||
GroupType groupType,
|
||||
@@ -2343,6 +2345,7 @@ class _$ProxyGroupSelectorStateCopyWithImpl<$Res,
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? testUrl = freezed,
|
||||
Object? proxiesSortType = null,
|
||||
Object? proxyCardType = null,
|
||||
Object? sortNum = null,
|
||||
@@ -2351,6 +2354,10 @@ class _$ProxyGroupSelectorStateCopyWithImpl<$Res,
|
||||
Object? columns = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
testUrl: freezed == testUrl
|
||||
? _value.testUrl
|
||||
: testUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
proxiesSortType: null == proxiesSortType
|
||||
? _value.proxiesSortType
|
||||
: proxiesSortType // ignore: cast_nullable_to_non_nullable
|
||||
@@ -2389,7 +2396,8 @@ abstract class _$$ProxyGroupSelectorStateImplCopyWith<$Res>
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{ProxiesSortType proxiesSortType,
|
||||
{String? testUrl,
|
||||
ProxiesSortType proxiesSortType,
|
||||
ProxyCardType proxyCardType,
|
||||
num sortNum,
|
||||
GroupType groupType,
|
||||
@@ -2412,6 +2420,7 @@ class __$$ProxyGroupSelectorStateImplCopyWithImpl<$Res>
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? testUrl = freezed,
|
||||
Object? proxiesSortType = null,
|
||||
Object? proxyCardType = null,
|
||||
Object? sortNum = null,
|
||||
@@ -2420,6 +2429,10 @@ class __$$ProxyGroupSelectorStateImplCopyWithImpl<$Res>
|
||||
Object? columns = null,
|
||||
}) {
|
||||
return _then(_$ProxyGroupSelectorStateImpl(
|
||||
testUrl: freezed == testUrl
|
||||
? _value.testUrl
|
||||
: testUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
proxiesSortType: null == proxiesSortType
|
||||
? _value.proxiesSortType
|
||||
: proxiesSortType // ignore: cast_nullable_to_non_nullable
|
||||
@@ -2452,7 +2465,8 @@ class __$$ProxyGroupSelectorStateImplCopyWithImpl<$Res>
|
||||
|
||||
class _$ProxyGroupSelectorStateImpl implements _ProxyGroupSelectorState {
|
||||
const _$ProxyGroupSelectorStateImpl(
|
||||
{required this.proxiesSortType,
|
||||
{required this.testUrl,
|
||||
required this.proxiesSortType,
|
||||
required this.proxyCardType,
|
||||
required this.sortNum,
|
||||
required this.groupType,
|
||||
@@ -2460,6 +2474,8 @@ class _$ProxyGroupSelectorStateImpl implements _ProxyGroupSelectorState {
|
||||
required this.columns})
|
||||
: _proxies = proxies;
|
||||
|
||||
@override
|
||||
final String? testUrl;
|
||||
@override
|
||||
final ProxiesSortType proxiesSortType;
|
||||
@override
|
||||
@@ -2481,7 +2497,7 @@ class _$ProxyGroupSelectorStateImpl implements _ProxyGroupSelectorState {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ProxyGroupSelectorState(proxiesSortType: $proxiesSortType, proxyCardType: $proxyCardType, sortNum: $sortNum, groupType: $groupType, proxies: $proxies, columns: $columns)';
|
||||
return 'ProxyGroupSelectorState(testUrl: $testUrl, proxiesSortType: $proxiesSortType, proxyCardType: $proxyCardType, sortNum: $sortNum, groupType: $groupType, proxies: $proxies, columns: $columns)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2489,6 +2505,7 @@ class _$ProxyGroupSelectorStateImpl implements _ProxyGroupSelectorState {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$ProxyGroupSelectorStateImpl &&
|
||||
(identical(other.testUrl, testUrl) || other.testUrl == testUrl) &&
|
||||
(identical(other.proxiesSortType, proxiesSortType) ||
|
||||
other.proxiesSortType == proxiesSortType) &&
|
||||
(identical(other.proxyCardType, proxyCardType) ||
|
||||
@@ -2503,6 +2520,7 @@ class _$ProxyGroupSelectorStateImpl implements _ProxyGroupSelectorState {
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
testUrl,
|
||||
proxiesSortType,
|
||||
proxyCardType,
|
||||
sortNum,
|
||||
@@ -2522,13 +2540,16 @@ class _$ProxyGroupSelectorStateImpl implements _ProxyGroupSelectorState {
|
||||
|
||||
abstract class _ProxyGroupSelectorState implements ProxyGroupSelectorState {
|
||||
const factory _ProxyGroupSelectorState(
|
||||
{required final ProxiesSortType proxiesSortType,
|
||||
{required final String? testUrl,
|
||||
required final ProxiesSortType proxiesSortType,
|
||||
required final ProxyCardType proxyCardType,
|
||||
required final num sortNum,
|
||||
required final GroupType groupType,
|
||||
required final List<Proxy> proxies,
|
||||
required final int columns}) = _$ProxyGroupSelectorStateImpl;
|
||||
|
||||
@override
|
||||
String? get testUrl;
|
||||
@override
|
||||
ProxiesSortType get proxiesSortType;
|
||||
@override
|
||||
|
||||
@@ -123,6 +123,7 @@ class ProxiesListSelectorState with _$ProxiesListSelectorState {
|
||||
@freezed
|
||||
class ProxyGroupSelectorState with _$ProxyGroupSelectorState {
|
||||
const factory ProxyGroupSelectorState({
|
||||
required String? testUrl,
|
||||
required ProxiesSortType proxiesSortType,
|
||||
required ProxyCardType proxyCardType,
|
||||
required num sortNum,
|
||||
|
||||
259
lib/pages/editor.dart
Normal file
259
lib/pages/editor.dart
Normal file
@@ -0,0 +1,259 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/widgets/scaffold.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:re_editor/re_editor.dart';
|
||||
import 'package:re_highlight/languages/yaml.dart';
|
||||
import 'package:re_highlight/styles/atom-one-light.dart';
|
||||
|
||||
import '../models/common.dart';
|
||||
|
||||
typedef EditingValueChangeBuilder = Widget Function(CodeLineEditingValue value);
|
||||
|
||||
class EditorPage extends StatefulWidget {
|
||||
final String title;
|
||||
final String content;
|
||||
final Function(BuildContext context, String text)? onSave;
|
||||
final Future<bool> Function(BuildContext context, String text)? onPop;
|
||||
|
||||
const EditorPage({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.content,
|
||||
this.onSave,
|
||||
this.onPop,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EditorPage> createState() => _EditorPageState();
|
||||
}
|
||||
|
||||
class _EditorPageState extends State<EditorPage> {
|
||||
late CodeLineEditingController _controller;
|
||||
final _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = CodeLineEditingController.fromText(widget.content);
|
||||
if (system.isDesktop) {
|
||||
return;
|
||||
}
|
||||
_focusNode.onKeyEvent = ((_, event) {
|
||||
final keys = HardwareKeyboard.instance.logicalKeysPressed;
|
||||
final key = event.logicalKey;
|
||||
if (!keys.contains(key)) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
if (key == LogicalKeyboardKey.arrowUp) {
|
||||
_controller.moveCursor(AxisDirection.up);
|
||||
return KeyEventResult.handled;
|
||||
} else if (key == LogicalKeyboardKey.arrowDown) {
|
||||
_controller.moveCursor(AxisDirection.down);
|
||||
return KeyEventResult.handled;
|
||||
} else if (key == LogicalKeyboardKey.arrowLeft) {
|
||||
_controller.selection.endIndex;
|
||||
_controller.moveCursor(AxisDirection.left);
|
||||
return KeyEventResult.handled;
|
||||
} else if (key == LogicalKeyboardKey.arrowRight) {
|
||||
_controller.moveCursor(AxisDirection.right);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _wrapController(EditingValueChangeBuilder builder) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _controller,
|
||||
builder: (_, value, ___) {
|
||||
return builder(value);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) async {
|
||||
if (didPop) return;
|
||||
if (widget.onPop != null) {
|
||||
final res = await widget.onPop!(context, _controller.text);
|
||||
if (res && context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: CommonScaffold(
|
||||
actions: [
|
||||
_wrapController(
|
||||
(value) => IconButton(
|
||||
onPressed: _controller.canUndo ? _controller.undo : null,
|
||||
icon: const Icon(Icons.undo),
|
||||
),
|
||||
),
|
||||
_wrapController(
|
||||
(value) => IconButton(
|
||||
onPressed: _controller.canRedo ? _controller.redo : null,
|
||||
icon: const Icon(Icons.redo),
|
||||
),
|
||||
),
|
||||
if (widget.onSave != null)
|
||||
_wrapController(
|
||||
(value) => IconButton(
|
||||
onPressed: _controller.text == widget.content
|
||||
? null
|
||||
: () {
|
||||
widget.onSave!(context, _controller.text);
|
||||
},
|
||||
icon: const Icon(Icons.save_sharp),
|
||||
),
|
||||
),
|
||||
],
|
||||
body: CodeEditor(
|
||||
focusNode: _focusNode,
|
||||
scrollbarBuilder: (context, child, details) {
|
||||
return Scrollbar(
|
||||
controller: details.controller,
|
||||
thickness: 8,
|
||||
radius: const Radius.circular(2),
|
||||
interactive: true,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
toolbarController: ContextMenuControllerImpl(),
|
||||
indicatorBuilder: (
|
||||
context,
|
||||
editingController,
|
||||
chunkController,
|
||||
notifier,
|
||||
) {
|
||||
return Row(
|
||||
children: [
|
||||
DefaultCodeLineNumber(
|
||||
controller: editingController,
|
||||
notifier: notifier,
|
||||
),
|
||||
DefaultCodeChunkIndicator(
|
||||
width: 20,
|
||||
controller: chunkController,
|
||||
notifier: notifier,
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
shortcutsActivatorsBuilder: DefaultCodeShortcutsActivatorsBuilder(),
|
||||
controller: _controller,
|
||||
style: CodeEditorStyle(
|
||||
fontSize: 14,
|
||||
codeTheme: CodeHighlightTheme(
|
||||
languages: {
|
||||
'yaml': CodeHighlightThemeMode(
|
||||
mode: langYaml,
|
||||
)
|
||||
},
|
||||
theme: atomOneLightTheme,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: widget.title,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ContextMenuControllerImpl implements SelectionToolbarController {
|
||||
OverlayEntry? _overlayEntry;
|
||||
bool _isFirstRender = true;
|
||||
|
||||
_removeOverLayEntry() {
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
_isFirstRender = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void hide(BuildContext context) {
|
||||
_removeOverLayEntry();
|
||||
}
|
||||
|
||||
@override
|
||||
void show({
|
||||
required context,
|
||||
required controller,
|
||||
required anchors,
|
||||
renderRect,
|
||||
required layerLink,
|
||||
required ValueNotifier<bool> visibility,
|
||||
}) {
|
||||
_removeOverLayEntry();
|
||||
_overlayEntry ??= OverlayEntry(
|
||||
builder: (context) => CodeEditorTapRegion(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: controller,
|
||||
builder: (_, __, child) {
|
||||
final isNotEmpty = controller.selectedText.isNotEmpty;
|
||||
final isAllSelected = controller.isAllSelected;
|
||||
final hasSelected = controller.selectedText.isNotEmpty;
|
||||
List<ActionItemData> menus = [
|
||||
if (isNotEmpty)
|
||||
ActionItemData(
|
||||
label: appLocalizations.copy,
|
||||
onPressed: controller.copy,
|
||||
),
|
||||
ActionItemData(
|
||||
label: appLocalizations.paste,
|
||||
onPressed: controller.paste,
|
||||
),
|
||||
if (isNotEmpty)
|
||||
ActionItemData(
|
||||
label: appLocalizations.cut,
|
||||
onPressed: controller.cut,
|
||||
),
|
||||
if (hasSelected && !isAllSelected)
|
||||
ActionItemData(
|
||||
label: appLocalizations.selectAll,
|
||||
onPressed: controller.selectAll,
|
||||
),
|
||||
];
|
||||
if (_isFirstRender) {
|
||||
_isFirstRender = false;
|
||||
} else if (controller.selectedText.isEmpty) {
|
||||
_removeOverLayEntry();
|
||||
}
|
||||
return TextSelectionToolbar(
|
||||
anchorAbove: anchors.primaryAnchor,
|
||||
anchorBelow: anchors.secondaryAnchor ?? Offset.zero,
|
||||
children: menus.asMap().entries.map(
|
||||
(MapEntry<int, ActionItemData> entry) {
|
||||
return TextSelectionToolbarTextButton(
|
||||
padding: TextSelectionToolbarTextButton.getPadding(
|
||||
entry.key,
|
||||
menus.length,
|
||||
),
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
onPressed: () {
|
||||
entry.value.onPressed();
|
||||
},
|
||||
child: Text(entry.value.label),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export 'home.dart';
|
||||
export 'scan.dart';
|
||||
export 'scan.dart';
|
||||
export 'editor.dart';
|
||||
@@ -1,8 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../clash/lib.dart';
|
||||
|
||||
class Service {
|
||||
static Service? _instance;
|
||||
late MethodChannel methodChannel;
|
||||
@@ -24,7 +28,17 @@ class Service {
|
||||
Future<bool?> destroy() async {
|
||||
return await methodChannel.invokeMethod<bool>("destroy");
|
||||
}
|
||||
|
||||
Future<bool?> startVpn() async {
|
||||
final options = await clashLib?.getAndroidVpnOptions();
|
||||
return await methodChannel.invokeMethod<bool>("startVpn", {
|
||||
'data': json.encode(options),
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool?> stopVpn() async {
|
||||
return await methodChannel.invokeMethod<bool>("stopVpn");
|
||||
}
|
||||
}
|
||||
|
||||
final service =
|
||||
Platform.isAndroid ? Service() : null;
|
||||
Service? get service => Platform.isAndroid && !globalState.isService ? Service() : null;
|
||||
|
||||
@@ -15,7 +15,6 @@ abstract mixin class TileListener {
|
||||
}
|
||||
|
||||
class Tile {
|
||||
StreamSubscription? subscription;
|
||||
|
||||
final MethodChannel _channel = const MethodChannel('tile');
|
||||
|
||||
|
||||
@@ -1,35 +1,46 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
abstract mixin class VpnListener {
|
||||
void onStarted(int fd) {}
|
||||
|
||||
void onDnsChanged(String dns) {}
|
||||
}
|
||||
|
||||
class Vpn {
|
||||
static Vpn? _instance;
|
||||
late MethodChannel methodChannel;
|
||||
ReceivePort? receiver;
|
||||
ServiceMessageListener? _serviceMessageHandler;
|
||||
FutureOr<String> Function()? handleGetStartForegroundParams;
|
||||
|
||||
Vpn._internal() {
|
||||
methodChannel = const MethodChannel("vpn");
|
||||
methodChannel.setMethodCallHandler((call) async {
|
||||
switch (call.method) {
|
||||
case "started":
|
||||
final fd = call.arguments as int;
|
||||
onStarted(fd);
|
||||
break;
|
||||
case "gc":
|
||||
clashCore.requestGc();
|
||||
case "dnsChanged":
|
||||
final dns = call.arguments as String;
|
||||
clashLib?.updateDns(dns);
|
||||
case "getStartForegroundParams":
|
||||
if (handleGetStartForegroundParams != null) {
|
||||
return await handleGetStartForegroundParams!();
|
||||
}
|
||||
return "";
|
||||
default:
|
||||
throw MissingPluginException();
|
||||
for (final VpnListener listener in _listeners) {
|
||||
switch (call.method) {
|
||||
case "started":
|
||||
final fd = call.arguments as int;
|
||||
listener.onStarted(fd);
|
||||
break;
|
||||
case "dnsChanged":
|
||||
final dns = call.arguments as String;
|
||||
listener.onDnsChanged(dns);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -39,14 +50,15 @@ class Vpn {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<bool?> startVpn() async {
|
||||
final options = clashLib?.getAndroidVpnOptions();
|
||||
final ObserverList<VpnListener> _listeners = ObserverList<VpnListener>();
|
||||
|
||||
Future<bool?> start(AndroidVpnOptions options) async {
|
||||
return await methodChannel.invokeMethod<bool>("start", {
|
||||
'data': json.encode(options),
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool?> stopVpn() async {
|
||||
Future<bool?> stop() async {
|
||||
return await methodChannel.invokeMethod<bool>("stop");
|
||||
}
|
||||
|
||||
@@ -60,45 +72,13 @@ class Vpn {
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool?> startForeground({
|
||||
required String title,
|
||||
required String content,
|
||||
}) async {
|
||||
return await methodChannel.invokeMethod<bool?>("startForeground", {
|
||||
'title': title,
|
||||
'content': content,
|
||||
});
|
||||
void addListener(VpnListener listener) {
|
||||
_listeners.add(listener);
|
||||
}
|
||||
|
||||
onStarted(int fd) {
|
||||
if (receiver != null) {
|
||||
receiver!.close();
|
||||
receiver == null;
|
||||
}
|
||||
receiver = ReceivePort();
|
||||
receiver!.listen((message) {
|
||||
_handleServiceMessage(message);
|
||||
});
|
||||
clashLib?.startTun(fd, receiver!.sendPort.nativePort);
|
||||
}
|
||||
|
||||
setServiceMessageHandler(ServiceMessageListener serviceMessageListener) {
|
||||
_serviceMessageHandler = serviceMessageListener;
|
||||
}
|
||||
|
||||
_handleServiceMessage(String message) {
|
||||
final m = ServiceMessage.fromJson(json.decode(message));
|
||||
switch (m.type) {
|
||||
case ServiceMessageType.protect:
|
||||
_serviceMessageHandler?.onProtect(Fd.fromJson(m.data));
|
||||
case ServiceMessageType.process:
|
||||
_serviceMessageHandler?.onProcess(ProcessData.fromJson(m.data));
|
||||
case ServiceMessageType.started:
|
||||
_serviceMessageHandler?.onStarted(m.data);
|
||||
case ServiceMessageType.loaded:
|
||||
_serviceMessageHandler?.onLoaded(m.data);
|
||||
}
|
||||
void removeListener(VpnListener listener) {
|
||||
_listeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
final vpn = Platform.isAndroid ? Vpn() : null;
|
||||
Vpn? get vpn => globalState.isService ? Vpn() : null;
|
||||
|
||||
168
lib/state.dart
168
lib/state.dart
@@ -5,7 +5,6 @@ import 'package:animations/animations.dart';
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/plugins/service.dart';
|
||||
import 'package:fl_clash/plugins/vpn.dart';
|
||||
import 'package:fl_clash/widgets/scaffold.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
@@ -15,37 +14,49 @@ import 'common/common.dart';
|
||||
import 'controller.dart';
|
||||
import 'models/models.dart';
|
||||
|
||||
typedef UpdateTasks = List<FutureOr Function()>;
|
||||
|
||||
class GlobalState {
|
||||
bool isService = false;
|
||||
Timer? timer;
|
||||
Timer? groupsUpdateTimer;
|
||||
var isVpnService = false;
|
||||
late PackageInfo packageInfo;
|
||||
Function? updateCurrentDelayDebounce;
|
||||
PageController? pageController;
|
||||
late Measure measure;
|
||||
DateTime? startTime;
|
||||
UpdateTasks tasks = [];
|
||||
final safeMessageOffsetNotifier = ValueNotifier(Offset.zero);
|
||||
final navigatorKey = GlobalKey<NavigatorState>();
|
||||
late AppController appController;
|
||||
GlobalKey<CommonScaffoldState> homeScaffoldKey = GlobalKey();
|
||||
List<Function> updateFunctionLists = [];
|
||||
bool lastTunEnable = false;
|
||||
int? lastProfileModified;
|
||||
|
||||
bool get isStart => startTime != null && startTime!.isBeforeNow;
|
||||
|
||||
startListenUpdate() {
|
||||
startUpdateTasks([UpdateTasks? tasks]) async {
|
||||
if (timer != null && timer!.isActive == true) return;
|
||||
timer = Timer.periodic(const Duration(seconds: 1), (Timer t) {
|
||||
for (final function in updateFunctionLists) {
|
||||
function();
|
||||
}
|
||||
if (tasks != null) {
|
||||
this.tasks = tasks;
|
||||
}
|
||||
await executorUpdateTask();
|
||||
timer = Timer(const Duration(seconds: 1), () async {
|
||||
startUpdateTasks();
|
||||
});
|
||||
}
|
||||
|
||||
stopListenUpdate() {
|
||||
executorUpdateTask() async {
|
||||
if (timer != null && timer!.isActive == true) return;
|
||||
for (final task in tasks) {
|
||||
await task();
|
||||
}
|
||||
}
|
||||
|
||||
stopUpdateTasks() {
|
||||
if (timer == null || timer?.isActive == false) return;
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
}
|
||||
|
||||
Future<void> initCore({
|
||||
@@ -100,33 +111,18 @@ class GlobalState {
|
||||
clashCore.stopLog();
|
||||
}
|
||||
final res = await clashCore.updateConfig(
|
||||
UpdateConfigParams(
|
||||
profileId: config.currentProfileId ?? "",
|
||||
config: useClashConfig,
|
||||
params: ConfigExtendedParams(
|
||||
isPatch: isPatch,
|
||||
isCompatible: true,
|
||||
selectedMap: config.currentSelectedMap,
|
||||
overrideDns: config.overrideDns,
|
||||
testUrl: config.appSetting.testUrl,
|
||||
),
|
||||
),
|
||||
getUpdateConfigParams(config, clashConfig, isPatch),
|
||||
);
|
||||
if (res.isNotEmpty) throw res;
|
||||
lastTunEnable = useClashConfig.tun.enable;
|
||||
lastProfileModified = await config.getCurrentProfile()?.profileLastModified;
|
||||
}
|
||||
|
||||
handleStart() async {
|
||||
handleStart([UpdateTasks? tasks]) async {
|
||||
await clashCore.startListener();
|
||||
if (globalState.isVpnService) {
|
||||
await vpn?.startVpn();
|
||||
startListenUpdate();
|
||||
return;
|
||||
}
|
||||
await service?.startVpn();
|
||||
startUpdateTasks(tasks);
|
||||
startTime ??= DateTime.now();
|
||||
await service?.init();
|
||||
startListenUpdate();
|
||||
}
|
||||
|
||||
restartCore({
|
||||
@@ -135,27 +131,28 @@ class GlobalState {
|
||||
required Config config,
|
||||
bool isPatch = true,
|
||||
}) async {
|
||||
await clashService?.startCore();
|
||||
await clashService?.reStart();
|
||||
await initCore(
|
||||
appState: appState,
|
||||
clashConfig: clashConfig,
|
||||
config: config,
|
||||
);
|
||||
|
||||
if (isStart) {
|
||||
await handleStart();
|
||||
}
|
||||
}
|
||||
|
||||
updateStartTime() {
|
||||
startTime = clashLib?.getRunTime();
|
||||
Future updateStartTime() async {
|
||||
startTime = await clashLib?.getRunTime();
|
||||
}
|
||||
|
||||
Future handleStop() async {
|
||||
startTime = null;
|
||||
await clashCore.stopListener();
|
||||
clashLib?.stopTun();
|
||||
await service?.destroy();
|
||||
stopListenUpdate();
|
||||
await service?.stopVpn();
|
||||
stopUpdateTasks();
|
||||
}
|
||||
|
||||
Future applyProfile({
|
||||
@@ -178,6 +175,35 @@ class GlobalState {
|
||||
appState.providers = await clashCore.getExternalProviders();
|
||||
}
|
||||
|
||||
CoreState getCoreState(Config config, ClashConfig clashConfig) {
|
||||
return CoreState(
|
||||
enable: config.vpnProps.enable,
|
||||
accessControl: config.isAccessControl ? config.accessControl : null,
|
||||
ipv6: config.vpnProps.ipv6,
|
||||
allowBypass: config.vpnProps.allowBypass,
|
||||
bypassDomain: config.networkProps.bypassDomain,
|
||||
systemProxy: config.vpnProps.systemProxy,
|
||||
currentProfileName:
|
||||
config.currentProfile?.label ?? config.currentProfileId ?? "",
|
||||
routeAddress: clashConfig.routeAddress,
|
||||
);
|
||||
}
|
||||
|
||||
getUpdateConfigParams(Config config, ClashConfig clashConfig, bool isPatch) {
|
||||
return UpdateConfigParams(
|
||||
profileId: config.currentProfileId ?? "",
|
||||
config: clashConfig,
|
||||
params: ConfigExtendedParams(
|
||||
isPatch: isPatch,
|
||||
isCompatible: true,
|
||||
selectedMap: config.currentSelectedMap,
|
||||
overrideDns: config.overrideDns,
|
||||
testUrl: config.appSetting.testUrl,
|
||||
onlyStatisticsProxy: config.appSetting.onlyStatisticsProxy,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
init({
|
||||
required AppState appState,
|
||||
required Config config,
|
||||
@@ -190,18 +216,7 @@ class GlobalState {
|
||||
clashConfig: clashConfig,
|
||||
);
|
||||
clashLib?.setState(
|
||||
CoreState(
|
||||
enable: config.vpnProps.enable,
|
||||
accessControl: config.isAccessControl ? config.accessControl : null,
|
||||
ipv6: config.vpnProps.ipv6,
|
||||
allowBypass: config.vpnProps.allowBypass,
|
||||
systemProxy: config.vpnProps.systemProxy,
|
||||
onlyProxy: config.appSetting.onlyProxy,
|
||||
bypassDomain: config.networkProps.bypassDomain,
|
||||
routeAddress: clashConfig.routeAddress,
|
||||
currentProfileName:
|
||||
config.currentProfile?.label ?? config.currentProfileId ?? "",
|
||||
),
|
||||
getCoreState(config, clashConfig),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -210,13 +225,12 @@ class GlobalState {
|
||||
appState.groups = await clashCore.getProxiesGroups();
|
||||
}
|
||||
|
||||
showMessage({
|
||||
Future<bool?> showMessage<bool>({
|
||||
required String title,
|
||||
required InlineSpan message,
|
||||
Function()? onTab,
|
||||
String? confirmText,
|
||||
}) {
|
||||
showCommonDialog(
|
||||
}) async {
|
||||
return await showCommonDialog<bool>(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
@@ -238,10 +252,15 @@ class GlobalState {
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: onTab ??
|
||||
() {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text(appLocalizations.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
child: Text(confirmText ?? appLocalizations.confirm),
|
||||
)
|
||||
],
|
||||
@@ -286,48 +305,53 @@ class GlobalState {
|
||||
required Config config,
|
||||
AppFlowingState? appFlowingState,
|
||||
}) async {
|
||||
final onlyProxy = config.appSetting.onlyProxy;
|
||||
final traffic = await clashCore.getTraffic(onlyProxy);
|
||||
if (Platform.isAndroid && isVpnService == true) {
|
||||
vpn?.startForeground(
|
||||
title: clashLib?.getCurrentProfileName() ?? "",
|
||||
content: "$traffic",
|
||||
);
|
||||
} else {
|
||||
if (appFlowingState != null) {
|
||||
appFlowingState.addTraffic(traffic);
|
||||
appFlowingState.totalTraffic =
|
||||
await clashCore.getTotalTraffic(onlyProxy);
|
||||
}
|
||||
final traffic = await clashCore.getTraffic();
|
||||
if (appFlowingState != null) {
|
||||
appFlowingState.addTraffic(traffic);
|
||||
appFlowingState.totalTraffic = await clashCore.getTotalTraffic();
|
||||
}
|
||||
}
|
||||
|
||||
Future<T?> safeRun<T>(
|
||||
FutureOr<T> Function() futureFunction, {
|
||||
String? title,
|
||||
bool silence = true,
|
||||
}) async {
|
||||
try {
|
||||
final res = await futureFunction();
|
||||
return res;
|
||||
} catch (e) {
|
||||
showNotifier(e.toString());
|
||||
if (silence) {
|
||||
showNotifier(e.toString());
|
||||
} else {
|
||||
showMessage(
|
||||
title: title ?? appLocalizations.tip,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
showNotifier(String text) {
|
||||
if (text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
navigatorKey.currentContext?.showNotifier(text);
|
||||
}
|
||||
|
||||
openUrl(String url) {
|
||||
showMessage(
|
||||
openUrl(String url) async {
|
||||
final res = await showMessage(
|
||||
message: TextSpan(text: url),
|
||||
title: appLocalizations.externalLink,
|
||||
confirmText: appLocalizations.go,
|
||||
onTab: () {
|
||||
launchUrl(Uri.parse(url));
|
||||
},
|
||||
);
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
launchUrl(Uri.parse(url));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ class CommonCard extends StatelessWidget {
|
||||
if (isSelected) {
|
||||
return colorScheme.secondaryContainer;
|
||||
}
|
||||
return colorScheme.surfaceContainerLow;
|
||||
return colorScheme.surfaceContainer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ class CommonCard extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (selectWidget != null && isSelected) {
|
||||
final List<Widget> children = [];
|
||||
children.add(childWidget);
|
||||
|
||||
@@ -42,17 +42,20 @@ class FadeScaleBox extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PageTransitionSwitcher(
|
||||
return AnimatedSwitcher(
|
||||
transitionBuilder: (
|
||||
child,
|
||||
animation,
|
||||
secondaryAnimation,
|
||||
) {
|
||||
return FadeScaleTransition(
|
||||
animation: animation,
|
||||
child: child,
|
||||
return Container(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: FadeScaleTransition(
|
||||
animation: animation,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
duration: Duration(milliseconds: 300),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
255
lib/widgets/popup.dart
Normal file
255
lib/widgets/popup.dart
Normal file
@@ -0,0 +1,255 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/common.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CommonPopupRoute<T> extends PopupRoute<T> {
|
||||
final WidgetBuilder builder;
|
||||
ValueNotifier<Offset> offsetNotifier;
|
||||
|
||||
CommonPopupRoute({
|
||||
required this.barrierLabel,
|
||||
required this.builder,
|
||||
required this.offsetNotifier,
|
||||
});
|
||||
|
||||
@override
|
||||
String? barrierLabel;
|
||||
|
||||
@override
|
||||
Color? get barrierColor => null;
|
||||
|
||||
@override
|
||||
bool get barrierDismissible => true;
|
||||
|
||||
@override
|
||||
Widget buildPage(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return builder(
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildTransitions(BuildContext context, Animation<double> animation,
|
||||
Animation<double> secondaryAnimation, Widget child) {
|
||||
final align = Alignment.topRight;
|
||||
final animationValue = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeIn,
|
||||
).value;
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: offsetNotifier,
|
||||
builder: (_, value, child) {
|
||||
return Align(
|
||||
alignment: align,
|
||||
child: CustomSingleChildLayout(
|
||||
delegate: OverflowAwareLayoutDelegate(
|
||||
offset: value.translate(
|
||||
48,
|
||||
12,
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (_, Widget? child) {
|
||||
return Opacity(
|
||||
opacity: 0.1 + 0.9 * animationValue,
|
||||
child: Transform.scale(
|
||||
alignment: align,
|
||||
scale: 0.8 + 0.2 * animationValue,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, -10) * (1 - animationValue),
|
||||
child: child!,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: builder(
|
||||
context,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => const Duration(milliseconds: 150);
|
||||
}
|
||||
|
||||
class CommonPopupBox extends StatefulWidget {
|
||||
final Widget target;
|
||||
final Widget popup;
|
||||
|
||||
const CommonPopupBox({
|
||||
super.key,
|
||||
required this.target,
|
||||
required this.popup,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CommonPopupBox> createState() => CommonPopupBoxState();
|
||||
}
|
||||
|
||||
class CommonPopupBoxState extends State<CommonPopupBox> {
|
||||
final _targetOffsetValueNotifier = ValueNotifier(Offset.zero);
|
||||
|
||||
_handleTargetOffset() {
|
||||
final renderBox = context.findRenderObject() as RenderBox?;
|
||||
if (renderBox == null) {
|
||||
return;
|
||||
}
|
||||
_targetOffsetValueNotifier.value = renderBox.localToGlobal(
|
||||
Offset.zero,
|
||||
);
|
||||
}
|
||||
|
||||
pop() {
|
||||
_handleTargetOffset();
|
||||
Navigator.of(context).push(
|
||||
CommonPopupRoute(
|
||||
barrierLabel: other.id,
|
||||
builder: (BuildContext context) {
|
||||
return widget.popup;
|
||||
},
|
||||
offsetNotifier: _targetOffsetValueNotifier,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.target;
|
||||
}
|
||||
}
|
||||
|
||||
class OverflowAwareLayoutDelegate extends SingleChildLayoutDelegate {
|
||||
final Offset offset;
|
||||
|
||||
OverflowAwareLayoutDelegate({
|
||||
required this.offset,
|
||||
});
|
||||
|
||||
@override
|
||||
Size getSize(BoxConstraints constraints) {
|
||||
return Size(constraints.maxWidth, constraints.maxHeight);
|
||||
}
|
||||
|
||||
@override
|
||||
Offset getPositionForChild(Size size, Size childSize) {
|
||||
final saveOffset = Offset(16, 16);
|
||||
double x = (offset.dx - childSize.width).clamp(
|
||||
0,
|
||||
size.width - saveOffset.dx - childSize.width,
|
||||
);
|
||||
double y = (offset.dy).clamp(
|
||||
0,
|
||||
size.height - saveOffset.dy - childSize.height,
|
||||
);
|
||||
return Offset(x, y);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRelayout(covariant OverflowAwareLayoutDelegate oldDelegate) {
|
||||
return oldDelegate.offset != offset;
|
||||
}
|
||||
}
|
||||
|
||||
class CommonPopupMenu extends StatelessWidget {
|
||||
final List<ActionItemData> items;
|
||||
|
||||
const CommonPopupMenu({
|
||||
super.key,
|
||||
required this.items,
|
||||
});
|
||||
|
||||
Widget _popupMenuItem(
|
||||
BuildContext context, {
|
||||
required ActionItemData item,
|
||||
required int index,
|
||||
}) {
|
||||
final isDanger = item.type == ActionType.danger;
|
||||
final color = isDanger
|
||||
? context.colorScheme.error
|
||||
: context.colorScheme.onSurfaceVariant;
|
||||
return InkWell(
|
||||
hoverColor:
|
||||
isDanger ? context.colorScheme.errorContainer.withOpacity(0.3) : null,
|
||||
splashColor:
|
||||
isDanger ? context.colorScheme.errorContainer.withOpacity(0.4) : null,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
item.onPressed();
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 64,
|
||||
top: 14,
|
||||
bottom: 14,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
if (item.icon != null) ...[
|
||||
Icon(
|
||||
item.icon,
|
||||
size: item.iconSize ?? 18,
|
||||
color: color,
|
||||
),
|
||||
SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
],
|
||||
Flexible(
|
||||
child: Text(
|
||||
item.label,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IntrinsicHeight(
|
||||
child: IntrinsicWidth(
|
||||
child: Card(
|
||||
elevation: 8,
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final item in items.asMap().entries) ...[
|
||||
_popupMenuItem(
|
||||
context,
|
||||
item: item.value,
|
||||
index: item.key,
|
||||
),
|
||||
if (item.value != items.last)
|
||||
Divider(
|
||||
height: 0,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CommonPopupMenuItem<T> {
|
||||
T action;
|
||||
String label;
|
||||
IconData? iconData;
|
||||
|
||||
CommonPopupMenuItem({
|
||||
required this.action,
|
||||
required this.label,
|
||||
this.iconData,
|
||||
});
|
||||
}
|
||||
|
||||
class CommonPopupMenu<T> extends StatefulWidget {
|
||||
final List<CommonPopupMenuItem> items;
|
||||
final PopupMenuItemSelected<T> onSelected;
|
||||
final T? selectedValue;
|
||||
final Widget? icon;
|
||||
|
||||
const CommonPopupMenu({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.onSelected,
|
||||
this.icon,
|
||||
}) : selectedValue = null;
|
||||
|
||||
const CommonPopupMenu.radio({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.onSelected,
|
||||
required T this.selectedValue,
|
||||
this.icon,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CommonPopupMenu<T>> createState() => _CommonPopupMenuState();
|
||||
}
|
||||
|
||||
class _CommonPopupMenuState<T> extends State<CommonPopupMenu<T>> {
|
||||
late ValueNotifier<T?> groupValue;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
groupValue = ValueNotifier(widget.selectedValue);
|
||||
}
|
||||
|
||||
handleSelect(T value) {
|
||||
if (widget.selectedValue != null) {
|
||||
this.groupValue.value = value;
|
||||
}
|
||||
widget.onSelected(value);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
groupValue.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<T>(
|
||||
icon: widget.icon,
|
||||
onSelected: handleSelect,
|
||||
itemBuilder: (_) {
|
||||
return [
|
||||
for (final item in widget.items)
|
||||
PopupMenuItem<T>(
|
||||
value: item.action,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
item.iconData != null
|
||||
? Flexible(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 16),
|
||||
child: Icon(item.iconData),
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
Flexible(
|
||||
flex: 0,
|
||||
child: SizedBox(
|
||||
child: Text(
|
||||
item.label,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.selectedValue != null)
|
||||
Flexible(
|
||||
flex: 0,
|
||||
child: ValueListenableBuilder<T?>(
|
||||
valueListenable: groupValue,
|
||||
builder: (_, groupValue, __) {
|
||||
return Radio<T>(
|
||||
value: item.action,
|
||||
groupValue: groupValue,
|
||||
onChanged: (T? value) {
|
||||
if (value != null) {
|
||||
handleSelect(value);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/fade_box.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
|
||||
class CommonScaffold extends StatefulWidget {
|
||||
final Widget body;
|
||||
final Widget? bottomNavigationBar;
|
||||
@@ -34,14 +34,11 @@ class CommonScaffold extends StatefulWidget {
|
||||
body: body,
|
||||
title: title,
|
||||
automaticallyImplyLeading: false,
|
||||
leading: SizedBox(
|
||||
height: kToolbarHeight,
|
||||
child: IconButton(
|
||||
icon: const BackButtonIcon(),
|
||||
onPressed: () {
|
||||
onBack();
|
||||
},
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const BackButtonIcon(),
|
||||
onPressed: () {
|
||||
onBack();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -110,7 +107,6 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scaffold = Scaffold(
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||
child: Stack(
|
||||
@@ -120,19 +116,19 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
valueListenable: _actions,
|
||||
builder: (_, actions, __) {
|
||||
final realActions =
|
||||
actions.isNotEmpty ? actions : widget.actions ?? [];
|
||||
actions.isNotEmpty ? actions : widget.actions ?? [];
|
||||
return AppBar(
|
||||
centerTitle: false,
|
||||
systemOverlayStyle: SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness:
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? Brightness.light
|
||||
: Brightness.dark,
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? Brightness.light
|
||||
: Brightness.dark,
|
||||
systemNavigationBarIconBrightness:
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? Brightness.light
|
||||
: Brightness.dark,
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? Brightness.light
|
||||
: Brightness.dark,
|
||||
systemNavigationBarColor: widget.bottomNavigationBar != null
|
||||
? context.colorScheme.surfaceContainer
|
||||
: context.colorScheme.surface,
|
||||
@@ -169,7 +165,9 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
floatingActionButton: ValueListenableBuilder<Widget?>(
|
||||
valueListenable: _floatingActionButton,
|
||||
builder: (_, value, __) {
|
||||
return value ?? Container();
|
||||
return FadeScaleBox(
|
||||
child: value ?? SizedBox(),
|
||||
);
|
||||
},
|
||||
),
|
||||
bottomNavigationBar: widget.bottomNavigationBar,
|
||||
|
||||
@@ -16,7 +16,7 @@ export 'line_chart.dart';
|
||||
export 'list.dart';
|
||||
export 'null_status.dart';
|
||||
export 'open_container.dart';
|
||||
export 'popup_menu.dart';
|
||||
export 'popup.dart';
|
||||
export 'scaffold.dart';
|
||||
export 'setting.dart';
|
||||
export 'sheet.dart';
|
||||
|
||||
82
pubspec.lock
82
pubspec.lock
@@ -5,23 +5,23 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
|
||||
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "76.0.0"
|
||||
version: "72.0.0"
|
||||
_macros:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
source: sdk
|
||||
version: "0.3.3"
|
||||
version: "0.3.2"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
|
||||
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.11.0"
|
||||
version: "6.7.0"
|
||||
animations:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -74,50 +74,50 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0
|
||||
sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
version: "2.4.1"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
|
||||
sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.1.1"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948"
|
||||
sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.3"
|
||||
version: "4.0.2"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e"
|
||||
sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
version: "2.4.2"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573"
|
||||
sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.14"
|
||||
version: "2.4.13"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021"
|
||||
sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.0.0"
|
||||
version: "7.3.2"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -210,10 +210,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.0"
|
||||
version: "1.18.0"
|
||||
connectivity_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -566,10 +566,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360"
|
||||
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.1"
|
||||
version: "4.0.2"
|
||||
image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -710,18 +710,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.7"
|
||||
version: "10.0.5"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.8"
|
||||
version: "3.0.5"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -734,10 +734,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: "4a16b3f03741e1252fda5de3ce712666d010ba2122f8e912c94f9f7b90e1a4c3"
|
||||
sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
version: "5.0.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -758,10 +758,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: macros
|
||||
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
|
||||
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3-main.0"
|
||||
version: "0.1.2-main.4"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1117,10 +1117,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.2"
|
||||
version: "1.4.1"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1141,7 +1141,7 @@ packages:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
version: "0.0.99"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1218,10 +1218,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.0"
|
||||
version: "1.11.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1242,10 +1242,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.2.0"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1266,10 +1266,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.3"
|
||||
version: "0.7.2"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1386,10 +1386,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
||||
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.3.0"
|
||||
version: "14.2.5"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1510,5 +1510,5 @@ packages:
|
||||
source: hosted
|
||||
version: "0.2.3"
|
||||
sdks:
|
||||
dart: ">=3.6.0 <4.0.0"
|
||||
dart: ">=3.5.0 <4.0.0"
|
||||
flutter: ">=3.24.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: fl_clash
|
||||
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
|
||||
publish_to: 'none'
|
||||
version: 0.8.71+202501091
|
||||
version: 0.8.74+202502031
|
||||
environment:
|
||||
sdk: '>=3.1.0 <4.0.0'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user