From b340feeb494caea7c287edf65d2a1f2a6f35d784 Mon Sep 17 00:00:00 2001 From: chen08209 Date: Mon, 13 Jan 2025 19:08:17 +0800 Subject: [PATCH] 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 --- android/app/src/main/AndroidManifest.xml | 2 +- .../com/follow/clash/FlClashApplication.kt | 18 + .../kotlin/com/follow/clash/GlobalState.kt | 26 +- .../kotlin/com/follow/clash/MainActivity.kt | 5 +- .../kotlin/com/follow/clash/TempActivity.kt | 4 +- .../kotlin/com/follow/clash/extensions/Ext.kt | 2 - .../kotlin/com/follow/clash/models/Process.kt | 2 +- .../kotlin/com/follow/clash/models/Props.kt | 5 + .../com/follow/clash/plugins/AppPlugin.kt | 97 +-- .../com/follow/clash/plugins/ServicePlugin.kt | 28 +- .../com/follow/clash/plugins/TilePlugin.kt | 7 +- .../com/follow/clash/plugins/VpnPlugin.kt | 112 +-- .../{ => services}/BaseServiceInterface.kt | 8 +- .../follow/clash/services/FlClashService.kt | 114 +-- .../clash/services/FlClashTileService.kt | 2 +- .../clash/services/FlClashVpnService.kt | 127 ++-- core/Clash.Meta | 2 +- core/action.go | 171 ++++- core/common.go | 16 - core/constant.go | 65 +- core/hub.go | 44 +- core/lib.go | 205 ++---- core/lib_android.go | 427 +++++++---- core/lib_no_android.go | 11 + core/message.go | 13 - core/message_cgo.go | 47 -- core/server.go | 151 +--- lib/application.dart | 11 +- lib/clash/core.dart | 47 +- lib/clash/generated/clash_ffi.dart | 412 +++-------- lib/clash/interface.dart | 363 ++++++++- lib/clash/lib.dart | 547 +++++++------- lib/clash/message.dart | 10 +- lib/clash/service.dart | 348 +-------- lib/common/common.dart | 1 + lib/common/constant.dart | 6 +- lib/common/future.dart | 4 +- lib/common/navigator.dart | 48 ++ lib/common/render.dart | 56 ++ lib/common/window.dart | 2 + lib/controller.dart | 87 ++- lib/enum/enum.dart | 16 +- lib/fragments/application_setting.dart | 4 +- lib/fragments/config/dns.dart | 39 +- lib/fragments/config/network.dart | 34 +- .../dashboard/widgets/memory_info.dart | 25 +- .../dashboard/widgets/network_speed.dart | 40 +- lib/fragments/profiles/edit_profile.dart | 210 ++++-- lib/fragments/profiles/profiles.dart | 134 ++-- lib/fragments/profiles/view_profile.dart | 232 ------ lib/fragments/proxies/card.dart | 12 +- lib/fragments/proxies/common.dart | 25 +- lib/fragments/proxies/list.dart | 18 +- lib/fragments/proxies/providers.dart | 40 +- lib/fragments/proxies/tab.dart | 16 +- lib/fragments/resources.dart | 14 +- lib/l10n/arb/intl_en.arb | 10 +- lib/l10n/arb/intl_zh_CN.arb | 10 +- lib/l10n/intl/messages_en.dart | 11 + lib/l10n/intl/messages_zh_CN.dart | 9 + lib/l10n/l10n.dart | 80 ++ lib/main.dart | 240 +++--- lib/manager/android_manager.dart | 15 +- lib/manager/app_state_manager.dart | 21 +- lib/manager/clash_manager.dart | 9 +- lib/manager/message_manager.dart | 250 ++----- lib/manager/tray_manager.dart | 6 + lib/manager/window_manager.dart | 12 + lib/models/app.dart | 17 +- lib/models/common.dart | 24 + lib/models/config.dart | 2 +- lib/models/core.dart | 55 +- lib/models/generated/common.freezed.dart | 33 +- lib/models/generated/common.g.dart | 2 + lib/models/generated/config.freezed.dart | 38 +- lib/models/generated/config.g.dart | 4 +- lib/models/generated/core.freezed.dart | 690 ++++++++++++++---- lib/models/generated/core.g.dart | 71 +- lib/models/generated/selector.freezed.dart | 31 +- lib/models/selector.dart | 1 + lib/pages/editor.dart | 259 +++++++ lib/pages/pages.dart | 3 +- lib/plugins/service.dart | 18 +- lib/plugins/tile.dart | 1 - lib/plugins/vpn.dart | 88 +-- lib/state.dart | 156 ++-- lib/widgets/card.dart | 7 +- lib/widgets/popup.dart | 263 +++++++ lib/widgets/popup_menu.dart | 127 ---- lib/widgets/widgets.dart | 2 +- pubspec.lock | 2 +- pubspec.yaml | 2 +- 92 files changed, 4000 insertions(+), 3081 deletions(-) create mode 100644 android/app/src/main/kotlin/com/follow/clash/FlClashApplication.kt rename android/app/src/main/kotlin/com/follow/clash/{ => services}/BaseServiceInterface.kt (57%) create mode 100644 core/lib_no_android.go delete mode 100644 core/message.go delete mode 100644 core/message_cgo.go create mode 100644 lib/common/render.dart delete mode 100644 lib/fragments/profiles/view_profile.dart create mode 100644 lib/pages/editor.dart create mode 100644 lib/widgets/popup.dart delete mode 100644 lib/widgets/popup_menu.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 340bd1c..185ac9c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ tools:ignore="QueryAllPackagesPermission" /> diff --git a/android/app/src/main/kotlin/com/follow/clash/FlClashApplication.kt b/android/app/src/main/kotlin/com/follow/clash/FlClashApplication.kt new file mode 100644 index 0000000..8326a75 --- /dev/null +++ b/android/app/src/main/kotlin/com/follow/clash/FlClashApplication.kt @@ -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 + } +} diff --git a/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt b/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt index e535b51..d3fef86 100644 --- a/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt +++ b/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt @@ -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 ) } } diff --git a/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt b/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt index 90ec24f..c404697 100644 --- a/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt +++ b/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt @@ -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 } diff --git a/android/app/src/main/kotlin/com/follow/clash/TempActivity.kt b/android/app/src/main/kotlin/com/follow/clash/TempActivity.kt index bbf7990..33f631a 100644 --- a/android/app/src/main/kotlin/com/follow/clash/TempActivity.kt +++ b/android/app/src/main/kotlin/com/follow/clash/TempActivity.kt @@ -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() diff --git a/android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt b/android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt index a047e0f..2f1eb91 100644 --- a/android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt +++ b/android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt @@ -97,7 +97,6 @@ fun String.toCIDR(): CIDR { return CIDR(address, prefixLength) } - fun ConnectivityManager.resolveDns(network: Network?): List { 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) { diff --git a/android/app/src/main/kotlin/com/follow/clash/models/Process.kt b/android/app/src/main/kotlin/com/follow/clash/models/Process.kt index e76b817..a889564 100644 --- a/android/app/src/main/kotlin/com/follow/clash/models/Process.kt +++ b/android/app/src/main/kotlin/com/follow/clash/models/Process.kt @@ -1,7 +1,7 @@ package com.follow.clash.models data class Process( - val id: Int, + val id: String, val metadata: Metadata, ) diff --git a/android/app/src/main/kotlin/com/follow/clash/models/Props.kt b/android/app/src/main/kotlin/com/follow/clash/models/Props.kt index 980aa86..673ba1a 100644 --- a/android/app/src/main/kotlin/com/follow/clash/models/Props.kt +++ b/android/app/src/main/kotlin/com/follow/clash/models/Props.kt @@ -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, ) \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt index 82914e2..640f854 100644 --- a/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt +++ b/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt @@ -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? = 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 { - 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("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() } } diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt index 2e0c8eb..2b768c5 100644 --- a/android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt +++ b/android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt @@ -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("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() } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/TilePlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/TilePlugin.kt index 6dc17c0..6a6bd97 100644 --- a/android/app/src/main/kotlin/com/follow/clash/plugins/TilePlugin.kt +++ b/android/app/src/main/kotlin/com/follow/clash/plugins/TilePlugin.kt @@ -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() { diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/VpnPlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/VpnPlugin.kt index 248e8f0..f97bc7d 100644 --- a/android/app/src/main/kotlin/com/follow/clash/plugins/VpnPlugin.kt +++ b/android/app/src/main/kotlin/com/follow/clash/plugins/VpnPlugin.kt @@ -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() + FlClashApplication.getAppContext().getSystemService() } 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("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("title") as String - val content = call.argument("content") as String - startForeground(title, content) - result.success(true) - } - "resolverProcess" -> { val data = call.argument("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("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) } - } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/follow/clash/BaseServiceInterface.kt b/android/app/src/main/kotlin/com/follow/clash/services/BaseServiceInterface.kt similarity index 57% rename from android/app/src/main/kotlin/com/follow/clash/BaseServiceInterface.kt rename to android/app/src/main/kotlin/com/follow/clash/services/BaseServiceInterface.kt index 96f6408..44ca809 100644 --- a/android/app/src/main/kotlin/com/follow/clash/BaseServiceInterface.kt +++ b/android/app/src/main/kotlin/com/follow/clash/services/BaseServiceInterface.kt @@ -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) } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/follow/clash/services/FlClashService.kt b/android/app/src/main/kotlin/com/follow/clash/services/FlClashService.kt index c4a676c..d46a9e3 100644 --- a/android/app/src/main/kotlin/com/follow/clash/services/FlClashService.kt +++ b/android/app/src/main/kotlin/com/follow/clash/services/FlClashService.kt @@ -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 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) + } } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/follow/clash/services/FlClashTileService.kt b/android/app/src/main/kotlin/com/follow/clash/services/FlClashTileService.kt index 2fe0423..ed0aac0 100644 --- a/android/app/src/main/kotlin/com/follow/clash/services/FlClashTileService.kt +++ b/android/app/src/main/kotlin/com/follow/clash/services/FlClashTileService.kt @@ -66,7 +66,7 @@ class FlClashTileService : TileService() { override fun onClick() { super.onClick() activityTransfer() - GlobalState.handleToggle(applicationContext) + GlobalState.handleToggle() } override fun onDestroy() { diff --git a/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt b/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt index 61676c1..092be19 100644 --- a/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt +++ b/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt @@ -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) { - 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 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) { diff --git a/core/Clash.Meta b/core/Clash.Meta index 3175efe..0c03d8e 160000 --- a/core/Clash.Meta +++ b/core/Clash.Meta @@ -1 +1 @@ -Subproject commit 3175efe8c04777b060ff4ebff6c22709531ad20e +Subproject commit 0c03d8e4b4b9ab7648359a00b939cdee7b066605 diff --git a/core/action.go b/core/action.go index 7dd8b09..01995fd 100644 --- a/core/action.go +++ b/core/action.go @@ -1,32 +1,167 @@ -//go:build !cgo - package main import ( "encoding/json" ) -func (action Action) Json() ([]byte, error) { - data, err := json.Marshal(action) +type Action struct { + Id string `json:"id"` + Method Method `json:"method"` + Data interface{} `json:"data"` + DefaultValue interface{} `json:"default-value"` +} + +type ActionResult struct { + Id string `json:"id"` + Method Method `json:"method"` + Data interface{} `json:"data"` +} + +func (result ActionResult) Json() ([]byte, error) { + data, err := json.Marshal(result) return data, err } -func (action Action) callback(data interface{}) bool { - if conn == nil { - return false - } - sendAction := Action{ +func (action Action) wrapMessage(data interface{}) []byte { + sendAction := ActionResult{ Id: action.Id, Method: action.Method, Data: data, } - res, err := sendAction.Json() - if err != nil { - return false - } - _, err = conn.Write(append(res, []byte("\n")...)) - if err != nil { - return false - } - return true + res, _ := sendAction.Json() + return res +} + +func handleAction(action *Action, send func([]byte)) { + switch action.Method { + case initClashMethod: + data := action.Data.(string) + send(action.wrapMessage(handleInitClash(data))) + return + case getIsInitMethod: + send(action.wrapMessage(handleGetIsInit())) + return + case forceGcMethod: + handleForceGc() + send(action.wrapMessage(true)) + return + case shutdownMethod: + send(action.wrapMessage(handleShutdown())) + return + case validateConfigMethod: + data := []byte(action.Data.(string)) + send(action.wrapMessage(handleValidateConfig(data))) + return + case updateConfigMethod: + data := []byte(action.Data.(string)) + send(action.wrapMessage(handleUpdateConfig(data))) + return + case getProxiesMethod: + send(action.wrapMessage(handleGetProxies())) + return + case changeProxyMethod: + data := action.Data.(string) + handleChangeProxy(data, func(value string) { + send(action.wrapMessage(value)) + }) + return + case getTrafficMethod: + send(action.wrapMessage(handleGetTraffic())) + return + case getTotalTrafficMethod: + send(action.wrapMessage(handleGetTotalTraffic())) + return + case resetTrafficMethod: + handleResetTraffic() + send(action.wrapMessage(true)) + return + case asyncTestDelayMethod: + data := action.Data.(string) + handleAsyncTestDelay(data, func(value string) { + send(action.wrapMessage(value)) + }) + return + case getConnectionsMethod: + send(action.wrapMessage(handleGetConnections())) + return + case closeConnectionsMethod: + send(action.wrapMessage(handleCloseConnections())) + return + case closeConnectionMethod: + id := action.Data.(string) + send(action.wrapMessage(handleCloseConnection(id))) + return + case getExternalProvidersMethod: + send(action.wrapMessage(handleGetExternalProviders())) + return + case getExternalProviderMethod: + externalProviderName := action.Data.(string) + send(action.wrapMessage(handleGetExternalProvider(externalProviderName))) + case updateGeoDataMethod: + paramsString := action.Data.(string) + var params = map[string]string{} + err := json.Unmarshal([]byte(paramsString), ¶ms) + if err != nil { + send(action.wrapMessage(err.Error())) + return + } + geoType := params["geo-type"] + geoName := params["geo-name"] + handleUpdateGeoData(geoType, geoName, func(value string) { + send(action.wrapMessage(value)) + }) + return + case updateExternalProviderMethod: + providerName := action.Data.(string) + handleUpdateExternalProvider(providerName, func(value string) { + send(action.wrapMessage(value)) + }) + return + case sideLoadExternalProviderMethod: + paramsString := action.Data.(string) + var params = map[string]string{} + err := json.Unmarshal([]byte(paramsString), ¶ms) + if err != nil { + send(action.wrapMessage(err.Error())) + return + } + providerName := params["providerName"] + data := params["data"] + handleSideLoadExternalProvider(providerName, []byte(data), func(value string) { + send(action.wrapMessage(value)) + }) + return + case startLogMethod: + handleStartLog() + send(action.wrapMessage(true)) + return + case stopLogMethod: + handleStopLog() + send(action.wrapMessage(true)) + return + case startListenerMethod: + send(action.wrapMessage(handleStartListener())) + return + case stopListenerMethod: + send(action.wrapMessage(handleStopListener())) + return + case getCountryCodeMethod: + ip := action.Data.(string) + handleGetCountryCode(ip, func(value string) { + send(action.wrapMessage(value)) + }) + return + case getMemoryMethod: + handleGetMemory(func(value string) { + send(action.wrapMessage(value)) + }) + return + default: + handle := nextHandle(action, send) + if handle { + return + } else { + send(action.wrapMessage(action.DefaultValue)) + } + } } diff --git a/core/common.go b/core/common.go index 61bbb92..5f9855c 100644 --- a/core/common.go +++ b/core/common.go @@ -2,7 +2,6 @@ package main import ( "context" - "encoding/json" "errors" "fmt" "github.com/metacubex/mihomo/adapter" @@ -42,11 +41,6 @@ func (a ExternalProviders) Len() int { return len(a) } func (a ExternalProviders) Less(i, j int) bool { return a[i].Name < a[j].Name } func (a ExternalProviders) 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 @@ -85,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) diff --git a/core/constant.go b/core/constant.go index f00c75f..c869315 100644 --- a/core/constant.go +++ b/core/constant.go @@ -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) +} diff --git a/core/hub.go b/core/hub.go index 6ae6e7a..7c9e824 100644 --- a/core/hub.go +++ b/core/hub.go @@ -26,8 +26,10 @@ import ( ) var ( - isInit = false - configParams = ConfigExtendedParams{} + isInit = false + configParams = ConfigExtendedParams{ + OnlyStatisticsProxy: false, + } externalProviders = map[string]cp.Provider{} logSubscriber observable.Subscription[log.Event] currentConfig *config.Config @@ -149,8 +151,8 @@ func handleChangeProxy(data string, fn func(string string)) { }() } -func handleGetTraffic(onlyProxy bool) string { - up, down := statistic.DefaultManager.Current(onlyProxy) +func handleGetTraffic() string { + up, down := statistic.DefaultManager.Current(configParams.OnlyStatisticsProxy) traffic := map[string]int64{ "up": up, "down": down, @@ -163,8 +165,8 @@ func handleGetTraffic(onlyProxy bool) string { return string(data) } -func handleGetTotalTraffic(onlyProxy bool) string { - up, down := statistic.DefaultManager.Total(onlyProxy) +func handleGetTotalTraffic() string { + up, down := statistic.DefaultManager.Total(configParams.OnlyStatisticsProxy) traffic := map[string]int64{ "up": up, "down": down, @@ -213,7 +215,13 @@ func handleAsyncTestDelay(paramsString string, fn func(string)) { return false, nil } - delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus) + testUrl := constant.DefaultTestURL + + if params.TestUrl != "" { + testUrl = params.TestUrl + } + + delay, err := proxy.URLTest(ctx, testUrl, expectedStatus) if err != nil || delay == 0 { delayData.Value = -1 data, _ := json.Marshal(delayData) @@ -240,17 +248,6 @@ func handleGetConnections() string { return string(data) } -func handleCloseConnectionsUnLock() bool { - statistic.DefaultManager.Range(func(c statistic.Tracker) bool { - err := c.Close() - if err != nil { - return false - } - return true - }) - return true -} - func handleCloseConnections() bool { runLock.Lock() defer runLock.Unlock() @@ -395,7 +392,7 @@ func handleStartLog() { Type: LogMessage, Data: logData, } - SendMessage(*message) + sendMessage(*message) } }() } @@ -427,8 +424,9 @@ func handleGetMemory(fn func(value string)) { } func init() { - adapter.UrlTestHook = func(name string, delay uint16) { + adapter.UrlTestHook = func(url string, name string, delay uint16) { delayData := &Delay{ + Url: url, Name: name, } if delay == 0 { @@ -436,19 +434,19 @@ func init() { } else { delayData.Value = int32(delay) } - SendMessage(Message{ + sendMessage(Message{ Type: DelayMessage, Data: delayData, }) } statistic.DefaultRequestNotify = func(c statistic.Tracker) { - SendMessage(Message{ + sendMessage(Message{ Type: RequestMessage, Data: c, }) } executor.DefaultProviderLoadedHook = func(providerName string) { - SendMessage(Message{ + sendMessage(Message{ Type: LoadedMessage, Data: providerName, }) diff --git a/core/lib.go b/core/lib.go index c5968db..7bf49ab 100644 --- a/core/lib.go +++ b/core/lib.go @@ -8,18 +8,30 @@ package main import "C" import ( bridge "core/dart-bridge" + "encoding/json" "unsafe" ) +var messagePort int64 = -1 + //export initNativeApiBridge func initNativeApiBridge(api unsafe.Pointer) { bridge.InitDartApi(api) } -//export initMessage -func initMessage(port C.longlong) { - i := int64(port) - Port = i +//export attachMessagePort +func attachMessagePort(mPort C.longlong) { + messagePort = int64(mPort) +} + +//export getTraffic +func getTraffic() *C.char { + return C.CString(handleGetTraffic()) +} + +//export getTotalTraffic +func getTotalTraffic() *C.char { + return C.CString(handleGetTotalTraffic()) } //export freeCString @@ -27,9 +39,32 @@ func freeCString(s *C.char) { C.free(unsafe.Pointer(s)) } -//export initClash -func initClash(homeDirStr *C.char) bool { - return handleInitClash(C.GoString(homeDirStr)) +//export invokeAction +func invokeAction(paramsChar *C.char, port C.longlong) { + params := C.GoString(paramsChar) + i := int64(port) + var action = &Action{} + err := json.Unmarshal([]byte(params), action) + if err != nil { + bridge.SendToPort(i, err.Error()) + return + } + go handleAction(action, func(bytes []byte) { + bridge.SendToPort(i, string(bytes)) + }) +} + +func sendMessage(message Message) { + if messagePort == -1 { + return + } + res, err := message.Json() + if err != nil { + return + } + bridge.SendToPort(messagePort, string(Action{ + Method: messageMethod, + }.wrapMessage(res))) } //export startListener @@ -41,159 +76,3 @@ func startListener() { func stopListener() { handleStopListener() } - -//export getIsInit -func getIsInit() bool { - return handleGetIsInit() -} - -//export shutdownClash -func shutdownClash() bool { - return handleShutdown() -} - -//export forceGc -func forceGc() { - handleForceGc() -} - -//export validateConfig -func validateConfig(s *C.char, port C.longlong) { - i := int64(port) - bytes := []byte(C.GoString(s)) - go func() { - bridge.SendToPort(i, handleValidateConfig(bytes)) - }() -} - -//export updateConfig -func updateConfig(s *C.char, port C.longlong) { - i := int64(port) - bytes := []byte(C.GoString(s)) - go func() { - bridge.SendToPort(i, handleUpdateConfig(bytes)) - }() -} - -//export getProxies -func getProxies() *C.char { - return C.CString(handleGetProxies()) -} - -//export changeProxy -func changeProxy(s *C.char, port C.longlong) { - i := int64(port) - paramsString := C.GoString(s) - handleChangeProxy(paramsString, func(value string) { - bridge.SendToPort(i, value) - }) -} - -//export getTraffic -func getTraffic(port C.int) *C.char { - onlyProxy := int(port) == 1 - return C.CString(handleGetTraffic(onlyProxy)) -} - -//export getTotalTraffic -func getTotalTraffic(port C.int) *C.char { - onlyProxy := int(port) == 1 - return C.CString(handleGetTotalTraffic(onlyProxy)) -} - -//export resetTraffic -func resetTraffic() { - handleResetTraffic() -} - -//export asyncTestDelay -func asyncTestDelay(s *C.char, port C.longlong) { - i := int64(port) - paramsString := C.GoString(s) - handleAsyncTestDelay(paramsString, func(value string) { - bridge.SendToPort(i, value) - }) -} - -//export getConnections -func getConnections() *C.char { - return C.CString(handleGetConnections()) -} - -//export getMemory -func getMemory(port C.longlong) { - i := int64(port) - handleGetMemory(func(value string) { - bridge.SendToPort(i, value) - }) -} - -//export closeConnections -func closeConnections() { - handleCloseConnections() -} - -//export closeConnection -func closeConnection(id *C.char) { - connectionId := C.GoString(id) - handleCloseConnection(connectionId) -} - -//export getExternalProviders -func getExternalProviders() *C.char { - return C.CString(handleGetExternalProviders()) -} - -//export getExternalProvider -func getExternalProvider(externalProviderNameChar *C.char) *C.char { - externalProviderName := C.GoString(externalProviderNameChar) - return C.CString(handleGetExternalProvider(externalProviderName)) -} - -//export updateGeoData -func updateGeoData(geoTypeChar *C.char, geoNameChar *C.char, port C.longlong) { - i := int64(port) - geoType := C.GoString(geoTypeChar) - geoName := C.GoString(geoNameChar) - handleUpdateGeoData(geoType, geoName, func(value string) { - bridge.SendToPort(i, value) - }) -} - -//export updateExternalProvider -func updateExternalProvider(providerNameChar *C.char, port C.longlong) { - i := int64(port) - providerName := C.GoString(providerNameChar) - handleUpdateExternalProvider(providerName, func(value string) { - bridge.SendToPort(i, value) - }) -} - -//export getCountryCode -func getCountryCode(ipChar *C.char, port C.longlong) { - ip := C.GoString(ipChar) - i := int64(port) - handleGetCountryCode(ip, func(value string) { - bridge.SendToPort(i, value) - }) -} - -//export sideLoadExternalProvider -func sideLoadExternalProvider(providerNameChar *C.char, dataChar *C.char, port C.longlong) { - i := int64(port) - providerName := C.GoString(providerNameChar) - data := []byte(C.GoString(dataChar)) - handleSideLoadExternalProvider(providerName, data, func(value string) { - bridge.SendToPort(i, value) - }) -} - -//export startLog -func startLog() { - handleStartLog() -} - -//export stopLog -func stopLog() { - handleStopLog() -} diff --git a/core/lib_android.go b/core/lib_android.go index b91d5d4..b853bf0 100644 --- a/core/lib_android.go +++ b/core/lib_android.go @@ -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) } diff --git a/core/lib_no_android.go b/core/lib_no_android.go new file mode 100644 index 0000000..5285f72 --- /dev/null +++ b/core/lib_no_android.go @@ -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 +} diff --git a/core/message.go b/core/message.go deleted file mode 100644 index 45faa47..0000000 --- a/core/message.go +++ /dev/null @@ -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) -} diff --git a/core/message_cgo.go b/core/message_cgo.go deleted file mode 100644 index 304f6db..0000000 --- a/core/message_cgo.go +++ /dev/null @@ -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 -} diff --git a/core/server.go b/core/server.go index dcc0534..a7b3250 100644 --- a/core/server.go +++ b/core/server.go @@ -10,10 +10,29 @@ import ( "strconv" ) -var conn net.Conn = nil +var conn net.Conn + +func sendMessage(message Message) { + res, err := message.Json() + if err != nil { + return + } + send(Action{ + Method: messageMethod, + }.wrapMessage(res)) +} + +func send(data []byte) { + if conn == nil { + return + } + _, _ = conn.Write(append(data, []byte("\n")...)) +} func startServer(arg string) { + _, err := strconv.Atoi(arg) + if err != nil { conn, err = net.Dial("unix", arg) } else { @@ -42,132 +61,12 @@ func startServer(arg string) { return } - go handleAction(action) + go handleAction(action, func(bytes []byte) { + send(bytes) + }) } } -func handleAction(action *Action) { - switch action.Method { - case initClashMethod: - data := action.Data.(string) - action.callback(handleInitClash(data)) - return - case getIsInitMethod: - action.callback(handleGetIsInit()) - return - case forceGcMethod: - handleForceGc() - return - case shutdownMethod: - action.callback(handleShutdown()) - return - case validateConfigMethod: - data := []byte(action.Data.(string)) - action.callback(handleValidateConfig(data)) - return - case updateConfigMethod: - data := []byte(action.Data.(string)) - action.callback(handleUpdateConfig(data)) - return - case getProxiesMethod: - action.callback(handleGetProxies()) - return - case changeProxyMethod: - data := action.Data.(string) - handleChangeProxy(data, func(value string) { - action.callback(value) - }) - return - case getTrafficMethod: - data := action.Data.(bool) - action.callback(handleGetTraffic(data)) - return - case getTotalTrafficMethod: - data := action.Data.(bool) - action.callback(handleGetTotalTraffic(data)) - return - case resetTrafficMethod: - handleResetTraffic() - return - case asyncTestDelayMethod: - data := action.Data.(string) - handleAsyncTestDelay(data, func(value string) { - action.callback(value) - }) - return - case getConnectionsMethod: - action.callback(handleGetConnections()) - return - case closeConnectionsMethod: - action.callback(handleCloseConnections()) - return - case closeConnectionMethod: - id := action.Data.(string) - action.callback(handleCloseConnection(id)) - return - case getExternalProvidersMethod: - action.callback(handleGetExternalProviders()) - return - case getExternalProviderMethod: - externalProviderName := action.Data.(string) - action.callback(handleGetExternalProvider(externalProviderName)) - case updateGeoDataMethod: - paramsString := action.Data.(string) - var params = map[string]string{} - err := json.Unmarshal([]byte(paramsString), ¶ms) - if err != nil { - action.callback(err.Error()) - return - } - geoType := params["geoType"] - geoName := params["geoName"] - handleUpdateGeoData(geoType, geoName, func(value string) { - action.callback(value) - }) - return - case updateExternalProviderMethod: - providerName := action.Data.(string) - handleUpdateExternalProvider(providerName, func(value string) { - action.callback(value) - }) - return - case sideLoadExternalProviderMethod: - paramsString := action.Data.(string) - var params = map[string]string{} - err := json.Unmarshal([]byte(paramsString), ¶ms) - if err != nil { - action.callback(err.Error()) - return - } - providerName := params["providerName"] - data := params["data"] - handleSideLoadExternalProvider(providerName, []byte(data), func(value string) { - action.callback(value) - }) - return - case startLogMethod: - handleStartLog() - return - case stopLogMethod: - handleStopLog() - return - case startListenerMethod: - action.callback(handleStartListener()) - return - case stopListenerMethod: - action.callback(handleStopListener()) - return - case getCountryCodeMethod: - ip := action.Data.(string) - handleGetCountryCode(ip, func(value string) { - action.callback(value) - }) - return - case getMemoryMethod: - handleGetMemory(func(value string) { - action.callback(value) - }) - return - } - +func nextHandle(action *Action, send func([]byte)) bool { + return false } diff --git a/lib/application.dart b/lib/application.dart index d45e568..0ef7330 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -153,7 +153,10 @@ class ApplicationState extends State { 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 { @override Widget build(context) { - return _buildWrap( - _buildPlatformWrap( + return _buildPlatformWrap( + _buildWrap( Selector2( selector: (_, appState, config) => ApplicationSelectorState( locale: config.appSetting.locale, @@ -252,7 +255,7 @@ class ApplicationState extends State { linkManager.destroy(); _autoUpdateGroupTaskTimer?.cancel(); _autoUpdateProfilesTaskTimer?.cancel(); - await clashService?.destroy(); + await clashCore.destroy(); await globalState.appController.savePreferences(); await globalState.appController.handleExit(); super.dispose(); diff --git a/lib/clash/core.dart b/lib/clash/core.dart index 83cc019..8f8f1a7 100644 --- a/lib/clash/core.dart +++ b/lib/clash/core.dart @@ -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 _initGeo() async { + Future preload() { + return clashInterface.preload(); + } + + static Future 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> getExternalProviders() async { final externalProvidersRawString = await clashInterface.getExternalProviders(); + if (externalProvidersRawString.isEmpty) { + return []; + } return Isolate.run>( () { 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 updateGeoData({ - required String geoType, - required String geoName, - }) { - return clashInterface.updateGeoData(geoType: geoType, geoName: geoName); + Future updateGeoData(UpdateGeoDataParams params) { + return clashInterface.updateGeoData(params); } Future sideLoadExternalProvider({ @@ -190,13 +194,16 @@ class ClashCore { await clashInterface.stopListener(); } - Future getDelay(String proxyName) async { - final data = await clashInterface.asyncTestDelay(proxyName); + Future getDelay(String url, String proxyName) async { + final data = await clashInterface.asyncTestDelay(url, proxyName); return Delay.fromJson(json.decode(data)); } - Future getTraffic(bool value) async { - final trafficString = await clashInterface.getTraffic(value); + Future 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 getTotalTraffic(bool value) async { - final totalTrafficString = await clashInterface.getTotalTraffic(value); + Future getTotalTraffic() async { + final totalTrafficString = await clashInterface.getTotalTraffic(); + if (totalTrafficString.isEmpty) { + return Traffic(); + } return Traffic.fromMap(json.decode(totalTrafficString)); } Future 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(); diff --git a/lib/clash/generated/clash_ffi.dart b/lib/clash/generated/clash_ffi.dart index 0c36c53..cd85477 100644 --- a/lib/clash/generated/clash_ffi.dart +++ b/lib/clash/generated/clash_ffi.dart @@ -2362,18 +2362,39 @@ class ClashFFI { late final _initNativeApiBridge = _initNativeApiBridgePtr .asFunction)>(); - void initMessage( - int port, + void attachMessagePort( + int mPort, ) { - return _initMessage( - port, + return _attachMessagePort( + mPort, ); } - late final _initMessagePtr = + late final _attachMessagePortPtr = _lookup>( - 'initMessage'); - late final _initMessage = _initMessagePtr.asFunction(); + 'attachMessagePort'); + late final _attachMessagePort = + _attachMessagePortPtr.asFunction(); + + ffi.Pointer getTraffic() { + return _getTraffic(); + } + + late final _getTrafficPtr = + _lookup Function()>>( + 'getTraffic'); + late final _getTraffic = + _getTrafficPtr.asFunction Function()>(); + + ffi.Pointer getTotalTraffic() { + return _getTotalTraffic(); + } + + late final _getTotalTrafficPtr = + _lookup Function()>>( + 'getTotalTraffic'); + late final _getTotalTraffic = + _getTotalTrafficPtr.asFunction Function()>(); void freeCString( ffi.Pointer s, @@ -2389,19 +2410,22 @@ class ClashFFI { late final _freeCString = _freeCStringPtr.asFunction)>(); - int initClash( - ffi.Pointer homeDirStr, + void invokeAction( + ffi.Pointer paramsChar, + int port, ) { - return _initClash( - homeDirStr, + return _invokeAction( + paramsChar, + port, ); } - late final _initClashPtr = - _lookup)>>( - 'initClash'); - late final _initClash = - _initClashPtr.asFunction)>(); + late final _invokeActionPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, ffi.LongLong)>>('invokeAction'); + late final _invokeAction = + _invokeActionPtr.asFunction, int)>(); void startListener() { return _startListener(); @@ -2419,317 +2443,55 @@ class ClashFFI { _lookup>('stopListener'); late final _stopListener = _stopListenerPtr.asFunction(); - int getIsInit() { - return _getIsInit(); + void attachInvokePort( + int mPort, + ) { + return _attachInvokePort( + mPort, + ); } - late final _getIsInitPtr = - _lookup>('getIsInit'); - late final _getIsInit = _getIsInitPtr.asFunction(); + late final _attachInvokePortPtr = + _lookup>( + 'attachInvokePort'); + late final _attachInvokePort = + _attachInvokePortPtr.asFunction(); - int shutdownClash() { - return _shutdownClash(); - } - - late final _shutdownClashPtr = - _lookup>('shutdownClash'); - late final _shutdownClash = _shutdownClashPtr.asFunction(); - - void forceGc() { - return _forceGc(); - } - - late final _forceGcPtr = - _lookup>('forceGc'); - late final _forceGc = _forceGcPtr.asFunction(); - - void validateConfig( - ffi.Pointer s, + void quickStart( + ffi.Pointer dirChar, + ffi.Pointer paramsChar, + ffi.Pointer stateParamsChar, int port, ) { - return _validateConfig( - s, + return _quickStart( + dirChar, + paramsChar, + stateParamsChar, port, ); } - late final _validateConfigPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, ffi.LongLong)>>('validateConfig'); - late final _validateConfig = _validateConfigPtr - .asFunction, int)>(); - - void updateConfig( - ffi.Pointer s, - int port, - ) { - return _updateConfig( - s, - port, - ); - } - - late final _updateConfigPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, ffi.LongLong)>>('updateConfig'); - late final _updateConfig = - _updateConfigPtr.asFunction, int)>(); - - ffi.Pointer getProxies() { - return _getProxies(); - } - - late final _getProxiesPtr = - _lookup Function()>>( - 'getProxies'); - late final _getProxies = - _getProxiesPtr.asFunction Function()>(); - - void changeProxy( - ffi.Pointer s, - int port, - ) { - return _changeProxy( - s, - port, - ); - } - - late final _changeProxyPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, ffi.LongLong)>>('changeProxy'); - late final _changeProxy = - _changeProxyPtr.asFunction, int)>(); - - ffi.Pointer getTraffic( - int port, - ) { - return _getTraffic( - port, - ); - } - - late final _getTrafficPtr = - _lookup Function(ffi.Int)>>( - 'getTraffic'); - late final _getTraffic = - _getTrafficPtr.asFunction Function(int)>(); - - ffi.Pointer getTotalTraffic( - int port, - ) { - return _getTotalTraffic( - port, - ); - } - - late final _getTotalTrafficPtr = - _lookup Function(ffi.Int)>>( - 'getTotalTraffic'); - late final _getTotalTraffic = - _getTotalTrafficPtr.asFunction Function(int)>(); - - void resetTraffic() { - return _resetTraffic(); - } - - late final _resetTrafficPtr = - _lookup>('resetTraffic'); - late final _resetTraffic = _resetTrafficPtr.asFunction(); - - void asyncTestDelay( - ffi.Pointer s, - int port, - ) { - return _asyncTestDelay( - s, - port, - ); - } - - late final _asyncTestDelayPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, ffi.LongLong)>>('asyncTestDelay'); - late final _asyncTestDelay = _asyncTestDelayPtr - .asFunction, int)>(); - - ffi.Pointer getConnections() { - return _getConnections(); - } - - late final _getConnectionsPtr = - _lookup Function()>>( - 'getConnections'); - late final _getConnections = - _getConnectionsPtr.asFunction Function()>(); - - void getMemory( - int port, - ) { - return _getMemory( - port, - ); - } - - late final _getMemoryPtr = - _lookup>('getMemory'); - late final _getMemory = _getMemoryPtr.asFunction(); - - void closeConnections() { - return _closeConnections(); - } - - late final _closeConnectionsPtr = - _lookup>('closeConnections'); - late final _closeConnections = - _closeConnectionsPtr.asFunction(); - - void closeConnection( - ffi.Pointer id, - ) { - return _closeConnection( - id, - ); - } - - late final _closeConnectionPtr = - _lookup)>>( - 'closeConnection'); - late final _closeConnection = - _closeConnectionPtr.asFunction)>(); - - ffi.Pointer getExternalProviders() { - return _getExternalProviders(); - } - - late final _getExternalProvidersPtr = - _lookup Function()>>( - 'getExternalProviders'); - late final _getExternalProviders = - _getExternalProvidersPtr.asFunction Function()>(); - - ffi.Pointer getExternalProvider( - ffi.Pointer externalProviderNameChar, - ) { - return _getExternalProvider( - externalProviderNameChar, - ); - } - - late final _getExternalProviderPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer)>>('getExternalProvider'); - late final _getExternalProvider = _getExternalProviderPtr - .asFunction Function(ffi.Pointer)>(); - - void updateGeoData( - ffi.Pointer geoTypeChar, - ffi.Pointer geoNameChar, - int port, - ) { - return _updateGeoData( - geoTypeChar, - geoNameChar, - port, - ); - } - - late final _updateGeoDataPtr = _lookup< + late final _quickStartPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Pointer, ffi.Pointer, - ffi.LongLong)>>('updateGeoData'); - late final _updateGeoData = _updateGeoDataPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer, int)>(); + ffi.Pointer, ffi.LongLong)>>('quickStart'); + late final _quickStart = _quickStartPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer, int)>(); - void updateExternalProvider( - ffi.Pointer providerNameChar, - int port, - ) { - return _updateExternalProvider( - providerNameChar, - port, - ); - } - - late final _updateExternalProviderPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, ffi.LongLong)>>('updateExternalProvider'); - late final _updateExternalProvider = _updateExternalProviderPtr - .asFunction, int)>(); - - void getCountryCode( - ffi.Pointer ipChar, - int port, - ) { - return _getCountryCode( - ipChar, - port, - ); - } - - late final _getCountryCodePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, ffi.LongLong)>>('getCountryCode'); - late final _getCountryCode = _getCountryCodePtr - .asFunction, int)>(); - - void sideLoadExternalProvider( - ffi.Pointer providerNameChar, - ffi.Pointer dataChar, - int port, - ) { - return _sideLoadExternalProvider( - providerNameChar, - dataChar, - port, - ); - } - - late final _sideLoadExternalProviderPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, ffi.Pointer, - ffi.LongLong)>>('sideLoadExternalProvider'); - late final _sideLoadExternalProvider = - _sideLoadExternalProviderPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer, int)>(); - - void startLog() { - return _startLog(); - } - - late final _startLogPtr = - _lookup>('startLog'); - late final _startLog = _startLogPtr.asFunction(); - - void stopLog() { - return _stopLog(); - } - - late final _stopLogPtr = - _lookup>('stopLog'); - late final _stopLog = _stopLogPtr.asFunction(); - - void startTUN( + ffi.Pointer startTUN( int fd, - int port, ) { return _startTUN( fd, - port, ); } late final _startTUNPtr = - _lookup>( + _lookup Function(ffi.Int)>>( 'startTUN'); - late final _startTUN = _startTUNPtr.asFunction(); + late final _startTUN = + _startTUNPtr.asFunction Function(int)>(); ffi.Pointer getRunTime() { return _getRunTime(); @@ -2750,30 +2512,18 @@ class ClashFFI { late final _stopTun = _stopTunPtr.asFunction(); void setFdMap( - int fd, + ffi.Pointer fdIdChar, ) { return _setFdMap( - fd, + fdIdChar, ); } late final _setFdMapPtr = - _lookup>('setFdMap'); - late final _setFdMap = _setFdMapPtr.asFunction(); - - void setProcessMap( - ffi.Pointer s, - ) { - return _setProcessMap( - s, - ); - } - - late final _setProcessMapPtr = _lookup)>>( - 'setProcessMap'); - late final _setProcessMap = - _setProcessMapPtr.asFunction)>(); + 'setFdMap'); + late final _setFdMap = + _setFdMapPtr.asFunction)>(); ffi.Pointer getCurrentProfileName() { return _getCurrentProfileName(); @@ -2822,6 +2572,20 @@ class ClashFFI { 'updateDns'); late final _updateDns = _updateDnsPtr.asFunction)>(); + + void setProcessMap( + ffi.Pointer s, + ) { + return _setProcessMap( + s, + ); + } + + late final _setProcessMapPtr = + _lookup)>>( + 'setProcessMap'); + late final _setProcessMap = + _setProcessMapPtr.asFunction)>(); } 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; diff --git a/lib/clash/interface.dart b/lib/clash/interface.dart index cac611e..1ff58c4 100644 --- a/lib/clash/interface.dart +++ b/lib/clash/interface.dart @@ -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 init(String homeDir); + Future init(String homeDir); - FutureOr shutdown(); + Future preload(); - FutureOr get isInit; + Future shutdown(); - forceGc(); + Future get isInit; + + Future forceGc(); FutureOr validateConfig(String data); - Future asyncTestDelay(String proxyName); + Future asyncTestDelay(String url, String proxyName); FutureOr updateConfig(UpdateConfigParams updateConfigParams); @@ -29,10 +38,7 @@ mixin ClashInterface { FutureOr? getExternalProvider(String externalProviderName); - Future updateGeoData({ - required String geoType, - required String geoName, - }); + Future updateGeoData(UpdateGeoDataParams params); Future sideLoadExternalProvider({ required String providerName, @@ -41,9 +47,9 @@ mixin ClashInterface { Future updateExternalProvider(String providerName); - FutureOr getTraffic(bool value); + FutureOr getTraffic(); - FutureOr getTotalTraffic(bool value); + FutureOr getTotalTraffic(); FutureOr getCountryCode(String ip); @@ -61,3 +67,338 @@ mixin ClashInterface { FutureOr closeConnections(); } + +mixin AndroidClashInterface { + Future setFdMap(int fd); + + Future setProcessMap(ProcessMapItem item); + + Future setState(CoreState state); + + Future stopTun(); + + Future updateDns(String value); + + Future startTun(int fd); + + Future getAndroidVpnOptions(); + + Future getCurrentProfileName(); + + Future getRunTime(); +} + +abstract class ClashHandlerInterface with ClashInterface { + Map callbackCompleterMap = {}; + + Future 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 destroy(); + + Future invoke({ + required ActionMethod method, + dynamic data, + Duration? timeout, + FutureOr Function()? onTimeout, + }) async { + final id = "${method.name}#${other.id}"; + + callbackCompleterMap[id] = Completer(); + + 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).safeFuture( + timeout: timeout, + onLast: () { + callbackCompleterMap.remove(id); + }, + onTimeout: onTimeout ?? + () { + return defaultValue; + }, + functionName: id, + ); + } + + @override + Future init(String homeDir) { + return invoke( + method: ActionMethod.initClash, + data: homeDir, + ); + } + + @override + shutdown() async { + return await invoke( + method: ActionMethod.shutdown, + ); + } + + @override + Future get isInit { + return invoke( + method: ActionMethod.getIsInit, + ); + } + + @override + Future forceGc() { + return invoke( + method: ActionMethod.forceGc, + ); + } + + @override + FutureOr validateConfig(String data) { + return invoke( + method: ActionMethod.validateConfig, + data: data, + ); + } + + @override + Future updateConfig(UpdateConfigParams updateConfigParams) async { + return await invoke( + method: ActionMethod.updateConfig, + data: json.encode(updateConfigParams), + ); + } + + @override + Future getProxies() { + return invoke( + method: ActionMethod.getProxies, + ); + } + + @override + FutureOr changeProxy(ChangeProxyParams changeProxyParams) { + return invoke( + method: ActionMethod.changeProxy, + data: json.encode(changeProxyParams), + ); + } + + @override + FutureOr getExternalProviders() { + return invoke( + method: ActionMethod.getExternalProviders, + ); + } + + @override + FutureOr getExternalProvider(String externalProviderName) { + return invoke( + method: ActionMethod.getExternalProvider, + data: externalProviderName, + ); + } + + @override + Future updateGeoData(UpdateGeoDataParams params) { + return invoke( + method: ActionMethod.updateGeoData, + data: json.encode(params), + ); + } + + @override + Future sideLoadExternalProvider({ + required String providerName, + required String data, + }) { + return invoke( + method: ActionMethod.sideLoadExternalProvider, + data: json.encode({ + "providerName": providerName, + "data": data, + }), + ); + } + + @override + Future updateExternalProvider(String providerName) { + return invoke( + method: ActionMethod.updateExternalProvider, + data: providerName, + ); + } + + @override + FutureOr getConnections() { + return invoke( + method: ActionMethod.getConnections, + ); + } + + @override + Future closeConnections() { + return invoke( + method: ActionMethod.closeConnections, + ); + } + + @override + Future closeConnection(String id) { + return invoke( + method: ActionMethod.closeConnection, + data: id, + ); + } + + @override + FutureOr getTotalTraffic() { + return invoke( + method: ActionMethod.getTotalTraffic, + ); + } + + @override + FutureOr getTraffic() { + return invoke( + method: ActionMethod.getTraffic, + ); + } + + @override + resetTraffic() { + invoke(method: ActionMethod.resetTraffic); + } + + @override + startLog() { + invoke(method: ActionMethod.startLog); + } + + @override + stopLog() { + invoke( + method: ActionMethod.stopLog, + ); + } + + @override + Future startListener() { + return invoke( + method: ActionMethod.startListener, + ); + } + + @override + stopListener() { + return invoke( + method: ActionMethod.stopListener, + ); + } + + @override + Future asyncTestDelay(String url, String proxyName) { + final delayParams = { + "proxy-name": proxyName, + "timeout": httpTimeoutDuration.inMilliseconds, + "test-url": url, + }; + return invoke( + method: ActionMethod.asyncTestDelay, + data: json.encode(delayParams), + timeout: Duration( + milliseconds: 6000, + ), + onTimeout: () { + return json.encode( + Delay( + name: proxyName, + value: -1, + url: url, + ), + ); + }, + ); + } + + @override + FutureOr getCountryCode(String ip) { + return invoke( + method: ActionMethod.getCountryCode, + data: ip, + ); + } + + @override + FutureOr getMemory() { + return invoke( + method: ActionMethod.getMemory, + ); + } +} diff --git a/lib/clash/lib.dart b/lib/clash/lib.dart index 56f5912..ede3a31 100644 --- a/lib/clash/lib.dart +++ b/lib/clash/lib.dart @@ -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 _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 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(); - 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 validateConfig(String data) { - final completer = Completer(); - final receiver = ReceivePort(); - receiver.listen((message) { - if (!completer.isCompleted) { - completer.complete(message); - receiver.close(); - } - }); - final dataChar = data.toNativeUtf8().cast(); - clashFFI.validateConfig( - dataChar, - receiver.sendPort.nativePort, - ); - malloc.free(dataChar); - return completer.future; - } - - @override - Future updateConfig(UpdateConfigParams updateConfigParams) { - final completer = Completer(); - final receiver = ReceivePort(); - receiver.listen((message) { - if (!completer.isCompleted) { - completer.complete(message); - receiver.close(); - } - }); - final params = json.encode(updateConfigParams); - final paramsChar = params.toNativeUtf8().cast(); - clashFFI.updateConfig( - paramsChar, - receiver.sendPort.nativePort, - ); - malloc.free(paramsChar); - return completer.future; - } - - @override - String getProxies() { - final proxiesRaw = clashFFI.getProxies(); - final proxiesRawString = proxiesRaw.cast().toDartString(); - clashFFI.freeCString(proxiesRaw); - return proxiesRawString; - } - - @override - String getExternalProviders() { - final externalProvidersRaw = clashFFI.getExternalProviders(); - final externalProvidersRawString = - externalProvidersRaw.cast().toDartString(); - clashFFI.freeCString(externalProvidersRaw); - return externalProvidersRawString; - } - - @override - String getExternalProvider(String externalProviderName) { - final externalProviderNameChar = - externalProviderName.toNativeUtf8().cast(); - final externalProviderRaw = - clashFFI.getExternalProvider(externalProviderNameChar); - malloc.free(externalProviderNameChar); - final externalProviderRawString = - externalProviderRaw.cast().toDartString(); - clashFFI.freeCString(externalProviderRaw); - return externalProviderRawString; - } - - @override - Future updateGeoData({ - required String geoType, - required String geoName, - }) { - final completer = Completer(); - final receiver = ReceivePort(); - receiver.listen((message) { - if (!completer.isCompleted) { - completer.complete(message); - receiver.close(); - } - }); - final geoTypeChar = geoType.toNativeUtf8().cast(); - final geoNameChar = geoName.toNativeUtf8().cast(); - clashFFI.updateGeoData( - geoTypeChar, - geoNameChar, - receiver.sendPort.nativePort, - ); - malloc.free(geoTypeChar); - malloc.free(geoNameChar); - return completer.future; - } - - @override - Future sideLoadExternalProvider({ - required String providerName, - required String data, - }) { - final completer = Completer(); - final receiver = ReceivePort(); - receiver.listen((message) { - if (!completer.isCompleted) { - completer.complete(message); - receiver.close(); - } - }); - final providerNameChar = providerName.toNativeUtf8().cast(); - final dataChar = data.toNativeUtf8().cast(); - clashFFI.sideLoadExternalProvider( - providerNameChar, - dataChar, - receiver.sendPort.nativePort, - ); - malloc.free(providerNameChar); - malloc.free(dataChar); - return completer.future; - } - - @override - Future updateExternalProvider(String providerName) { - final completer = Completer(); - final receiver = ReceivePort(); - receiver.listen((message) { - if (!completer.isCompleted) { - completer.complete(message); - receiver.close(); - } - }); - final providerNameChar = providerName.toNativeUtf8().cast(); - clashFFI.updateExternalProvider( - providerNameChar, - receiver.sendPort.nativePort, - ); - malloc.free(providerNameChar); - return completer.future; - } - - @override - Future changeProxy(ChangeProxyParams changeProxyParams) { - final completer = Completer(); - final receiver = ReceivePort(); - receiver.listen((message) { - if (!completer.isCompleted) { - completer.complete(message); - receiver.close(); - } - }); - final params = json.encode(changeProxyParams); - final paramsChar = params.toNativeUtf8().cast(); - clashFFI.changeProxy( - paramsChar, - receiver.sendPort.nativePort, - ); - malloc.free(paramsChar); - return completer.future; - } - - @override - String getConnections() { - final connectionsDataRaw = clashFFI.getConnections(); - final connectionsString = connectionsDataRaw.cast().toDartString(); - clashFFI.freeCString(connectionsDataRaw); - return connectionsString; - } - - @override - closeConnection(String id) { - final idChar = id.toNativeUtf8().cast(); - clashFFI.closeConnection(idChar); - malloc.free(idChar); + destroy() async { + await service?.destroy(); return true; } @override - closeConnections() { - clashFFI.closeConnections(); + reStart() { + _initService(); + } + + @override + Future 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 setFdMap(int fd) { + return invoke( + method: ActionMethod.setFdMap, + data: json.encode(fd), + ); } @override - Future asyncTestDelay(String proxyName) { - final delayParams = { - "proxy-name": proxyName, - "timeout": httpTimeoutDuration.inMilliseconds, - }; + Future setProcessMap(item) { + return invoke( + method: ActionMethod.setProcessMap, + data: item, + ); + } + + @override + Future setState(CoreState state) { + return invoke( + method: ActionMethod.setState, + data: json.encode(state), + ); + } + + @override + Future startTun(int fd) async { + final res = await invoke( + method: ActionMethod.startTun, + data: json.encode(fd), + ); + + if (res.isEmpty) { + return null; + } + return DateTime.fromMillisecondsSinceEpoch(int.parse(res)); + } + + @override + Future stopTun() { + return invoke( + method: ActionMethod.stopTun, + ); + } + + @override + Future getAndroidVpnOptions() async { + final res = await invoke( + method: ActionMethod.getAndroidVpnOptions, + ); + if (res.isEmpty) { + return null; + } + return AndroidVpnOptions.fromJson(json.decode(res)); + } + + @override + Future updateDns(String value) { + return invoke( + method: ActionMethod.updateDns, + data: value, + ); + } + + @override + Future getRunTime() async { + final runTimeString = await invoke( + method: ActionMethod.getRunTime, + ); + if (runTimeString.isEmpty) { + return null; + } + return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString)); + } + + @override + Future getCurrentProfileName() { + return invoke( + 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 invokeAction(String actionParams) { final completer = Completer(); final receiver = ReceivePort(); receiver.listen((message) { @@ -261,89 +218,33 @@ class ClashLib with ClashInterface { receiver.close(); } }); - final delayParamsChar = - json.encode(delayParams).toNativeUtf8().cast(); - clashFFI.asyncTestDelay( - delayParamsChar, + final actionParamsChar = actionParams.toNativeUtf8().cast(); + 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().toDartString(); - clashFFI.freeCString(trafficRaw); - return trafficString; - } - - @override - String getTotalTraffic(bool value) { - final trafficRaw = clashFFI.getTotalTraffic(value ? 1 : 0); - clashFFI.freeCString(trafficRaw); - return trafficRaw.cast().toDartString(); - } - - @override - void resetTraffic() { - clashFFI.resetTraffic(); - } - - @override - void startLog() { - clashFFI.startLog(); - } - - @override - stopLog() { - clashFFI.stopLog(); - } - - @override - forceGc() { - clashFFI.forceGc(); - } - - @override - FutureOr getCountryCode(String ip) { - final completer = Completer(); - final receiver = ReceivePort(); - receiver.listen((message) { - if (!completer.isCompleted) { - completer.complete(message); - receiver.close(); - } - }); - final ipChar = ip.toNativeUtf8().cast(); - clashFFI.getCountryCode( - ipChar, - receiver.sendPort.nativePort, + attachMessagePort(int messagePort) { + clashFFI.attachMessagePort( + messagePort, ); - malloc.free(ipChar); - return completer.future; } - @override - FutureOr getMemory() { - final completer = Completer(); - 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().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(); 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().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().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(); + clashFFI.setFdMap(idChar); + malloc.free(idChar); + } + + Future quickStart( + String homeDir, + UpdateConfigParams updateConfigParams, + CoreState state, + ) { + final completer = Completer(); + 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(); + final paramsChar = params.toNativeUtf8().cast(); + final stateParamsChar = stateParams.toNativeUtf8().cast(); + 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; diff --git a/lib/clash/message.dart b/lib/clash/message.dart index 2f50a4e..81b929b 100644 --- a/lib/clash/message.dart +++ b/lib/clash/message.dart @@ -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(); 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; diff --git a/lib/clash/service.dart b/lib/clash/service.dart index 26d3eb6..096ee98 100644 --- a/lib/clash/service.dart +++ b/lib/clash/service.dart @@ -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 serverCompleter = Completer(); Completer socketCompleter = Completer(); - Map 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 _invoke({ - required ActionMethod method, - dynamic data, - Duration? timeout, - FutureOr Function()? onTimeout, - }) async { - final id = "${method.name}#${other.id}"; - final socket = await socketCompleter.future; - callbackCompleterMap[id] = Completer(); - socket.writeln( - json.encode( - Action( - id: id, - method: method, - data: data, - ), - ), - ); - return (callbackCompleterMap[id] as Completer).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 init(String homeDir) { - return _invoke( - method: ActionMethod.initClash, - data: homeDir, - ); - } - @override shutdown() async { - await _invoke( - method: ActionMethod.shutdown, - ); + await super.shutdown(); if (Platform.isWindows) { await request.stopCoreByHelper(); } await _destroySocket(); process?.kill(); process = null; + return true; } @override - Future get isInit { - return _invoke( - method: ActionMethod.getIsInit, - ); - } - - @override - forceGc() { - _prueInvoke(method: ActionMethod.forceGc); - } - - @override - FutureOr validateConfig(String data) { - return _invoke( - method: ActionMethod.validateConfig, - data: data, - ); - } - - @override - Future updateConfig(UpdateConfigParams updateConfigParams) async { - return await _invoke( - method: ActionMethod.updateConfig, - data: json.encode(updateConfigParams), - timeout: const Duration(seconds: 20), - ); - } - - @override - Future getProxies() { - return _invoke( - method: ActionMethod.getProxies, - ); - } - - @override - FutureOr changeProxy(ChangeProxyParams changeProxyParams) { - return _invoke( - method: ActionMethod.changeProxy, - data: json.encode(changeProxyParams), - ); - } - - @override - FutureOr getExternalProviders() { - return _invoke( - method: ActionMethod.getExternalProviders, - ); - } - - @override - FutureOr getExternalProvider(String externalProviderName) { - return _invoke( - method: ActionMethod.getExternalProvider, - data: externalProviderName, - ); - } - - @override - Future updateGeoData({ - required String geoType, - required String geoName, - }) { - return _invoke( - method: ActionMethod.updateGeoData, - data: json.encode( - { - "geoType": geoType, - "geoName": geoName, - }, - ), - ); - } - - @override - Future sideLoadExternalProvider({ - required String providerName, - required String data, - }) { - return _invoke( - method: ActionMethod.sideLoadExternalProvider, - data: json.encode({ - "providerName": providerName, - "data": data, - }), - ); - } - - @override - Future updateExternalProvider(String providerName) { - return _invoke( - method: ActionMethod.updateExternalProvider, - data: providerName, - ); - } - - @override - FutureOr getConnections() { - return _invoke( - method: ActionMethod.getConnections, - ); - } - - @override - Future closeConnections() { - return _invoke( - method: ActionMethod.closeConnections, - ); - } - - @override - Future closeConnection(String id) { - return _invoke( - method: ActionMethod.closeConnection, - data: id, - ); - } - - @override - FutureOr getTotalTraffic(bool value) { - return _invoke( - method: ActionMethod.getTotalTraffic, - data: value, - ); - } - - @override - FutureOr getTraffic(bool value) { - return _invoke( - 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 startListener() { - return _invoke( - method: ActionMethod.startListener, - ); - } - - @override - stopListener() { - return _invoke( - method: ActionMethod.stopListener, - ); - } - - @override - Future asyncTestDelay(String proxyName) { - final delayParams = { - "proxy-name": proxyName, - "timeout": httpTimeoutDuration.inMilliseconds, - }; - return _invoke( - 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 getCountryCode(String ip) { - return _invoke( - method: ActionMethod.getCountryCode, - data: ip, - ); - } - - @override - FutureOr getMemory() { - return _invoke( - method: ActionMethod.getMemory, - ); + Future preload() async { + await serverCompleter.future; + return true; } } diff --git a/lib/common/common.dart b/lib/common/common.dart index fb141c9..54fa057 100644 --- a/lib/common/common.dart +++ b/lib/common/common.dart @@ -34,3 +34,4 @@ export 'text.dart'; export 'tray.dart'; export 'window.dart'; export 'windows.dart'; +export 'render.dart'; \ No newline at end of file diff --git a/lib/common/constant.dart b/lib/common/constant.dart index 0338ccb..0e16980 100644 --- a/lib/common/constant.dart +++ b/lib/common/constant.dart @@ -73,7 +73,7 @@ const hotKeyActionListEquality = ListEquality(); const stringAndStringMapEquality = MapEquality(); const stringAndStringMapEntryIterableEquality = IterableEquality>(); -const stringAndIntQMapEquality = MapEquality(); +const delayMapEquality = MapEquality>(); const stringSetEquality = SetEquality(); const keyboardModifierListEquality = SetEquality(); @@ -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"; diff --git a/lib/common/future.dart b/lib/common/future.dart index 6f4d0a5..339dcf2 100644 --- a/lib/common/future.dart +++ b/lib/common/future.dart @@ -10,8 +10,8 @@ extension CompleterExt on Completer { FutureOr Function()? onTimeout, required String functionName, }) { - final realTimeout = timeout ?? const Duration(minutes: 1); - Timer(realTimeout + moreDuration, () { + final realTimeout = timeout ?? const Duration(seconds: 1); + Timer(realTimeout + commonDuration, () { if (onLast != null) { onLast(); } diff --git a/lib/common/navigator.dart b/lib/common/navigator.dart index 77ed57e..83fe288 100644 --- a/lib/common/navigator.dart +++ b/lib/common/navigator.dart @@ -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 push(BuildContext context, Widget child) async { + if (!globalState.appController.isMobileView) { + return await Navigator.of(context).push( + CommonDesktopRoute( + builder: (context) => child, + ), + ); + } return await Navigator.of(context).push( CommonRoute( builder: (context) => child, @@ -11,6 +19,46 @@ class BaseNavigator { } } +class CommonDesktopRoute extends PageRoute { + 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 animation, + Animation 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 extends MaterialPageRoute { CommonRoute({ required super.builder, diff --git a/lib/common/render.dart b/lib/common/render.dart new file mode 100644 index 0000000..d427e91 --- /dev/null +++ b/lib/common/render.dart @@ -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; diff --git a/lib/common/window.dart b/lib/common/window.dart index e509de7..575308f 100755 --- a/lib/common/window.dart +++ b/lib/common/window.dart @@ -63,6 +63,7 @@ class Window { await windowManager.show(); await windowManager.focus(); await windowManager.setSkipTaskbar(false); + render?.resume(); } Future isVisible() async { @@ -76,6 +77,7 @@ class Window { hide() async { await windowManager.hide(); await windowManager.setSkipTaskbar(true); + render?.pause(); } } diff --git a/lib/controller.dart b/lib/controller.dart index 9e72e36..32c2d9c 100644 --- a/lib/controller.dart +++ b/lib/controller.dart @@ -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 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 _sortOfName(List proxies) { return List.of(proxies) ..sort( @@ -532,12 +557,12 @@ class AppController { ); } - List _sortOfDelay(List proxies) { - return proxies = List.of(proxies) + List _sortOfDelay(String url, List 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 getSortProxies(List proxies) { + List getSortProxies(List 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, diff --git a/lib/enum/enum.dart b/lib/enum/enum.dart index 728f674..39e3097 100644 --- a/lib/enum/enum.dart +++ b/lib/enum/enum.dart @@ -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 } diff --git a/lib/fragments/application_setting.dart b/lib/fragments/application_setting.dart index 94ee09b..d3d2221 100644 --- a/lib/fragments/application_setting.dart +++ b/lib/fragments/application_setting.dart @@ -40,7 +40,7 @@ class UsageSwitch extends StatelessWidget { @override Widget build(BuildContext context) { return Selector( - 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, ); }, ), diff --git a/lib/fragments/config/dns.dart b/lib/fragments/config/dns.dart index c2cc508..7f11af6 100644 --- a/lib/fragments/config/dns.dart +++ b/lib/fragments/config/dns.dart @@ -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?.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( diff --git a/lib/fragments/config/network.dart b/lib/fragments/config/network.dart index 5ed362a..1674e38 100644 --- a/lib/fragments/config/network.dart +++ b/lib/fragments/config/network.dart @@ -210,19 +210,19 @@ class BypassDomainItem extends StatelessWidget { context.findAncestorStateOfType(); 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?.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( diff --git a/lib/fragments/dashboard/widgets/memory_info.dart b/lib/fragments/dashboard/widgets/memory_info.dart index 6b20552..b5554dd 100644 --- a/lib/fragments/dashboard/widgets/memory_info.dart +++ b/lib/fragments/dashboard/widgets/memory_info.dart @@ -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(value: 0)); +final _memoryInfoStateNotifier = ValueNotifier( + TrafficValue(value: 0), +); class MemoryInfo extends StatefulWidget { const MemoryInfo({super.key}); @@ -22,10 +24,7 @@ class _MemoryInfoState extends State { @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 { 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(); + }); }); } diff --git a/lib/fragments/dashboard/widgets/network_speed.dart b/lib/fragments/dashboard/widgets/network_speed.dart index c06b557..4446ac0 100644 --- a/lib/fragments/dashboard/widgets/network_speed.dart +++ b/lib/fragments/dashboard/widgets/network_speed.dart @@ -76,41 +76,11 @@ class _NetworkSpeedState extends State { -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, + ), ), ), ), diff --git a/lib/fragments/profiles/edit_profile.dart b/lib/fragments/profiles/edit_profile.dart index b22752a..a6b0f99 100644 --- a/lib/fragments/profiles/edit_profile.dart +++ b/lib/fragments/profiles/edit_profile.dart @@ -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 { late TextEditingController urlController; late TextEditingController autoUpdateDurationController; late bool autoUpdate; + String? rawText; final GlobalKey _formKey = GlobalKey(); final fileInfoNotifier = ValueNotifier(null); Uint8List? fileData; + Profile get profile => widget.profile; + @override void initState() { super.initState(); @@ -51,28 +54,43 @@ class _EditProfileState extends State { _handleConfirm() async { if (!_formKey.currentState!.validate()) return; - final config = widget.context.read(); - final 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 { ); } - _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( + () 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( + 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 { ); } + _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 { }, ), ]; - 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, ), ), ), diff --git a/lib/fragments/profiles/profiles.dart b/lib/fragments/profiles/profiles.dart index 8514b82..af8a768 100644 --- a/lib/fragments/profiles/profiles.dart +++ b/lib/fragments/profiles/profiles.dart @@ -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 { @@ -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( + () 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( - 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, diff --git a/lib/fragments/profiles/view_profile.dart b/lib/fragments/profiles/view_profile.dart deleted file mode 100644 index f05d47b..0000000 --- a/lib/fragments/profiles/view_profile.dart +++ /dev/null @@ -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 createState() => _ViewProfileState(); -} - -class _ViewProfileState extends State { - bool readOnly = true; - final CodeLineEditingController _controller = CodeLineEditingController(); - final key = GlobalKey(); - 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(() 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 { - 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 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( - 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!); - } -} diff --git a/lib/fragments/proxies/card.dart b/lib/fragments/proxies/card.dart index 638a3af..51c5170 100644 --- a/lib/fragments/proxies/card.dart +++ b/lib/fragments/proxies/card.dart @@ -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( - selector: (context, appState) => appState.getDelay( - proxy.name, - ), + selector: (context, appState) => + globalState.appController.getDelay(proxy.name,testUrl), builder: (context, delay, __) { return FadeBox( child: Builder( diff --git a/lib/fragments/proxies/common.dart b/lib/fragments/proxies/common.dart index aac605a..1cc3f27 100644 --- a/lib/fragments/proxies/common.dart +++ b/lib/fragments/proxies/common.dart @@ -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 proxies) async { +delayTest(List 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((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(); diff --git a/lib/fragments/proxies/list.dart b/lib/fragments/proxies/list.dart index 8549f14..48c12d2 100644 --- a/lib/fragments/proxies/list.dart +++ b/lib/fragments/proxies/list.dart @@ -134,6 +134,7 @@ class _ProxiesListFragmentState extends State { 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 { .map( (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 { 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 bool get isExpand => widget.isExpand; - _delayTest(List 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 ), ), IconButton( - onPressed: () { - _delayTest(widget.group.all); - }, + onPressed: _delayTest, icon: const Icon( Icons.network_ping, ), diff --git a/lib/fragments/proxies/providers.dart b/lib/fragments/proxies/providers.dart index 6e7836f..986a555 100644 --- a/lib/fragments/proxies/providers.dart +++ b/lib/fragments/proxies/providers.dart @@ -47,21 +47,40 @@ class _ProvidersState extends State { _updateProviders() async { final appState = globalState.appController.appState; final providers = globalState.appController.appState.providers; + final messages = []; final updateProviders = providers.map( (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(() 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(); } diff --git a/lib/fragments/proxies/tab.dart b/lib/fragments/proxies/tab.dart index 5babe29..01144ff 100644 --- a/lib/fragments/proxies/tab.dart +++ b/lib/fragments/proxies/tab.dart @@ -12,6 +12,7 @@ import 'card.dart'; import 'common.dart'; List currentProxies = []; +String? currentTestUrl; typedef GroupNameKeyMap = Map>; @@ -114,6 +115,7 @@ class ProxiesTabFragmentState extends State } 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 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 { _delayTest() async { if (isLock) return; isLock = true; - await delayTest(currentProxies); + await delayTest( + currentProxies, + currentTestUrl, + ); isLock = false; } @@ -289,6 +299,7 @@ class ProxyGroupViewState extends State { } final sortedProxies = globalState.appController.getSortProxies( currentProxies, + currentTestUrl, ); _controller.animateTo( min( @@ -334,6 +345,7 @@ class ProxyGroupViewState extends State { sortNum: appState.sortNum, proxies: group.all, groupType: group.type, + testUrl: group.testUrl, ); }, builder: (_, state, __) { @@ -342,6 +354,7 @@ class ProxyGroupViewState extends State { final proxyCardType = state.proxyCardType; final sortedProxies = globalState.appController.getSortProxies( proxies, + state.testUrl, ); return ActiveBuilder( label: "proxies", @@ -369,6 +382,7 @@ class ProxyGroupViewState extends State { itemBuilder: (_, index) { final proxy = sortedProxies[index]; return ProxyCard( + testUrl: state.testUrl, groupType: state.groupType, type: proxyCardType, key: ValueKey('$groupName.${proxy.name}'), diff --git a/lib/fragments/resources.dart b/lib/fragments/resources.dart index 14a7f45..ffc0be7 100644 --- a/lib/fragments/resources.dart +++ b/lib/fragments/resources.dart @@ -191,8 +191,10 @@ class _GeoDataListItemState extends State { 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 createState() => _UpdateGeoUrlFormDialogState(); diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb index 940c2f3..fc888f5 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -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" } \ No newline at end of file diff --git a/lib/l10n/arb/intl_zh_CN.arb b/lib/l10n/arb/intl_zh_CN.arb index 731aa01..db8941a 100644 --- a/lib/l10n/arb/intl_zh_CN.arb +++ b/lib/l10n/arb/intl_zh_CN.arb @@ -333,5 +333,13 @@ "routeAddressDesc": "配置监听路由地址", "pleaseInputAdminPassword": "请输入管理员密码", "copyEnvVar": "复制环境变量", - "memoryInfo": "内存信息" + "memoryInfo": "内存信息", + "cancel": "取消", + "fileIsUpdate": "文件有修改,是否保存修改", + "profileHasUpdate": "配置文件已经修改,是否关闭自动更新 ", + "hasCacheChange": "是否缓存修改", + "nullProxies": "暂无代理", + "copySuccess": "复制成功", + "copyLink": "复制链接", + "exportFile": "导出文件" } diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index 21166c9..b31a1fa 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -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": diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index b4a4dcd..e145047 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -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": diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 2b9df9d..095ea69 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -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 { diff --git a/lib/main.dart b/lib/main.dart index 16b25e3..b53307c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,11 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi'; import 'dart:io'; +import 'dart:isolate'; +import 'dart:ui'; -import 'package:fl_clash/clash/clash.dart'; +import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/plugins/tile.dart'; import 'package:fl_clash/plugins/vpn.dart'; @@ -10,13 +14,16 @@ import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'application.dart'; +import 'clash/core.dart'; +import 'clash/lib.dart'; import 'common/common.dart'; import 'l10n/l10n.dart'; import 'models/models.dart'; Future 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 main() async { } @pragma('vm:entry-point') -Future vpnService() async { +Future _service(List 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); + } +} diff --git a/lib/manager/android_manager.dart b/lib/manager/android_manager.dart index 4c9c05b..2fdf7c6 100644 --- a/lib/manager/android_manager.dart +++ b/lib/manager/android_manager.dart @@ -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 { Widget _updateCoreState(Widget child) { return Selector2( - 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); diff --git a/lib/manager/app_state_manager.dart b/lib/manager/app_state_manager.dart index 013e915..1b1782a 100644 --- a/lib/manager/app_state_manager.dart +++ b/lib/manager/app_state_manager.dart @@ -21,11 +21,10 @@ class _AppStateManagerState extends State _updateNavigationsContainer(Widget child) { return Selector2( 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 @override Future 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 @override Widget build(BuildContext context) { - return _cacheStateChange( - _updateNavigationsContainer( - widget.child, + return Listener( + onPointerDown: (_) { + render?.resume(); + }, + child: _cacheStateChange( + _updateNavigationsContainer( + widget.child, + ), ), ); } diff --git a/lib/manager/clash_manager.dart b/lib/manager/clash_manager.dart index 33a89b9..72dc671 100644 --- a/lib/manager/clash_manager.dart +++ b/lib/manager/clash_manager.dart @@ -99,14 +99,13 @@ class _ClashContainerState extends State with AppMessageListener { @override Future 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 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); diff --git a/lib/manager/message_manager.dart b/lib/manager/message_manager.dart index ba06a6b..79842cd 100644 --- a/lib/manager/message_manager.dart +++ b/lib/manager/message_manager.dart @@ -19,45 +19,26 @@ class MessageManager extends StatefulWidget { class MessageManagerState extends State with SingleTickerProviderStateMixin { - final _floatMessageKey = GlobalKey(); - List bufferMessages = []; final _messagesNotifier = ValueNotifier>([]); - final _floatMessageNotifier = ValueNotifier(null); double maxWidth = 0; + Offset offset = Offset.zero; late AnimationController _animationController; - Completer? _animationCompleter; - late Animation _floatOffsetAnimation; - late Animation _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 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.symmetric(vertical: 12, horizontal: 15), - child: Text( - message.text, - style: context.textTheme.bodyMedium?.copyWith( - color: context.colorScheme.onSecondaryFixedVariant, - ), - maxLines: 5, - overflow: TextOverflow.ellipsis, - ), - ), - ); - } - - Widget _floatMessage() { - return ValueListenableBuilder( - valueListenable: _floatMessageNotifier, - builder: (_, message, ___) { - if (message == null) { - return SizedBox(); - } - return AnimatedBuilder( - key: _floatMessageKey, - animation: _animationController.view, - builder: (_, child) { - if (!_animationController.isAnimating) { - return Opacity( - opacity: 0, - child: child, - ); - } - return Transform.translate( - offset: _floatOffsetAnimation.value, - child: child, - ); - }, - child: _wrapMessage( - message, - ), - ); - }, - ); - } - - _removeMessage(CommonMessage commonMessage) async { - final itemWrapState = GlobalObjectKey(commonMessage.id).currentState - as _MessageItemWrapState?; - await itemWrapState?.transform( - Offset(-maxWidth, 0), - ); + _handleRemove(CommonMessage commonMessage) async { _messagesNotifier.value = List.from(_messagesNotifier.value) ..remove(commonMessage); } @@ -204,6 +72,7 @@ class MessageManagerState extends State child: ValueListenableBuilder( valueListenable: globalState.safeMessageOffsetNotifier, builder: (_, offset, child) { + this.offset = offset; if (offset == Offset.zero) { return SizedBox(); } @@ -234,15 +103,14 @@ class MessageManagerState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ for (final message in messages) ...[ - if (message != messages.last) + if (message != messages.first) SizedBox( - height: 8, + height: 12, ), - _MessageItemWrap( + _MessageItem( key: GlobalObjectKey(message.id), - child: _wrapOffset( - _wrapMessage(message), - ), + message: message, + onRemove: _handleRemove, ), ], ], @@ -250,7 +118,6 @@ class MessageManagerState extends State }, ), ), - _floatMessage(), ], ), ), @@ -263,22 +130,25 @@ class MessageManagerState extends State } } -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 _offsetAnimation; + late Animation _fadeAnimation; @override void initState() { @@ -287,11 +157,41 @@ class _MessageItemWrapState extends State<_MessageItemWrap> vsync: this, duration: commonDuration * 1.5, ); - } + _offsetAnimation = Tween( + 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( + 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 @@ -305,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, ); } } diff --git a/lib/manager/tray_manager.dart b/lib/manager/tray_manager.dart index 87f24d4..1be9d21 100755 --- a/lib/manager/tray_manager.dart +++ b/lib/manager/tray_manager.dart @@ -58,6 +58,12 @@ class _TrayContainerState extends State with TrayListener { trayManager.popUpContextMenu(); } + @override + void onTrayMenuItemClick(MenuItem menuItem) { + render?.active(); + super.onTrayMenuItemClick(menuItem); + } + @override onTrayIconMouseDown() { window?.show(); diff --git a/lib/manager/window_manager.dart b/lib/manager/window_manager.dart index 3ba7586..e6333a5 100644 --- a/lib/manager/window_manager.dart +++ b/lib/manager/window_manager.dart @@ -64,6 +64,18 @@ class _WindowContainerState extends State super.onWindowClose(); } + @override + void onWindowFocus() { + super.onWindowFocus(); + render?.resume(); + } + + @override + void onWindowBlur() { + super.onWindowBlur(); + render?.pause(); + } + @override Future onShouldTerminate() async { await globalState.appController.handleExit(); diff --git a/lib/models/app.dart b/lib/models/app.dart index b12d607..55615d8 100644 --- a/lib/models/app.dart +++ b/lib/models/app.dart @@ -6,7 +6,7 @@ import 'common.dart'; import 'core.dart'; import 'profile.dart'; -typedef DelayMap = Map; +typedef DelayMap = Map>; class AppState with ChangeNotifier { List _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(); } } diff --git a/lib/models/common.dart b/lib/models/common.dart index 0112daa..f12c390 100644 --- a/lib/models/common.dart +++ b/lib/models/common.dart @@ -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 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; +} diff --git a/lib/models/config.dart b/lib/models/config.dart index c389c17..1dce9f1 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -46,7 +46,7 @@ class AppSetting with _$AppSetting { @JsonKey(fromJson: dashboardWidgetsRealFormJson) @Default(defaultDashboardWidgets) List dashboardWidgets, - @Default(false) bool onlyProxy, + @Default(false) bool onlyStatisticsProxy, @Default(false) bool autoLaunch, @Default(false) bool silentLaunch, @Default(false) bool autoRun, diff --git a/lib/models/core.dart b/lib/models/core.dart index 9fe716d..7073db1 100644 --- a/lib/models/core.dart +++ b/lib/models/core.dart @@ -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 bypassDomain, required List routeAddress, required bool ipv6, - required bool onlyProxy, }) = _CoreState; factory CoreState.fromJson(Map 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 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 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 json) => - _$ServiceMessageFromJson(json); + factory InvokeMessage.fromJson(Map 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 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 json) => + _$ActionResultFromJson(json); } diff --git a/lib/models/generated/common.freezed.dart b/lib/models/generated/common.freezed.dart index 99836a7..c085112 100644 --- a/lib/models/generated/common.freezed.dart +++ b/lib/models/generated/common.freezed.dart @@ -1998,6 +1998,7 @@ mixin _$Group { List 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 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 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 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 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; diff --git a/lib/models/generated/common.g.dart b/lib/models/generated/common.g.dart index 3f70cbe..442f0ba 100644 --- a/lib/models/generated/common.g.dart +++ b/lib/models/generated/common.g.dart @@ -161,6 +161,7 @@ _$GroupImpl _$$GroupImplFromJson(Map 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 _$$GroupImplToJson(_$GroupImpl instance) => 'all': instance.all, 'now': instance.now, 'hidden': instance.hidden, + 'testUrl': instance.testUrl, 'icon': instance.icon, 'name': instance.name, }; diff --git a/lib/models/generated/config.freezed.dart b/lib/models/generated/config.freezed.dart index e60efc9..1d82785 100644 --- a/lib/models/generated/config.freezed.dart +++ b/lib/models/generated/config.freezed.dart @@ -24,7 +24,7 @@ mixin _$AppSetting { @JsonKey(fromJson: dashboardWidgetsRealFormJson) List 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 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, - 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 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, - 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 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 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 get dashboardWidgets; @override - bool get onlyProxy; + bool get onlyStatisticsProxy; @override bool get autoLaunch; @override diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart index 1ccbfe1..5c9e27b 100644 --- a/lib/models/generated/config.g.dart +++ b/lib/models/generated/config.g.dart @@ -57,7 +57,7 @@ _$AppSettingImpl _$$AppSettingImplFromJson(Map 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 _$$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, diff --git a/lib/models/generated/core.freezed.dart b/lib/models/generated/core.freezed.dart index e4b3381..aa77900 100644 --- a/lib/models/generated/core.freezed.dart +++ b/lib/models/generated/core.freezed.dart @@ -28,7 +28,6 @@ mixin _$CoreState { List get bypassDomain => throw _privateConstructorUsedError; List get routeAddress => throw _privateConstructorUsedError; bool get ipv6 => throw _privateConstructorUsedError; - bool get onlyProxy => throw _privateConstructorUsedError; /// Serializes this CoreState to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -53,8 +52,7 @@ abstract class $CoreStateCopyWith<$Res> { bool systemProxy, List bypassDomain, List routeAddress, - bool ipv6, - bool onlyProxy}); + bool ipv6}); $AccessControlCopyWith<$Res>? get accessControl; } @@ -82,7 +80,6 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState> Object? bypassDomain = null, Object? routeAddress = null, Object? ipv6 = null, - Object? onlyProxy = null, }) { return _then(_value.copyWith( enable: null == enable @@ -117,10 +114,6 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState> ? _value.ipv6 : ipv6 // ignore: cast_nullable_to_non_nullable as bool, - onlyProxy: null == onlyProxy - ? _value.onlyProxy - : onlyProxy // ignore: cast_nullable_to_non_nullable - as bool, ) as $Val); } @@ -155,8 +148,7 @@ abstract class _$$CoreStateImplCopyWith<$Res> bool systemProxy, List bypassDomain, List routeAddress, - bool ipv6, - bool onlyProxy}); + bool ipv6}); @override $AccessControlCopyWith<$Res>? get accessControl; @@ -183,7 +175,6 @@ class __$$CoreStateImplCopyWithImpl<$Res> Object? bypassDomain = null, Object? routeAddress = null, Object? ipv6 = null, - Object? onlyProxy = null, }) { return _then(_$CoreStateImpl( enable: null == enable @@ -218,10 +209,6 @@ class __$$CoreStateImplCopyWithImpl<$Res> ? _value.ipv6 : ipv6 // ignore: cast_nullable_to_non_nullable as bool, - onlyProxy: null == onlyProxy - ? _value.onlyProxy - : onlyProxy // ignore: cast_nullable_to_non_nullable - as bool, )); } } @@ -237,8 +224,7 @@ class _$CoreStateImpl implements _CoreState { required this.systemProxy, required final List bypassDomain, required final List routeAddress, - required this.ipv6, - required this.onlyProxy}) + required this.ipv6}) : _bypassDomain = bypassDomain, _routeAddress = routeAddress; @@ -273,12 +259,10 @@ class _$CoreStateImpl implements _CoreState { @override final bool ipv6; - @override - final bool onlyProxy; @override String toString() { - return 'CoreState(enable: $enable, accessControl: $accessControl, currentProfileName: $currentProfileName, allowBypass: $allowBypass, systemProxy: $systemProxy, bypassDomain: $bypassDomain, routeAddress: $routeAddress, ipv6: $ipv6, onlyProxy: $onlyProxy)'; + return 'CoreState(enable: $enable, accessControl: $accessControl, currentProfileName: $currentProfileName, allowBypass: $allowBypass, systemProxy: $systemProxy, bypassDomain: $bypassDomain, routeAddress: $routeAddress, ipv6: $ipv6)'; } @override @@ -299,9 +283,7 @@ class _$CoreStateImpl implements _CoreState { .equals(other._bypassDomain, _bypassDomain) && const DeepCollectionEquality() .equals(other._routeAddress, _routeAddress) && - (identical(other.ipv6, ipv6) || other.ipv6 == ipv6) && - (identical(other.onlyProxy, onlyProxy) || - other.onlyProxy == onlyProxy)); + (identical(other.ipv6, ipv6) || other.ipv6 == ipv6)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -315,8 +297,7 @@ class _$CoreStateImpl implements _CoreState { systemProxy, const DeepCollectionEquality().hash(_bypassDomain), const DeepCollectionEquality().hash(_routeAddress), - ipv6, - onlyProxy); + ipv6); /// Create a copy of CoreState /// with the given fields replaced by the non-null parameter values. @@ -343,8 +324,7 @@ abstract class _CoreState implements CoreState { required final bool systemProxy, required final List bypassDomain, required final List routeAddress, - required final bool ipv6, - required final bool onlyProxy}) = _$CoreStateImpl; + required final bool ipv6}) = _$CoreStateImpl; factory _CoreState.fromJson(Map json) = _$CoreStateImpl.fromJson; @@ -365,8 +345,6 @@ abstract class _CoreState implements CoreState { List get routeAddress; @override bool get ipv6; - @override - bool get onlyProxy; /// Create a copy of CoreState /// with the given fields replaced by the non-null parameter values. @@ -778,6 +756,8 @@ mixin _$ConfigExtendedParams { bool get overrideDns => throw _privateConstructorUsedError; @JsonKey(name: "test-url") String get testUrl => throw _privateConstructorUsedError; + @JsonKey(name: "only-statistics-proxy") + bool get onlyStatisticsProxy => throw _privateConstructorUsedError; /// Serializes this ConfigExtendedParams to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -800,7 +780,8 @@ abstract class $ConfigExtendedParamsCopyWith<$Res> { @JsonKey(name: "is-compatible") bool isCompatible, @JsonKey(name: "selected-map") Map selectedMap, @JsonKey(name: "override-dns") bool overrideDns, - @JsonKey(name: "test-url") String testUrl}); + @JsonKey(name: "test-url") String testUrl, + @JsonKey(name: "only-statistics-proxy") bool onlyStatisticsProxy}); } /// @nodoc @@ -824,6 +805,7 @@ class _$ConfigExtendedParamsCopyWithImpl<$Res, Object? selectedMap = null, Object? overrideDns = null, Object? testUrl = null, + Object? onlyStatisticsProxy = null, }) { return _then(_value.copyWith( isPatch: null == isPatch @@ -846,6 +828,10 @@ class _$ConfigExtendedParamsCopyWithImpl<$Res, ? _value.testUrl : testUrl // ignore: cast_nullable_to_non_nullable as String, + onlyStatisticsProxy: null == onlyStatisticsProxy + ? _value.onlyStatisticsProxy + : onlyStatisticsProxy // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } } @@ -863,7 +849,8 @@ abstract class _$$ConfigExtendedParamsImplCopyWith<$Res> @JsonKey(name: "is-compatible") bool isCompatible, @JsonKey(name: "selected-map") Map selectedMap, @JsonKey(name: "override-dns") bool overrideDns, - @JsonKey(name: "test-url") String testUrl}); + @JsonKey(name: "test-url") String testUrl, + @JsonKey(name: "only-statistics-proxy") bool onlyStatisticsProxy}); } /// @nodoc @@ -884,6 +871,7 @@ class __$$ConfigExtendedParamsImplCopyWithImpl<$Res> Object? selectedMap = null, Object? overrideDns = null, Object? testUrl = null, + Object? onlyStatisticsProxy = null, }) { return _then(_$ConfigExtendedParamsImpl( isPatch: null == isPatch @@ -906,6 +894,10 @@ class __$$ConfigExtendedParamsImplCopyWithImpl<$Res> ? _value.testUrl : testUrl // ignore: cast_nullable_to_non_nullable as String, + onlyStatisticsProxy: null == onlyStatisticsProxy + ? _value.onlyStatisticsProxy + : onlyStatisticsProxy // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -919,7 +911,9 @@ class _$ConfigExtendedParamsImpl implements _ConfigExtendedParams { @JsonKey(name: "selected-map") required final Map selectedMap, @JsonKey(name: "override-dns") required this.overrideDns, - @JsonKey(name: "test-url") required this.testUrl}) + @JsonKey(name: "test-url") required this.testUrl, + @JsonKey(name: "only-statistics-proxy") + required this.onlyStatisticsProxy}) : _selectedMap = selectedMap; factory _$ConfigExtendedParamsImpl.fromJson(Map json) => @@ -946,10 +940,13 @@ class _$ConfigExtendedParamsImpl implements _ConfigExtendedParams { @override @JsonKey(name: "test-url") final String testUrl; + @override + @JsonKey(name: "only-statistics-proxy") + final bool onlyStatisticsProxy; @override String toString() { - return 'ConfigExtendedParams(isPatch: $isPatch, isCompatible: $isCompatible, selectedMap: $selectedMap, overrideDns: $overrideDns, testUrl: $testUrl)'; + return 'ConfigExtendedParams(isPatch: $isPatch, isCompatible: $isCompatible, selectedMap: $selectedMap, overrideDns: $overrideDns, testUrl: $testUrl, onlyStatisticsProxy: $onlyStatisticsProxy)'; } @override @@ -964,13 +961,21 @@ class _$ConfigExtendedParamsImpl implements _ConfigExtendedParams { .equals(other._selectedMap, _selectedMap) && (identical(other.overrideDns, overrideDns) || other.overrideDns == overrideDns) && - (identical(other.testUrl, testUrl) || other.testUrl == testUrl)); + (identical(other.testUrl, testUrl) || other.testUrl == testUrl) && + (identical(other.onlyStatisticsProxy, onlyStatisticsProxy) || + other.onlyStatisticsProxy == onlyStatisticsProxy)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, isPatch, isCompatible, - const DeepCollectionEquality().hash(_selectedMap), overrideDns, testUrl); + int get hashCode => Object.hash( + runtimeType, + isPatch, + isCompatible, + const DeepCollectionEquality().hash(_selectedMap), + overrideDns, + testUrl, + onlyStatisticsProxy); /// Create a copy of ConfigExtendedParams /// with the given fields replaced by the non-null parameter values. @@ -992,13 +997,14 @@ class _$ConfigExtendedParamsImpl implements _ConfigExtendedParams { abstract class _ConfigExtendedParams implements ConfigExtendedParams { const factory _ConfigExtendedParams( - {@JsonKey(name: "is-patch") required final bool isPatch, - @JsonKey(name: "is-compatible") required final bool isCompatible, - @JsonKey(name: "selected-map") - required final Map selectedMap, - @JsonKey(name: "override-dns") required final bool overrideDns, - @JsonKey(name: "test-url") required final String testUrl}) = - _$ConfigExtendedParamsImpl; + {@JsonKey(name: "is-patch") required final bool isPatch, + @JsonKey(name: "is-compatible") required final bool isCompatible, + @JsonKey(name: "selected-map") + required final Map selectedMap, + @JsonKey(name: "override-dns") required final bool overrideDns, + @JsonKey(name: "test-url") required final String testUrl, + @JsonKey(name: "only-statistics-proxy") + required final bool onlyStatisticsProxy}) = _$ConfigExtendedParamsImpl; factory _ConfigExtendedParams.fromJson(Map json) = _$ConfigExtendedParamsImpl.fromJson; @@ -1018,6 +1024,9 @@ abstract class _ConfigExtendedParams implements ConfigExtendedParams { @override @JsonKey(name: "test-url") String get testUrl; + @override + @JsonKey(name: "only-statistics-proxy") + bool get onlyStatisticsProxy; /// Create a copy of ConfigExtendedParams /// with the given fields replaced by the non-null parameter values. @@ -1423,6 +1432,187 @@ abstract class _ChangeProxyParams implements ChangeProxyParams { throw _privateConstructorUsedError; } +UpdateGeoDataParams _$UpdateGeoDataParamsFromJson(Map json) { + return _UpdateGeoDataParams.fromJson(json); +} + +/// @nodoc +mixin _$UpdateGeoDataParams { + @JsonKey(name: "geo-type") + String get geoType => throw _privateConstructorUsedError; + @JsonKey(name: "geo-name") + String get geoName => throw _privateConstructorUsedError; + + /// Serializes this UpdateGeoDataParams to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of UpdateGeoDataParams + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UpdateGeoDataParamsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UpdateGeoDataParamsCopyWith<$Res> { + factory $UpdateGeoDataParamsCopyWith( + UpdateGeoDataParams value, $Res Function(UpdateGeoDataParams) then) = + _$UpdateGeoDataParamsCopyWithImpl<$Res, UpdateGeoDataParams>; + @useResult + $Res call( + {@JsonKey(name: "geo-type") String geoType, + @JsonKey(name: "geo-name") String geoName}); +} + +/// @nodoc +class _$UpdateGeoDataParamsCopyWithImpl<$Res, $Val extends UpdateGeoDataParams> + implements $UpdateGeoDataParamsCopyWith<$Res> { + _$UpdateGeoDataParamsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UpdateGeoDataParams + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? geoType = null, + Object? geoName = null, + }) { + return _then(_value.copyWith( + geoType: null == geoType + ? _value.geoType + : geoType // ignore: cast_nullable_to_non_nullable + as String, + geoName: null == geoName + ? _value.geoName + : geoName // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$UpdateGeoDataParamsImplCopyWith<$Res> + implements $UpdateGeoDataParamsCopyWith<$Res> { + factory _$$UpdateGeoDataParamsImplCopyWith(_$UpdateGeoDataParamsImpl value, + $Res Function(_$UpdateGeoDataParamsImpl) then) = + __$$UpdateGeoDataParamsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@JsonKey(name: "geo-type") String geoType, + @JsonKey(name: "geo-name") String geoName}); +} + +/// @nodoc +class __$$UpdateGeoDataParamsImplCopyWithImpl<$Res> + extends _$UpdateGeoDataParamsCopyWithImpl<$Res, _$UpdateGeoDataParamsImpl> + implements _$$UpdateGeoDataParamsImplCopyWith<$Res> { + __$$UpdateGeoDataParamsImplCopyWithImpl(_$UpdateGeoDataParamsImpl _value, + $Res Function(_$UpdateGeoDataParamsImpl) _then) + : super(_value, _then); + + /// Create a copy of UpdateGeoDataParams + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? geoType = null, + Object? geoName = null, + }) { + return _then(_$UpdateGeoDataParamsImpl( + geoType: null == geoType + ? _value.geoType + : geoType // ignore: cast_nullable_to_non_nullable + as String, + geoName: null == geoName + ? _value.geoName + : geoName // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$UpdateGeoDataParamsImpl implements _UpdateGeoDataParams { + const _$UpdateGeoDataParamsImpl( + {@JsonKey(name: "geo-type") required this.geoType, + @JsonKey(name: "geo-name") required this.geoName}); + + factory _$UpdateGeoDataParamsImpl.fromJson(Map json) => + _$$UpdateGeoDataParamsImplFromJson(json); + + @override + @JsonKey(name: "geo-type") + final String geoType; + @override + @JsonKey(name: "geo-name") + final String geoName; + + @override + String toString() { + return 'UpdateGeoDataParams(geoType: $geoType, geoName: $geoName)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UpdateGeoDataParamsImpl && + (identical(other.geoType, geoType) || other.geoType == geoType) && + (identical(other.geoName, geoName) || other.geoName == geoName)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, geoType, geoName); + + /// Create a copy of UpdateGeoDataParams + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UpdateGeoDataParamsImplCopyWith<_$UpdateGeoDataParamsImpl> get copyWith => + __$$UpdateGeoDataParamsImplCopyWithImpl<_$UpdateGeoDataParamsImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$UpdateGeoDataParamsImplToJson( + this, + ); + } +} + +abstract class _UpdateGeoDataParams implements UpdateGeoDataParams { + const factory _UpdateGeoDataParams( + {@JsonKey(name: "geo-type") required final String geoType, + @JsonKey(name: "geo-name") required final String geoName}) = + _$UpdateGeoDataParamsImpl; + + factory _UpdateGeoDataParams.fromJson(Map json) = + _$UpdateGeoDataParamsImpl.fromJson; + + @override + @JsonKey(name: "geo-type") + String get geoType; + @override + @JsonKey(name: "geo-name") + String get geoName; + + /// Create a copy of UpdateGeoDataParams + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UpdateGeoDataParamsImplCopyWith<_$UpdateGeoDataParamsImpl> get copyWith => + throw _privateConstructorUsedError; +} + AppMessage _$AppMessageFromJson(Map json) { return _AppMessage.fromJson(json); } @@ -1591,45 +1781,45 @@ abstract class _AppMessage implements AppMessage { throw _privateConstructorUsedError; } -ServiceMessage _$ServiceMessageFromJson(Map json) { - return _ServiceMessage.fromJson(json); +InvokeMessage _$InvokeMessageFromJson(Map json) { + return _InvokeMessage.fromJson(json); } /// @nodoc -mixin _$ServiceMessage { - ServiceMessageType get type => throw _privateConstructorUsedError; +mixin _$InvokeMessage { + InvokeMessageType get type => throw _privateConstructorUsedError; dynamic get data => throw _privateConstructorUsedError; - /// Serializes this ServiceMessage to a JSON map. + /// Serializes this InvokeMessage to a JSON map. Map toJson() => throw _privateConstructorUsedError; - /// Create a copy of ServiceMessage + /// Create a copy of InvokeMessage /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) - $ServiceMessageCopyWith get copyWith => + $InvokeMessageCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class $ServiceMessageCopyWith<$Res> { - factory $ServiceMessageCopyWith( - ServiceMessage value, $Res Function(ServiceMessage) then) = - _$ServiceMessageCopyWithImpl<$Res, ServiceMessage>; +abstract class $InvokeMessageCopyWith<$Res> { + factory $InvokeMessageCopyWith( + InvokeMessage value, $Res Function(InvokeMessage) then) = + _$InvokeMessageCopyWithImpl<$Res, InvokeMessage>; @useResult - $Res call({ServiceMessageType type, dynamic data}); + $Res call({InvokeMessageType type, dynamic data}); } /// @nodoc -class _$ServiceMessageCopyWithImpl<$Res, $Val extends ServiceMessage> - implements $ServiceMessageCopyWith<$Res> { - _$ServiceMessageCopyWithImpl(this._value, this._then); +class _$InvokeMessageCopyWithImpl<$Res, $Val extends InvokeMessage> + implements $InvokeMessageCopyWith<$Res> { + _$InvokeMessageCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of ServiceMessage + /// Create a copy of InvokeMessage /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override @@ -1641,7 +1831,7 @@ class _$ServiceMessageCopyWithImpl<$Res, $Val extends ServiceMessage> type: null == type ? _value.type : type // ignore: cast_nullable_to_non_nullable - as ServiceMessageType, + as InvokeMessageType, data: freezed == data ? _value.data : data // ignore: cast_nullable_to_non_nullable @@ -1651,25 +1841,25 @@ class _$ServiceMessageCopyWithImpl<$Res, $Val extends ServiceMessage> } /// @nodoc -abstract class _$$ServiceMessageImplCopyWith<$Res> - implements $ServiceMessageCopyWith<$Res> { - factory _$$ServiceMessageImplCopyWith(_$ServiceMessageImpl value, - $Res Function(_$ServiceMessageImpl) then) = - __$$ServiceMessageImplCopyWithImpl<$Res>; +abstract class _$$InvokeMessageImplCopyWith<$Res> + implements $InvokeMessageCopyWith<$Res> { + factory _$$InvokeMessageImplCopyWith( + _$InvokeMessageImpl value, $Res Function(_$InvokeMessageImpl) then) = + __$$InvokeMessageImplCopyWithImpl<$Res>; @override @useResult - $Res call({ServiceMessageType type, dynamic data}); + $Res call({InvokeMessageType type, dynamic data}); } /// @nodoc -class __$$ServiceMessageImplCopyWithImpl<$Res> - extends _$ServiceMessageCopyWithImpl<$Res, _$ServiceMessageImpl> - implements _$$ServiceMessageImplCopyWith<$Res> { - __$$ServiceMessageImplCopyWithImpl( - _$ServiceMessageImpl _value, $Res Function(_$ServiceMessageImpl) _then) +class __$$InvokeMessageImplCopyWithImpl<$Res> + extends _$InvokeMessageCopyWithImpl<$Res, _$InvokeMessageImpl> + implements _$$InvokeMessageImplCopyWith<$Res> { + __$$InvokeMessageImplCopyWithImpl( + _$InvokeMessageImpl _value, $Res Function(_$InvokeMessageImpl) _then) : super(_value, _then); - /// Create a copy of ServiceMessage + /// Create a copy of InvokeMessage /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override @@ -1677,11 +1867,11 @@ class __$$ServiceMessageImplCopyWithImpl<$Res> Object? type = null, Object? data = freezed, }) { - return _then(_$ServiceMessageImpl( + return _then(_$InvokeMessageImpl( type: null == type ? _value.type : type // ignore: cast_nullable_to_non_nullable - as ServiceMessageType, + as InvokeMessageType, data: freezed == data ? _value.data : data // ignore: cast_nullable_to_non_nullable @@ -1692,27 +1882,27 @@ class __$$ServiceMessageImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$ServiceMessageImpl implements _ServiceMessage { - const _$ServiceMessageImpl({required this.type, this.data}); +class _$InvokeMessageImpl implements _InvokeMessage { + const _$InvokeMessageImpl({required this.type, this.data}); - factory _$ServiceMessageImpl.fromJson(Map json) => - _$$ServiceMessageImplFromJson(json); + factory _$InvokeMessageImpl.fromJson(Map json) => + _$$InvokeMessageImplFromJson(json); @override - final ServiceMessageType type; + final InvokeMessageType type; @override final dynamic data; @override String toString() { - return 'ServiceMessage(type: $type, data: $data)'; + return 'InvokeMessage(type: $type, data: $data)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$ServiceMessageImpl && + other is _$InvokeMessageImpl && (identical(other.type, type) || other.type == type) && const DeepCollectionEquality().equals(other.data, data)); } @@ -1722,41 +1912,40 @@ class _$ServiceMessageImpl implements _ServiceMessage { int get hashCode => Object.hash(runtimeType, type, const DeepCollectionEquality().hash(data)); - /// Create a copy of ServiceMessage + /// Create a copy of InvokeMessage /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') - _$$ServiceMessageImplCopyWith<_$ServiceMessageImpl> get copyWith => - __$$ServiceMessageImplCopyWithImpl<_$ServiceMessageImpl>( - this, _$identity); + _$$InvokeMessageImplCopyWith<_$InvokeMessageImpl> get copyWith => + __$$InvokeMessageImplCopyWithImpl<_$InvokeMessageImpl>(this, _$identity); @override Map toJson() { - return _$$ServiceMessageImplToJson( + return _$$InvokeMessageImplToJson( this, ); } } -abstract class _ServiceMessage implements ServiceMessage { - const factory _ServiceMessage( - {required final ServiceMessageType type, - final dynamic data}) = _$ServiceMessageImpl; +abstract class _InvokeMessage implements InvokeMessage { + const factory _InvokeMessage( + {required final InvokeMessageType type, + final dynamic data}) = _$InvokeMessageImpl; - factory _ServiceMessage.fromJson(Map json) = - _$ServiceMessageImpl.fromJson; + factory _InvokeMessage.fromJson(Map json) = + _$InvokeMessageImpl.fromJson; @override - ServiceMessageType get type; + InvokeMessageType get type; @override dynamic get data; - /// Create a copy of ServiceMessage + /// Create a copy of InvokeMessage /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) - _$$ServiceMessageImplCopyWith<_$ServiceMessageImpl> get copyWith => + _$$InvokeMessageImplCopyWith<_$InvokeMessageImpl> get copyWith => throw _privateConstructorUsedError; } @@ -1767,6 +1956,7 @@ Delay _$DelayFromJson(Map json) { /// @nodoc mixin _$Delay { String get name => throw _privateConstructorUsedError; + String get url => throw _privateConstructorUsedError; int? get value => throw _privateConstructorUsedError; /// Serializes this Delay to a JSON map. @@ -1783,7 +1973,7 @@ abstract class $DelayCopyWith<$Res> { factory $DelayCopyWith(Delay value, $Res Function(Delay) then) = _$DelayCopyWithImpl<$Res, Delay>; @useResult - $Res call({String name, int? value}); + $Res call({String name, String url, int? value}); } /// @nodoc @@ -1802,6 +1992,7 @@ class _$DelayCopyWithImpl<$Res, $Val extends Delay> @override $Res call({ Object? name = null, + Object? url = null, Object? value = freezed, }) { return _then(_value.copyWith( @@ -1809,6 +2000,10 @@ class _$DelayCopyWithImpl<$Res, $Val extends Delay> ? _value.name : name // ignore: cast_nullable_to_non_nullable as String, + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, value: freezed == value ? _value.value : value // ignore: cast_nullable_to_non_nullable @@ -1824,7 +2019,7 @@ abstract class _$$DelayImplCopyWith<$Res> implements $DelayCopyWith<$Res> { __$$DelayImplCopyWithImpl<$Res>; @override @useResult - $Res call({String name, int? value}); + $Res call({String name, String url, int? value}); } /// @nodoc @@ -1841,6 +2036,7 @@ class __$$DelayImplCopyWithImpl<$Res> @override $Res call({ Object? name = null, + Object? url = null, Object? value = freezed, }) { return _then(_$DelayImpl( @@ -1848,6 +2044,10 @@ class __$$DelayImplCopyWithImpl<$Res> ? _value.name : name // ignore: cast_nullable_to_non_nullable as String, + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, value: freezed == value ? _value.value : value // ignore: cast_nullable_to_non_nullable @@ -1859,7 +2059,7 @@ class __$$DelayImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() class _$DelayImpl implements _Delay { - const _$DelayImpl({required this.name, this.value}); + const _$DelayImpl({required this.name, required this.url, this.value}); factory _$DelayImpl.fromJson(Map json) => _$$DelayImplFromJson(json); @@ -1867,11 +2067,13 @@ class _$DelayImpl implements _Delay { @override final String name; @override + final String url; + @override final int? value; @override String toString() { - return 'Delay(name: $name, value: $value)'; + return 'Delay(name: $name, url: $url, value: $value)'; } @override @@ -1880,12 +2082,13 @@ class _$DelayImpl implements _Delay { (other.runtimeType == runtimeType && other is _$DelayImpl && (identical(other.name, name) || other.name == name) && + (identical(other.url, url) || other.url == url) && (identical(other.value, value) || other.value == value)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, name, value); + int get hashCode => Object.hash(runtimeType, name, url, value); /// Create a copy of Delay /// with the given fields replaced by the non-null parameter values. @@ -1904,14 +2107,18 @@ class _$DelayImpl implements _Delay { } abstract class _Delay implements Delay { - const factory _Delay({required final String name, final int? value}) = - _$DelayImpl; + const factory _Delay( + {required final String name, + required final String url, + final int? value}) = _$DelayImpl; factory _Delay.fromJson(Map json) = _$DelayImpl.fromJson; @override String get name; @override + String get url; + @override int? get value; /// Create a copy of Delay @@ -2086,7 +2293,7 @@ ProcessData _$ProcessDataFromJson(Map json) { /// @nodoc mixin _$ProcessData { - int get id => throw _privateConstructorUsedError; + String get id => throw _privateConstructorUsedError; Metadata get metadata => throw _privateConstructorUsedError; /// Serializes this ProcessData to a JSON map. @@ -2105,7 +2312,7 @@ abstract class $ProcessDataCopyWith<$Res> { ProcessData value, $Res Function(ProcessData) then) = _$ProcessDataCopyWithImpl<$Res, ProcessData>; @useResult - $Res call({int id, Metadata metadata}); + $Res call({String id, Metadata metadata}); $MetadataCopyWith<$Res> get metadata; } @@ -2132,7 +2339,7 @@ class _$ProcessDataCopyWithImpl<$Res, $Val extends ProcessData> id: null == id ? _value.id : id // ignore: cast_nullable_to_non_nullable - as int, + as String, metadata: null == metadata ? _value.metadata : metadata // ignore: cast_nullable_to_non_nullable @@ -2159,7 +2366,7 @@ abstract class _$$ProcessDataImplCopyWith<$Res> __$$ProcessDataImplCopyWithImpl<$Res>; @override @useResult - $Res call({int id, Metadata metadata}); + $Res call({String id, Metadata metadata}); @override $MetadataCopyWith<$Res> get metadata; @@ -2185,7 +2392,7 @@ class __$$ProcessDataImplCopyWithImpl<$Res> id: null == id ? _value.id : id // ignore: cast_nullable_to_non_nullable - as int, + as String, metadata: null == metadata ? _value.metadata : metadata // ignore: cast_nullable_to_non_nullable @@ -2203,7 +2410,7 @@ class _$ProcessDataImpl implements _ProcessData { _$$ProcessDataImplFromJson(json); @override - final int id; + final String id; @override final Metadata metadata; @@ -2244,14 +2451,14 @@ class _$ProcessDataImpl implements _ProcessData { abstract class _ProcessData implements ProcessData { const factory _ProcessData( - {required final int id, + {required final String id, required final Metadata metadata}) = _$ProcessDataImpl; factory _ProcessData.fromJson(Map json) = _$ProcessDataImpl.fromJson; @override - int get id; + String get id; @override Metadata get metadata; @@ -2269,7 +2476,7 @@ Fd _$FdFromJson(Map json) { /// @nodoc mixin _$Fd { - int get id => throw _privateConstructorUsedError; + String get id => throw _privateConstructorUsedError; int get value => throw _privateConstructorUsedError; /// Serializes this Fd to a JSON map. @@ -2286,7 +2493,7 @@ abstract class $FdCopyWith<$Res> { factory $FdCopyWith(Fd value, $Res Function(Fd) then) = _$FdCopyWithImpl<$Res, Fd>; @useResult - $Res call({int id, int value}); + $Res call({String id, int value}); } /// @nodoc @@ -2310,7 +2517,7 @@ class _$FdCopyWithImpl<$Res, $Val extends Fd> implements $FdCopyWith<$Res> { id: null == id ? _value.id : id // ignore: cast_nullable_to_non_nullable - as int, + as String, value: null == value ? _value.value : value // ignore: cast_nullable_to_non_nullable @@ -2325,7 +2532,7 @@ abstract class _$$FdImplCopyWith<$Res> implements $FdCopyWith<$Res> { __$$FdImplCopyWithImpl<$Res>; @override @useResult - $Res call({int id, int value}); + $Res call({String id, int value}); } /// @nodoc @@ -2346,7 +2553,7 @@ class __$$FdImplCopyWithImpl<$Res> extends _$FdCopyWithImpl<$Res, _$FdImpl> id: null == id ? _value.id : id // ignore: cast_nullable_to_non_nullable - as int, + as String, value: null == value ? _value.value : value // ignore: cast_nullable_to_non_nullable @@ -2364,7 +2571,7 @@ class _$FdImpl implements _Fd { _$$FdImplFromJson(json); @override - final int id; + final String id; @override final int value; @@ -2403,13 +2610,13 @@ class _$FdImpl implements _Fd { } abstract class _Fd implements Fd { - const factory _Fd({required final int id, required final int value}) = + const factory _Fd({required final String id, required final int value}) = _$FdImpl; factory _Fd.fromJson(Map json) = _$FdImpl.fromJson; @override - int get id; + String get id; @override int get value; @@ -2427,7 +2634,7 @@ ProcessMapItem _$ProcessMapItemFromJson(Map json) { /// @nodoc mixin _$ProcessMapItem { - int get id => throw _privateConstructorUsedError; + String get id => throw _privateConstructorUsedError; String get value => throw _privateConstructorUsedError; /// Serializes this ProcessMapItem to a JSON map. @@ -2446,7 +2653,7 @@ abstract class $ProcessMapItemCopyWith<$Res> { ProcessMapItem value, $Res Function(ProcessMapItem) then) = _$ProcessMapItemCopyWithImpl<$Res, ProcessMapItem>; @useResult - $Res call({int id, String value}); + $Res call({String id, String value}); } /// @nodoc @@ -2471,7 +2678,7 @@ class _$ProcessMapItemCopyWithImpl<$Res, $Val extends ProcessMapItem> id: null == id ? _value.id : id // ignore: cast_nullable_to_non_nullable - as int, + as String, value: null == value ? _value.value : value // ignore: cast_nullable_to_non_nullable @@ -2488,7 +2695,7 @@ abstract class _$$ProcessMapItemImplCopyWith<$Res> __$$ProcessMapItemImplCopyWithImpl<$Res>; @override @useResult - $Res call({int id, String value}); + $Res call({String id, String value}); } /// @nodoc @@ -2511,7 +2718,7 @@ class __$$ProcessMapItemImplCopyWithImpl<$Res> id: null == id ? _value.id : id // ignore: cast_nullable_to_non_nullable - as int, + as String, value: null == value ? _value.value : value // ignore: cast_nullable_to_non_nullable @@ -2529,7 +2736,7 @@ class _$ProcessMapItemImpl implements _ProcessMapItem { _$$ProcessMapItemImplFromJson(json); @override - final int id; + final String id; @override final String value; @@ -2570,14 +2777,14 @@ class _$ProcessMapItemImpl implements _ProcessMapItem { abstract class _ProcessMapItem implements ProcessMapItem { const factory _ProcessMapItem( - {required final int id, + {required final String id, required final String value}) = _$ProcessMapItemImpl; factory _ProcessMapItem.fromJson(Map json) = _$ProcessMapItemImpl.fromJson; @override - int get id; + String get id; @override String get value; @@ -3437,6 +3644,8 @@ Action _$ActionFromJson(Map json) { mixin _$Action { ActionMethod get method => throw _privateConstructorUsedError; dynamic get data => throw _privateConstructorUsedError; + @JsonKey(name: "default-value") + dynamic get defaultValue => throw _privateConstructorUsedError; String get id => throw _privateConstructorUsedError; /// Serializes this Action to a JSON map. @@ -3453,7 +3662,11 @@ abstract class $ActionCopyWith<$Res> { factory $ActionCopyWith(Action value, $Res Function(Action) then) = _$ActionCopyWithImpl<$Res, Action>; @useResult - $Res call({ActionMethod method, dynamic data, String id}); + $Res call( + {ActionMethod method, + dynamic data, + @JsonKey(name: "default-value") dynamic defaultValue, + String id}); } /// @nodoc @@ -3473,6 +3686,7 @@ class _$ActionCopyWithImpl<$Res, $Val extends Action> $Res call({ Object? method = null, Object? data = freezed, + Object? defaultValue = freezed, Object? id = null, }) { return _then(_value.copyWith( @@ -3484,6 +3698,10 @@ class _$ActionCopyWithImpl<$Res, $Val extends Action> ? _value.data : data // ignore: cast_nullable_to_non_nullable as dynamic, + defaultValue: freezed == defaultValue + ? _value.defaultValue + : defaultValue // ignore: cast_nullable_to_non_nullable + as dynamic, id: null == id ? _value.id : id // ignore: cast_nullable_to_non_nullable @@ -3499,7 +3717,11 @@ abstract class _$$ActionImplCopyWith<$Res> implements $ActionCopyWith<$Res> { __$$ActionImplCopyWithImpl<$Res>; @override @useResult - $Res call({ActionMethod method, dynamic data, String id}); + $Res call( + {ActionMethod method, + dynamic data, + @JsonKey(name: "default-value") dynamic defaultValue, + String id}); } /// @nodoc @@ -3517,6 +3739,7 @@ class __$$ActionImplCopyWithImpl<$Res> $Res call({ Object? method = null, Object? data = freezed, + Object? defaultValue = freezed, Object? id = null, }) { return _then(_$ActionImpl( @@ -3528,6 +3751,10 @@ class __$$ActionImplCopyWithImpl<$Res> ? _value.data : data // ignore: cast_nullable_to_non_nullable as dynamic, + defaultValue: freezed == defaultValue + ? _value.defaultValue + : defaultValue // ignore: cast_nullable_to_non_nullable + as dynamic, id: null == id ? _value.id : id // ignore: cast_nullable_to_non_nullable @@ -3540,7 +3767,10 @@ class __$$ActionImplCopyWithImpl<$Res> @JsonSerializable() class _$ActionImpl implements _Action { const _$ActionImpl( - {required this.method, required this.data, required this.id}); + {required this.method, + required this.data, + @JsonKey(name: "default-value") required this.defaultValue, + required this.id}); factory _$ActionImpl.fromJson(Map json) => _$$ActionImplFromJson(json); @@ -3550,11 +3780,14 @@ class _$ActionImpl implements _Action { @override final dynamic data; @override + @JsonKey(name: "default-value") + final dynamic defaultValue; + @override final String id; @override String toString() { - return 'Action(method: $method, data: $data, id: $id)'; + return 'Action(method: $method, data: $data, defaultValue: $defaultValue, id: $id)'; } @override @@ -3564,13 +3797,19 @@ class _$ActionImpl implements _Action { other is _$ActionImpl && (identical(other.method, method) || other.method == method) && const DeepCollectionEquality().equals(other.data, data) && + const DeepCollectionEquality() + .equals(other.defaultValue, defaultValue) && (identical(other.id, id) || other.id == id)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( - runtimeType, method, const DeepCollectionEquality().hash(data), id); + runtimeType, + method, + const DeepCollectionEquality().hash(data), + const DeepCollectionEquality().hash(defaultValue), + id); /// Create a copy of Action /// with the given fields replaced by the non-null parameter values. @@ -3592,6 +3831,7 @@ abstract class _Action implements Action { const factory _Action( {required final ActionMethod method, required final dynamic data, + @JsonKey(name: "default-value") required final dynamic defaultValue, required final String id}) = _$ActionImpl; factory _Action.fromJson(Map json) = _$ActionImpl.fromJson; @@ -3601,6 +3841,9 @@ abstract class _Action implements Action { @override dynamic get data; @override + @JsonKey(name: "default-value") + dynamic get defaultValue; + @override String get id; /// Create a copy of Action @@ -3610,3 +3853,188 @@ abstract class _Action implements Action { _$$ActionImplCopyWith<_$ActionImpl> get copyWith => throw _privateConstructorUsedError; } + +ActionResult _$ActionResultFromJson(Map json) { + return _ActionResult.fromJson(json); +} + +/// @nodoc +mixin _$ActionResult { + ActionMethod get method => throw _privateConstructorUsedError; + dynamic get data => throw _privateConstructorUsedError; + String? get id => throw _privateConstructorUsedError; + + /// Serializes this ActionResult to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ActionResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ActionResultCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ActionResultCopyWith<$Res> { + factory $ActionResultCopyWith( + ActionResult value, $Res Function(ActionResult) then) = + _$ActionResultCopyWithImpl<$Res, ActionResult>; + @useResult + $Res call({ActionMethod method, dynamic data, String? id}); +} + +/// @nodoc +class _$ActionResultCopyWithImpl<$Res, $Val extends ActionResult> + implements $ActionResultCopyWith<$Res> { + _$ActionResultCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ActionResult + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? method = null, + Object? data = freezed, + Object? id = freezed, + }) { + return _then(_value.copyWith( + method: null == method + ? _value.method + : method // ignore: cast_nullable_to_non_nullable + as ActionMethod, + data: freezed == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as dynamic, + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ActionResultImplCopyWith<$Res> + implements $ActionResultCopyWith<$Res> { + factory _$$ActionResultImplCopyWith( + _$ActionResultImpl value, $Res Function(_$ActionResultImpl) then) = + __$$ActionResultImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ActionMethod method, dynamic data, String? id}); +} + +/// @nodoc +class __$$ActionResultImplCopyWithImpl<$Res> + extends _$ActionResultCopyWithImpl<$Res, _$ActionResultImpl> + implements _$$ActionResultImplCopyWith<$Res> { + __$$ActionResultImplCopyWithImpl( + _$ActionResultImpl _value, $Res Function(_$ActionResultImpl) _then) + : super(_value, _then); + + /// Create a copy of ActionResult + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? method = null, + Object? data = freezed, + Object? id = freezed, + }) { + return _then(_$ActionResultImpl( + method: null == method + ? _value.method + : method // ignore: cast_nullable_to_non_nullable + as ActionMethod, + data: freezed == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as dynamic, + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ActionResultImpl implements _ActionResult { + const _$ActionResultImpl({required this.method, required this.data, this.id}); + + factory _$ActionResultImpl.fromJson(Map json) => + _$$ActionResultImplFromJson(json); + + @override + final ActionMethod method; + @override + final dynamic data; + @override + final String? id; + + @override + String toString() { + return 'ActionResult(method: $method, data: $data, id: $id)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ActionResultImpl && + (identical(other.method, method) || other.method == method) && + const DeepCollectionEquality().equals(other.data, data) && + (identical(other.id, id) || other.id == id)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, method, const DeepCollectionEquality().hash(data), id); + + /// Create a copy of ActionResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ActionResultImplCopyWith<_$ActionResultImpl> get copyWith => + __$$ActionResultImplCopyWithImpl<_$ActionResultImpl>(this, _$identity); + + @override + Map toJson() { + return _$$ActionResultImplToJson( + this, + ); + } +} + +abstract class _ActionResult implements ActionResult { + const factory _ActionResult( + {required final ActionMethod method, + required final dynamic data, + final String? id}) = _$ActionResultImpl; + + factory _ActionResult.fromJson(Map json) = + _$ActionResultImpl.fromJson; + + @override + ActionMethod get method; + @override + dynamic get data; + @override + String? get id; + + /// Create a copy of ActionResult + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ActionResultImplCopyWith<_$ActionResultImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/generated/core.g.dart b/lib/models/generated/core.g.dart index d296742..0d01bf4 100644 --- a/lib/models/generated/core.g.dart +++ b/lib/models/generated/core.g.dart @@ -23,7 +23,6 @@ _$CoreStateImpl _$$CoreStateImplFromJson(Map json) => .map((e) => e as String) .toList(), ipv6: json['ipv6'] as bool, - onlyProxy: json['onlyProxy'] as bool, ); Map _$$CoreStateImplToJson(_$CoreStateImpl instance) => @@ -36,7 +35,6 @@ Map _$$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.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 _$$ConfigExtendedParamsImplToJson( @@ -94,6 +93,7 @@ Map _$$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 _$$ChangeProxyParamsImplToJson( 'proxy-name': instance.proxyName, }; +_$UpdateGeoDataParamsImpl _$$UpdateGeoDataParamsImplFromJson( + Map json) => + _$UpdateGeoDataParamsImpl( + geoType: json['geo-type'] as String, + geoName: json['geo-name'] as String, + ); + +Map _$$UpdateGeoDataParamsImplToJson( + _$UpdateGeoDataParamsImpl instance) => + { + 'geo-type': instance.geoType, + 'geo-name': instance.geoName, + }; + _$AppMessageImpl _$$AppMessageImplFromJson(Map 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 json) => - _$ServiceMessageImpl( - type: $enumDecode(_$ServiceMessageTypeEnumMap, json['type']), +_$InvokeMessageImpl _$$InvokeMessageImplFromJson(Map json) => + _$InvokeMessageImpl( + type: $enumDecode(_$InvokeMessageTypeEnumMap, json['type']), data: json['data'], ); -Map _$$ServiceMessageImplToJson( - _$ServiceMessageImpl instance) => +Map _$$InvokeMessageImplToJson(_$InvokeMessageImpl instance) => { - '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 json) => _$DelayImpl( name: json['name'] as String, + url: json['url'] as String, value: (json['value'] as num?)?.toInt(), ); Map _$$DelayImplToJson(_$DelayImpl instance) => { 'name': instance.name, + 'url': instance.url, 'value': instance.value, }; @@ -190,7 +202,7 @@ Map _$$NowImplToJson(_$NowImpl instance) => { _$ProcessDataImpl _$$ProcessDataImplFromJson(Map json) => _$ProcessDataImpl( - id: (json['id'] as num).toInt(), + id: json['id'] as String, metadata: Metadata.fromJson(json['metadata'] as Map), ); @@ -201,7 +213,7 @@ Map _$$ProcessDataImplToJson(_$ProcessDataImpl instance) => }; _$FdImpl _$$FdImplFromJson(Map json) => _$FdImpl( - id: (json['id'] as num).toInt(), + id: json['id'] as String, value: (json['value'] as num).toInt(), ); @@ -212,7 +224,7 @@ Map _$$FdImplToJson(_$FdImpl instance) => { _$ProcessMapItemImpl _$$ProcessMapItemImplFromJson(Map json) => _$ProcessMapItemImpl( - id: (json['id'] as num).toInt(), + id: json['id'] as String, value: json['value'] as String, ); @@ -293,6 +305,7 @@ Map _$$TunPropsImplToJson(_$TunPropsImpl instance) => _$ActionImpl _$$ActionImplFromJson(Map json) => _$ActionImpl( method: $enumDecode(_$ActionMethodEnumMap, json['method']), data: json['data'], + defaultValue: json['default-value'], id: json['id'] as String, ); @@ -300,6 +313,7 @@ Map _$$ActionImplToJson(_$ActionImpl instance) => { '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 json) => + _$ActionResultImpl( + method: $enumDecode(_$ActionMethodEnumMap, json['method']), + data: json['data'], + id: json['id'] as String?, + ); + +Map _$$ActionResultImplToJson(_$ActionResultImpl instance) => + { + 'method': _$ActionMethodEnumMap[instance.method]!, + 'data': instance.data, + 'id': instance.id, + }; diff --git a/lib/models/generated/selector.freezed.dart b/lib/models/generated/selector.freezed.dart index a683036..43c96ed 100644 --- a/lib/models/generated/selector.freezed.dart +++ b/lib/models/generated/selector.freezed.dart @@ -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 proxies, required final int columns}) = _$ProxyGroupSelectorStateImpl; + @override + String? get testUrl; @override ProxiesSortType get proxiesSortType; @override diff --git a/lib/models/selector.dart b/lib/models/selector.dart index 3ee6736..e28ee12 100644 --- a/lib/models/selector.dart +++ b/lib/models/selector.dart @@ -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, diff --git a/lib/pages/editor.dart b/lib/pages/editor.dart new file mode 100644 index 0000000..0b195e4 --- /dev/null +++ b/lib/pages/editor.dart @@ -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 Function(BuildContext context, String text)? onPop; + + const EditorPage({ + super.key, + required this.title, + required this.content, + this.onSave, + this.onPop, + }); + + @override + State createState() => _EditorPageState(); +} + +class _EditorPageState extends State { + 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 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 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 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!); + } +} diff --git a/lib/pages/pages.dart b/lib/pages/pages.dart index ef42ae9..1a41eff 100644 --- a/lib/pages/pages.dart +++ b/lib/pages/pages.dart @@ -1,2 +1,3 @@ export 'home.dart'; -export 'scan.dart'; \ No newline at end of file +export 'scan.dart'; +export 'editor.dart'; \ No newline at end of file diff --git a/lib/plugins/service.dart b/lib/plugins/service.dart index 11b35f0..f324601 100644 --- a/lib/plugins/service.dart +++ b/lib/plugins/service.dart @@ -1,8 +1,12 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; +import 'package:fl_clash/state.dart'; import 'package:flutter/services.dart'; +import '../clash/lib.dart'; + class Service { static Service? _instance; late MethodChannel methodChannel; @@ -24,7 +28,17 @@ class Service { Future destroy() async { return await methodChannel.invokeMethod("destroy"); } + + Future startVpn() async { + final options = await clashLib?.getAndroidVpnOptions(); + return await methodChannel.invokeMethod("startVpn", { + 'data': json.encode(options), + }); + } + + Future stopVpn() async { + return await methodChannel.invokeMethod("stopVpn"); + } } -final service = - Platform.isAndroid ? Service() : null; +Service? get service => Platform.isAndroid && !globalState.isService ? Service() : null; diff --git a/lib/plugins/tile.dart b/lib/plugins/tile.dart index c37c4d4..f325db9 100644 --- a/lib/plugins/tile.dart +++ b/lib/plugins/tile.dart @@ -15,7 +15,6 @@ abstract mixin class TileListener { } class Tile { - StreamSubscription? subscription; final MethodChannel _channel = const MethodChannel('tile'); diff --git a/lib/plugins/vpn.dart b/lib/plugins/vpn.dart index 8cba609..2349805 100644 --- a/lib/plugins/vpn.dart +++ b/lib/plugins/vpn.dart @@ -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 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 startVpn() async { - final options = clashLib?.getAndroidVpnOptions(); + final ObserverList _listeners = ObserverList(); + + Future start(AndroidVpnOptions options) async { return await methodChannel.invokeMethod("start", { 'data': json.encode(options), }); } - Future stopVpn() async { + Future stop() async { return await methodChannel.invokeMethod("stop"); } @@ -60,45 +72,13 @@ class Vpn { }); } - Future startForeground({ - required String title, - required String content, - }) async { - return await methodChannel.invokeMethod("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; diff --git a/lib/state.dart b/lib/state.dart index 90d6c15..5bd144c 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -5,7 +5,6 @@ import 'package:animations/animations.dart'; import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/plugins/service.dart'; -import 'package:fl_clash/plugins/vpn.dart'; import 'package:fl_clash/widgets/scaffold.dart'; import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -15,37 +14,49 @@ import 'common/common.dart'; import 'controller.dart'; import 'models/models.dart'; +typedef UpdateTasks = List; + 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(); late AppController appController; GlobalKey homeScaffoldKey = GlobalKey(); - List 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 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 showMessage({ required String title, required InlineSpan message, - Function()? onTab, String? confirmText, - }) { - showCommonDialog( + }) async { + return await showCommonDialog( 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,19 +305,10 @@ 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(); } } @@ -326,18 +336,22 @@ class GlobalState { } 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)); } } diff --git a/lib/widgets/card.dart b/lib/widgets/card.dart index 81bbe0b..daecc9c 100644 --- a/lib/widgets/card.dart +++ b/lib/widgets/card.dart @@ -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 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( diff --git a/lib/widgets/popup.dart b/lib/widgets/popup.dart new file mode 100644 index 0000000..d588313 --- /dev/null +++ b/lib/widgets/popup.dart @@ -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 extends PopupRoute { + final WidgetBuilder builder; + ValueNotifier 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 animation, + Animation secondaryAnimation, + ) { + return builder( + context, + ); + } + + @override + Widget buildTransitions(BuildContext context, Animation animation, + Animation 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 createState() => _CommonPopupBoxState(); +} + +class _CommonPopupBoxState extends State { + 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 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, + ), + ], + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/popup_menu.dart b/lib/widgets/popup_menu.dart deleted file mode 100644 index 4b22715..0000000 --- a/lib/widgets/popup_menu.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'package:flutter/material.dart'; - -class CommonPopupMenuItem { - T action; - String label; - IconData? iconData; - - CommonPopupMenuItem({ - required this.action, - required this.label, - this.iconData, - }); -} - -class CommonPopupMenu extends StatefulWidget { - final List items; - final PopupMenuItemSelected 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> createState() => _CommonPopupMenuState(); -} - -class _CommonPopupMenuState extends State> { - late ValueNotifier 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( - icon: widget.icon, - onSelected: handleSelect, - itemBuilder: (_) { - return [ - for (final item in widget.items) - PopupMenuItem( - 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( - valueListenable: groupValue, - builder: (_, groupValue, __) { - return Radio( - value: item.action, - groupValue: groupValue, - onChanged: (T? value) { - if (value != null) { - handleSelect(value); - Navigator.of(context).pop(); - } - }, - ); - }, - ), - ), - ], - ), - ), - ]; - }, - ); - } -} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index abd8847..7c604e4 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -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'; diff --git a/pubspec.lock b/pubspec.lock index c2c0e7c..cc98b11 100755 --- a/pubspec.lock +++ b/pubspec.lock @@ -1511,4 +1511,4 @@ packages: version: "0.2.3" sdks: dart: ">=3.5.0 <4.0.0" - flutter: ">=3.24.5" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index f43089c..fe4027e 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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.72+202501101 +version: 0.8.73+202502021 environment: sdk: '>=3.1.0 <4.0.0'