Compare commits

..

4 Commits

Author SHA1 Message Date
chen08209
b340feeb49 Update popup menu
Add file editor

Fix android service issues

Optimize desktop background performance

Optimize android main process performance

Optimize delay test

Optimize vpn protect
2025-02-02 19:34:42 +08:00
chen08209
6a39b7ef5a Update changelog 2025-01-10 11:22:18 +00:00
chen08209
35f89fea90 Update core
Fix some issues
2025-01-10 19:08:15 +08:00
chen08209
58acd9c1ab Update changelog 2025-01-09 02:24:30 +00:00
98 changed files with 4188 additions and 3250 deletions

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
package com.follow.clash;
import android.app.Application
import android.content.Context;
class FlClashApplication : Application() {
companion object {
private lateinit var instance: FlClashApplication
fun getAppContext(): Context {
return instance.applicationContext
}
}
override fun onCreate() {
super.onCreate()
instance = this
}
}

View File

@@ -31,7 +31,7 @@ object GlobalState {
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
}
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
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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), &params)
if err != nil {
send(action.wrapMessage(err.Error()))
return
}
geoType := params["geo-type"]
geoName := params["geo-name"]
handleUpdateGeoData(geoType, geoName, func(value string) {
send(action.wrapMessage(value))
})
return
case updateExternalProviderMethod:
providerName := action.Data.(string)
handleUpdateExternalProvider(providerName, func(value string) {
send(action.wrapMessage(value))
})
return
case sideLoadExternalProviderMethod:
paramsString := action.Data.(string)
var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
send(action.wrapMessage(err.Error()))
return
}
providerName := params["providerName"]
data := params["data"]
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
send(action.wrapMessage(value))
})
return
case startLogMethod:
handleStartLog()
send(action.wrapMessage(true))
return
case stopLogMethod:
handleStopLog()
send(action.wrapMessage(true))
return
case startListenerMethod:
send(action.wrapMessage(handleStartListener()))
return
case stopListenerMethod:
send(action.wrapMessage(handleStopListener()))
return
case getCountryCodeMethod:
ip := action.Data.(string)
handleGetCountryCode(ip, func(value string) {
send(action.wrapMessage(value))
})
return
case getMemoryMethod:
handleGetMemory(func(value string) {
send(action.wrapMessage(value))
})
return
default:
handle := nextHandle(action, send)
if handle {
return
} else {
send(action.wrapMessage(action.DefaultValue))
}
}
}

View File

@@ -2,7 +2,6 @@ package main
import (
"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()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,137 @@ 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() {
handleInitClash(dir)
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
View File

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

View File

@@ -1,13 +0,0 @@
//go:build !cgo
package main
func SendMessage(message Message) {
s, err := message.Json()
if err != nil {
return
}
Action{
Method: messageMethod,
}.callback(s)
}

View File

@@ -1,47 +0,0 @@
//go:build cgo
package main
import (
bridge "core/dart-bridge"
)
var (
Port int64 = -1
ServicePort int64 = -1
)
func SendMessage(message Message) {
s, err := message.Json()
if err != nil {
return
}
if handler, ok := messageHandlers[message.Type]; ok {
handler(s)
} else {
sendToPort(s)
}
}
var messageHandlers = map[MessageType]func(string) bool{
ProtectMessage: sendToServicePort,
ProcessMessage: sendToServicePort,
StartedMessage: conditionalSend,
LoadedMessage: conditionalSend,
}
func sendToPort(s string) bool {
return bridge.SendToPort(Port, s)
}
func sendToServicePort(s string) bool {
return bridge.SendToPort(ServicePort, s)
}
func conditionalSend(s string) bool {
isSuccess := sendToPort(s)
if !isSuccess {
return sendToServicePort(s)
}
return isSuccess
}

View File

@@ -10,10 +10,29 @@ import (
"strconv"
)
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), &params)
if err != nil {
action.callback(err.Error())
return
}
geoType := params["geoType"]
geoName := params["geoName"]
handleUpdateGeoData(geoType, geoName, func(value string) {
action.callback(value)
})
return
case updateExternalProviderMethod:
providerName := action.Data.(string)
handleUpdateExternalProvider(providerName, func(value string) {
action.callback(value)
})
return
case sideLoadExternalProviderMethod:
paramsString := action.Data.(string)
var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
action.callback(err.Error())
return
}
providerName := params["providerName"]
data := params["data"]
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
action.callback(value)
})
return
case startLogMethod:
handleStartLog()
return
case stopLogMethod:
handleStopLog()
return
case startListenerMethod:
action.callback(handleStartListener())
return
case stopListenerMethod:
action.callback(handleStopListener())
return
case getCountryCodeMethod:
ip := action.Data.(string)
handleGetCountryCode(ip, func(value string) {
action.callback(value)
})
return
case getMemoryMethod:
handleGetMemory(func(value string) {
action.callback(value)
})
return
}
func nextHandle(action *Action, send func([]byte)) bool {
return false
}

View File

@@ -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();

View File

@@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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, __) {

View File

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

View File

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

View File

@@ -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,6 +257,36 @@ 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) {
return CommonCard(
@@ -286,46 +307,59 @@ 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(
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: () {},
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,

View File

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

View File

@@ -12,10 +12,12 @@ class ProxyCard extends StatelessWidget {
final Proxy proxy;
final 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(

View File

@@ -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();

View File

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

View File

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

View File

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

View File

@@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,121 @@ 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 {
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 +184,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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();

View File

@@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
View 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!);
}
}

View File

@@ -1,2 +1,3 @@
export 'home.dart';
export 'scan.dart';
export 'scan.dart';
export 'editor.dart';

View File

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

View File

@@ -15,7 +15,6 @@ abstract mixin class TileListener {
}
class Tile {
StreamSubscription? subscription;
final MethodChannel _channel = const MethodChannel('tile');

View File

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

View File

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

View File

@@ -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);
@@ -182,6 +182,9 @@ class CommonCard extends StatelessWidget {
return OutlinedButton(
clipBehavior: Clip.antiAlias,
onLongPress: (){
},
style: ButtonStyle(
padding: const WidgetStatePropertyAll(EdgeInsets.zero),
shape: WidgetStatePropertyAll(

View File

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

263
lib/widgets/popup.dart Normal file
View File

@@ -0,0 +1,263 @@
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 _targetKey = GlobalKey();
final _targetOffsetValueNotifier = ValueNotifier(Offset.zero);
_handleTargetOffset() {
final renderBox =
_targetKey.currentContext?.findRenderObject() as RenderBox?;
if (renderBox == null) {
return;
}
_targetOffsetValueNotifier.value = renderBox.localToGlobal(
Offset.zero,
);
}
@override
Widget build(BuildContext context) {
return Listener(
onPointerDown: (details) {
_handleTargetOffset();
Navigator.of(context).push(
CommonPopupRoute(
barrierLabel: other.id,
builder: (BuildContext context) {
return widget.popup;
},
offsetNotifier: _targetOffsetValueNotifier,
),
);
},
key: _targetKey,
child: LayoutBuilder(
builder: (_, __) {
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,
),
],
],
),
),
),
);
}
}

View File

@@ -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();
}
},
);
},
),
),
],
),
),
];
},
);
}
}

View File

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

View File

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

View File

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

View File

@@ -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.73+202502021
environment:
sdk: '>=3.1.0 <4.0.0'